@hyperbrowser/sdk 0.93.0 → 1.0.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/README.md +190 -0
- package/dist/client.d.ts +2 -20
- package/dist/client.js +5 -17
- package/dist/error.d.ts +27 -0
- package/dist/error.js +32 -0
- package/dist/image-builds.d.ts +6 -0
- package/dist/image-builds.js +12 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +12 -1
- package/dist/retry.d.ts +14 -0
- package/dist/retry.js +49 -0
- package/dist/sandbox/base.d.ts +27 -0
- package/dist/sandbox/base.js +298 -99
- package/dist/sandbox/files.d.ts +24 -25
- package/dist/sandbox/files.js +146 -37
- package/dist/sandbox/image-build/artifacts.d.ts +16 -0
- package/dist/sandbox/image-build/artifacts.js +16 -0
- package/dist/sandbox/image-build/blake2b.d.ts +2 -0
- package/dist/sandbox/image-build/blake2b.js +89 -0
- package/dist/sandbox/image-build/common.d.ts +19 -0
- package/dist/sandbox/image-build/common.js +109 -0
- package/dist/sandbox/image-build/context.d.ts +43 -0
- package/dist/sandbox/image-build/context.js +599 -0
- package/dist/sandbox/image-build/docker-image.d.ts +34 -0
- package/dist/sandbox/image-build/docker-image.js +472 -0
- package/dist/sandbox/image-build/dockerfile-analysis.d.ts +17 -0
- package/dist/sandbox/image-build/dockerfile-analysis.js +409 -0
- package/dist/sandbox/image-build/dockerignore.d.ts +18 -0
- package/dist/sandbox/image-build/dockerignore.js +275 -0
- package/dist/sandbox/image-build/gzip.d.ts +11 -0
- package/dist/sandbox/image-build/gzip.js +91 -0
- package/dist/sandbox/image-build/image-init.d.ts +5 -0
- package/dist/sandbox/image-build/image-init.js +97 -0
- package/dist/sandbox/image-build/index.d.ts +13 -0
- package/dist/sandbox/image-build/index.js +36 -0
- package/dist/sandbox/image-build/local-docker.d.ts +11 -0
- package/dist/sandbox/image-build/local-docker.js +72 -0
- package/dist/sandbox/image-build/resolution.d.ts +27 -0
- package/dist/sandbox/image-build/resolution.js +137 -0
- package/dist/sandbox/image-build/tar.d.ts +41 -0
- package/dist/sandbox/image-build/tar.js +378 -0
- package/dist/sandbox/image-build/upload.d.ts +10 -0
- package/dist/sandbox/image-build/upload.js +143 -0
- package/dist/sandbox/index.d.ts +2 -1
- package/dist/sandbox/index.js +4 -1
- package/dist/sandbox/process-output.d.ts +36 -0
- package/dist/sandbox/process-output.js +117 -0
- package/dist/sandbox/process.d.ts +25 -1
- package/dist/sandbox/process.js +197 -29
- package/dist/sandbox/terminal.js +9 -5
- package/dist/sandbox/ws.d.ts +4 -1
- package/dist/sandbox/ws.js +33 -7
- package/dist/services/base.d.ts +2 -0
- package/dist/services/base.js +46 -30
- package/dist/services/normalize.d.ts +3 -0
- package/dist/services/normalize.js +21 -0
- package/dist/services/sandboxes.d.ts +40 -3
- package/dist/services/sandboxes.js +569 -53
- package/dist/services/volumes.js +29 -14
- package/dist/types/index.d.ts +2 -1
- package/dist/types/sandbox.d.ts +246 -30
- package/dist/types/volume.d.ts +7 -7
- package/package.json +24 -5
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/** Collection and validation of the receiver's command event stream. */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.validateOutputLimit = exports.ProcessOutput = exports.normalizeProcessResult = exports.DEFAULT_MAX_PROCESS_OUTPUT_BYTES = void 0;
|
|
5
|
+
const string_decoder_1 = require("string_decoder");
|
|
6
|
+
const error_1 = require("../error");
|
|
7
|
+
exports.DEFAULT_MAX_PROCESS_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
8
|
+
const BASE64_PATTERN = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
9
|
+
const normalizeProcessResult = (result) => ({
|
|
10
|
+
id: result.id,
|
|
11
|
+
status: result.status,
|
|
12
|
+
exitCode: result.exit_code !== undefined ? result.exit_code : result.exitCode,
|
|
13
|
+
stdout: result.stdout,
|
|
14
|
+
stderr: result.stderr,
|
|
15
|
+
startedAt: result.started_at ?? result.startedAt,
|
|
16
|
+
completedAt: result.completed_at !== undefined ? result.completed_at : result.completedAt,
|
|
17
|
+
error: result.error,
|
|
18
|
+
outputTruncated: result.output_truncated,
|
|
19
|
+
lastSeq: result.last_seq,
|
|
20
|
+
});
|
|
21
|
+
exports.normalizeProcessResult = normalizeProcessResult;
|
|
22
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23
|
+
class ProcessOutput {
|
|
24
|
+
constructor(processId, maxBytes) {
|
|
25
|
+
this.processId = processId;
|
|
26
|
+
this.maxBytes = maxBytes;
|
|
27
|
+
this.size = 0;
|
|
28
|
+
this.seq = 0;
|
|
29
|
+
this.events = [];
|
|
30
|
+
this.result = null;
|
|
31
|
+
this.error = null;
|
|
32
|
+
this.chunks = { stdout: [], stderr: [] };
|
|
33
|
+
this.decoders = {
|
|
34
|
+
stdout: new string_decoder_1.StringDecoder("utf8"),
|
|
35
|
+
stderr: new string_decoder_1.StringDecoder("utf8"),
|
|
36
|
+
system: new string_decoder_1.StringDecoder("utf8"),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
failure(message, code = "incomplete_output") {
|
|
40
|
+
return new error_1.HyperbrowserError(message, {
|
|
41
|
+
code,
|
|
42
|
+
service: "runtime",
|
|
43
|
+
retryable: false,
|
|
44
|
+
details: { process_id: this.processId, last_seq: this.seq },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
consume(event) {
|
|
48
|
+
if (event.event === "output") {
|
|
49
|
+
this.consumeOutput(event.data);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (event.event === "done") {
|
|
53
|
+
this.consumeDone(event.data);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (event.event === "error") {
|
|
57
|
+
const data = isRecord(event.data) ? event.data : {};
|
|
58
|
+
throw this.failure(String(data.error ?? "Command stream failed"), typeof data.code === "string" ? data.code : "incomplete_output");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
decodePayload(data) {
|
|
62
|
+
if (data.encoding === "base64") {
|
|
63
|
+
const text = data.data;
|
|
64
|
+
if (text.length % 4 !== 0 || !BASE64_PATTERN.test(text)) {
|
|
65
|
+
throw this.failure("Command output contains invalid base64 data");
|
|
66
|
+
}
|
|
67
|
+
return Buffer.from(text, "base64");
|
|
68
|
+
}
|
|
69
|
+
return Buffer.from(data.data, "utf8");
|
|
70
|
+
}
|
|
71
|
+
consumeOutput(payload) {
|
|
72
|
+
if (!isRecord(payload)) {
|
|
73
|
+
throw this.failure("Command output event is malformed");
|
|
74
|
+
}
|
|
75
|
+
const data = payload;
|
|
76
|
+
if (data.seq !== this.seq + 1) {
|
|
77
|
+
throw this.failure("Command output contains a sequence gap");
|
|
78
|
+
}
|
|
79
|
+
const stream = data.stream;
|
|
80
|
+
if (!Object.prototype.hasOwnProperty.call(this.decoders, stream)) {
|
|
81
|
+
throw this.failure("Unknown command output stream");
|
|
82
|
+
}
|
|
83
|
+
const raw = this.decodePayload(data);
|
|
84
|
+
this.size += raw.length;
|
|
85
|
+
if (this.size > this.maxBytes) {
|
|
86
|
+
throw this.failure("Command output exceeds maxOutputBytes; increase the collection limit or disconnect a detached process", "output_limit_exceeded");
|
|
87
|
+
}
|
|
88
|
+
this.seq = data.seq;
|
|
89
|
+
const text = this.decoders[stream].write(raw);
|
|
90
|
+
this.chunks[stream === "stdout" ? "stdout" : "stderr"].push(text);
|
|
91
|
+
this.events.push({ type: stream, seq: this.seq, data: text, timestamp: data.timestamp });
|
|
92
|
+
}
|
|
93
|
+
consumeDone(payload) {
|
|
94
|
+
if (!isRecord(payload)) {
|
|
95
|
+
throw this.failure("Command completion event is malformed");
|
|
96
|
+
}
|
|
97
|
+
const data = payload;
|
|
98
|
+
if (data.last_seq !== this.seq || data.output_truncated) {
|
|
99
|
+
throw this.failure("Receiver reported incomplete command output");
|
|
100
|
+
}
|
|
101
|
+
for (const stream of Object.keys(this.decoders)) {
|
|
102
|
+
this.chunks[stream === "stdout" ? "stdout" : "stderr"].push(this.decoders[stream].end());
|
|
103
|
+
}
|
|
104
|
+
this.result = (0, exports.normalizeProcessResult)({
|
|
105
|
+
...data,
|
|
106
|
+
stdout: this.chunks.stdout.join(""),
|
|
107
|
+
stderr: this.chunks.stderr.join(""),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
exports.ProcessOutput = ProcessOutput;
|
|
112
|
+
const validateOutputLimit = (value) => {
|
|
113
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
114
|
+
throw new error_1.HyperbrowserError("maxOutputBytes must be a positive integer");
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
exports.validateOutputLimit = validateOutputLimit;
|
|
@@ -1,25 +1,49 @@
|
|
|
1
|
-
import { RuntimeTransport } from "./base";
|
|
1
|
+
import { RuntimeSSEStream, RuntimeTransport } from "./base";
|
|
2
2
|
import { SandboxExecParams, SandboxExecOptions, SandboxProcessListParams, SandboxProcessListResponse, SandboxProcessResult, SandboxProcessSignal, SandboxProcessStdinParams, SandboxProcessStreamEvent, SandboxProcessSummary, SandboxProcessWaitParams } from "../types/sandbox";
|
|
3
3
|
export declare class SandboxProcessHandle {
|
|
4
4
|
private readonly transport;
|
|
5
5
|
private summary;
|
|
6
|
+
private output;
|
|
7
|
+
private collector;
|
|
8
|
+
private closeStream;
|
|
9
|
+
private readonly changed;
|
|
6
10
|
constructor(transport: RuntimeTransport, summary: SandboxProcessSummary);
|
|
11
|
+
/** @internal Collect output from the start-stream that created this process. */
|
|
12
|
+
attachCollector(stream: RuntimeSSEStream, maxOutputBytes: number): void;
|
|
13
|
+
private collect;
|
|
14
|
+
private collectedResult;
|
|
15
|
+
/** Stop collecting output; the detached command continues running. */
|
|
16
|
+
disconnect(): void;
|
|
7
17
|
get id(): string;
|
|
8
18
|
get status(): SandboxProcessSummary["status"];
|
|
9
19
|
toJSON(): SandboxProcessSummary;
|
|
10
20
|
refresh(): Promise<SandboxProcessHandle>;
|
|
21
|
+
/**
|
|
22
|
+
* Wait for completion. Processes started by this client resolve from the
|
|
23
|
+
* collected stream; a local timeout rejects without stopping collection.
|
|
24
|
+
*/
|
|
11
25
|
wait(params?: SandboxProcessWaitParams): Promise<SandboxProcessResult>;
|
|
12
26
|
signal(signal: SandboxProcessSignal): Promise<void>;
|
|
13
27
|
kill(params?: SandboxProcessWaitParams): Promise<SandboxProcessResult>;
|
|
14
28
|
writeStdin(input: string | Uint8Array | SandboxProcessStdinParams): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Replay and follow output. Processes started by this client stream from the
|
|
31
|
+
* collected events; other handles attach to the receiver's stream endpoint.
|
|
32
|
+
*/
|
|
15
33
|
stream(fromSeq?: number): AsyncGenerator<SandboxProcessStreamEvent>;
|
|
16
34
|
result(): Promise<SandboxProcessResult>;
|
|
17
35
|
}
|
|
18
36
|
export declare class SandboxProcessesApi {
|
|
19
37
|
private readonly transport;
|
|
20
38
|
constructor(transport: RuntimeTransport);
|
|
39
|
+
/** Run a command to completion, collecting its full output from process start. */
|
|
21
40
|
exec(command: string, options?: SandboxExecOptions): Promise<SandboxProcessResult>;
|
|
22
41
|
exec(input: SandboxExecParams): Promise<SandboxProcessResult>;
|
|
42
|
+
/**
|
|
43
|
+
* Start a process and collect its output in the background from the same
|
|
44
|
+
* streamed request, so output is complete even beyond the receiver's replay
|
|
45
|
+
* limit. Call `disconnect()` to stop collecting without killing the process.
|
|
46
|
+
*/
|
|
23
47
|
start(command: string, options?: SandboxExecOptions): Promise<SandboxProcessHandle>;
|
|
24
48
|
start(input: SandboxExecParams): Promise<SandboxProcessHandle>;
|
|
25
49
|
get(processId: string): Promise<SandboxProcessHandle>;
|
package/dist/sandbox/process.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SandboxProcessesApi = exports.SandboxProcessHandle = void 0;
|
|
4
|
+
const error_1 = require("../error");
|
|
5
|
+
const process_output_1 = require("./process-output");
|
|
4
6
|
const DEFAULT_PROCESS_KILL_WAIT_MS = 5000;
|
|
5
7
|
const SHELL_SAFE_TOKEN_PATTERN = /^[A-Za-z0-9_@%+=:,./-]+$/;
|
|
6
8
|
const normalizeProcessSummary = (process) => ({
|
|
@@ -10,19 +12,9 @@ const normalizeProcessSummary = (process) => ({
|
|
|
10
12
|
args: process.args,
|
|
11
13
|
cwd: process.cwd,
|
|
12
14
|
pid: process.pid,
|
|
13
|
-
exitCode: process.exit_code,
|
|
14
|
-
startedAt: process.started_at,
|
|
15
|
-
completedAt: process.completed_at,
|
|
16
|
-
});
|
|
17
|
-
const normalizeProcessResult = (result) => ({
|
|
18
|
-
id: result.id,
|
|
19
|
-
status: result.status,
|
|
20
|
-
exitCode: result.exit_code,
|
|
21
|
-
stdout: result.stdout,
|
|
22
|
-
stderr: result.stderr,
|
|
23
|
-
startedAt: result.started_at,
|
|
24
|
-
completedAt: result.completed_at,
|
|
25
|
-
error: result.error,
|
|
15
|
+
exitCode: process.exit_code !== undefined ? process.exit_code : process.exitCode,
|
|
16
|
+
startedAt: process.started_at ?? process.startedAt,
|
|
17
|
+
completedAt: process.completed_at !== undefined ? process.completed_at : process.completedAt,
|
|
26
18
|
});
|
|
27
19
|
const normalizeResultToSummary = (result) => ({
|
|
28
20
|
id: result.id,
|
|
@@ -46,7 +38,7 @@ const normalizeStreamEvent = (event) => {
|
|
|
46
38
|
if (event.event === "done") {
|
|
47
39
|
return {
|
|
48
40
|
type: "exit",
|
|
49
|
-
result: normalizeProcessResult(event.data),
|
|
41
|
+
result: (0, process_output_1.normalizeProcessResult)(event.data),
|
|
50
42
|
};
|
|
51
43
|
}
|
|
52
44
|
return null;
|
|
@@ -69,6 +61,44 @@ const normalizeLegacyProcessParams = (input) => ({
|
|
|
69
61
|
args: undefined,
|
|
70
62
|
useShell: undefined,
|
|
71
63
|
});
|
|
64
|
+
class ChangeSignal {
|
|
65
|
+
constructor() {
|
|
66
|
+
this.waiters = [];
|
|
67
|
+
}
|
|
68
|
+
notify() {
|
|
69
|
+
const waiters = this.waiters;
|
|
70
|
+
this.waiters = [];
|
|
71
|
+
waiters.forEach((resolve) => resolve());
|
|
72
|
+
}
|
|
73
|
+
/** Resolve true on the next notification, or false once `timeoutMs` elapses. */
|
|
74
|
+
wait(timeoutMs) {
|
|
75
|
+
return new Promise((resolve) => {
|
|
76
|
+
let timer;
|
|
77
|
+
const onChange = () => {
|
|
78
|
+
if (timer) {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
}
|
|
81
|
+
resolve(true);
|
|
82
|
+
};
|
|
83
|
+
this.waiters.push(onChange);
|
|
84
|
+
if (timeoutMs !== undefined) {
|
|
85
|
+
timer = setTimeout(() => {
|
|
86
|
+
this.waiters = this.waiters.filter((waiter) => waiter !== onChange);
|
|
87
|
+
resolve(false);
|
|
88
|
+
}, timeoutMs);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const localWaitTimeoutMs = (params) => {
|
|
94
|
+
if (params.timeoutSec !== undefined && params.timeoutSec > 0) {
|
|
95
|
+
return params.timeoutSec * 1000;
|
|
96
|
+
}
|
|
97
|
+
if (params.timeoutMs !== undefined && params.timeoutMs > 0) {
|
|
98
|
+
return params.timeoutMs;
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
};
|
|
72
102
|
const buildProcessPayload = (input) => ({
|
|
73
103
|
command: input.command,
|
|
74
104
|
cwd: input.cwd,
|
|
@@ -106,6 +136,79 @@ class SandboxProcessHandle {
|
|
|
106
136
|
constructor(transport, summary) {
|
|
107
137
|
this.transport = transport;
|
|
108
138
|
this.summary = summary;
|
|
139
|
+
this.output = null;
|
|
140
|
+
this.collector = null;
|
|
141
|
+
this.closeStream = null;
|
|
142
|
+
this.changed = new ChangeSignal();
|
|
143
|
+
}
|
|
144
|
+
/** @internal Collect output from the start-stream that created this process. */
|
|
145
|
+
attachCollector(stream, maxOutputBytes) {
|
|
146
|
+
this.output = new process_output_1.ProcessOutput(this.id, maxOutputBytes);
|
|
147
|
+
this.closeStream = stream.close;
|
|
148
|
+
this.collector = this.collect(stream.events);
|
|
149
|
+
}
|
|
150
|
+
async collect(events) {
|
|
151
|
+
const output = this.output;
|
|
152
|
+
try {
|
|
153
|
+
for await (const event of events) {
|
|
154
|
+
if (output.error !== null) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
output.consume(event);
|
|
158
|
+
this.changed.notify();
|
|
159
|
+
if (output.result !== null) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
output.error = output.failure("Command stream ended before its completion event");
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
if (output.error === null) {
|
|
167
|
+
output.error =
|
|
168
|
+
error instanceof error_1.HyperbrowserError
|
|
169
|
+
? error
|
|
170
|
+
: output.failure(error instanceof Error ? error.message : String(error));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
try {
|
|
175
|
+
await events.return(undefined);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
if (output.result === null && output.error === null) {
|
|
179
|
+
output.error = output.failure(error instanceof Error ? error.message : String(error));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
this.closeStream?.();
|
|
183
|
+
this.changed.notify();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
collectedResult() {
|
|
187
|
+
const output = this.output;
|
|
188
|
+
if (output.error !== null) {
|
|
189
|
+
throw output.error;
|
|
190
|
+
}
|
|
191
|
+
if (output.result === null) {
|
|
192
|
+
throw output.failure("Command stream ended before its completion event");
|
|
193
|
+
}
|
|
194
|
+
const result = output.result;
|
|
195
|
+
this.summary = {
|
|
196
|
+
...this.summary,
|
|
197
|
+
status: result.status,
|
|
198
|
+
exitCode: result.exitCode,
|
|
199
|
+
completedAt: result.completedAt,
|
|
200
|
+
};
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
/** Stop collecting output; the detached command continues running. */
|
|
204
|
+
disconnect() {
|
|
205
|
+
if (this.output !== null) {
|
|
206
|
+
if (this.output.result === null && this.output.error === null) {
|
|
207
|
+
this.output.error = this.output.failure("Command output collection disconnected");
|
|
208
|
+
}
|
|
209
|
+
this.changed.notify();
|
|
210
|
+
this.closeStream?.();
|
|
211
|
+
}
|
|
109
212
|
}
|
|
110
213
|
get id() {
|
|
111
214
|
return this.summary.id;
|
|
@@ -121,7 +224,28 @@ class SandboxProcessHandle {
|
|
|
121
224
|
this.summary = normalizeProcessSummary(response.process);
|
|
122
225
|
return this;
|
|
123
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Wait for completion. Processes started by this client resolve from the
|
|
229
|
+
* collected stream; a local timeout rejects without stopping collection.
|
|
230
|
+
*/
|
|
124
231
|
async wait(params = {}) {
|
|
232
|
+
if (this.output !== null && this.collector !== null) {
|
|
233
|
+
const output = this.output;
|
|
234
|
+
const timeoutMs = localWaitTimeoutMs(params);
|
|
235
|
+
const deadline = timeoutMs === undefined ? undefined : Date.now() + timeoutMs;
|
|
236
|
+
while (output.result === null && output.error === null) {
|
|
237
|
+
const remaining = deadline === undefined ? undefined : deadline - Date.now();
|
|
238
|
+
const ready = remaining !== undefined && remaining <= 0 ? false : await this.changed.wait(remaining);
|
|
239
|
+
if (!ready) {
|
|
240
|
+
throw new error_1.HyperbrowserError("Timed out waiting for command output", {
|
|
241
|
+
code: "wait_timeout",
|
|
242
|
+
service: "runtime",
|
|
243
|
+
retryable: false,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return this.collectedResult();
|
|
248
|
+
}
|
|
125
249
|
const response = await this.transport.requestJSON(`/sandbox/processes/${this.id}/wait`, {
|
|
126
250
|
method: "POST",
|
|
127
251
|
body: JSON.stringify({
|
|
@@ -132,7 +256,10 @@ class SandboxProcessHandle {
|
|
|
132
256
|
"content-type": "application/json",
|
|
133
257
|
},
|
|
134
258
|
});
|
|
135
|
-
const result = normalizeProcessResult(response.result);
|
|
259
|
+
const result = (0, process_output_1.normalizeProcessResult)(response.result);
|
|
260
|
+
if (result.outputTruncated) {
|
|
261
|
+
throw new process_output_1.ProcessOutput(this.id, 0).failure("Retained process output is incomplete; collect output from process start");
|
|
262
|
+
}
|
|
136
263
|
this.summary = {
|
|
137
264
|
...this.summary,
|
|
138
265
|
...normalizeResultToSummary(result),
|
|
@@ -186,7 +313,33 @@ class SandboxProcessHandle {
|
|
|
186
313
|
},
|
|
187
314
|
});
|
|
188
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* Replay and follow output. Processes started by this client stream from the
|
|
318
|
+
* collected events; other handles attach to the receiver's stream endpoint.
|
|
319
|
+
*/
|
|
189
320
|
async *stream(fromSeq) {
|
|
321
|
+
if (this.output !== null) {
|
|
322
|
+
const output = this.output;
|
|
323
|
+
let index = 0;
|
|
324
|
+
for (;;) {
|
|
325
|
+
const events = output.events.slice(index);
|
|
326
|
+
index += events.length;
|
|
327
|
+
const done = output.result !== null || output.error !== null;
|
|
328
|
+
if (events.length === 0 && !done) {
|
|
329
|
+
await this.changed.wait();
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
for (const event of events) {
|
|
333
|
+
if (fromSeq === undefined || event.seq >= fromSeq) {
|
|
334
|
+
yield event;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (done) {
|
|
338
|
+
yield { type: "exit", result: this.collectedResult() };
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
190
343
|
const params = fromSeq && fromSeq > 0
|
|
191
344
|
? {
|
|
192
345
|
from_seq: fromSeq,
|
|
@@ -209,26 +362,41 @@ class SandboxProcessesApi {
|
|
|
209
362
|
this.transport = transport;
|
|
210
363
|
}
|
|
211
364
|
async exec(input, options) {
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
});
|
|
220
|
-
return normalizeProcessResult(response.result);
|
|
365
|
+
const handle = typeof input === "string" ? await this.start(input, options) : await this.start(input);
|
|
366
|
+
try {
|
|
367
|
+
return await handle.wait();
|
|
368
|
+
}
|
|
369
|
+
finally {
|
|
370
|
+
handle.disconnect();
|
|
371
|
+
}
|
|
221
372
|
}
|
|
222
373
|
async start(input, options) {
|
|
223
374
|
const params = normalizeExecParams(input, options);
|
|
224
|
-
const
|
|
375
|
+
const maxOutputBytes = params.maxOutputBytes ?? process_output_1.DEFAULT_MAX_PROCESS_OUTPUT_BYTES;
|
|
376
|
+
(0, process_output_1.validateOutputLimit)(maxOutputBytes);
|
|
377
|
+
const stream = await this.transport.openSSE("/sandbox/processes", undefined, {
|
|
225
378
|
method: "POST",
|
|
226
379
|
body: JSON.stringify(buildProcessPayload(params)),
|
|
227
|
-
|
|
228
|
-
"content-type": "application/json",
|
|
229
|
-
},
|
|
380
|
+
signal: params.signal,
|
|
230
381
|
});
|
|
231
|
-
|
|
382
|
+
let handle;
|
|
383
|
+
try {
|
|
384
|
+
const started = await stream.events.next();
|
|
385
|
+
if (started.done || started.value.event !== "started") {
|
|
386
|
+
throw new error_1.HyperbrowserError("Expected process start event", {
|
|
387
|
+
service: "runtime",
|
|
388
|
+
retryable: false,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
handle = new SandboxProcessHandle(this.transport, normalizeProcessSummary(started.value.data));
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
stream.close();
|
|
395
|
+
await stream.events.return(undefined).catch(() => undefined);
|
|
396
|
+
throw error;
|
|
397
|
+
}
|
|
398
|
+
handle.attachCollector(stream, maxOutputBytes);
|
|
399
|
+
return handle;
|
|
232
400
|
}
|
|
233
401
|
async get(processId) {
|
|
234
402
|
const response = await this.transport.requestJSON(`/sandbox/processes/${processId}`);
|
package/dist/sandbox/terminal.js
CHANGED
|
@@ -54,10 +54,12 @@ class SandboxTerminalConnection {
|
|
|
54
54
|
});
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
if (parsed.type === "exit") {
|
|
58
|
+
this.eventsQueue.push({
|
|
59
|
+
type: "exit",
|
|
60
|
+
status: normalizeTerminalStatus(parsed.status),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
61
63
|
}
|
|
62
64
|
catch (error) {
|
|
63
65
|
this.eventsQueue.fail(error);
|
|
@@ -195,7 +197,9 @@ class SandboxTerminalHandle {
|
|
|
195
197
|
headers.Host = target.hostHeader;
|
|
196
198
|
}
|
|
197
199
|
const ws = await (0, ws_2.openRuntimeWebSocket)(target, headers);
|
|
198
|
-
|
|
200
|
+
const connection = new SandboxTerminalConnection(ws);
|
|
201
|
+
ws.resume?.();
|
|
202
|
+
return connection;
|
|
199
203
|
}
|
|
200
204
|
}
|
|
201
205
|
exports.SandboxTerminalHandle = SandboxTerminalHandle;
|
package/dist/sandbox/ws.d.ts
CHANGED
|
@@ -15,4 +15,7 @@ export interface RuntimeTransportTarget {
|
|
|
15
15
|
}
|
|
16
16
|
export declare const resolveRuntimeTransportTarget: (baseUrl: string, path: string, runtimeProxyOverride?: string) => RuntimeTransportTarget;
|
|
17
17
|
export declare const toWebSocketUrl: (baseUrl: string, path: string, runtimeProxyOverride?: string) => RuntimeTransportTarget;
|
|
18
|
-
export declare const openRuntimeWebSocket: (target: RuntimeTransportTarget, headers: Record<string, string
|
|
18
|
+
export declare const openRuntimeWebSocket: (target: RuntimeTransportTarget, headers: Record<string, string>, options?: {
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
}) => Promise<WebSocket>;
|
package/dist/sandbox/ws.js
CHANGED
|
@@ -5,7 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.openRuntimeWebSocket = exports.toWebSocketUrl = exports.resolveRuntimeTransportTarget = exports.AsyncEventQueue = void 0;
|
|
7
7
|
const ws_1 = __importDefault(require("ws"));
|
|
8
|
-
const
|
|
8
|
+
const error_1 = require("../error");
|
|
9
9
|
const runtime_path_1 = require("./runtime-path");
|
|
10
10
|
class AsyncEventQueue {
|
|
11
11
|
constructor() {
|
|
@@ -157,11 +157,11 @@ const readIncomingMessageBody = (response) => new Promise((resolve, reject) => {
|
|
|
157
157
|
response.on("error", reject);
|
|
158
158
|
});
|
|
159
159
|
const normalizeWebSocketError = (error) => {
|
|
160
|
-
if (error instanceof
|
|
160
|
+
if (error instanceof error_1.HyperbrowserError) {
|
|
161
161
|
return error;
|
|
162
162
|
}
|
|
163
163
|
const networkError = error;
|
|
164
|
-
return new
|
|
164
|
+
return new error_1.HyperbrowserError(error instanceof Error ? error.message : "Unknown runtime websocket error", {
|
|
165
165
|
retryable: Boolean(networkError?.code && RETRYABLE_NETWORK_CODES.has(networkError.code)),
|
|
166
166
|
service: "runtime",
|
|
167
167
|
cause: error,
|
|
@@ -184,7 +184,7 @@ const buildHandshakeError = async (response) => {
|
|
|
184
184
|
message = rawText;
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
|
-
return new
|
|
187
|
+
return new error_1.HyperbrowserError(message, {
|
|
188
188
|
statusCode: response.statusCode,
|
|
189
189
|
code,
|
|
190
190
|
requestId: (typeof response.headers["x-request-id"] === "string"
|
|
@@ -198,25 +198,51 @@ const buildHandshakeError = async (response) => {
|
|
|
198
198
|
details,
|
|
199
199
|
});
|
|
200
200
|
};
|
|
201
|
-
const openRuntimeWebSocket = async (target, headers) => new Promise((resolve, reject) => {
|
|
201
|
+
const openRuntimeWebSocket = async (target, headers, options = {}) => new Promise((resolve, reject) => {
|
|
202
202
|
let settled = false;
|
|
203
|
+
let response;
|
|
204
|
+
if (options.signal?.aborted) {
|
|
205
|
+
reject(new error_1.HyperbrowserError("Runtime websocket request canceled", {
|
|
206
|
+
code: "request_aborted", service: "runtime", retryable: false,
|
|
207
|
+
}));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
203
210
|
const socket = new ws_1.default(target.url, { headers });
|
|
211
|
+
const cleanup = () => {
|
|
212
|
+
clearTimeout(timer);
|
|
213
|
+
options.signal?.removeEventListener("abort", abort);
|
|
214
|
+
};
|
|
204
215
|
const rejectOnce = (error) => {
|
|
205
216
|
if (settled) {
|
|
206
217
|
return;
|
|
207
218
|
}
|
|
208
219
|
settled = true;
|
|
220
|
+
cleanup();
|
|
221
|
+
response?.destroy();
|
|
222
|
+
socket.terminate();
|
|
209
223
|
reject(normalizeWebSocketError(error));
|
|
210
224
|
};
|
|
225
|
+
const abort = () => rejectOnce(new error_1.HyperbrowserError("Runtime websocket request canceled", {
|
|
226
|
+
code: "request_aborted", service: "runtime", retryable: false,
|
|
227
|
+
}));
|
|
228
|
+
const timer = setTimeout(() => rejectOnce(new error_1.HyperbrowserError("Runtime websocket handshake timed out", {
|
|
229
|
+
code: "request_timeout", service: "runtime", retryable: true,
|
|
230
|
+
})), options.timeoutMs ?? 30000);
|
|
231
|
+
timer.unref?.();
|
|
232
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
211
233
|
socket.once("open", () => {
|
|
212
234
|
if (settled) {
|
|
213
235
|
return;
|
|
214
236
|
}
|
|
215
237
|
settled = true;
|
|
238
|
+
cleanup();
|
|
239
|
+
// Frames can arrive with the upgrade response, before an awaiting caller resumes.
|
|
240
|
+
socket.pause();
|
|
216
241
|
resolve(socket);
|
|
217
242
|
});
|
|
218
|
-
socket.once("unexpected-response", (_request,
|
|
219
|
-
|
|
243
|
+
socket.once("unexpected-response", (_request, incoming) => {
|
|
244
|
+
response = incoming;
|
|
245
|
+
void buildHandshakeError(incoming).then(rejectOnce).catch(rejectOnce);
|
|
220
246
|
});
|
|
221
247
|
socket.once("error", rejectOnce);
|
|
222
248
|
});
|
package/dist/services/base.d.ts
CHANGED
|
@@ -4,5 +4,7 @@ export declare class BaseService {
|
|
|
4
4
|
protected readonly baseUrl: string;
|
|
5
5
|
protected readonly timeout: number;
|
|
6
6
|
constructor(apiKey: string, baseUrl: string, timeout?: number);
|
|
7
|
+
/** Transient GET failures (429/502/503/504, network errors) retry up to three attempts. */
|
|
7
8
|
protected request<T>(path: string, init?: RequestInit, params?: Record<string, string | number | string[] | undefined>, fullUrl?: boolean): Promise<T>;
|
|
9
|
+
private requestOnce;
|
|
8
10
|
}
|