@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/cjs/pty.d.ts
CHANGED
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
*/
|
|
14
14
|
/** Largest terminal input frame sent; the server's cap is 1 MiB per client frame. */
|
|
15
15
|
export declare const PTY_INPUT_CHUNK: number;
|
|
16
|
+
/** How much of what was typed a session keeps, to send again to a terminal picked up again. */
|
|
17
|
+
export declare const PTY_INPUT_KEEP: number;
|
|
18
|
+
/** The header a terminal is picked up again with (its resume token). */
|
|
19
|
+
export declare const PTY_RESUME_HEADER = "promigence-resume";
|
|
16
20
|
export interface PtySize {
|
|
17
21
|
cols: number;
|
|
18
22
|
rows: number;
|
|
@@ -53,6 +57,33 @@ export interface PtyConnectOpts {
|
|
|
53
57
|
caller?: string;
|
|
54
58
|
onData?: (data: Uint8Array) => void;
|
|
55
59
|
onExit?: (exit: PtyExit) => void;
|
|
60
|
+
/**
|
|
61
|
+
* Reconnect in a new shell when the service is updated (a WebSocket `Close` 1012) and the
|
|
62
|
+
* terminal cannot be picked up again ([`resume`]). The session goes on over a new connection,
|
|
63
|
+
* with its callbacks, its size and what was typed meanwhile, in a NEW shell: what ran in the old
|
|
64
|
+
* one has ended. Tried for up to 30 s ([`PTY_RECONNECT_FOR_MS`]); past that the session ends with
|
|
65
|
+
* `error.code` `"service_restart"`.
|
|
66
|
+
*
|
|
67
|
+
* Default: on for a shell, off when `command` is set (reconnecting starts the command again,
|
|
68
|
+
* which may not be safe to run twice); `true` or `false` decides either way.
|
|
69
|
+
*/
|
|
70
|
+
reconnect?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Pick the same terminal up again when its connection drops or the service restarts (default
|
|
73
|
+
* on): the terminal keeps running for a short while, and the session picks up where it left off
|
|
74
|
+
* over a new connection, with what was typed meanwhile delivered once — the same shell, or the
|
|
75
|
+
* same command, still running. Where it cannot be picked up (it ended meanwhile, or too much time
|
|
76
|
+
* passed), a restart falls back to [`reconnect`]; a dropped connection ends the session, as
|
|
77
|
+
* before.
|
|
78
|
+
*/
|
|
79
|
+
resume?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Called after each reconnect: `resumed` is `true` when it is the same terminal picked up
|
|
82
|
+
* again, `false` when it is a new shell (what ran in the old one has ended).
|
|
83
|
+
*/
|
|
84
|
+
onReconnected?: (how: {
|
|
85
|
+
resumed: boolean;
|
|
86
|
+
}) => void;
|
|
56
87
|
/** Handshake bound (default 30 s). The session itself has none. */
|
|
57
88
|
timeoutMs?: number;
|
|
58
89
|
/** Maps a refused upgrade (`status`, body text, its `Retry-After`) onto the SDK's error classes. */
|
|
@@ -80,15 +111,28 @@ export declare class FrameReader {
|
|
|
80
111
|
*/
|
|
81
112
|
export declare class PtySession {
|
|
82
113
|
#private;
|
|
83
|
-
private
|
|
84
|
-
/**
|
|
114
|
+
private socket;
|
|
115
|
+
/**
|
|
116
|
+
* The id of the terminal's process inside the sandbox (`req_id`); what `pty.kill(pid)`
|
|
117
|
+
* addresses. It names this session: it stays the same after a reconnect.
|
|
118
|
+
*/
|
|
85
119
|
readonly pid: number;
|
|
86
120
|
cols: number;
|
|
87
121
|
rows: number;
|
|
88
122
|
private closed;
|
|
89
123
|
private readonly done;
|
|
124
|
+
/** Frames written while the session reconnects, sent once it has (at most 1 MiB). */
|
|
125
|
+
private pending;
|
|
126
|
+
/** Keystrokes written to a connection, in all, and the last of them, from `inputBase` on: what a
|
|
127
|
+
* terminal picked up again did not take is sent again. */
|
|
128
|
+
private inputSent;
|
|
129
|
+
private inputBase;
|
|
130
|
+
private inputLog;
|
|
90
131
|
/** `true` once the terminal has ended (its exit arrived, or the socket closed). */
|
|
91
132
|
get ended(): boolean;
|
|
133
|
+
private flush;
|
|
134
|
+
/** Write a frame to the connection now, counting keystrokes. */
|
|
135
|
+
private writeNow;
|
|
92
136
|
private send;
|
|
93
137
|
/** Keystrokes / a paste. A string is sent as UTF-8. */
|
|
94
138
|
write(data: string | Uint8Array): void;
|
|
@@ -105,5 +149,23 @@ export declare class PtySession {
|
|
|
105
149
|
*/
|
|
106
150
|
disconnect(): void;
|
|
107
151
|
}
|
|
108
|
-
/**
|
|
152
|
+
/** The `Close` code a terminal is asked to reconnect with: 1012, "service restart" (RFC 6455's registry). */
|
|
153
|
+
export declare const PTY_RECONNECT_CODE = 1012;
|
|
154
|
+
/** How long a terminal tries to reconnect before it ends (`error.code` `"service_restart"`). */
|
|
155
|
+
export declare const PTY_RECONNECT_FOR_MS = 30000;
|
|
156
|
+
/** The first wait before a reconnect, doubled each try up to [`PTY_RECONNECT_MAX_WAIT_MS`]; each wait is jittered (50-100 %). */
|
|
157
|
+
export declare const PTY_RECONNECT_FIRST_WAIT_MS = 250;
|
|
158
|
+
export declare const PTY_RECONNECT_MAX_WAIT_MS = 4000;
|
|
159
|
+
/** How a session that could not reconnect within [`PTY_RECONNECT_FOR_MS`] ends. */
|
|
160
|
+
export declare const PTY_RESTART_EXIT: PtyExit;
|
|
161
|
+
/** How a session that is not to reconnect (`reconnect` off, or a command's) ends at a 1012. */
|
|
162
|
+
export declare const PTY_RESTART_CLOSED_EXIT: PtyExit;
|
|
163
|
+
/**
|
|
164
|
+
* Open a terminal. Rejects with `opts.refuse(...)` if the server does not switch protocols.
|
|
165
|
+
*
|
|
166
|
+
* When its connection drops, or the service asks it to reconnect (`Close` 1012), the session goes
|
|
167
|
+
* on over a new connection: the same terminal picked up again where the service still holds it
|
|
168
|
+
* ([`PtyConnectOpts.resume`]), else, at a restart, a new shell ([`PtyConnectOpts.reconnect`]).
|
|
169
|
+
* `wait()` resolves only when the session really ends.
|
|
170
|
+
*/
|
|
109
171
|
export declare function connectPty(opts: PtyConnectOpts): Promise<PtySession>;
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
...(
|
|
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
|
-
|
|
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 = (
|
|
276
|
+
const fail = (status, body, retryAfter, restart = false) => {
|
|
277
|
+
if (settled)
|
|
278
|
+
return;
|
|
176
279
|
settled = true;
|
|
177
|
-
reject(
|
|
178
|
-
}
|
|
280
|
+
reject(new LegRefused(status, opts.refuse(status, body, retryAfter), restart));
|
|
281
|
+
};
|
|
179
282
|
const timer = setTimeout(() => {
|
|
180
283
|
req.destroy();
|
|
181
|
-
fail(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
202
|
-
let
|
|
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" && !
|
|
229
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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",
|
|
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
|
}
|
package/dist/cjs/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/cjs/sandbox.js
CHANGED
|
@@ -327,6 +327,9 @@ class Sandbox {
|
|
|
327
327
|
...(opts.secrets?.length ? { secrets: opts.secrets } : {}),
|
|
328
328
|
...(opts.onData ? { onData: opts.onData } : {}),
|
|
329
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 } : {}),
|
|
330
333
|
onExit: (e) => { this.terminals.delete(pid); opts.onExit?.(e); },
|
|
331
334
|
});
|
|
332
335
|
pid = session.pid;
|
package/dist/cjs/ssh.js
CHANGED
package/dist/client.d.ts
CHANGED
|
@@ -748,6 +748,8 @@ export interface CreditOffer {
|
|
|
748
748
|
state: string;
|
|
749
749
|
credit_usd: number;
|
|
750
750
|
card_bonus_usd: number;
|
|
751
|
+
/** USD charged to confirm Apple Pay, Google Pay or Amazon Pay when one is added (added to the credit). */
|
|
752
|
+
method_check_usd?: number;
|
|
751
753
|
expires_at?: string | null;
|
|
752
754
|
campaign?: string | null;
|
|
753
755
|
message: string;
|
|
@@ -906,6 +908,16 @@ export interface BillingUsage {
|
|
|
906
908
|
brand?: string;
|
|
907
909
|
last4?: string;
|
|
908
910
|
} | null;
|
|
911
|
+
/** Set when the card on file does not unlock paid limits for this organization (it already pays for another one,
|
|
912
|
+
* it is being confirmed with a $1 charge, or that charge did not go through). */
|
|
913
|
+
card_note?: string | null;
|
|
914
|
+
/** The $1 that confirms Apple Pay, Google Pay or Amazon Pay on file: `confirming`, `paid` or `failed`. */
|
|
915
|
+
method_check?: {
|
|
916
|
+
method: string;
|
|
917
|
+
status: string;
|
|
918
|
+
amount_usd: number;
|
|
919
|
+
bonus_usd?: number | null;
|
|
920
|
+
} | null;
|
|
909
921
|
suspended?: boolean;
|
|
910
922
|
suspend_reason?: string | null;
|
|
911
923
|
period?: {
|
|
@@ -929,6 +941,10 @@ export interface BillingUsage {
|
|
|
929
941
|
upcoming_invoice_error?: string | null;
|
|
930
942
|
[extra: string]: unknown;
|
|
931
943
|
}
|
|
944
|
+
/** One row of `GET /billing/history`: a credit the organization received, or a payment. */
|
|
945
|
+
export type BillingHistoryEntry = M.BillingHistoryEntry;
|
|
946
|
+
/** What `billing.historyPage()` lists: everything, only credits, or only payments. */
|
|
947
|
+
export type BillingHistoryKind = "all" | "credits" | "payments";
|
|
932
948
|
/** One metered row of `GET /me/ledger`. */
|
|
933
949
|
export interface LedgerEntry {
|
|
934
950
|
entry_id: string;
|
|
@@ -2033,6 +2049,16 @@ export declare class PromigenceClient {
|
|
|
2033
2049
|
limit?: number;
|
|
2034
2050
|
cursor?: string;
|
|
2035
2051
|
}) => Promise<Page<LedgerEntry>>;
|
|
2052
|
+
/**
|
|
2053
|
+
* `GET /billing/history` — the credits the organization received and its payments, newest
|
|
2054
|
+
* first (one page). `kind` narrows it to `credits` or `payments`; feed `nextCursor` back as
|
|
2055
|
+
* `cursor` until it is `null`. An owner's key.
|
|
2056
|
+
*/
|
|
2057
|
+
historyPage: (opts?: {
|
|
2058
|
+
kind?: BillingHistoryKind;
|
|
2059
|
+
limit?: number;
|
|
2060
|
+
cursor?: string;
|
|
2061
|
+
}) => Promise<Page<BillingHistoryEntry>>;
|
|
2036
2062
|
/** `POST /billing/checkout` — the hosted page that puts a card on file. */
|
|
2037
2063
|
checkout: () => Promise<{
|
|
2038
2064
|
url: string;
|
|
@@ -2433,6 +2459,18 @@ export declare class PromigenceClient {
|
|
|
2433
2459
|
onData?: (data: Uint8Array) => void;
|
|
2434
2460
|
onExit?: (exit: PtyExit) => void;
|
|
2435
2461
|
timeoutMs?: number;
|
|
2462
|
+
/** Pick the same terminal up again when its connection drops or the service is updated;
|
|
2463
|
+
* see `PtyConnectOpts.resume`. Default: on. */
|
|
2464
|
+
resume?: boolean;
|
|
2465
|
+
/** Reconnect in a new shell when the service is updated (a WebSocket `Close` 1012) and the
|
|
2466
|
+
* terminal cannot be picked up again; see `PtyConnectOpts.reconnect`. Default: on without
|
|
2467
|
+
* `cmd`, off with one. */
|
|
2468
|
+
reconnect?: boolean;
|
|
2469
|
+
/** Called after each reconnect: `resumed` says whether it is the same terminal or a new
|
|
2470
|
+
* shell. */
|
|
2471
|
+
onReconnected?: (how: {
|
|
2472
|
+
resumed: boolean;
|
|
2473
|
+
}) => void;
|
|
2436
2474
|
}) => Promise<PtySession>;
|
|
2437
2475
|
/** `GET /sandboxes/:id`. */
|
|
2438
2476
|
get: (sandboxId: string) => Promise<SandboxRecord>;
|