@promigence/sdk 0.1.0-beta.7 → 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/README.md +3 -2
- package/dist/cjs/client.d.ts +38 -0
- package/dist/cjs/client.js +28 -12
- package/dist/cjs/core.d.ts +10 -1
- package/dist/cjs/core.js +11 -3
- package/dist/cjs/credstore.d.ts +3 -2
- package/dist/cjs/credstore.js +17 -4
- package/dist/cjs/models.gen.d.ts +148 -6
- package/dist/cjs/proxy.js +2 -1
- package/dist/cjs/pty.d.ts +65 -3
- package/dist/cjs/pty.js +270 -34
- package/dist/cjs/sandbox.d.ts +32 -0
- package/dist/cjs/sandbox.js +3 -0
- package/dist/cjs/ssh.js +1 -0
- package/dist/client.d.ts +38 -0
- package/dist/client.js +29 -13
- package/dist/core.d.ts +10 -1
- package/dist/core.js +9 -2
- package/dist/credstore.d.ts +3 -2
- package/dist/credstore.js +17 -4
- package/dist/models.gen.d.ts +148 -6
- package/dist/proxy.js +2 -1
- package/dist/pty.d.ts +65 -3
- package/dist/pty.js +269 -33
- package/dist/sandbox.d.ts +32 -0
- package/dist/sandbox.js +3 -0
- package/dist/ssh.js +1 -0
- package/package.json +1 -1
package/dist/pty.js
CHANGED
|
@@ -2,6 +2,8 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
2
2
|
import { CALLER_HEADER } from "./caller.js";
|
|
3
3
|
import { requestVia } from "./proxy.js";
|
|
4
4
|
export const PTY_INPUT_CHUNK = 64 * 1024;
|
|
5
|
+
export const PTY_INPUT_KEEP = 256 * 1024;
|
|
6
|
+
export const PTY_RESUME_HEADER = "promigence-resume";
|
|
5
7
|
const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
6
8
|
export const PTY_COMMAND_HEADER = "promigence-pty-command";
|
|
7
9
|
export const PTY_SECRETS_HEADER = "promigence-pty-secrets";
|
|
@@ -92,11 +94,17 @@ export class PtySession {
|
|
|
92
94
|
rows;
|
|
93
95
|
closed = false;
|
|
94
96
|
done;
|
|
97
|
+
pending;
|
|
98
|
+
resumeToken;
|
|
99
|
+
inputSent = 0;
|
|
100
|
+
inputBase = 0;
|
|
101
|
+
inputLog = Buffer.alloc(0);
|
|
95
102
|
constructor(socket, open, done) {
|
|
96
103
|
this.socket = socket;
|
|
97
104
|
this.pid = open.pid;
|
|
98
105
|
this.cols = open.cols;
|
|
99
106
|
this.rows = open.rows;
|
|
107
|
+
this.resumeToken = open.resume;
|
|
100
108
|
this.done = done;
|
|
101
109
|
void done.then(() => { this.#ended = true; });
|
|
102
110
|
}
|
|
@@ -104,10 +112,67 @@ export class PtySession {
|
|
|
104
112
|
get ended() {
|
|
105
113
|
return this.#ended;
|
|
106
114
|
}
|
|
107
|
-
|
|
115
|
+
holdInput() {
|
|
116
|
+
this.pending ??= [];
|
|
117
|
+
}
|
|
118
|
+
rebind(socket, open) {
|
|
119
|
+
this.socket = socket;
|
|
120
|
+
this.resumeToken = open.resume;
|
|
121
|
+
this.inputSent = 0;
|
|
122
|
+
this.inputBase = 0;
|
|
123
|
+
this.inputLog = Buffer.alloc(0);
|
|
124
|
+
this.flush(open);
|
|
125
|
+
}
|
|
126
|
+
resumed(socket, open) {
|
|
127
|
+
this.socket = socket;
|
|
128
|
+
this.resumeToken = open.resume ?? this.resumeToken;
|
|
129
|
+
const took = open.input ?? this.inputSent;
|
|
130
|
+
if (took < this.inputSent) {
|
|
131
|
+
const from = Math.max(took, this.inputBase) - this.inputBase;
|
|
132
|
+
const again = this.inputLog.subarray(from);
|
|
133
|
+
this.inputSent = Math.max(took, this.inputBase);
|
|
134
|
+
for (let at = 0; at < again.length; at += PTY_INPUT_CHUNK) {
|
|
135
|
+
this.writeNow(0x2, again.subarray(at, at + PTY_INPUT_CHUNK));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
this.flush(open);
|
|
139
|
+
}
|
|
140
|
+
flush(open) {
|
|
141
|
+
const queued = this.pending ?? [];
|
|
142
|
+
this.pending = undefined;
|
|
143
|
+
if (open.cols !== this.cols || open.rows !== this.rows) {
|
|
144
|
+
this.resize({ cols: this.cols, rows: this.rows });
|
|
145
|
+
}
|
|
146
|
+
for (const f of queued)
|
|
147
|
+
this.writeNow(f.opcode, f.payload);
|
|
148
|
+
}
|
|
149
|
+
writeNow(opcode, payload) {
|
|
108
150
|
if (this.closed || this.socket.destroyed)
|
|
109
151
|
return;
|
|
110
152
|
this.socket.write(encodeFrame(opcode, payload));
|
|
153
|
+
if (opcode === 0x2) {
|
|
154
|
+
this.inputSent += payload.length;
|
|
155
|
+
this.inputLog = Buffer.concat([this.inputLog, payload]);
|
|
156
|
+
if (this.inputLog.length > PTY_INPUT_KEEP) {
|
|
157
|
+
const drop = this.inputLog.length - PTY_INPUT_KEEP;
|
|
158
|
+
this.inputLog = this.inputLog.subarray(drop);
|
|
159
|
+
this.inputBase += drop;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
get hungUp() {
|
|
164
|
+
return this.closed;
|
|
165
|
+
}
|
|
166
|
+
send(opcode, payload) {
|
|
167
|
+
if (this.closed)
|
|
168
|
+
return;
|
|
169
|
+
if (this.pending) {
|
|
170
|
+
if (this.pending.reduce((n, f) => n + f.payload.length, 0) + payload.length <= 1 << 20) {
|
|
171
|
+
this.pending.push({ opcode, payload: Buffer.from(payload) });
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
this.writeNow(opcode, payload);
|
|
111
176
|
}
|
|
112
177
|
write(data) {
|
|
113
178
|
const bytes = typeof data === "string" ? Buffer.from(data, "utf8") : data;
|
|
@@ -134,16 +199,49 @@ export class PtySession {
|
|
|
134
199
|
disconnect() {
|
|
135
200
|
if (this.closed)
|
|
136
201
|
return;
|
|
202
|
+
this.pending = undefined;
|
|
137
203
|
this.send(0x8, Buffer.from([0x03, 0xe8]));
|
|
138
204
|
this.closed = true;
|
|
139
205
|
this.socket.end();
|
|
140
206
|
}
|
|
141
207
|
}
|
|
142
|
-
export
|
|
143
|
-
|
|
208
|
+
export const PTY_RECONNECT_CODE = 1012;
|
|
209
|
+
export const PTY_RECONNECT_FOR_MS = 30_000;
|
|
210
|
+
export const PTY_RECONNECT_FIRST_WAIT_MS = 250;
|
|
211
|
+
export const PTY_RECONNECT_MAX_WAIT_MS = 4_000;
|
|
212
|
+
export const PTY_RESTART_EXIT = {
|
|
213
|
+
exitCode: null,
|
|
214
|
+
signal: null,
|
|
215
|
+
error: {
|
|
216
|
+
code: "service_restart",
|
|
217
|
+
message: "the service restarted and the terminal could not reconnect within 30 s; open it again",
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
export const PTY_RESTART_CLOSED_EXIT = {
|
|
221
|
+
exitCode: null,
|
|
222
|
+
signal: null,
|
|
223
|
+
error: { code: "service_restart", message: "the service restarted and closed the terminal; open it again" },
|
|
224
|
+
};
|
|
225
|
+
const CLOSED_EXIT = {
|
|
226
|
+
exitCode: null,
|
|
227
|
+
signal: null,
|
|
228
|
+
error: { code: "closed", message: "the terminal connection closed without an exit record" },
|
|
229
|
+
};
|
|
230
|
+
class LegRefused {
|
|
231
|
+
status;
|
|
232
|
+
error;
|
|
233
|
+
restart;
|
|
234
|
+
constructor(status, error, restart = false) {
|
|
235
|
+
this.status = status;
|
|
236
|
+
this.error = error;
|
|
237
|
+
this.restart = restart;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function openLeg(opts, url, resume) {
|
|
144
241
|
const key = randomBytes(16).toString("base64");
|
|
145
242
|
const req = requestVia(url, {
|
|
146
243
|
method: "GET",
|
|
244
|
+
agent: false,
|
|
147
245
|
headers: {
|
|
148
246
|
connection: "Upgrade",
|
|
149
247
|
upgrade: "websocket",
|
|
@@ -152,58 +250,51 @@ export function connectPty(opts) {
|
|
|
152
250
|
authorization: `Bearer ${opts.apiKey}`,
|
|
153
251
|
"user-agent": opts.userAgent,
|
|
154
252
|
...(opts.caller ? { [CALLER_HEADER]: opts.caller } : {}),
|
|
155
|
-
...(
|
|
253
|
+
...(resume ? { [PTY_RESUME_HEADER]: resume } : {}),
|
|
254
|
+
...(opts.command && !resume
|
|
156
255
|
? { [PTY_COMMAND_HEADER]: Buffer.from(opts.command, "utf8").toString("base64") }
|
|
157
256
|
: {}),
|
|
158
|
-
...(opts.secrets?.length
|
|
257
|
+
...(opts.secrets?.length && !resume
|
|
159
258
|
? {
|
|
160
259
|
[PTY_SECRETS_HEADER]: Buffer.from(JSON.stringify(opts.secrets), "utf8").toString("base64"),
|
|
161
260
|
}
|
|
162
261
|
: {}),
|
|
163
262
|
},
|
|
164
263
|
});
|
|
165
|
-
|
|
264
|
+
let resolveEnded;
|
|
265
|
+
const ended = new Promise((r) => { resolveEnded = r; });
|
|
266
|
+
const opened = new Promise((resolve, reject) => {
|
|
166
267
|
let settled = false;
|
|
167
|
-
const fail = (
|
|
268
|
+
const fail = (status, body, retryAfter, restart = false) => {
|
|
269
|
+
if (settled)
|
|
270
|
+
return;
|
|
168
271
|
settled = true;
|
|
169
|
-
reject(
|
|
170
|
-
}
|
|
272
|
+
reject(new LegRefused(status, opts.refuse(status, body, retryAfter), restart));
|
|
273
|
+
};
|
|
171
274
|
const timer = setTimeout(() => {
|
|
172
275
|
req.destroy();
|
|
173
|
-
fail(
|
|
276
|
+
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>`)." } }));
|
|
174
277
|
}, opts.timeoutMs ?? 30_000);
|
|
175
|
-
req.on("error", (e) => { clearTimeout(timer); fail(
|
|
278
|
+
req.on("error", (e) => { clearTimeout(timer); fail(0, JSON.stringify({ error: { code: "unreachable", message: `cannot reach ${url.host}: ${e.code ?? e.message}` } })); });
|
|
176
279
|
req.on("response", (res) => {
|
|
177
280
|
clearTimeout(timer);
|
|
178
281
|
const parts = [];
|
|
179
282
|
res.on("data", (d) => parts.push(d));
|
|
180
283
|
const retryAfter = res.headers["retry-after"];
|
|
181
|
-
res.on("end", () => fail(
|
|
284
|
+
res.on("end", () => fail(res.statusCode ?? 0, Buffer.concat(parts).toString("utf8"), typeof retryAfter === "string" ? retryAfter : undefined));
|
|
182
285
|
});
|
|
183
286
|
req.on("upgrade", (res, socket, head) => {
|
|
184
287
|
clearTimeout(timer);
|
|
185
288
|
if (res.headers["sec-websocket-accept"] !== wsAccept(key)) {
|
|
186
289
|
socket.destroy();
|
|
187
|
-
fail(
|
|
290
|
+
fail(502, JSON.stringify({ error: { code: "internal", message: "the terminal server answered the WebSocket handshake with the wrong Sec-WebSocket-Accept" } }));
|
|
188
291
|
return;
|
|
189
292
|
}
|
|
190
293
|
socket.setNoDelay(true);
|
|
191
294
|
const reader = new FrameReader();
|
|
192
295
|
let exit;
|
|
193
|
-
let
|
|
194
|
-
let
|
|
195
|
-
const done = new Promise((r) => { resolveDone = r; });
|
|
196
|
-
let finished = false;
|
|
197
|
-
const finish = () => {
|
|
198
|
-
if (finished)
|
|
199
|
-
return;
|
|
200
|
-
finished = true;
|
|
201
|
-
const e = exit ?? { exitCode: null, signal: null, error: { code: "closed", message: "the terminal connection closed without an exit record" } };
|
|
202
|
-
opts.onExit?.(e);
|
|
203
|
-
resolveDone(e);
|
|
204
|
-
if (!settled)
|
|
205
|
-
fail(opts.refuse(502, JSON.stringify({ error: { code: e.error?.code ?? "internal", message: e.error?.message ?? "the terminal ended before it opened" } })));
|
|
206
|
-
};
|
|
296
|
+
let closeCode;
|
|
297
|
+
let wasOpen = false;
|
|
207
298
|
const onFrames = (chunk) => {
|
|
208
299
|
for (const f of reader.push(chunk)) {
|
|
209
300
|
if (f.opcode === 0x2) {
|
|
@@ -217,14 +308,18 @@ export function connectPty(opts) {
|
|
|
217
308
|
catch {
|
|
218
309
|
continue;
|
|
219
310
|
}
|
|
220
|
-
if (v["event"] === "open" && !
|
|
221
|
-
|
|
311
|
+
if (v["event"] === "open" && !wasOpen) {
|
|
312
|
+
wasOpen = true;
|
|
313
|
+
settled = true;
|
|
314
|
+
resolve({
|
|
315
|
+
socket,
|
|
222
316
|
pid: typeof v["req_id"] === "number" ? v["req_id"] : 0,
|
|
223
317
|
cols: Number(v["cols"] ?? 80),
|
|
224
318
|
rows: Number(v["rows"] ?? 24),
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
319
|
+
...(typeof v["resume"] === "string" ? { resume: v["resume"] } : {}),
|
|
320
|
+
...(v["reattached"] === true ? { reattached: true } : {}),
|
|
321
|
+
...(typeof v["input"] === "number" ? { input: v["input"] } : {}),
|
|
322
|
+
});
|
|
228
323
|
}
|
|
229
324
|
else if (v["event"] === "exit") {
|
|
230
325
|
exit = {
|
|
@@ -244,17 +339,158 @@ export function connectPty(opts) {
|
|
|
244
339
|
socket.write(encodeFrame(0xa, f.payload));
|
|
245
340
|
}
|
|
246
341
|
else if (f.opcode === 0x8) {
|
|
342
|
+
if (closeCode === undefined)
|
|
343
|
+
closeCode = f.payload.length >= 2 ? f.payload.readUInt16BE(0) : 1005;
|
|
247
344
|
if (!socket.destroyed)
|
|
248
345
|
socket.end(encodeFrame(0x8, f.payload.subarray(0, 2)));
|
|
249
346
|
}
|
|
250
347
|
}
|
|
251
348
|
};
|
|
252
349
|
socket.on("data", onFrames);
|
|
253
|
-
socket.on("close",
|
|
350
|
+
socket.on("close", () => {
|
|
351
|
+
if (!wasOpen) {
|
|
352
|
+
const restart = closeCode === PTY_RECONNECT_CODE && exit === undefined;
|
|
353
|
+
const e = restart ? PTY_RESTART_CLOSED_EXIT : (exit ?? CLOSED_EXIT);
|
|
354
|
+
fail(restart ? 503 : 502, JSON.stringify({ error: { code: e.error?.code ?? "internal", message: e.error?.message ?? "the terminal ended before it opened" } }), undefined, restart);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
resolveEnded({ code: closeCode, exit });
|
|
358
|
+
});
|
|
254
359
|
socket.on("error", () => { });
|
|
255
360
|
if (head.length)
|
|
256
361
|
onFrames(head);
|
|
257
362
|
});
|
|
258
363
|
req.end();
|
|
259
364
|
});
|
|
365
|
+
return { opened, ended };
|
|
366
|
+
}
|
|
367
|
+
function sizedUrl(url, size) {
|
|
368
|
+
const u = new URL(url);
|
|
369
|
+
u.searchParams.set("cols", String(Math.floor(size.cols)));
|
|
370
|
+
u.searchParams.set("rows", String(Math.floor(size.rows)));
|
|
371
|
+
return u;
|
|
372
|
+
}
|
|
373
|
+
function resumeUrl(url, size, received) {
|
|
374
|
+
const u = sizedUrl(url, size);
|
|
375
|
+
u.searchParams.set("received", String(received));
|
|
376
|
+
return u;
|
|
377
|
+
}
|
|
378
|
+
function worthRetrying(status) {
|
|
379
|
+
return status === 0 || status === 429 || status >= 500;
|
|
380
|
+
}
|
|
381
|
+
export function connectPty(opts) {
|
|
382
|
+
const reconnect = opts.reconnect ?? opts.command === undefined;
|
|
383
|
+
const resumeOn = opts.resume ?? true;
|
|
384
|
+
let received = 0;
|
|
385
|
+
const legOpts = {
|
|
386
|
+
...opts,
|
|
387
|
+
onData: (d) => {
|
|
388
|
+
received += d.length;
|
|
389
|
+
opts.onData?.(d);
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
let resolveDone;
|
|
393
|
+
const done = new Promise((r) => { resolveDone = r; });
|
|
394
|
+
let finished = false;
|
|
395
|
+
const finish = (e) => {
|
|
396
|
+
if (finished)
|
|
397
|
+
return;
|
|
398
|
+
finished = true;
|
|
399
|
+
opts.onExit?.(e);
|
|
400
|
+
resolveDone(e);
|
|
401
|
+
};
|
|
402
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
403
|
+
const follow = (session, leg) => {
|
|
404
|
+
void leg.ended.then(async ({ code, exit }) => {
|
|
405
|
+
if (exit !== undefined || session.hungUp) {
|
|
406
|
+
finish(exit ?? CLOSED_EXIT);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const restart = code === PTY_RECONNECT_CODE;
|
|
410
|
+
const dropped = code === undefined || code === 1006;
|
|
411
|
+
let resumable = resumeOn && session.resumeToken !== undefined && (restart || dropped);
|
|
412
|
+
if (!restart && !resumable) {
|
|
413
|
+
finish(CLOSED_EXIT);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (!resumable && !reconnect) {
|
|
417
|
+
finish(PTY_RESTART_CLOSED_EXIT);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
session.holdInput();
|
|
421
|
+
const t0 = Date.now();
|
|
422
|
+
for (let attempt = 0;; attempt++) {
|
|
423
|
+
const wait = Math.min(PTY_RECONNECT_FIRST_WAIT_MS * 2 ** attempt, PTY_RECONNECT_MAX_WAIT_MS) * (0.5 + Math.random() / 2);
|
|
424
|
+
if (Date.now() - t0 + wait > (opts.reconnectForMs ?? PTY_RECONNECT_FOR_MS))
|
|
425
|
+
break;
|
|
426
|
+
await sleep(wait);
|
|
427
|
+
if (session.hungUp) {
|
|
428
|
+
finish(CLOSED_EXIT);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const next = resumable
|
|
432
|
+
? openLeg(legOpts, resumeUrl(opts.url, session, received), session.resumeToken)
|
|
433
|
+
: openLeg(legOpts, sizedUrl(opts.url, session));
|
|
434
|
+
try {
|
|
435
|
+
const o = await next.opened;
|
|
436
|
+
if (session.hungUp) {
|
|
437
|
+
o.socket.destroy();
|
|
438
|
+
finish(CLOSED_EXIT);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (resumable) {
|
|
442
|
+
session.resumed(o.socket, o);
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
received = 0;
|
|
446
|
+
session.rebind(o.socket, o);
|
|
447
|
+
}
|
|
448
|
+
follow(session, next);
|
|
449
|
+
opts.onReconnected?.({ resumed: resumable });
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
catch (e) {
|
|
453
|
+
const refused = e instanceof LegRefused ? e : undefined;
|
|
454
|
+
if (resumable && refused?.status === 404) {
|
|
455
|
+
resumable = false;
|
|
456
|
+
if (!restart) {
|
|
457
|
+
finish(CLOSED_EXIT);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (!reconnect) {
|
|
461
|
+
finish(PTY_RESTART_CLOSED_EXIT);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (refused && !worthRetrying(refused.status)) {
|
|
467
|
+
const err = refused.error;
|
|
468
|
+
finish({ exitCode: null, signal: null, error: { code: typeof err.code === "string" ? err.code : "internal", message: err.message } });
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
finish(restart ? PTY_RESTART_EXIT : CLOSED_EXIT);
|
|
474
|
+
});
|
|
475
|
+
};
|
|
476
|
+
return (async () => {
|
|
477
|
+
const t0 = Date.now();
|
|
478
|
+
for (let attempt = 0;; attempt++) {
|
|
479
|
+
const first = openLeg(legOpts, new URL(opts.url));
|
|
480
|
+
try {
|
|
481
|
+
const o = await first.opened;
|
|
482
|
+
const session = new PtySession(o.socket, o, done);
|
|
483
|
+
follow(session, first);
|
|
484
|
+
return session;
|
|
485
|
+
}
|
|
486
|
+
catch (e) {
|
|
487
|
+
const wait = Math.min(PTY_RECONNECT_FIRST_WAIT_MS * 2 ** attempt, PTY_RECONNECT_MAX_WAIT_MS) * (0.5 + Math.random() / 2);
|
|
488
|
+
if (e instanceof LegRefused && e.restart && reconnect && Date.now() - t0 + wait <= (opts.reconnectForMs ?? PTY_RECONNECT_FOR_MS)) {
|
|
489
|
+
await sleep(wait);
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
throw e instanceof LegRefused ? e.error : e;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
})();
|
|
260
496
|
}
|
package/dist/sandbox.d.ts
CHANGED
|
@@ -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";
|
|
@@ -156,6 +174,20 @@ export interface SandboxCreateOpts extends SandboxOpts {
|
|
|
156
174
|
* nobody), then it rejects with `TimeoutError` ("aborted by the caller's signal").
|
|
157
175
|
*/
|
|
158
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;
|
|
159
191
|
}
|
|
160
192
|
/**
|
|
161
193
|
* Create-or-reuse the snapshot for an image digest and return its `snapshot_id`.
|
package/dist/sandbox.js
CHANGED
|
@@ -317,6 +317,9 @@ export class Sandbox {
|
|
|
317
317
|
...(opts.secrets?.length ? { secrets: opts.secrets } : {}),
|
|
318
318
|
...(opts.onData ? { onData: opts.onData } : {}),
|
|
319
319
|
...(opts.timeoutMs === undefined ? {} : { timeoutMs: opts.timeoutMs }),
|
|
320
|
+
...(opts.resume === undefined ? {} : { resume: opts.resume }),
|
|
321
|
+
reconnect: opts.reconnect ?? opts.cmd === undefined,
|
|
322
|
+
...(opts.onReconnected ? { onReconnected: opts.onReconnected } : {}),
|
|
320
323
|
onExit: (e) => { this.terminals.delete(pid); opts.onExit?.(e); },
|
|
321
324
|
});
|
|
322
325
|
pid = session.pid;
|
package/dist/ssh.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@promigence/sdk",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.8",
|
|
4
4
|
"description": "The fastest sandboxes for AI agents and code. The Promigence TypeScript SDK: snapshot an environment once, fork it as often as you need, run commands in every copy. A simple Sandbox class and the full PromigenceClient, with zero runtime dependencies.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://www.promigence.ai",
|