@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,378 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal tar support: a PAX-format writer that mirrors CPython's `tarfile`
|
|
4
|
+
* output for the normalized entries used in build contexts, and a streaming
|
|
5
|
+
* reader for `docker image save` archives.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.PaxTarWriter = exports.createTarEntryHeader = void 0;
|
|
9
|
+
exports.readTarEntries = readTarEntries;
|
|
10
|
+
const BLOCK_SIZE = 512;
|
|
11
|
+
const RECORD_SIZE = BLOCK_SIZE * 20;
|
|
12
|
+
const POSIX_MAGIC = Buffer.from("ustar\x0000", "binary");
|
|
13
|
+
const NUL = Buffer.alloc(1);
|
|
14
|
+
const TYPE_FLAGS = {
|
|
15
|
+
file: "0",
|
|
16
|
+
directory: "5",
|
|
17
|
+
symlink: "2",
|
|
18
|
+
};
|
|
19
|
+
// Tar header validation intentionally includes NUL.
|
|
20
|
+
// eslint-disable-next-line no-control-regex
|
|
21
|
+
const isAscii = (value) => /^[\x00-\x7f]*$/.test(value);
|
|
22
|
+
/** Python `stn`: encode with ASCII "replace" errors, then NUL-pad or truncate. */
|
|
23
|
+
const stringField = (value, length) => {
|
|
24
|
+
// eslint-disable-next-line no-control-regex
|
|
25
|
+
const encoded = Buffer.from(value.replace(/[^\x00-\x7f]/g, "?"), "latin1");
|
|
26
|
+
const field = Buffer.alloc(length);
|
|
27
|
+
encoded.copy(field, 0, 0, Math.min(encoded.length, length));
|
|
28
|
+
return field;
|
|
29
|
+
};
|
|
30
|
+
/** Python `itn` for values that fit the octal field. */
|
|
31
|
+
const numberField = (value, digits) => {
|
|
32
|
+
if (value < 0 || value >= 8 ** (digits - 1)) {
|
|
33
|
+
throw new RangeError(`tar header value ${value} does not fit in ${digits} digits`);
|
|
34
|
+
}
|
|
35
|
+
return Buffer.concat([Buffer.from(value.toString(8).padStart(digits - 1, "0"), "ascii"), NUL]);
|
|
36
|
+
};
|
|
37
|
+
const createHeader = (fields) => {
|
|
38
|
+
const parts = [
|
|
39
|
+
stringField(fields.name, 100),
|
|
40
|
+
numberField((fields.mode ?? 0) & 0o7777, 8),
|
|
41
|
+
numberField(0, 8),
|
|
42
|
+
numberField(0, 8),
|
|
43
|
+
numberField(fields.size, 12),
|
|
44
|
+
numberField(0, 12),
|
|
45
|
+
Buffer.from(" ", "ascii"),
|
|
46
|
+
Buffer.from(fields.type, "ascii"),
|
|
47
|
+
stringField(fields.linkname ?? "", 100),
|
|
48
|
+
POSIX_MAGIC,
|
|
49
|
+
stringField("", 32),
|
|
50
|
+
stringField("", 32),
|
|
51
|
+
numberField(0, 8),
|
|
52
|
+
numberField(0, 8),
|
|
53
|
+
stringField("", 155),
|
|
54
|
+
];
|
|
55
|
+
const header = Buffer.alloc(BLOCK_SIZE);
|
|
56
|
+
Buffer.concat(parts).copy(header);
|
|
57
|
+
let checksum = 0;
|
|
58
|
+
for (const byte of header) {
|
|
59
|
+
checksum += byte;
|
|
60
|
+
}
|
|
61
|
+
Buffer.from(checksum.toString(8).padStart(6, "0") + "\0 ", "ascii").copy(header, 148);
|
|
62
|
+
return header;
|
|
63
|
+
};
|
|
64
|
+
const padToBlock = (size) => {
|
|
65
|
+
const remainder = size % BLOCK_SIZE;
|
|
66
|
+
return remainder === 0 ? Buffer.alloc(0) : Buffer.alloc(BLOCK_SIZE - remainder);
|
|
67
|
+
};
|
|
68
|
+
const createPaxHeader = (records) => {
|
|
69
|
+
const encodedRecords = [];
|
|
70
|
+
for (const [keyword, value] of records) {
|
|
71
|
+
const key = Buffer.from(keyword, "utf8");
|
|
72
|
+
const data = Buffer.from(value, "utf8");
|
|
73
|
+
const base = key.length + data.length + 3;
|
|
74
|
+
let n = 0;
|
|
75
|
+
let p = 0;
|
|
76
|
+
while (true) {
|
|
77
|
+
n = base + String(p).length;
|
|
78
|
+
if (n === p) {
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
p = n;
|
|
82
|
+
}
|
|
83
|
+
encodedRecords.push(Buffer.from(`${p} `, "ascii"), key, Buffer.from("=", "ascii"), data);
|
|
84
|
+
encodedRecords.push(Buffer.from("\n", "ascii"));
|
|
85
|
+
}
|
|
86
|
+
const payload = Buffer.concat(encodedRecords);
|
|
87
|
+
return Buffer.concat([
|
|
88
|
+
createHeader({ name: "././@PaxHeader", size: payload.length, type: "x" }),
|
|
89
|
+
payload,
|
|
90
|
+
padToBlock(payload.length),
|
|
91
|
+
]);
|
|
92
|
+
};
|
|
93
|
+
const createTarEntryHeader = (info) => {
|
|
94
|
+
const pax = [];
|
|
95
|
+
if (!isAscii(info.name) || info.name.length > 100) {
|
|
96
|
+
pax.push(["path", info.name]);
|
|
97
|
+
}
|
|
98
|
+
if (!isAscii(info.linkname) || info.linkname.length > 100) {
|
|
99
|
+
pax.push(["linkpath", info.linkname]);
|
|
100
|
+
}
|
|
101
|
+
let size = info.size;
|
|
102
|
+
if (size < 0 || size >= 8 ** 11) {
|
|
103
|
+
pax.push(["size", String(info.size)]);
|
|
104
|
+
size = 0;
|
|
105
|
+
}
|
|
106
|
+
const header = createHeader({
|
|
107
|
+
name: info.name,
|
|
108
|
+
mode: info.mode,
|
|
109
|
+
size,
|
|
110
|
+
type: TYPE_FLAGS[info.type],
|
|
111
|
+
linkname: info.linkname,
|
|
112
|
+
});
|
|
113
|
+
return pax.length > 0 ? Buffer.concat([createPaxHeader(pax), header]) : header;
|
|
114
|
+
};
|
|
115
|
+
exports.createTarEntryHeader = createTarEntryHeader;
|
|
116
|
+
/** Sequential PAX tar writer; `close` writes the end-of-archive blocks. */
|
|
117
|
+
class PaxTarWriter {
|
|
118
|
+
constructor(sink) {
|
|
119
|
+
this.sink = sink;
|
|
120
|
+
this.written = 0;
|
|
121
|
+
}
|
|
122
|
+
async emit(chunk) {
|
|
123
|
+
if (chunk.length === 0) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
this.written += chunk.length;
|
|
127
|
+
await this.sink(chunk);
|
|
128
|
+
}
|
|
129
|
+
async addEntry(info, content) {
|
|
130
|
+
await this.emit((0, exports.createTarEntryHeader)(info));
|
|
131
|
+
if (info.type !== "file") {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
let copied = 0;
|
|
135
|
+
if (content) {
|
|
136
|
+
for await (const chunk of content) {
|
|
137
|
+
copied += chunk.length;
|
|
138
|
+
if (copied > info.size) {
|
|
139
|
+
throw new Error(`tar entry "${info.name}" produced more data than its declared size`);
|
|
140
|
+
}
|
|
141
|
+
await this.emit(chunk);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (copied !== info.size) {
|
|
145
|
+
throw new Error(`tar entry "${info.name}" was truncated`);
|
|
146
|
+
}
|
|
147
|
+
await this.emit(padToBlock(info.size));
|
|
148
|
+
}
|
|
149
|
+
async close() {
|
|
150
|
+
await this.emit(Buffer.alloc(BLOCK_SIZE * 2));
|
|
151
|
+
const remainder = this.written % RECORD_SIZE;
|
|
152
|
+
if (remainder !== 0) {
|
|
153
|
+
await this.emit(Buffer.alloc(RECORD_SIZE - remainder));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
exports.PaxTarWriter = PaxTarWriter;
|
|
158
|
+
class ByteReader {
|
|
159
|
+
constructor(source) {
|
|
160
|
+
this.pending = Buffer.alloc(0);
|
|
161
|
+
this.done = false;
|
|
162
|
+
const raw = source[Symbol.asyncIterator]();
|
|
163
|
+
this.iterator = {
|
|
164
|
+
next: async () => {
|
|
165
|
+
const result = await raw.next();
|
|
166
|
+
return result.done
|
|
167
|
+
? { done: true, value: undefined }
|
|
168
|
+
: {
|
|
169
|
+
done: false,
|
|
170
|
+
value: Buffer.isBuffer(result.value) ? result.value : Buffer.from(result.value),
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
async fill() {
|
|
176
|
+
if (this.done) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
const result = await this.iterator.next();
|
|
180
|
+
if (result.done) {
|
|
181
|
+
this.done = true;
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
this.pending = this.pending.length ? Buffer.concat([this.pending, result.value]) : result.value;
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
/** Read exactly `length` bytes, or return null at a clean end of stream. */
|
|
188
|
+
async readExact(length) {
|
|
189
|
+
while (this.pending.length < length) {
|
|
190
|
+
if (!(await this.fill())) {
|
|
191
|
+
if (this.pending.length === 0) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
throw new Error("unexpected end of tar stream");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const chunk = this.pending.subarray(0, length);
|
|
198
|
+
this.pending = this.pending.subarray(length);
|
|
199
|
+
return chunk;
|
|
200
|
+
}
|
|
201
|
+
async *readChunks(length) {
|
|
202
|
+
let remaining = length;
|
|
203
|
+
while (remaining > 0) {
|
|
204
|
+
if (this.pending.length === 0 && !(await this.fill())) {
|
|
205
|
+
throw new Error("unexpected end of tar stream");
|
|
206
|
+
}
|
|
207
|
+
const take = Math.min(remaining, this.pending.length);
|
|
208
|
+
const chunk = this.pending.subarray(0, take);
|
|
209
|
+
this.pending = this.pending.subarray(take);
|
|
210
|
+
remaining -= take;
|
|
211
|
+
yield chunk;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async skip(length) {
|
|
215
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
216
|
+
for await (const _chunk of this.readChunks(length)) {
|
|
217
|
+
// discard
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const parseOctal = (field) => {
|
|
222
|
+
if (field.length > 0 && field[0] & 0x80) {
|
|
223
|
+
let value = 0n;
|
|
224
|
+
for (let index = 0; index < field.length; index += 1) {
|
|
225
|
+
value = (value << 8n) | BigInt(index === 0 ? field[0] & 0x7f : field[index]);
|
|
226
|
+
}
|
|
227
|
+
return Number(value);
|
|
228
|
+
}
|
|
229
|
+
const text = field.toString("ascii").replace(/\0.*$/s, "").trim();
|
|
230
|
+
if (text === "") {
|
|
231
|
+
return 0;
|
|
232
|
+
}
|
|
233
|
+
if (!/^[0-7]+$/.test(text)) {
|
|
234
|
+
throw new Error("invalid tar header number field");
|
|
235
|
+
}
|
|
236
|
+
return parseInt(text, 8);
|
|
237
|
+
};
|
|
238
|
+
const parseString = (field) => {
|
|
239
|
+
const end = field.indexOf(0);
|
|
240
|
+
return (end === -1 ? field : field.subarray(0, end)).toString("utf8");
|
|
241
|
+
};
|
|
242
|
+
const parsePaxRecords = (payload) => {
|
|
243
|
+
const records = new Map();
|
|
244
|
+
let offset = 0;
|
|
245
|
+
while (offset < payload.length) {
|
|
246
|
+
const space = payload.indexOf(0x20, offset);
|
|
247
|
+
if (space === -1) {
|
|
248
|
+
throw new Error("invalid PAX header record");
|
|
249
|
+
}
|
|
250
|
+
const length = parseInt(payload.subarray(offset, space).toString("ascii"), 10);
|
|
251
|
+
if (!Number.isInteger(length) || length <= 0 || offset + length > payload.length) {
|
|
252
|
+
throw new Error("invalid PAX header record");
|
|
253
|
+
}
|
|
254
|
+
const record = payload.subarray(space + 1, offset + length - 1).toString("utf8");
|
|
255
|
+
const separator = record.indexOf("=");
|
|
256
|
+
if (separator === -1) {
|
|
257
|
+
throw new Error("invalid PAX header record");
|
|
258
|
+
}
|
|
259
|
+
records.set(record.slice(0, separator), record.slice(separator + 1));
|
|
260
|
+
offset += length;
|
|
261
|
+
}
|
|
262
|
+
return records;
|
|
263
|
+
};
|
|
264
|
+
/**
|
|
265
|
+
* Iterate over tar entries from a stream. Callers must fully consume an
|
|
266
|
+
* entry's content before advancing; unread content is skipped.
|
|
267
|
+
*/
|
|
268
|
+
async function* readTarEntries(source) {
|
|
269
|
+
const reader = new ByteReader(source);
|
|
270
|
+
const globalPax = new Map();
|
|
271
|
+
let paxOverrides = null;
|
|
272
|
+
let gnuLongName = null;
|
|
273
|
+
let gnuLongLink = null;
|
|
274
|
+
while (true) {
|
|
275
|
+
const header = await reader.readExact(BLOCK_SIZE);
|
|
276
|
+
if (header === null || header.every((byte) => byte === 0)) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const typeflag = header.subarray(156, 157).toString("ascii");
|
|
280
|
+
const size = parseOctal(header.subarray(124, 136));
|
|
281
|
+
if (!Number.isSafeInteger(size) || size < 0)
|
|
282
|
+
throw new Error("invalid tar entry size");
|
|
283
|
+
if (["x", "g", "L", "K"].includes(typeflag) && size > 16 * 1024 * 1024)
|
|
284
|
+
throw new Error("tar metadata exceeds the size limit");
|
|
285
|
+
const padded = size + (BLOCK_SIZE - (size % BLOCK_SIZE || BLOCK_SIZE));
|
|
286
|
+
if (typeflag === "x" || typeflag === "g") {
|
|
287
|
+
const payload = await reader.readExact(padded);
|
|
288
|
+
if (payload === null) {
|
|
289
|
+
throw new Error("unexpected end of tar stream");
|
|
290
|
+
}
|
|
291
|
+
if (typeflag === "x") {
|
|
292
|
+
paxOverrides = parsePaxRecords(payload.subarray(0, size));
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
for (const [key, value] of parsePaxRecords(payload.subarray(0, size))) {
|
|
296
|
+
if (value)
|
|
297
|
+
globalPax.set(key, value);
|
|
298
|
+
else
|
|
299
|
+
globalPax.delete(key);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (typeflag === "L" || typeflag === "K") {
|
|
305
|
+
const payload = await reader.readExact(padded);
|
|
306
|
+
if (payload === null) {
|
|
307
|
+
throw new Error("unexpected end of tar stream");
|
|
308
|
+
}
|
|
309
|
+
const value = parseString(payload.subarray(0, size));
|
|
310
|
+
if (typeflag === "L") {
|
|
311
|
+
gnuLongName = value;
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
gnuLongLink = value;
|
|
315
|
+
}
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
let name = parseString(header.subarray(0, 100));
|
|
319
|
+
const magic = header.subarray(257, 263).toString("binary");
|
|
320
|
+
const prefix = parseString(header.subarray(345, 500));
|
|
321
|
+
if (magic.startsWith("ustar") && prefix && typeflag !== "L") {
|
|
322
|
+
name = `${prefix}/${name}`;
|
|
323
|
+
}
|
|
324
|
+
if (gnuLongName !== null) {
|
|
325
|
+
name = gnuLongName;
|
|
326
|
+
}
|
|
327
|
+
let linkname = parseString(header.subarray(157, 257));
|
|
328
|
+
if (gnuLongLink !== null) {
|
|
329
|
+
linkname = gnuLongLink;
|
|
330
|
+
}
|
|
331
|
+
const mode = parseOctal(header.subarray(100, 108)) & 0o7777;
|
|
332
|
+
let entrySize = size;
|
|
333
|
+
paxOverrides = new Map([...globalPax, ...(paxOverrides ?? [])]);
|
|
334
|
+
if (paxOverrides) {
|
|
335
|
+
const path = paxOverrides.get("path");
|
|
336
|
+
if (path !== undefined) {
|
|
337
|
+
name = path;
|
|
338
|
+
}
|
|
339
|
+
const linkpath = paxOverrides.get("linkpath");
|
|
340
|
+
if (linkpath !== undefined) {
|
|
341
|
+
linkname = linkpath;
|
|
342
|
+
}
|
|
343
|
+
const paxSize = paxOverrides.get("size");
|
|
344
|
+
if (paxSize !== undefined) {
|
|
345
|
+
entrySize = Number(paxSize);
|
|
346
|
+
if (!Number.isSafeInteger(entrySize) || entrySize < 0) {
|
|
347
|
+
throw new Error("invalid PAX size record");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
paxOverrides = null;
|
|
352
|
+
gnuLongName = null;
|
|
353
|
+
gnuLongLink = null;
|
|
354
|
+
let remaining = entrySize;
|
|
355
|
+
const content = async function* () {
|
|
356
|
+
const total = remaining;
|
|
357
|
+
remaining = 0;
|
|
358
|
+
yield* reader.readChunks(total);
|
|
359
|
+
};
|
|
360
|
+
yield {
|
|
361
|
+
name,
|
|
362
|
+
size: entrySize,
|
|
363
|
+
typeflag,
|
|
364
|
+
mode,
|
|
365
|
+
linkname,
|
|
366
|
+
isFile: typeflag === "0" || typeflag === "\0" || typeflag === "7",
|
|
367
|
+
content,
|
|
368
|
+
};
|
|
369
|
+
if (remaining > 0) {
|
|
370
|
+
await reader.skip(remaining);
|
|
371
|
+
remaining = 0;
|
|
372
|
+
}
|
|
373
|
+
const padding = entrySize % BLOCK_SIZE === 0 ? 0 : BLOCK_SIZE - (entrySize % BLOCK_SIZE);
|
|
374
|
+
if (padding > 0) {
|
|
375
|
+
await reader.skip(padding);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { SandboxImageBuildUpload } from "../../types/sandbox";
|
|
2
|
+
import { DockerImageBuildArtifact } from "./artifacts";
|
|
3
|
+
export declare const uploadImageBuildArtifact: (upload: SandboxImageBuildUpload, artifactPath: string, options?: {
|
|
4
|
+
timeout?: number | null;
|
|
5
|
+
}) => Promise<void>;
|
|
6
|
+
/** Upload only the artifacts the server requested, verifying each request. */
|
|
7
|
+
export declare const uploadMissingImageBuildArtifacts: (uploads: SandboxImageBuildUpload[] | undefined, artifacts: Record<string, DockerImageBuildArtifact>, options: {
|
|
8
|
+
label: string;
|
|
9
|
+
timeout?: number | null;
|
|
10
|
+
}) => Promise<void>;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.uploadMissingImageBuildArtifacts = exports.uploadImageBuildArtifact = void 0;
|
|
7
|
+
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
8
|
+
const fs_1 = require("fs");
|
|
9
|
+
const stream_1 = require("stream");
|
|
10
|
+
const common_1 = require("./common");
|
|
11
|
+
class UploadStatusError extends Error {
|
|
12
|
+
constructor(statusCode, body) {
|
|
13
|
+
super(`image artifact upload failed: ${statusCode}: ${body}`.trimEnd());
|
|
14
|
+
this.statusCode = statusCode;
|
|
15
|
+
this.name = "UploadStatusError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const uploadOnce = async (upload, artifactPath, method, timeoutSeconds) => {
|
|
19
|
+
const size = (0, fs_1.statSync)(artifactPath).size;
|
|
20
|
+
const headers = { ...(upload.headers || {}) };
|
|
21
|
+
if (!Object.keys(headers).some((key) => key.toLowerCase() === "content-length")) {
|
|
22
|
+
headers["content-length"] = String(size);
|
|
23
|
+
}
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
let timer;
|
|
26
|
+
const resetTimer = () => {
|
|
27
|
+
if (timeoutSeconds === null || timeoutSeconds === undefined) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (timer) {
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
}
|
|
33
|
+
timer = setTimeout(() => controller.abort(), timeoutSeconds * 1000);
|
|
34
|
+
};
|
|
35
|
+
const source = (0, fs_1.createReadStream)(artifactPath);
|
|
36
|
+
const body = new stream_1.PassThrough();
|
|
37
|
+
source.on("data", () => resetTimer());
|
|
38
|
+
source.on("error", (error) => body.destroy(error));
|
|
39
|
+
source.pipe(body);
|
|
40
|
+
resetTimer();
|
|
41
|
+
try {
|
|
42
|
+
const response = await (0, node_fetch_1.default)(upload.url, {
|
|
43
|
+
method,
|
|
44
|
+
headers,
|
|
45
|
+
body: size === 0 ? undefined : body,
|
|
46
|
+
signal: controller.signal,
|
|
47
|
+
});
|
|
48
|
+
resetTimer();
|
|
49
|
+
const text = await response.text();
|
|
50
|
+
if (response.ok) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
throw new UploadStatusError(response.status, text.trim());
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
57
|
+
throw new Error(`image artifact upload timed out after ${timeoutSeconds}s of inactivity`);
|
|
58
|
+
}
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
if (timer) {
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
}
|
|
65
|
+
source.destroy();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const uploadImageBuildArtifact = async (upload, artifactPath, options = {}) => {
|
|
69
|
+
const artifactSize = (0, fs_1.statSync)(artifactPath).size;
|
|
70
|
+
if (upload.maxUploadBytes > 0 && artifactSize > upload.maxUploadBytes) {
|
|
71
|
+
throw new Error(`image artifact exceeds the server upload limit (${artifactSize} > ${upload.maxUploadBytes})`);
|
|
72
|
+
}
|
|
73
|
+
const method = (upload.method || "PUT").trim().toUpperCase();
|
|
74
|
+
const attempts = method === "PUT" ? 3 : 1;
|
|
75
|
+
let lastError;
|
|
76
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
77
|
+
try {
|
|
78
|
+
await uploadOnce(upload, artifactPath, method, options.timeout);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
lastError = error;
|
|
83
|
+
if (error instanceof UploadStatusError) {
|
|
84
|
+
if (error.statusCode !== 408 && error.statusCode !== 429 && error.statusCode < 500) {
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (attempt < attempts) {
|
|
90
|
+
await (0, common_1.sleep)(attempt * 0.25);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
throw lastError;
|
|
94
|
+
};
|
|
95
|
+
exports.uploadImageBuildArtifact = uploadImageBuildArtifact;
|
|
96
|
+
/** Upload only the artifacts the server requested, verifying each request. */
|
|
97
|
+
const uploadMissingImageBuildArtifacts = async (uploads, artifacts, options) => {
|
|
98
|
+
const requested = [];
|
|
99
|
+
const seen = new Set();
|
|
100
|
+
for (const upload of uploads ?? []) {
|
|
101
|
+
const digest = (upload.sha256 || "").trim().toLowerCase();
|
|
102
|
+
const artifact = artifacts[digest];
|
|
103
|
+
if (!digest || artifact === undefined) {
|
|
104
|
+
throw new Error(`server requested unknown ${options.label} "${upload.sha256}"`);
|
|
105
|
+
}
|
|
106
|
+
if (seen.has(digest)) {
|
|
107
|
+
throw new Error(`server requested duplicate ${options.label} ${digest}`);
|
|
108
|
+
}
|
|
109
|
+
seen.add(digest);
|
|
110
|
+
const method = (upload.method || "PUT").trim().toUpperCase();
|
|
111
|
+
if (method !== "PUT") {
|
|
112
|
+
throw new Error(`server requested unsupported ${options.label} upload method "${upload.method}"`);
|
|
113
|
+
}
|
|
114
|
+
if (upload.maxUploadBytes > 0 && artifact.sizeBytes > upload.maxUploadBytes) {
|
|
115
|
+
throw new Error(`${options.label} ${digest} exceeds the server upload limit ` +
|
|
116
|
+
`(${artifact.sizeBytes} > ${upload.maxUploadBytes})`);
|
|
117
|
+
}
|
|
118
|
+
requested.push([upload, artifact, digest]);
|
|
119
|
+
}
|
|
120
|
+
if (requested.length === 0) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
let next = 0;
|
|
124
|
+
let failed = false;
|
|
125
|
+
const worker = async () => {
|
|
126
|
+
while (!failed && next < requested.length) {
|
|
127
|
+
const [upload, artifact, digest] = requested[next];
|
|
128
|
+
next += 1;
|
|
129
|
+
try {
|
|
130
|
+
await (0, exports.uploadImageBuildArtifact)(upload, artifact.path, { timeout: options.timeout });
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
failed = true;
|
|
134
|
+
throw new Error(`upload ${options.label} ${digest}: ${error instanceof Error ? error.message : error}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
const results = await Promise.allSettled(Array.from({ length: Math.min(4, requested.length) }, () => worker()));
|
|
139
|
+
const failure = results.find((result) => result.status === "rejected");
|
|
140
|
+
if (failure?.status === "rejected")
|
|
141
|
+
throw failure.reason;
|
|
142
|
+
};
|
|
143
|
+
exports.uploadMissingImageBuildArtifacts = uploadMissingImageBuildArtifacts;
|
package/dist/sandbox/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { RuntimeTransport } from "./base";
|
|
2
2
|
export { SandboxProcessesApi, SandboxProcessHandle } from "./process";
|
|
3
|
-
export { SandboxFilesApi, SandboxWatchDirHandle } from "./files";
|
|
3
|
+
export { SandboxFilesApi, SandboxFileWatchHandle, SandboxWatchDirHandle } from "./files";
|
|
4
4
|
export { SandboxTerminalApi, SandboxTerminalConnection, SandboxTerminalHandle } from "./terminal";
|
|
5
|
+
export { SandboxHandle } from "../services/sandboxes";
|
package/dist/sandbox/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SandboxTerminalHandle = exports.SandboxTerminalConnection = exports.SandboxTerminalApi = exports.SandboxWatchDirHandle = exports.SandboxFilesApi = exports.SandboxProcessHandle = exports.SandboxProcessesApi = exports.RuntimeTransport = void 0;
|
|
3
|
+
exports.SandboxHandle = exports.SandboxTerminalHandle = exports.SandboxTerminalConnection = exports.SandboxTerminalApi = exports.SandboxWatchDirHandle = exports.SandboxFileWatchHandle = exports.SandboxFilesApi = exports.SandboxProcessHandle = exports.SandboxProcessesApi = exports.RuntimeTransport = void 0;
|
|
4
4
|
var base_1 = require("./base");
|
|
5
5
|
Object.defineProperty(exports, "RuntimeTransport", { enumerable: true, get: function () { return base_1.RuntimeTransport; } });
|
|
6
6
|
var process_1 = require("./process");
|
|
@@ -8,8 +8,11 @@ Object.defineProperty(exports, "SandboxProcessesApi", { enumerable: true, get: f
|
|
|
8
8
|
Object.defineProperty(exports, "SandboxProcessHandle", { enumerable: true, get: function () { return process_1.SandboxProcessHandle; } });
|
|
9
9
|
var files_1 = require("./files");
|
|
10
10
|
Object.defineProperty(exports, "SandboxFilesApi", { enumerable: true, get: function () { return files_1.SandboxFilesApi; } });
|
|
11
|
+
Object.defineProperty(exports, "SandboxFileWatchHandle", { enumerable: true, get: function () { return files_1.SandboxFileWatchHandle; } });
|
|
11
12
|
Object.defineProperty(exports, "SandboxWatchDirHandle", { enumerable: true, get: function () { return files_1.SandboxWatchDirHandle; } });
|
|
12
13
|
var terminal_1 = require("./terminal");
|
|
13
14
|
Object.defineProperty(exports, "SandboxTerminalApi", { enumerable: true, get: function () { return terminal_1.SandboxTerminalApi; } });
|
|
14
15
|
Object.defineProperty(exports, "SandboxTerminalConnection", { enumerable: true, get: function () { return terminal_1.SandboxTerminalConnection; } });
|
|
15
16
|
Object.defineProperty(exports, "SandboxTerminalHandle", { enumerable: true, get: function () { return terminal_1.SandboxTerminalHandle; } });
|
|
17
|
+
var sandboxes_1 = require("../services/sandboxes");
|
|
18
|
+
Object.defineProperty(exports, "SandboxHandle", { enumerable: true, get: function () { return sandboxes_1.SandboxHandle; } });
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Collection and validation of the receiver's command event stream. */
|
|
2
|
+
import { HyperbrowserError } from "../error";
|
|
3
|
+
import { SandboxProcessOutputEvent, SandboxProcessResult } from "../types/sandbox";
|
|
4
|
+
import { RuntimeSSEEvent } from "./base";
|
|
5
|
+
export declare const DEFAULT_MAX_PROCESS_OUTPUT_BYTES: number;
|
|
6
|
+
export interface RawProcessResult extends Partial<SandboxProcessResult> {
|
|
7
|
+
id: string;
|
|
8
|
+
status: SandboxProcessResult["status"];
|
|
9
|
+
exit_code?: number | null;
|
|
10
|
+
stdout: string;
|
|
11
|
+
stderr: string;
|
|
12
|
+
started_at: number;
|
|
13
|
+
completed_at?: number | null;
|
|
14
|
+
error?: string | null;
|
|
15
|
+
output_truncated?: boolean;
|
|
16
|
+
last_seq?: number | null;
|
|
17
|
+
}
|
|
18
|
+
export declare const normalizeProcessResult: (result: RawProcessResult) => SandboxProcessResult;
|
|
19
|
+
export declare class ProcessOutput {
|
|
20
|
+
readonly processId: string;
|
|
21
|
+
readonly maxBytes: number;
|
|
22
|
+
size: number;
|
|
23
|
+
seq: number;
|
|
24
|
+
readonly events: SandboxProcessOutputEvent[];
|
|
25
|
+
result: SandboxProcessResult | null;
|
|
26
|
+
error: HyperbrowserError | null;
|
|
27
|
+
private readonly chunks;
|
|
28
|
+
private readonly decoders;
|
|
29
|
+
constructor(processId: string, maxBytes: number);
|
|
30
|
+
failure(message: string, code?: string): HyperbrowserError;
|
|
31
|
+
consume(event: RuntimeSSEEvent): void;
|
|
32
|
+
private decodePayload;
|
|
33
|
+
private consumeOutput;
|
|
34
|
+
private consumeDone;
|
|
35
|
+
}
|
|
36
|
+
export declare const validateOutputLimit: (value: unknown) => void;
|