@cloudflare/sandbox 0.13.0-next.769.1 → 1.0.0-rc.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.
Files changed (63) hide show
  1. package/README.md +62 -219
  2. package/dist/index.d.mts +548 -0
  3. package/dist/index.mjs +2452 -0
  4. package/package.json +17 -118
  5. package/Dockerfile +0 -327
  6. package/dist/bridge/index.d.ts +0 -181
  7. package/dist/bridge/index.d.ts.map +0 -1
  8. package/dist/bridge/index.js +0 -3053
  9. package/dist/bridge/index.js.map +0 -1
  10. package/dist/contexts-1EsLHByO.d.ts +0 -463
  11. package/dist/contexts-1EsLHByO.d.ts.map +0 -1
  12. package/dist/dist-Duor5GbS.js +0 -752
  13. package/dist/dist-Duor5GbS.js.map +0 -1
  14. package/dist/errors/index.d.ts +0 -4
  15. package/dist/errors/index.js +0 -4
  16. package/dist/errors-CXR0xBpw.js +0 -285
  17. package/dist/errors-CXR0xBpw.js.map +0 -1
  18. package/dist/errors-QYlSkVGz.js +0 -893
  19. package/dist/errors-QYlSkVGz.js.map +0 -1
  20. package/dist/extensions/index.d.ts +0 -4
  21. package/dist/extensions/index.js +0 -6
  22. package/dist/extensions-CFB2xHqY.js +0 -1023
  23. package/dist/extensions-CFB2xHqY.js.map +0 -1
  24. package/dist/filesystem-BWAZCZER.d.ts +0 -732
  25. package/dist/filesystem-BWAZCZER.d.ts.map +0 -1
  26. package/dist/git/index.d.ts +0 -63
  27. package/dist/git/index.d.ts.map +0 -1
  28. package/dist/git/index.js +0 -338
  29. package/dist/git/index.js.map +0 -1
  30. package/dist/index-Bs4bqXDR.d.ts +0 -438
  31. package/dist/index-Bs4bqXDR.d.ts.map +0 -1
  32. package/dist/index-HNYBk-az.d.ts +0 -444
  33. package/dist/index-HNYBk-az.d.ts.map +0 -1
  34. package/dist/index.d.ts +0 -576
  35. package/dist/index.d.ts.map +0 -1
  36. package/dist/index.js +0 -33
  37. package/dist/index.js.map +0 -1
  38. package/dist/interpreter/index.d.ts +0 -311
  39. package/dist/interpreter/index.d.ts.map +0 -1
  40. package/dist/interpreter/index.js +0 -292
  41. package/dist/interpreter/index.js.map +0 -1
  42. package/dist/openai/index.d.ts +0 -68
  43. package/dist/openai/index.d.ts.map +0 -1
  44. package/dist/openai/index.js +0 -367
  45. package/dist/openai/index.js.map +0 -1
  46. package/dist/opencode/index.d.ts +0 -182
  47. package/dist/opencode/index.d.ts.map +0 -1
  48. package/dist/opencode/index.js +0 -454
  49. package/dist/opencode/index.js.map +0 -1
  50. package/dist/process-types-GStiZ8f8.d.ts +0 -73
  51. package/dist/process-types-GStiZ8f8.d.ts.map +0 -1
  52. package/dist/sandbox-BbAabq93.d.ts +0 -42
  53. package/dist/sandbox-BbAabq93.d.ts.map +0 -1
  54. package/dist/sandbox-cmlgGVYX.js +0 -10056
  55. package/dist/sandbox-cmlgGVYX.js.map +0 -1
  56. package/dist/sidecar/index.d.ts +0 -77
  57. package/dist/sidecar/index.d.ts.map +0 -1
  58. package/dist/sidecar/index.js +0 -201
  59. package/dist/sidecar/index.js.map +0 -1
  60. package/dist/xterm/index.d.ts +0 -93
  61. package/dist/xterm/index.d.ts.map +0 -1
  62. package/dist/xterm/index.js +0 -220
  63. package/dist/xterm/index.js.map +0 -1
package/dist/index.mjs ADDED
@@ -0,0 +1,2452 @@
1
+ import { WorkerEntrypoint } from "cloudflare:workers";
2
+ import * as z from "zod/mini";
3
+ import { constants } from "node:os";
4
+ import { AwsClient } from "aws4fetch";
5
+ //#region src/directory-backups/gateway.ts
6
+ const PART_PATH = /^\/parts\/([1-9][0-9]{0,8})$/;
7
+ const RANGE = /^bytes=([0-9]{1,15})-([0-9]{1,15})$/;
8
+ const MAX_DETAIL_LENGTH = 1024;
9
+ /**
10
+ * Serves the container's requests for the operation that holds the lock: part uploads for a
11
+ * write grant, byte ranges for a read grant, and nothing else. The key comes from props, never
12
+ * from the request.
13
+ */
14
+ async function handleDirectoryBackupRequest(request, props, resolveBucket) {
15
+ if (props.protocolVersion !== 1) return text(500, "gateway protocol is incompatible");
16
+ switch (props.mode) {
17
+ case "write": return servePart(request, resolveBucket(props.binding), props.key, props.uploadId);
18
+ case "read": return serveRange(request, resolveBucket(props.binding), props.key);
19
+ default: return text(403, "no directory backup operation holds a grant");
20
+ }
21
+ }
22
+ /** `PUT /parts/<N>`: uploads one part of the granted multipart upload. */
23
+ async function servePart(request, bucket, key, uploadId) {
24
+ const part = PART_PATH.exec(new URL(request.url).pathname);
25
+ if (request.method !== "PUT" || part === null) return text(403, "the grant allows only part uploads");
26
+ const length = Number(request.headers.get("content-length") ?? NaN);
27
+ if (!Number.isSafeInteger(length) || length <= 0 || request.body === null) return text(411, "a part needs a Content-Length");
28
+ try {
29
+ const { readable, writable } = new FixedLengthStream(length);
30
+ const upload = bucket.resumeMultipartUpload(key, uploadId);
31
+ const [uploaded] = await Promise.all([upload.uploadPart(Number(part[1]), readable), request.body.pipeTo(writable)]);
32
+ return Response.json({ etag: uploaded.etag });
33
+ } catch (error) {
34
+ return text(502, error instanceof Error ? error.message : "R2 rejected the part");
35
+ }
36
+ }
37
+ /** `GET /object` with `Range: bytes=a-b`: reads one range of the granted object. */
38
+ async function serveRange(request, bucket, key) {
39
+ const range = RANGE.exec(request.headers.get("range") ?? "");
40
+ if (request.method !== "GET" || new URL(request.url).pathname !== "/object" || range === null) return text(403, "the grant allows only ranged reads of the backup");
41
+ const offset = Number(range[1]);
42
+ const last = Number(range[2]);
43
+ if (last < offset) return text(416, "invalid range");
44
+ try {
45
+ const object = await bucket.get(key, { range: {
46
+ offset,
47
+ length: last - offset + 1
48
+ } });
49
+ if (object === null) return text(404, "the backup object does not exist");
50
+ const end = Math.min(last, object.size - 1);
51
+ if (end < offset) return text(416, "the range starts past the end of the object");
52
+ return new Response(object.body, {
53
+ status: 206,
54
+ headers: { "content-range": `bytes ${offset}-${end}/${object.size}` }
55
+ });
56
+ } catch (error) {
57
+ return text(502, error instanceof Error ? error.message : "R2 rejected the range");
58
+ }
59
+ }
60
+ function text(status, detail) {
61
+ return new Response(detail.slice(0, MAX_DETAIL_LENGTH), { status });
62
+ }
63
+ //#endregion
64
+ //#region src/directory-backups/directory-backup-gateway.ts
65
+ /**
66
+ * Moves directory backups between a container and an R2 bucket binding. Export it from the
67
+ * Worker and pass `ctx.exports.DirectoryBackupGateway` to `DirectoryBackups`.
68
+ *
69
+ * The container reaches `fetch()` through the outbound intercept and can only use the grant
70
+ * its current operation holds. The other methods are for the Durable Object alone.
71
+ */
72
+ var DirectoryBackupGateway = class extends WorkerEntrypoint {
73
+ fetch(request) {
74
+ return handleDirectoryBackupRequest(request, this.ctx.props, this.#bucket);
75
+ }
76
+ async createUpload(name) {
77
+ const { bucket, key } = this.#control();
78
+ const options = { httpMetadata: { contentType: "application/zstd" } };
79
+ if (name !== void 0) options.customMetadata = { name };
80
+ return (await bucket.createMultipartUpload(key, options)).uploadId;
81
+ }
82
+ async completeUpload(uploadId, parts) {
83
+ const { bucket, key } = this.#control();
84
+ return (await bucket.resumeMultipartUpload(key, uploadId).complete(parts.map(({ partNumber, etag }) => ({
85
+ partNumber,
86
+ etag
87
+ })))).size;
88
+ }
89
+ async abortUpload(uploadId) {
90
+ const { bucket, key } = this.#control();
91
+ await bucket.resumeMultipartUpload(key, uploadId).abort();
92
+ }
93
+ async deleteObject() {
94
+ const { bucket, key } = this.#control();
95
+ await bucket.delete(key);
96
+ }
97
+ /** The bucket and key of a control call. Only the Durable Object holds control props. */
98
+ #control() {
99
+ const props = this.ctx.props;
100
+ if (props.protocolVersion !== 1 || props.mode !== "control") throw new Error("DirectoryBackupGateway control methods require control props");
101
+ return {
102
+ bucket: this.#bucket(props.binding),
103
+ key: props.key
104
+ };
105
+ }
106
+ #bucket = (binding) => {
107
+ const value = Object.getOwnPropertyDescriptor(this.env, binding)?.value;
108
+ if (value === void 0 || !bucketSchema.safeParse(value).success) throw new TypeError(`env.${binding} is not an R2 bucket binding`);
109
+ return value;
110
+ };
111
+ };
112
+ const bucketSchema = z.object({
113
+ get: z.function(),
114
+ delete: z.function(),
115
+ createMultipartUpload: z.function(),
116
+ resumeMultipartUpload: z.function()
117
+ });
118
+ //#endregion
119
+ //#region src/shared/errors.ts
120
+ const FILE_OPERATIONS = [
121
+ "readFile",
122
+ "writeFile",
123
+ "stat",
124
+ "lstat",
125
+ "readDirectory",
126
+ "mkdir",
127
+ "rename",
128
+ "remove",
129
+ "backup",
130
+ "restore"
131
+ ];
132
+ const CANONICAL_ERRNO_NAMES = /* @__PURE__ */ new Map();
133
+ for (const [name, value] of Object.entries(constants.errno)) if (name.startsWith("E") && !CANONICAL_ERRNO_NAMES.has(value)) CANONICAL_ERRNO_NAMES.set(value, name);
134
+ for (const preferred of [
135
+ "EAGAIN",
136
+ "EDEADLK",
137
+ "EOPNOTSUPP"
138
+ ]) {
139
+ const value = constants.errno[preferred];
140
+ if (value !== void 0) CANONICAL_ERRNO_NAMES.set(value, preferred);
141
+ }
142
+ var FileError = class extends Error {
143
+ name = "SandboxFileError";
144
+ code;
145
+ operation;
146
+ path;
147
+ destination;
148
+ detail;
149
+ constructor(context, code, detail) {
150
+ const subject = context.destination === void 0 ? `'${context.path}'` : `'${context.path}' to '${context.destination}'`;
151
+ super(`${context.operation} ${subject}: ${detail}`);
152
+ this.code = code;
153
+ this.operation = context.operation;
154
+ this.path = context.path;
155
+ if (context.destination !== void 0) this.destination = context.destination;
156
+ this.detail = detail;
157
+ }
158
+ };
159
+ const SandboxFileError = {
160
+ /** Recognizes local and JSRPC-crossed SandboxFileError values. */
161
+ is(cause) {
162
+ return cause instanceof Error && cause.name === "SandboxFileError" && hasOwn(cause, "code", isFileErrorCode) && hasOwn(cause, "operation", isFileOperation) && hasOwn(cause, "path", isString) && hasOptionalOwn(cause, "destination", isString) && hasOwn(cause, "detail", isString);
163
+ } };
164
+ var ProtocolError = class extends Error {
165
+ name = "SandboxProtocolError";
166
+ code = "SANDBOX_PROTOCOL_ERROR";
167
+ detail;
168
+ constructor(detail, cause) {
169
+ super(detail, cause === void 0 ? void 0 : { cause });
170
+ this.detail = detail;
171
+ }
172
+ };
173
+ const SandboxProtocolError = {
174
+ /** Recognizes local and JSRPC-crossed SandboxProtocolError values. */
175
+ is(cause) {
176
+ return cause instanceof Error && cause.name === "SandboxProtocolError" && hasOwn(cause, "code", (value) => value === "SANDBOX_PROTOCOL_ERROR") && hasOwn(cause, "detail", isString);
177
+ } };
178
+ var S3MountError = class extends Error {
179
+ name = "SandboxS3MountError";
180
+ code;
181
+ operation;
182
+ path;
183
+ detail;
184
+ constructor(code, operation, path, detail) {
185
+ super(`${operation} '${path}': ${detail}`);
186
+ this.code = code;
187
+ this.operation = operation;
188
+ this.path = path;
189
+ this.detail = detail;
190
+ }
191
+ };
192
+ const SandboxS3MountError = {
193
+ /** Recognizes local and JSRPC-crossed SandboxS3MountError values. */
194
+ is(cause) {
195
+ return cause instanceof Error && cause.name === "SandboxS3MountError" && hasOwn(cause, "code", isS3MountErrorCode) && hasOwn(cause, "operation", isS3MountOperation) && hasOwn(cause, "path", isString) && hasOwn(cause, "detail", isString);
196
+ } };
197
+ var BackupError = class extends Error {
198
+ name = "SandboxBackupError";
199
+ code;
200
+ operation;
201
+ path;
202
+ detail;
203
+ constructor(code, operation, path, detail) {
204
+ super(`${operation} '${path}': ${detail}`);
205
+ this.code = code;
206
+ this.operation = operation;
207
+ this.path = path;
208
+ this.detail = detail;
209
+ }
210
+ };
211
+ const SandboxBackupError = {
212
+ /** Recognizes local and JSRPC-crossed SandboxBackupError values. */
213
+ is(cause) {
214
+ return cause instanceof Error && cause.name === "SandboxBackupError" && hasOwn(cause, "code", isBackupErrorCode) && hasOwn(cause, "operation", isBackupOperation) && hasOwn(cause, "path", isString) && hasOwn(cause, "detail", isString);
215
+ } };
216
+ function backupError(code, operation, path, detail) {
217
+ return new BackupError(code, operation, path, detail);
218
+ }
219
+ function protocolError(detail, cause) {
220
+ return new ProtocolError(detail, cause);
221
+ }
222
+ function s3MountError(code, operation, path, detail) {
223
+ return new S3MountError(code, operation, path, detail);
224
+ }
225
+ function fileErrorFromErrno(context, errno, detail) {
226
+ const code = CANONICAL_ERRNO_NAMES.get(errno) ?? "UNKNOWN";
227
+ return new FileError(context, code, detail.length > 0 ? detail : code);
228
+ }
229
+ function hasOwn(owner, key, predicate) {
230
+ const descriptor = Object.getOwnPropertyDescriptor(owner, key);
231
+ return descriptor !== void 0 && predicate(descriptor.value);
232
+ }
233
+ function hasOptionalOwn(owner, key, predicate) {
234
+ const descriptor = Object.getOwnPropertyDescriptor(owner, key);
235
+ return descriptor === void 0 || descriptor.value === void 0 || predicate(descriptor.value);
236
+ }
237
+ function isString(value) {
238
+ return typeof value === "string";
239
+ }
240
+ function isFileErrorCode(value) {
241
+ return isString(value) && (value === "UNKNOWN" || /^E[A-Z0-9]+$/.test(value));
242
+ }
243
+ function isFileOperation(value) {
244
+ return FILE_OPERATIONS.some((operation) => operation === value);
245
+ }
246
+ function isS3MountErrorCode(value) {
247
+ return value === "S3_MOUNT_CONFLICT" || value === "S3_MOUNT_BUSY" || value === "S3_MOUNT_FAILED" || value === "S3_MOUNT_INCOMPATIBLE";
248
+ }
249
+ function isBackupErrorCode(value) {
250
+ return value === "BACKUP_NOT_FOUND" || value === "BACKUP_INTEGRITY" || value === "BACKUP_TRANSFER";
251
+ }
252
+ function isBackupOperation(value) {
253
+ return value === "backup" || value === "restore" || value === "delete";
254
+ }
255
+ function isS3MountOperation(value) {
256
+ return value === "mount" || value === "inspect" || value === "unmount";
257
+ }
258
+ //#endregion
259
+ //#region src/shared/options.ts
260
+ const optionsSchema = z.object({});
261
+ function validateOptions(options, rules) {
262
+ if (!optionsSchema.safeParse(options).success) throw new TypeError("options must be an object");
263
+ for (const [name, value] of Object.entries(options)) {
264
+ if (value === void 0) continue;
265
+ if (!Object.hasOwn(rules, name)) throw new TypeError(`unknown option "${name}"`);
266
+ const rule = rules[name];
267
+ if (!rule.schema.safeParse(value).success) throw new TypeError(`${name} ${rule.requirement}`);
268
+ }
269
+ }
270
+ //#endregion
271
+ //#region src/directory-backups/contracts.ts
272
+ /** The only archive format this version writes and reads. */
273
+ const DIRECTORY_BACKUP_FORMAT = "tar+zstd/1";
274
+ //#endregion
275
+ //#region src/shared/shim.ts
276
+ const SHIM_PATH = "/usr/local/bin/sandbox-shim";
277
+ const MAGIC = new Uint8Array([
278
+ 83,
279
+ 66,
280
+ 88,
281
+ 70
282
+ ]);
283
+ const PROTOCOL_VERSION = 1;
284
+ const HEADER_LENGTH = 10;
285
+ const ERROR_PREFIX_LENGTH = 4;
286
+ const FRAME_SUCCESS = 0;
287
+ const FRAME_FILE_ERROR = 1;
288
+ const FRAME_DATA = 2;
289
+ const jsonSchema = z.json();
290
+ const jsonDecoder = new TextDecoder("utf-8", { fatal: true });
291
+ var AbortMonitor = class {
292
+ /**
293
+ * The signal to pass to `exec()`, which follows the caller's until the shim settles. The
294
+ * caller's signal can outlive the call, as AbortSignal.timeout() does, and signalling an exited
295
+ * process logs a runtime error. Undefined when the process must not be signalled.
296
+ */
297
+ signal;
298
+ #promise;
299
+ #dispose = () => void 0;
300
+ constructor(signal, abort) {
301
+ if (signal === void 0) return;
302
+ const linked = new AbortController();
303
+ if (abort === "kill") this.signal = linked.signal;
304
+ this.#promise = new Promise((_, reject) => {
305
+ if (signal.aborted) {
306
+ linked.abort(signal.reason);
307
+ reject(signal.reason);
308
+ return;
309
+ }
310
+ const onAbort = () => {
311
+ this.dispose();
312
+ linked.abort(signal.reason);
313
+ reject(signal.reason);
314
+ };
315
+ signal.addEventListener("abort", onAbort, { once: true });
316
+ this.#dispose = () => signal.removeEventListener("abort", onAbort);
317
+ });
318
+ this.#promise.catch(() => void 0);
319
+ }
320
+ waitFor(operation) {
321
+ if (this.#promise === void 0) return operation;
322
+ return Promise.race([this.#promise, operation]);
323
+ }
324
+ dispose() {
325
+ this.#dispose();
326
+ this.#dispose = () => void 0;
327
+ }
328
+ };
329
+ var ShimSession = class ShimSession {
330
+ process;
331
+ #abort;
332
+ #settled = false;
333
+ constructor(process, abort) {
334
+ this.process = process;
335
+ this.#abort = abort;
336
+ }
337
+ static async start(container, command, options, abort = "kill") {
338
+ const monitor = new AbortMonitor(options.signal, abort);
339
+ const starting = container.exec(command, {
340
+ ...options,
341
+ signal: monitor.signal
342
+ });
343
+ try {
344
+ return new ShimSession(await monitor.waitFor(starting), monitor);
345
+ } catch (error) {
346
+ monitor.dispose();
347
+ if (abort === "stdin") starting.then(closeStdin$1, () => void 0);
348
+ throw error;
349
+ }
350
+ }
351
+ openStderrControl() {
352
+ if (this.process.stderr === null) throw protocolError("sandbox-shim did not provide stderr");
353
+ return new ShimControl(this.process.stderr.getReader(), this);
354
+ }
355
+ openStdoutControl() {
356
+ if (this.process.stdout === null) throw protocolError("sandbox-shim did not provide stdout");
357
+ return new ShimControl(this.process.stdout.getReader(), this);
358
+ }
359
+ openStdoutReader() {
360
+ if (this.process.stdout === null) throw protocolError("sandbox-shim did not provide stdout");
361
+ return this.process.stdout.getReader();
362
+ }
363
+ openStdinWriter() {
364
+ if (this.process.stdin === null) throw protocolError("sandbox-shim did not provide stdin");
365
+ return this.process.stdin.getWriter();
366
+ }
367
+ waitFor(operation) {
368
+ return this.#abort.waitFor(operation);
369
+ }
370
+ finish() {
371
+ if (this.#settled) return;
372
+ this.#settled = true;
373
+ this.#abort.dispose();
374
+ }
375
+ /**
376
+ * Waits for a shim that already reported its outcome to exit, so cleanup does not signal an
377
+ * exited process. Never throws: the reported outcome stays authoritative, and if the exit is
378
+ * not observed, terminate() still cleans up.
379
+ */
380
+ async settle() {
381
+ try {
382
+ await this.waitFor(this.process.exitCode);
383
+ this.finish();
384
+ } catch {}
385
+ }
386
+ terminate() {
387
+ if (this.#settled) return;
388
+ this.#settled = true;
389
+ this.#abort.dispose();
390
+ try {
391
+ this.process.kill(9);
392
+ } catch {}
393
+ }
394
+ };
395
+ var ShimControl = class {
396
+ #reader;
397
+ #session;
398
+ #pending = /* @__PURE__ */ new Uint8Array();
399
+ constructor(reader, session) {
400
+ this.#reader = reader;
401
+ this.#session = session;
402
+ }
403
+ async readFrame() {
404
+ const header = await this.#readExactly(HEADER_LENGTH);
405
+ validateHeader(header);
406
+ const frameKind = header[5];
407
+ const payloadLength = new DataView(header.buffer, header.byteOffset + 6, 4).getUint32(0, true);
408
+ if (frameKind === FRAME_SUCCESS) {
409
+ if (payloadLength !== 0) throw protocolError("sandbox-shim returned invalid control data");
410
+ return { kind: "success" };
411
+ }
412
+ if (frameKind === FRAME_DATA) return {
413
+ kind: "data",
414
+ payload: await this.#readExactly(payloadLength)
415
+ };
416
+ if (frameKind !== FRAME_FILE_ERROR) throw protocolError(`sandbox-shim returned unknown control status ${frameKind}`);
417
+ if (payloadLength < ERROR_PREFIX_LENGTH) throw protocolError("sandbox-shim returned invalid control data");
418
+ if (payloadLength > 65540) throw protocolError("sandbox-shim error message exceeded its size limit");
419
+ const payload = await this.#readExactly(payloadLength);
420
+ const errno = new DataView(payload.buffer, payload.byteOffset, ERROR_PREFIX_LENGTH).getInt32(0, true);
421
+ if (errno <= 0) throw protocolError(`sandbox-shim returned invalid errno ${errno}`);
422
+ return {
423
+ kind: "fileError",
424
+ errno,
425
+ detail: decodeErrorDetail(payload.subarray(ERROR_PREFIX_LENGTH))
426
+ };
427
+ }
428
+ async expectEnd() {
429
+ if (!(this.#pending.length === 0 ? await this.#session.waitFor(this.#reader.read()) : {
430
+ done: false,
431
+ value: this.#pending
432
+ }).done) throw protocolError("sandbox-shim returned trailing control data");
433
+ }
434
+ discard(reason) {
435
+ this.#reader.cancel(reason).then(() => this.#reader.releaseLock(), () => this.#reader.releaseLock());
436
+ }
437
+ releaseLock() {
438
+ this.#reader.releaseLock();
439
+ }
440
+ async #readExactly(length) {
441
+ const result = new Uint8Array(length);
442
+ let offset = 0;
443
+ while (offset < length) {
444
+ if (this.#pending.length === 0) {
445
+ const next = await this.#session.waitFor(this.#reader.read());
446
+ if (next.done) throw protocolError("sandbox-shim returned truncated control data");
447
+ this.#pending = next.value;
448
+ }
449
+ const count = Math.min(length - offset, this.#pending.length);
450
+ result.set(this.#pending.subarray(0, count), offset);
451
+ this.#pending = this.#pending.subarray(count);
452
+ offset += count;
453
+ }
454
+ return result;
455
+ }
456
+ };
457
+ /** Decodes a data frame's payload as JSON, or throws a protocol error that says `invalid`. */
458
+ function parseJsonPayload(payload, invalid) {
459
+ try {
460
+ const parsed = jsonSchema.safeParse(JSON.parse(jsonDecoder.decode(payload)));
461
+ if (parsed.success) return parsed.data;
462
+ } catch (cause) {
463
+ throw protocolError(invalid, cause);
464
+ }
465
+ throw protocolError(invalid);
466
+ }
467
+ function closeStdin$1(process) {
468
+ process.stdin?.close().catch(() => void 0);
469
+ }
470
+ function decodeErrorDetail(bytes) {
471
+ try {
472
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
473
+ } catch (cause) {
474
+ throw protocolError("sandbox-shim returned a non-UTF-8 error message", cause);
475
+ }
476
+ }
477
+ function validateHeader(header) {
478
+ for (let index = 0; index < MAGIC.length; index += 1) if (header[index] !== MAGIC[index]) throw protocolError("sandbox-shim returned invalid protocol magic");
479
+ if (header[4] !== PROTOCOL_VERSION) throw protocolError(`sandbox-shim protocol ${header[4]} is not supported`);
480
+ }
481
+ //#endregion
482
+ //#region src/directory-backups/protocol.ts
483
+ /** The one intercepted host every operation's grant is registered on. */
484
+ const GATEWAY_HOST = "backups.sandbox.internal";
485
+ const ACKNOWLEDGEMENT = new Uint8Array([1]);
486
+ const messageSchema = z.object({ kind: z.string() });
487
+ const errorSchema = z.object({
488
+ kind: z.literal("error"),
489
+ code: z.string(),
490
+ detail: z.string()
491
+ });
492
+ const lockedSchema = z.strictObject({ kind: z.literal("locked") });
493
+ /**
494
+ * Runs one `directory-backup` shim exchange: wait for the lock, register the grant, acknowledge,
495
+ * wait for the result, then deny and close stdin, in that order on every path. The shim holds
496
+ * the lock until stdin closes, so a later operation's grant can't be overwritten by this
497
+ * operation's deny.
498
+ *
499
+ * An abort closes stdin rather than killing the shim, which then removes whatever it had
500
+ * partly written, and rejects at once with the signal's reason.
501
+ */
502
+ async function runShimExchange(container, exchange) {
503
+ const { signal } = exchange;
504
+ signal?.throwIfAborted();
505
+ const operation = exchange.command;
506
+ const session = await ShimSession.start(container, [
507
+ SHIM_PATH,
508
+ "directory-backup",
509
+ exchange.command,
510
+ JSON.stringify(exchange.request)
511
+ ], {
512
+ stdin: "pipe",
513
+ stdout: "pipe",
514
+ stderr: "ignore",
515
+ signal
516
+ }, "stdin");
517
+ let control;
518
+ let input;
519
+ let granting;
520
+ let released = false;
521
+ const release = async () => {
522
+ if (released) return;
523
+ released = true;
524
+ if (granting !== void 0) {
525
+ await granting.catch(() => void 0);
526
+ await exchange.deny().catch(() => void 0);
527
+ }
528
+ if (input !== void 0) closeStdin(input);
529
+ };
530
+ try {
531
+ control = session.openStdoutControl();
532
+ input = session.openStdinWriter();
533
+ const locked = await readMessage(control, operation, exchange.path);
534
+ if (!lockedSchema.safeParse(locked).success) throw protocolError("sandbox-shim did not report that it holds the backup lock");
535
+ granting = exchange.grant();
536
+ await session.waitFor(granting);
537
+ await session.waitFor(input.write(ACKNOWLEDGEMENT));
538
+ const result = await readMessage(control, operation, exchange.path);
539
+ const done = exchange.done.safeParse(result);
540
+ if (!done.success) throw protocolError("sandbox-shim returned an invalid backup result");
541
+ await release();
542
+ await control.expectEnd();
543
+ const exitCode = await session.waitFor(session.process.exitCode);
544
+ if (exitCode !== 0) throw protocolError(`sandbox-shim exited with code ${exitCode}`);
545
+ control.releaseLock();
546
+ return done.data;
547
+ } catch (error) {
548
+ await release();
549
+ control?.discard(error);
550
+ throw error;
551
+ } finally {
552
+ session.finish();
553
+ }
554
+ }
555
+ async function readMessage(control, operation, path) {
556
+ const frame = await control.readFrame();
557
+ if (frame.kind === "fileError") throw fileErrorFromErrno({
558
+ operation: operation === "backup" ? "backup" : "restore",
559
+ path
560
+ }, frame.errno, frame.detail);
561
+ if (frame.kind !== "data") throw protocolError("sandbox-shim did not return backup data");
562
+ const value = parseJsonPayload(frame.payload, "sandbox-shim returned invalid backup data");
563
+ if (!messageSchema.safeParse(value).success) throw protocolError("sandbox-shim returned invalid backup data");
564
+ const failure = errorSchema.safeParse(value);
565
+ if (failure.success) throw shimFailure(failure.data.code, failure.data.detail, operation, path);
566
+ return value;
567
+ }
568
+ function shimFailure(code, detail, operation, path) {
569
+ switch (code) {
570
+ case "integrity": return backupError("BACKUP_INTEGRITY", operation, path, detail);
571
+ case "notFound": return backupError("BACKUP_NOT_FOUND", operation, path, detail);
572
+ case "transfer": return backupError("BACKUP_TRANSFER", operation, path, detail);
573
+ case "protocol": return protocolError(detail);
574
+ default: return protocolError(`sandbox-shim returned unknown backup error code "${code}"`);
575
+ }
576
+ }
577
+ function closeStdin(input) {
578
+ input.close().catch(() => void 0);
579
+ }
580
+ //#endregion
581
+ //#region src/directory-backups/directory-backups.ts
582
+ const absolutePath = z.string().check(z.startsWith("/"), z.refine((value) => !value.includes("\0")));
583
+ const SIGNAL = {
584
+ schema: z.instanceof(AbortSignal),
585
+ requirement: "must be an AbortSignal"
586
+ };
587
+ const DIR = {
588
+ schema: absolutePath,
589
+ requirement: "must be an absolute path without NUL"
590
+ };
591
+ const BACKUP_OPTIONS = {
592
+ dir: DIR,
593
+ name: {
594
+ schema: z.string(),
595
+ requirement: "must be a string"
596
+ },
597
+ exclude: {
598
+ schema: z.array(z.string()),
599
+ requirement: "must be an array of strings"
600
+ },
601
+ gitignore: {
602
+ schema: z.boolean(),
603
+ requirement: "must be a boolean"
604
+ },
605
+ signal: SIGNAL
606
+ };
607
+ const RESTORE_OPTIONS = {
608
+ dir: DIR,
609
+ signal: SIGNAL
610
+ };
611
+ const DELETE_OPTIONS = { signal: SIGNAL };
612
+ const sha256Schema = z.string().check(z.regex(/^[0-9a-f]{64}$/));
613
+ const recordSchema = z.object({
614
+ id: z.string().check(z.regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)),
615
+ dir: absolutePath,
616
+ size: z.number().check(z.refine((value) => Number.isSafeInteger(value) && value > 0)),
617
+ name: z.optional(z.string()),
618
+ sha256: sha256Schema,
619
+ format: z.literal(DIRECTORY_BACKUP_FORMAT)
620
+ });
621
+ const backupDoneSchema = z.strictObject({
622
+ kind: z.literal("done"),
623
+ size: z.number().check(z.refine((value) => Number.isSafeInteger(value) && value > 0)),
624
+ sha256: sha256Schema,
625
+ parts: z.array(z.strictObject({
626
+ partNumber: z.number().check(z.int(), z.positive()),
627
+ etag: z.string()
628
+ })).check(z.minLength(1))
629
+ });
630
+ const restoreDoneSchema = z.strictObject({ kind: z.literal("done") });
631
+ /**
632
+ * Saves one directory from the running Container to an R2 bucket, and restores it as
633
+ * ordinary files into a Container, which may run a different image.
634
+ *
635
+ * One backup or restore runs at a time per Container; others wait their turn, so
636
+ * `Promise.all()` over several directories works. Start the Container first: this class never
637
+ * starts, retries, or times out anything. The application stores the returned records and
638
+ * decides when to delete them.
639
+ */
640
+ var DirectoryBackups = class {
641
+ #container;
642
+ #gateway;
643
+ #binding;
644
+ #prefix;
645
+ constructor(container, gateway, storage) {
646
+ if (!z.string().check(z.minLength(1)).safeParse(storage.binding).success) throw new TypeError("storage.binding must name an R2 bucket binding");
647
+ const prefix = storage.prefix ?? "";
648
+ if (!z.string().safeParse(prefix).success || prefix !== "" && !prefix.endsWith("/")) throw new TypeError("storage.prefix must be a string that ends in \"/\"");
649
+ this.#container = container;
650
+ this.#gateway = gateway;
651
+ this.#binding = storage.binding;
652
+ this.#prefix = prefix;
653
+ }
654
+ /**
655
+ * Backs up `dir` and returns its record. Pause writers in `dir` first: files that change
656
+ * while it's read are captured as they are at that moment.
657
+ *
658
+ * @throws {SandboxFileError} `dir` is missing or not a directory, a file can't be read, or an
659
+ * exclude pattern is invalid (`EINVAL`).
660
+ * @throws {SandboxBackupError} `BACKUP_TRANSFER` when a part upload fails, or
661
+ * `BACKUP_INTEGRITY` when R2 stored a different size than was uploaded.
662
+ */
663
+ async backup(options) {
664
+ validateOptions(options, BACKUP_OPTIONS);
665
+ if (options.dir === void 0) throw new TypeError("dir must be an absolute path without NUL");
666
+ const { dir, name, signal } = options;
667
+ const id = crypto.randomUUID();
668
+ const control = this.#control(this.#key(id));
669
+ const { uploadId, done } = await this.#upload(control, this.#key(id), options);
670
+ let size;
671
+ try {
672
+ signal?.throwIfAborted();
673
+ size = await control.completeUpload(uploadId, done.parts);
674
+ } catch (error) {
675
+ await control.abortUpload(uploadId).catch(() => void 0);
676
+ throw error;
677
+ }
678
+ if (size !== done.size) {
679
+ await control.deleteObject().catch(() => void 0);
680
+ throw backupError("BACKUP_INTEGRITY", "backup", dir, `R2 stored ${size} bytes, but the container uploaded ${done.size}`);
681
+ }
682
+ const record = {
683
+ id,
684
+ dir,
685
+ size,
686
+ sha256: done.sha256,
687
+ format: DIRECTORY_BACKUP_FORMAT
688
+ };
689
+ return name === void 0 ? record : {
690
+ ...record,
691
+ name
692
+ };
693
+ }
694
+ /**
695
+ * Replaces `options.dir`, or the record's `dir`, with the backup's contents. The directory is
696
+ * extracted beside the target and swapped in only after the download is verified, so a failed
697
+ * or aborted restore leaves the target as it was. The target's parent must exist; the target
698
+ * need not.
699
+ *
700
+ * @throws {SandboxFileError} The target's parent is missing (`ENOENT`), the target isn't a
701
+ * directory (`ENOTDIR`) or is a mount point (`EBUSY`), the swap fails (for example `EXDEV`),
702
+ * or the disk fills (`ENOSPC`).
703
+ * @throws {SandboxBackupError} `BACKUP_NOT_FOUND`, `BACKUP_INTEGRITY`, or `BACKUP_TRANSFER`.
704
+ */
705
+ async restore(backup, options = {}) {
706
+ validateOptions(options, RESTORE_OPTIONS);
707
+ const record = parseRecord(backup);
708
+ const dir = options.dir ?? record.dir;
709
+ const key = this.#key(record.id);
710
+ await runShimExchange(this.#container, {
711
+ command: "restore",
712
+ request: {
713
+ gateway: GATEWAY_HOST,
714
+ dir,
715
+ size: record.size,
716
+ sha256: record.sha256
717
+ },
718
+ path: dir,
719
+ signal: options.signal,
720
+ done: restoreDoneSchema,
721
+ grant: () => this.#register({
722
+ protocolVersion: 1,
723
+ mode: "read",
724
+ binding: this.#binding,
725
+ key
726
+ }),
727
+ deny: () => this.#deny()
728
+ });
729
+ }
730
+ /**
731
+ * Deletes the backup's object. Needs no running Container. Deleting an object that is already
732
+ * gone succeeds. A restore reading it at the same time fails, and nothing is swapped.
733
+ */
734
+ async delete(backup, options = {}) {
735
+ validateOptions(options, DELETE_OPTIONS);
736
+ const record = parseRecord(backup);
737
+ options.signal?.throwIfAborted();
738
+ await this.#control(this.#key(record.id)).deleteObject();
739
+ }
740
+ /**
741
+ * Runs the shim's backup into a new multipart upload, which its grant creates once the shim
742
+ * holds the lock. Aborts the upload if the backup fails.
743
+ */
744
+ async #upload(control, key, options) {
745
+ const { dir } = options;
746
+ let uploadId;
747
+ try {
748
+ const done = await runShimExchange(this.#container, {
749
+ command: "backup",
750
+ request: {
751
+ gateway: GATEWAY_HOST,
752
+ dir,
753
+ exclude: [...options.exclude ?? []],
754
+ gitignore: options.gitignore ?? false
755
+ },
756
+ path: dir,
757
+ signal: options.signal,
758
+ done: backupDoneSchema,
759
+ grant: async () => {
760
+ uploadId = await control.createUpload(options.name);
761
+ await this.#register({
762
+ protocolVersion: 1,
763
+ mode: "write",
764
+ binding: this.#binding,
765
+ key,
766
+ uploadId
767
+ });
768
+ },
769
+ deny: () => this.#deny()
770
+ });
771
+ if (uploadId === void 0) throw new Error("the backup finished without an upload");
772
+ return {
773
+ uploadId,
774
+ done
775
+ };
776
+ } catch (error) {
777
+ if (uploadId !== void 0) await control.abortUpload(uploadId).catch(() => void 0);
778
+ throw error;
779
+ }
780
+ }
781
+ #key(id) {
782
+ return `${this.#prefix}${id}.tar.zst`;
783
+ }
784
+ #control(key) {
785
+ return this.#gateway({ props: {
786
+ protocolVersion: 1,
787
+ mode: "control",
788
+ binding: this.#binding,
789
+ key
790
+ } });
791
+ }
792
+ #register(props) {
793
+ return this.#container.interceptOutboundHttp(GATEWAY_HOST, this.#gateway({ props }));
794
+ }
795
+ #deny() {
796
+ return this.#register({
797
+ protocolVersion: 1,
798
+ mode: "deny"
799
+ });
800
+ }
801
+ };
802
+ function parseRecord(backup) {
803
+ const parsed = recordSchema.safeParse(backup);
804
+ if (!parsed.success) throw new TypeError(`backup must be a directory backup record in format "${DIRECTORY_BACKUP_FORMAT}"`);
805
+ return parsed.data;
806
+ }
807
+ //#endregion
808
+ //#region src/files/command.ts
809
+ /**
810
+ * Starts the shim for one file command. A relative path is joined onto `cwd`, so the shim always
811
+ * receives absolute paths and `exec()` never receives `cwd`: a missing `cwd` then fails the file
812
+ * operation with `ENOENT` instead of failing to start the process. Only `user` and `signal`
813
+ * reach `exec()`.
814
+ */
815
+ function startFileCommand(container, command, stdio) {
816
+ const { cwd, user, signal } = command.options;
817
+ const execOptions = { ...stdio };
818
+ if (user !== void 0) execOptions.user = user;
819
+ if (signal !== void 0) execOptions.signal = signal;
820
+ const paths = command.paths.map((path) => resolvePath(path, cwd));
821
+ return ShimSession.start(container, [
822
+ SHIM_PATH,
823
+ command.name,
824
+ ...paths,
825
+ ...command.flags ?? []
826
+ ], execOptions);
827
+ }
828
+ function resolvePath(path, cwd) {
829
+ return cwd === void 0 || path.startsWith("/") ? path : `${cwd}/${path}`;
830
+ }
831
+ async function runFileCommand(container, request) {
832
+ const session = await startFileCommand(container, request, {
833
+ stdout: "pipe",
834
+ stderr: "ignore"
835
+ });
836
+ let control;
837
+ try {
838
+ control = session.openStdoutControl();
839
+ const frame = await control.readFrame();
840
+ await control.expectEnd();
841
+ if (frame.kind === "fileError") {
842
+ await session.settle();
843
+ throw fileErrorFromErrno(request.error, frame.errno, frame.detail);
844
+ }
845
+ if (frame.kind !== request.expected) throw protocolError(request.expected === "data" ? "sandbox-shim did not return command data" : "sandbox-shim did not confirm command completion");
846
+ const exitCode = await session.waitFor(session.process.exitCode);
847
+ if (exitCode !== 0) throw protocolError(`sandbox-shim exited with code ${exitCode}`);
848
+ control.releaseLock();
849
+ session.finish();
850
+ return frame.kind === "data" ? frame.payload : void 0;
851
+ } catch (error) {
852
+ session.terminate();
853
+ control?.discard(error);
854
+ throw error;
855
+ }
856
+ }
857
+ //#endregion
858
+ //#region src/files/content.ts
859
+ function fileContentStream(content) {
860
+ if (content instanceof ReadableStream) return content;
861
+ if (content instanceof Blob) return content.stream();
862
+ let bytes;
863
+ if (typeof content === "string") bytes = new TextEncoder().encode(content);
864
+ else if (content instanceof ArrayBuffer) bytes = new Uint8Array(content);
865
+ else {
866
+ bytes = new Uint8Array(content.byteLength);
867
+ bytes.set(new Uint8Array(content.buffer, content.byteOffset, content.byteLength));
868
+ }
869
+ return new ReadableStream({ start(controller) {
870
+ controller.enqueue(bytes);
871
+ controller.close();
872
+ } });
873
+ }
874
+ //#endregion
875
+ //#region src/files/file-type.ts
876
+ function decodeFileType(value) {
877
+ switch (value) {
878
+ case 0: return "file";
879
+ case 1: return "directory";
880
+ case 2: return "symlink";
881
+ case 3: return "blockDevice";
882
+ case 4: return "characterDevice";
883
+ case 5: return "fifo";
884
+ case 6: return "socket";
885
+ default: throw protocolError("sandbox-shim returned an unknown file type");
886
+ }
887
+ }
888
+ //#endregion
889
+ //#region src/files/read-directory.ts
890
+ async function readDirectory(container, path, options) {
891
+ return decodeEntries(await runFileCommand(container, {
892
+ name: "read-directory",
893
+ paths: [path],
894
+ options,
895
+ error: {
896
+ operation: "readDirectory",
897
+ path
898
+ },
899
+ expected: "data"
900
+ }));
901
+ }
902
+ function decodeEntries(payload) {
903
+ const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
904
+ let offset = 0;
905
+ const count = readUint32(view, offset);
906
+ offset += 4;
907
+ const entries = [];
908
+ for (let index = 0; index < count; index += 1) {
909
+ const type = decodeFileType(readByte(payload, offset));
910
+ offset += 1;
911
+ const nameLength = readUint16(view, offset);
912
+ offset += 2;
913
+ const name = decodeText(readBytes(payload, offset, nameLength));
914
+ offset += nameLength;
915
+ entries.push({
916
+ name,
917
+ type
918
+ });
919
+ }
920
+ if (offset !== payload.length) throw protocolError("sandbox-shim returned trailing directory data");
921
+ return entries;
922
+ }
923
+ function readByte(payload, offset) {
924
+ const value = payload[offset];
925
+ if (value === void 0) throw protocolError("sandbox-shim returned truncated directory data");
926
+ return value;
927
+ }
928
+ function readUint16(view, offset) {
929
+ if (offset + 2 > view.byteLength) throw protocolError("sandbox-shim returned truncated directory data");
930
+ return view.getUint16(offset, true);
931
+ }
932
+ function readUint32(view, offset) {
933
+ if (offset + 4 > view.byteLength) throw protocolError("sandbox-shim returned truncated directory data");
934
+ return view.getUint32(offset, true);
935
+ }
936
+ function readBytes(payload, offset, length) {
937
+ if (offset + length > payload.length) throw protocolError("sandbox-shim returned truncated directory data");
938
+ return payload.subarray(offset, offset + length);
939
+ }
940
+ function decodeText(bytes) {
941
+ try {
942
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
943
+ } catch (cause) {
944
+ throw protocolError("sandbox-shim returned invalid UTF-8 in directory entry name", cause);
945
+ }
946
+ }
947
+ //#endregion
948
+ //#region src/files/read-file.ts
949
+ async function readFile(container, path, options) {
950
+ const session = await startFileCommand(container, {
951
+ name: "read",
952
+ paths: [path],
953
+ options
954
+ }, {
955
+ stdout: "pipe",
956
+ stderr: "pipe"
957
+ });
958
+ let control;
959
+ let output;
960
+ try {
961
+ control = session.openStderrControl();
962
+ output = session.openStdoutReader();
963
+ const opening = await control.readFrame();
964
+ if (opening.kind === "fileError") {
965
+ await control.expectEnd();
966
+ await session.settle();
967
+ throw fileErrorFromErrno({
968
+ operation: "readFile",
969
+ path
970
+ }, opening.errno, opening.detail);
971
+ }
972
+ if (opening.kind !== "success") throw protocolError("sandbox-shim returned data before file bytes");
973
+ const terminal = readTerminalControl(control);
974
+ terminal.catch(() => void 0);
975
+ return new Response(responseBody(session, control, output, terminal, path));
976
+ } catch (error) {
977
+ terminateRead(session, control, output, error);
978
+ throw error;
979
+ }
980
+ }
981
+ function responseBody(session, control, output, terminalFrame, path) {
982
+ return new ReadableStream({
983
+ pull: async (controller) => {
984
+ try {
985
+ const next = await session.waitFor(output.read());
986
+ if (!next.done) {
987
+ controller.enqueue(next.value);
988
+ return;
989
+ }
990
+ const terminal = await session.waitFor(terminalFrame);
991
+ if (terminal.kind === "fileError") {
992
+ await session.settle();
993
+ throw fileErrorFromErrno({
994
+ operation: "readFile",
995
+ path
996
+ }, terminal.errno, terminal.detail);
997
+ }
998
+ if (terminal.kind !== "success") throw protocolError("sandbox-shim returned data after file bytes");
999
+ output.releaseLock();
1000
+ control.releaseLock();
1001
+ session.finish();
1002
+ controller.close();
1003
+ } catch (error) {
1004
+ terminateRead(session, control, output, error);
1005
+ controller.error(error);
1006
+ }
1007
+ },
1008
+ cancel: (reason) => terminateRead(session, control, output, reason)
1009
+ });
1010
+ }
1011
+ async function readTerminalControl(control) {
1012
+ const frame = await control.readFrame();
1013
+ await control.expectEnd();
1014
+ return frame;
1015
+ }
1016
+ function terminateRead(session, control, output, reason) {
1017
+ session.terminate();
1018
+ control?.discard(reason);
1019
+ if (output !== void 0) output.cancel(reason).then(() => output.releaseLock(), () => output.releaseLock());
1020
+ }
1021
+ //#endregion
1022
+ //#region src/files/stat-file.ts
1023
+ const STAT_PAYLOAD_LENGTH = 45;
1024
+ async function statFile(container, path, options, operation) {
1025
+ const payload = await runFileCommand(container, {
1026
+ name: operation,
1027
+ paths: [path],
1028
+ options,
1029
+ error: {
1030
+ operation,
1031
+ path
1032
+ },
1033
+ expected: "data"
1034
+ });
1035
+ if (payload.length !== STAT_PAYLOAD_LENGTH) throw protocolError(`sandbox-shim returned invalid ${operation} data`);
1036
+ const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
1037
+ return {
1038
+ type: decodeFileType(payload[0]),
1039
+ size: view.getBigUint64(1, true),
1040
+ mode: view.getUint32(9, true),
1041
+ uid: view.getUint32(13, true),
1042
+ gid: view.getUint32(17, true),
1043
+ accessedAt: decodeDate(view.getBigInt64(21, true)),
1044
+ modifiedAt: decodeDate(view.getBigInt64(29, true)),
1045
+ changedAt: decodeDate(view.getBigInt64(37, true))
1046
+ };
1047
+ }
1048
+ function decodeDate(milliseconds) {
1049
+ const value = Number(milliseconds);
1050
+ if (!Number.isSafeInteger(value)) throw protocolError("sandbox-shim returned an out-of-range timestamp");
1051
+ const date = new Date(value);
1052
+ if (Number.isNaN(date.valueOf())) throw protocolError("sandbox-shim returned an out-of-range timestamp");
1053
+ return date;
1054
+ }
1055
+ //#endregion
1056
+ //#region src/files/write-file.ts
1057
+ async function writeFile(container, path, source, options) {
1058
+ let session;
1059
+ let control;
1060
+ let input;
1061
+ let sourceReader;
1062
+ try {
1063
+ session = await startFileCommand(container, {
1064
+ name: "write",
1065
+ paths: [path],
1066
+ options
1067
+ }, {
1068
+ stdin: "pipe",
1069
+ stdout: "pipe",
1070
+ stderr: "ignore"
1071
+ });
1072
+ control = session.openStdoutControl();
1073
+ input = session.openStdinWriter();
1074
+ const opening = await control.readFrame();
1075
+ if (opening.kind === "fileError") {
1076
+ await control.expectEnd();
1077
+ await session.settle();
1078
+ }
1079
+ expectSuccess(opening, path);
1080
+ sourceReader = source.getReader();
1081
+ const pumping = pumpSource(session, sourceReader, input);
1082
+ const terminal = terminalResult(session, control);
1083
+ const first = await Promise.race([pumping, terminal]);
1084
+ if (first.kind === "frame" || first.kind === "failure") {
1085
+ handleTerminal(first, path);
1086
+ handlePump(await pumping);
1087
+ } else if (first.kind === "sourceFailure") throw first.error;
1088
+ else if (first.kind === "inputFailure") {
1089
+ const result = await terminal;
1090
+ if (result.kind === "frame" && result.frame.kind === "fileError") expectSuccess(result.frame, path);
1091
+ if (result.kind === "failure") throw result.error;
1092
+ throw first.error;
1093
+ } else handleTerminal(await terminal, path);
1094
+ const exitCode = await session.waitFor(session.process.exitCode);
1095
+ if (exitCode !== 0) throw protocolError(`sandbox-shim exited with code ${exitCode}`);
1096
+ sourceReader.releaseLock();
1097
+ input.releaseLock();
1098
+ control.releaseLock();
1099
+ session.finish();
1100
+ } catch (error) {
1101
+ session?.terminate();
1102
+ if (input !== void 0) discardInput(input, error);
1103
+ control?.discard(error);
1104
+ if (sourceReader === void 0) source.cancel(error).catch(() => void 0);
1105
+ else discardSource(sourceReader, error);
1106
+ throw error;
1107
+ }
1108
+ }
1109
+ async function pumpSource(session, source, input) {
1110
+ while (true) {
1111
+ try {
1112
+ await session.waitFor(input.ready);
1113
+ } catch (error) {
1114
+ return {
1115
+ kind: "inputFailure",
1116
+ error
1117
+ };
1118
+ }
1119
+ let next;
1120
+ try {
1121
+ next = await session.waitFor(source.read());
1122
+ } catch (error) {
1123
+ return {
1124
+ kind: "sourceFailure",
1125
+ error
1126
+ };
1127
+ }
1128
+ if (next.done) try {
1129
+ await session.waitFor(input.close());
1130
+ return { kind: "complete" };
1131
+ } catch (error) {
1132
+ return {
1133
+ kind: "inputFailure",
1134
+ error
1135
+ };
1136
+ }
1137
+ if (!(next.value instanceof Uint8Array)) return {
1138
+ kind: "sourceFailure",
1139
+ error: /* @__PURE__ */ new TypeError("writeFile stream chunks must be Uint8Array values")
1140
+ };
1141
+ try {
1142
+ await session.waitFor(input.write(next.value));
1143
+ } catch (error) {
1144
+ return {
1145
+ kind: "inputFailure",
1146
+ error
1147
+ };
1148
+ }
1149
+ }
1150
+ }
1151
+ async function terminalResult(session, control) {
1152
+ try {
1153
+ const frame = await control.readFrame();
1154
+ await control.expectEnd();
1155
+ if (frame.kind === "fileError") await session.settle();
1156
+ return {
1157
+ kind: "frame",
1158
+ frame
1159
+ };
1160
+ } catch (error) {
1161
+ return {
1162
+ kind: "failure",
1163
+ error
1164
+ };
1165
+ }
1166
+ }
1167
+ function handleTerminal(result, path) {
1168
+ if (result.kind === "failure") throw result.error;
1169
+ expectSuccess(result.frame, path);
1170
+ }
1171
+ function handlePump(result) {
1172
+ if (result.kind !== "complete") throw result.error;
1173
+ }
1174
+ function expectSuccess(frame, path) {
1175
+ if (frame.kind === "fileError") throw fileErrorFromErrno({
1176
+ operation: "writeFile",
1177
+ path
1178
+ }, frame.errno, frame.detail);
1179
+ if (frame.kind !== "success") throw protocolError("sandbox-shim returned data while writing a file");
1180
+ }
1181
+ function discardInput(input, reason) {
1182
+ input.abort(reason).then(() => input.releaseLock(), () => input.releaseLock());
1183
+ }
1184
+ function discardSource(source, reason) {
1185
+ source.cancel(reason).then(() => source.releaseLock(), () => source.releaseLock());
1186
+ }
1187
+ //#endregion
1188
+ //#region src/files/files.ts
1189
+ const FLAG = {
1190
+ schema: z.boolean(),
1191
+ requirement: "must be a boolean"
1192
+ };
1193
+ const FILE_OPTIONS = {
1194
+ cwd: {
1195
+ schema: z.string().check(z.startsWith("/")),
1196
+ requirement: "must be an absolute path"
1197
+ },
1198
+ user: {
1199
+ schema: z.string().check(z.regex(/^[0-9]+:[0-9]+$/)),
1200
+ requirement: "must be numeric user and group IDs, as \"uid:gid\""
1201
+ },
1202
+ signal: {
1203
+ schema: z.instanceof(AbortSignal),
1204
+ requirement: "must be an AbortSignal"
1205
+ }
1206
+ };
1207
+ const MKDIR_OPTIONS = {
1208
+ ...FILE_OPTIONS,
1209
+ recursive: FLAG
1210
+ };
1211
+ const REMOVE_OPTIONS = {
1212
+ ...FILE_OPTIONS,
1213
+ recursive: FLAG,
1214
+ force: FLAG
1215
+ };
1216
+ /**
1217
+ * Structured file operations for a sandbox workspace.
1218
+ *
1219
+ * Operations run against the current native container execution. Its image must provide the matching shim at
1220
+ * `/usr/local/bin/sandbox-shim`.
1221
+ */
1222
+ var Files = class {
1223
+ #container;
1224
+ constructor(container) {
1225
+ this.#container = container;
1226
+ }
1227
+ /**
1228
+ * Streams bytes from a path in the running container using native Linux file semantics.
1229
+ *
1230
+ * Native container, transport, and abort failures propagate unchanged.
1231
+ *
1232
+ * @param path - Absolute path, or a relative path when `options.cwd` is provided.
1233
+ * @param options - Native execution options relevant to opening the file.
1234
+ * @returns A binary response whose body applies backpressure to the container process. A
1235
+ * file-streaming or native transport failure can surface while the body is consumed.
1236
+ * @throws {TypeError} The path is empty, contains NUL, or is relative without `cwd`, or an
1237
+ * option is unknown or invalid.
1238
+ * @throws {SandboxFileError} The container reports a filesystem failure before returning the
1239
+ * response. A late file-streaming failure errors the response body with the same error type.
1240
+ * @throws {SandboxProtocolError} The package and `sandbox-shim` cannot complete their protocol.
1241
+ */
1242
+ async readFile(path, options = {}) {
1243
+ validateOptions(options, FILE_OPTIONS);
1244
+ validatePath(path, options.cwd);
1245
+ return readFile(this.#container, path, options);
1246
+ }
1247
+ /**
1248
+ * Creates or truncates a file and streams content into it using native Linux semantics.
1249
+ *
1250
+ * The destination is opened before a caller-provided stream is consumed. Failures after that
1251
+ * point can leave a created, truncated, or partially written file. Native container, transport,
1252
+ * source-stream, and abort failures propagate unchanged.
1253
+ *
1254
+ * @throws {TypeError} The path is empty, contains NUL, or is relative without an absolute `cwd`,
1255
+ * or an option is unknown or invalid.
1256
+ * @throws {SandboxFileError} The container reports a filesystem failure.
1257
+ * @throws {SandboxProtocolError} The package and `sandbox-shim` cannot complete their protocol.
1258
+ */
1259
+ async writeFile(path, content, options = {}) {
1260
+ validateOptions(options, FILE_OPTIONS);
1261
+ validatePath(path, options.cwd);
1262
+ await writeFile(this.#container, path, fileContentStream(content), options);
1263
+ }
1264
+ /**
1265
+ * Returns metadata for a path using native Linux filesystem semantics.
1266
+ *
1267
+ * Native container, transport, and abort failures propagate unchanged.
1268
+ *
1269
+ * @throws {TypeError} The path is empty, contains NUL, or is relative without an absolute `cwd`,
1270
+ * or an option is unknown or invalid.
1271
+ * @throws {SandboxFileError} The container reports a filesystem failure.
1272
+ * @throws {SandboxProtocolError} The package and `sandbox-shim` cannot complete their protocol.
1273
+ */
1274
+ async stat(path, options = {}) {
1275
+ validateOptions(options, FILE_OPTIONS);
1276
+ validatePath(path, options.cwd);
1277
+ return statFile(this.#container, path, options, "stat");
1278
+ }
1279
+ /**
1280
+ * Returns metadata for a path without following its final symlink.
1281
+ *
1282
+ * Native container, transport, and abort failures propagate unchanged.
1283
+ *
1284
+ * @throws {TypeError} The path is empty, contains NUL, or is relative without an absolute `cwd`,
1285
+ * or an option is unknown or invalid.
1286
+ * @throws {SandboxFileError} The container reports a filesystem failure.
1287
+ * @throws {SandboxProtocolError} The package and `sandbox-shim` cannot complete their protocol.
1288
+ */
1289
+ async lstat(path, options = {}) {
1290
+ validateOptions(options, FILE_OPTIONS);
1291
+ validatePath(path, options.cwd);
1292
+ return statFile(this.#container, path, options, "lstat");
1293
+ }
1294
+ /**
1295
+ * Returns the immediate entries from a directory in native enumeration order.
1296
+ *
1297
+ * The directory path may resolve through a symlink, but entry types describe the entries
1298
+ * themselves and do not follow symlinks. The operation does not recurse or retrieve metadata
1299
+ * for each child.
1300
+ * Native container, transport, and abort failures propagate unchanged.
1301
+ *
1302
+ * @throws {TypeError} The path is empty, contains NUL, or is relative without an absolute `cwd`,
1303
+ * or an option is unknown or invalid.
1304
+ * @throws {SandboxFileError} The container reports a filesystem failure.
1305
+ * @throws {SandboxProtocolError} The package and `sandbox-shim` cannot complete their protocol.
1306
+ */
1307
+ async readDirectory(path, options = {}) {
1308
+ validateOptions(options, FILE_OPTIONS);
1309
+ validatePath(path, options.cwd);
1310
+ return readDirectory(this.#container, path, options);
1311
+ }
1312
+ /**
1313
+ * Creates a directory using native Linux filesystem semantics.
1314
+ *
1315
+ * By default only the final directory is created. With `recursive`, missing parents are
1316
+ * created and an existing target directory is accepted. Partial parent creation can remain
1317
+ * after failure or cancellation.
1318
+ * Native container, transport, and abort failures propagate unchanged.
1319
+ *
1320
+ * @throws {TypeError} The path is empty, contains NUL, or is relative without an absolute `cwd`,
1321
+ * or an option is unknown or invalid.
1322
+ * @throws {SandboxFileError} The container reports a filesystem failure.
1323
+ * @throws {SandboxProtocolError} The package and `sandbox-shim` cannot complete their protocol.
1324
+ */
1325
+ async mkdir(path, options = {}) {
1326
+ validateOptions(options, MKDIR_OPTIONS);
1327
+ validatePath(path, options.cwd);
1328
+ await runFileCommand(this.#container, {
1329
+ name: "mkdir",
1330
+ paths: [path],
1331
+ flags: options.recursive ? ["--recursive"] : [],
1332
+ options,
1333
+ error: {
1334
+ operation: "mkdir",
1335
+ path
1336
+ },
1337
+ expected: "success"
1338
+ });
1339
+ }
1340
+ /**
1341
+ * Renames a file, directory, or symlink using native Linux filesystem semantics.
1342
+ *
1343
+ * Existing destinations are replaced when Linux permits it. Cross-filesystem renames fail
1344
+ * with `EXDEV`; no copy-and-remove fallback is attempted.
1345
+ * Native container, transport, and abort failures propagate unchanged.
1346
+ *
1347
+ * @throws {TypeError} A path is empty, contains NUL, or is relative without an absolute `cwd`,
1348
+ * or an option is unknown or invalid.
1349
+ * @throws {SandboxFileError} The container reports a filesystem failure.
1350
+ * @throws {SandboxProtocolError} The package and `sandbox-shim` cannot complete their protocol.
1351
+ */
1352
+ async rename(source, destination, options = {}) {
1353
+ validateOptions(options, FILE_OPTIONS);
1354
+ validatePath(source, options.cwd);
1355
+ validatePath(destination, options.cwd);
1356
+ await runFileCommand(this.#container, {
1357
+ name: "rename",
1358
+ paths: [source, destination],
1359
+ options,
1360
+ error: {
1361
+ operation: "rename",
1362
+ path: source,
1363
+ destination
1364
+ },
1365
+ expected: "success"
1366
+ });
1367
+ }
1368
+ /**
1369
+ * Removes a file or symlink using native Linux filesystem semantics.
1370
+ *
1371
+ * Directories are rejected unless `recursive` is set. Recursive removal does not follow
1372
+ * symlinks and can leave partial effects after failure or cancellation. `force` ignores only
1373
+ * a missing target. Native container, transport, and abort failures propagate unchanged.
1374
+ *
1375
+ * @throws {TypeError} The path is empty, contains NUL, or is relative without an absolute `cwd`,
1376
+ * or an option is unknown or invalid.
1377
+ * @throws {SandboxFileError} The container reports a filesystem failure.
1378
+ * @throws {SandboxProtocolError} The package and `sandbox-shim` cannot complete their protocol.
1379
+ */
1380
+ async remove(path, options = {}) {
1381
+ validateOptions(options, REMOVE_OPTIONS);
1382
+ validatePath(path, options.cwd);
1383
+ const flags = [];
1384
+ if (options.recursive) flags.push("--recursive");
1385
+ if (options.force) flags.push("--force");
1386
+ await runFileCommand(this.#container, {
1387
+ name: "remove",
1388
+ paths: [path],
1389
+ flags,
1390
+ options,
1391
+ error: {
1392
+ operation: "remove",
1393
+ path
1394
+ },
1395
+ expected: "success"
1396
+ });
1397
+ }
1398
+ };
1399
+ function validatePath(path, cwd) {
1400
+ if (typeof path !== "string") throw new TypeError("path must be a string");
1401
+ if (path.length === 0) throw new TypeError("path must not be empty");
1402
+ if (path.includes("\0")) throw new TypeError("path cannot contain NUL characters");
1403
+ if (!path.startsWith("/") && cwd === void 0) throw new TypeError("cwd is required when path is relative");
1404
+ }
1405
+ //#endregion
1406
+ //#region src/shared/bounded-body.ts
1407
+ async function readBoundedBody(body, limit, cancelReason) {
1408
+ if (body === null) return { status: "absent" };
1409
+ const reader = body.getReader();
1410
+ const chunks = [];
1411
+ let length = 0;
1412
+ try {
1413
+ while (true) {
1414
+ const result = await reader.read();
1415
+ if (result.done) break;
1416
+ length += result.value.byteLength;
1417
+ if (length > limit) {
1418
+ await reader.cancel(cancelReason);
1419
+ return { status: "exceeded" };
1420
+ }
1421
+ chunks.push(result.value);
1422
+ }
1423
+ } finally {
1424
+ reader.releaseLock();
1425
+ }
1426
+ const bytes = new Uint8Array(length);
1427
+ let offset = 0;
1428
+ for (const chunk of chunks) {
1429
+ bytes.set(chunk, offset);
1430
+ offset += chunk.byteLength;
1431
+ }
1432
+ return {
1433
+ status: "complete",
1434
+ bytes
1435
+ };
1436
+ }
1437
+ //#endregion
1438
+ //#region src/s3-mounts/credentials.ts
1439
+ const CREDENTIAL_PROVIDER_URL = "https://credentials.sandbox.internal/";
1440
+ const MAX_CREDENTIAL_RESPONSE_BYTES = 16384;
1441
+ const decoder = new TextDecoder("utf-8", { fatal: true });
1442
+ const credentialResponseSchema = z.strictObject({
1443
+ accessKeyId: z.string().check(z.minLength(1), z.regex(/^[^\0]+$/)),
1444
+ secretAccessKey: z.string().check(z.minLength(1), z.regex(/^[^\0]+$/)),
1445
+ sessionToken: z.optional(z.string().check(z.minLength(1), z.regex(/^[^\0]+$/))),
1446
+ expiresAt: z.number().check(z.int())
1447
+ });
1448
+ var CredentialProviderError = class extends Error {
1449
+ constructor(message, options) {
1450
+ super(message, options);
1451
+ this.name = "CredentialProviderError";
1452
+ }
1453
+ };
1454
+ async function resolveS3Credentials(credentials, signal) {
1455
+ if (credentials.type === "static") return credentials;
1456
+ signal.throwIfAborted();
1457
+ let response;
1458
+ try {
1459
+ response = await credentials.fetcher.fetch(CREDENTIAL_PROVIDER_URL, {
1460
+ headers: { accept: "application/json" },
1461
+ signal
1462
+ });
1463
+ } catch (error) {
1464
+ if (signal.aborted) throw signal.reason;
1465
+ throw new CredentialProviderError("credential provider request failed", { cause: error });
1466
+ }
1467
+ if (!response.ok) throw new CredentialProviderError(`credential provider returned HTTP ${response.status}`);
1468
+ const credentialsResponse = await readCredentialResponse(response.body);
1469
+ if (credentialsResponse.expiresAt <= Date.now()) throw new CredentialProviderError("credential provider returned invalid or expired credentials");
1470
+ return {
1471
+ accessKeyId: credentialsResponse.accessKeyId,
1472
+ secretAccessKey: credentialsResponse.secretAccessKey,
1473
+ sessionToken: credentialsResponse.sessionToken
1474
+ };
1475
+ }
1476
+ async function readCredentialResponse(body) {
1477
+ const result = await readBoundedBody(body, MAX_CREDENTIAL_RESPONSE_BYTES, "credential provider response exceeded the gateway limit");
1478
+ if (result.status === "absent") throw new CredentialProviderError("credential provider returned invalid JSON");
1479
+ if (result.status === "exceeded") throw new CredentialProviderError("credential provider response is too large");
1480
+ try {
1481
+ const parsed = credentialResponseSchema.safeParse(JSON.parse(decoder.decode(result.bytes)));
1482
+ if (!parsed.success) throw new CredentialProviderError("credential provider returned invalid or expired credentials");
1483
+ return parsed.data;
1484
+ } catch (error) {
1485
+ if (error instanceof CredentialProviderError) throw error;
1486
+ throw new CredentialProviderError("credential provider returned invalid JSON", { cause: error });
1487
+ }
1488
+ }
1489
+ //#endregion
1490
+ //#region src/s3-mounts/route.ts
1491
+ function routeHost(routeId) {
1492
+ return `s3-${routeId}.sandbox.internal`;
1493
+ }
1494
+ //#endregion
1495
+ //#region src/s3-mounts/gateway-policy.ts
1496
+ const LIST_V1_PARAMETERS = /* @__PURE__ */ new Set([
1497
+ "delimiter",
1498
+ "encoding-type",
1499
+ "marker",
1500
+ "max-keys",
1501
+ "prefix"
1502
+ ]);
1503
+ const LIST_V2_PARAMETERS = /* @__PURE__ */ new Set([
1504
+ "continuation-token",
1505
+ "delimiter",
1506
+ "encoding-type",
1507
+ "fetch-owner",
1508
+ "list-type",
1509
+ "max-keys",
1510
+ "prefix",
1511
+ "start-after"
1512
+ ]);
1513
+ const SUPPORTED_AMZ_HEADERS = /* @__PURE__ */ new Set([
1514
+ "authorization",
1515
+ "x-amz-content-sha256",
1516
+ "x-amz-copy-source",
1517
+ "x-amz-copy-source-if-match",
1518
+ "x-amz-copy-source-if-modified-since",
1519
+ "x-amz-copy-source-if-none-match",
1520
+ "x-amz-copy-source-if-unmodified-since",
1521
+ "x-amz-copy-source-range",
1522
+ "x-amz-date",
1523
+ "x-amz-metadata-directive",
1524
+ "x-amz-security-token"
1525
+ ]);
1526
+ const COPY_CONTROL_HEADERS = [
1527
+ "x-amz-copy-source-if-match",
1528
+ "x-amz-copy-source-if-modified-since",
1529
+ "x-amz-copy-source-if-none-match",
1530
+ "x-amz-copy-source-if-unmodified-since",
1531
+ "x-amz-copy-source-range"
1532
+ ];
1533
+ async function authorizeS3Request(request, props) {
1534
+ const url = new URL(request.url);
1535
+ if (url.hostname !== routeHost(props.routeId)) return { detail: "request host does not match the mount route" };
1536
+ if (usesAwsChunkedPayload(request.headers)) return { detail: "aws-chunked payloads are not supported" };
1537
+ const unsupportedHeader = unsupportedAmzHeader(request.headers);
1538
+ if (unsupportedHeader !== void 0) return { detail: `S3 header ${unsupportedHeader} is not permitted for this mount` };
1539
+ const target = parseTarget(url.pathname);
1540
+ if (target === void 0 || target.bucket !== props.source.bucket) return { detail: "request is outside the mounted bucket" };
1541
+ const operation = classifyS3fsOperation(request.method.toUpperCase(), target.key, url);
1542
+ if (operation === void 0) return { detail: "request is not a supported s3fs operation" };
1543
+ if (props.access === "read-only" && operation.mutation) return { detail: "the mount is read-only" };
1544
+ if (operation.kind === "object") {
1545
+ if (!isObjectWithinPrefix(target.key, props.keyPrefix)) return { detail: "request is outside the mounted key prefix" };
1546
+ } else if (operation.kind === "list" && !isListWithinPrefix(operation.prefix, props.keyPrefix)) return { detail: "list request is outside the mounted key prefix" };
1547
+ const copySource = request.headers.get("x-amz-copy-source");
1548
+ if (copySource === null && COPY_CONTROL_HEADERS.some((name) => request.headers.has(name))) return { detail: "copy controls require a copy source" };
1549
+ if (copySource !== null) {
1550
+ if (operation.kind !== "object" || request.method.toUpperCase() !== "PUT") return { detail: "copy source is not valid for this operation" };
1551
+ if (!isScopedCopySource(copySource, props)) return { detail: "copy source is outside the mounted bucket or key prefix" };
1552
+ }
1553
+ const metadataDirective = request.headers.get("x-amz-metadata-directive");
1554
+ if (metadataDirective !== null && (copySource === null || metadataDirective !== "COPY" && metadataDirective !== "REPLACE")) return { detail: "invalid copy metadata directive" };
1555
+ if (hasMetadataHeader(request.headers) && (operation.kind !== "object" || request.method.toUpperCase() !== "PUT" && request.method.toUpperCase() !== "POST")) return { detail: "object metadata is not valid for this operation" };
1556
+ return {
1557
+ inspection: isInspectionRequest(request, operation, props),
1558
+ upstreamUrl: upstreamUrl(url, props.source.endpoint)
1559
+ };
1560
+ }
1561
+ function unsupportedAmzHeader(headers) {
1562
+ for (const [name] of headers) {
1563
+ const lowerName = name.toLowerCase();
1564
+ if (lowerName.startsWith("x-amz-") && !lowerName.startsWith("x-amz-meta-") && !SUPPORTED_AMZ_HEADERS.has(lowerName)) return lowerName;
1565
+ }
1566
+ }
1567
+ function hasMetadataHeader(headers) {
1568
+ for (const [name] of headers) if (name.toLowerCase().startsWith("x-amz-meta-")) return true;
1569
+ return false;
1570
+ }
1571
+ function isAuthorizedS3Request(authorization) {
1572
+ return "upstreamUrl" in authorization;
1573
+ }
1574
+ function classifyS3fsOperation(method, key, url) {
1575
+ if (hasDuplicateParameters(url.searchParams)) return void 0;
1576
+ if (key === "") return classifyBucketOperation(method, url.searchParams);
1577
+ return classifyObjectOperation(method, url.searchParams);
1578
+ }
1579
+ function classifyBucketOperation(method, parameters) {
1580
+ if (method === "HEAD" && parameters.size === 0) return {
1581
+ kind: "bucket",
1582
+ mutation: false
1583
+ };
1584
+ if (method !== "GET") return void 0;
1585
+ if (hasExactEmptyParameters(parameters, ["location"])) return {
1586
+ kind: "bucket",
1587
+ mutation: false
1588
+ };
1589
+ return classifyListOperation(parameters);
1590
+ }
1591
+ function classifyListOperation(parameters) {
1592
+ const listType = parameters.get("list-type");
1593
+ if (listType === "2") {
1594
+ if (!hasOnlyAllowedParameters(parameters, LIST_V2_PARAMETERS)) return void 0;
1595
+ if (!hasValidListParameters(parameters, 2)) return void 0;
1596
+ return {
1597
+ kind: "list",
1598
+ mutation: false,
1599
+ prefix: parameters.get("prefix") ?? void 0,
1600
+ version: 2
1601
+ };
1602
+ }
1603
+ if (listType !== null || !hasOnlyAllowedParameters(parameters, LIST_V1_PARAMETERS)) return;
1604
+ if (!hasValidListParameters(parameters, 1)) return void 0;
1605
+ return {
1606
+ kind: "list",
1607
+ mutation: false,
1608
+ prefix: parameters.get("prefix") ?? void 0,
1609
+ version: 1
1610
+ };
1611
+ }
1612
+ function classifyObjectOperation(method, parameters) {
1613
+ if (parameters.size === 0) {
1614
+ if (method === "GET" || method === "HEAD") return {
1615
+ kind: "object",
1616
+ mutation: false
1617
+ };
1618
+ if (method === "PUT" || method === "DELETE") return {
1619
+ kind: "object",
1620
+ mutation: true
1621
+ };
1622
+ return;
1623
+ }
1624
+ if (method === "POST" && hasExactEmptyParameters(parameters, ["uploads"])) return {
1625
+ kind: "object",
1626
+ mutation: true
1627
+ };
1628
+ if (method === "PUT" && hasExactParameters(parameters, ["partNumber", "uploadId"]) && isIntegerInRange(parameters.get("partNumber"), 1, 1e4) && isNonEmpty(parameters.get("uploadId"))) return {
1629
+ kind: "object",
1630
+ mutation: true
1631
+ };
1632
+ if ((method === "POST" || method === "DELETE") && hasExactParameters(parameters, ["uploadId"]) && isNonEmpty(parameters.get("uploadId"))) return {
1633
+ kind: "object",
1634
+ mutation: true
1635
+ };
1636
+ }
1637
+ function hasValidListParameters(parameters, version) {
1638
+ const maxKeys = parameters.get("max-keys");
1639
+ if (maxKeys !== null && !isIntegerInRange(maxKeys, 0, 1e3)) return false;
1640
+ const encodingType = parameters.get("encoding-type");
1641
+ if (encodingType !== null && encodingType !== "url") return false;
1642
+ if (version === 2) {
1643
+ const fetchOwner = parameters.get("fetch-owner");
1644
+ if (fetchOwner !== null && fetchOwner !== "true" && fetchOwner !== "false") return false;
1645
+ if (parameters.has("continuation-token") && parameters.has("start-after")) return false;
1646
+ if (parameters.has("continuation-token") && !isNonEmpty(parameters.get("continuation-token"))) return false;
1647
+ }
1648
+ return true;
1649
+ }
1650
+ function parseTarget(pathname) {
1651
+ if (!pathname.startsWith("/")) return void 0;
1652
+ const separator = pathname.indexOf("/", 1);
1653
+ const encodedBucket = separator === -1 ? pathname.slice(1) : pathname.slice(1, separator);
1654
+ if (encodedBucket === "") return void 0;
1655
+ const encodedKey = separator === -1 ? "" : pathname.slice(separator + 1);
1656
+ try {
1657
+ return {
1658
+ bucket: decodeURIComponent(encodedBucket),
1659
+ key: decodeURIComponent(encodedKey)
1660
+ };
1661
+ } catch {
1662
+ return;
1663
+ }
1664
+ }
1665
+ function isObjectWithinPrefix(key, prefix) {
1666
+ if (prefix === void 0) return true;
1667
+ return key === prefix.slice(0, -1) || key.startsWith(prefix);
1668
+ }
1669
+ function isListWithinPrefix(value, prefix) {
1670
+ if (prefix === void 0) return true;
1671
+ return value !== void 0 && (value === prefix || value.startsWith(prefix));
1672
+ }
1673
+ function hasDuplicateParameters(parameters) {
1674
+ const names = /* @__PURE__ */ new Set();
1675
+ for (const name of parameters.keys()) {
1676
+ if (names.has(name)) return true;
1677
+ names.add(name);
1678
+ }
1679
+ return false;
1680
+ }
1681
+ function hasOnlyAllowedParameters(parameters, allowed) {
1682
+ for (const name of parameters.keys()) if (!allowed.has(name)) return false;
1683
+ return true;
1684
+ }
1685
+ function hasExactParameters(parameters, expected) {
1686
+ if (parameters.size !== expected.length) return false;
1687
+ return expected.every((name) => parameters.has(name));
1688
+ }
1689
+ function hasExactEmptyParameters(parameters, expected) {
1690
+ return hasExactParameters(parameters, expected) && expected.every((name) => parameters.get(name) === "");
1691
+ }
1692
+ function isIntegerInRange(value, minimum, maximum) {
1693
+ if (value === null) return false;
1694
+ if (!/^(?:0|[1-9][0-9]*)$/.test(value)) return false;
1695
+ const parsed = Number(value);
1696
+ return Number.isSafeInteger(parsed) && parsed >= minimum && parsed <= maximum;
1697
+ }
1698
+ function isNonEmpty(value) {
1699
+ return value !== null && value !== "";
1700
+ }
1701
+ function isScopedCopySource(value, props) {
1702
+ if (value === "" || value.includes("?")) return false;
1703
+ let decodedPath;
1704
+ try {
1705
+ decodedPath = decodeURIComponent(value);
1706
+ } catch {
1707
+ return false;
1708
+ }
1709
+ const path = decodedPath.startsWith("/") ? decodedPath.slice(1) : decodedPath;
1710
+ const separator = path.indexOf("/");
1711
+ if (separator <= 0 || separator === path.length - 1) return false;
1712
+ const key = path.slice(separator + 1);
1713
+ return path.slice(0, separator) === props.source.bucket && isObjectWithinPrefix(key, props.keyPrefix);
1714
+ }
1715
+ function isInspectionRequest(request, operation, props) {
1716
+ if (operation.kind !== "list" || operation.version !== 2 || request.headers.get("user-agent") !== "sandbox-shim/1") return false;
1717
+ return new URL(request.url).searchParams.get("max-keys") === "1" && operation.prefix === props.keyPrefix;
1718
+ }
1719
+ function upstreamUrl(url, endpoint) {
1720
+ const upstream = new URL(endpoint);
1721
+ upstream.pathname = url.pathname;
1722
+ upstream.search = url.search;
1723
+ return upstream.toString();
1724
+ }
1725
+ function usesAwsChunkedPayload(headers) {
1726
+ if (headers.get("x-amz-content-sha256")?.startsWith("STREAMING-") === true) return true;
1727
+ return (headers.get("content-encoding") ?? "").split(",").some((encoding) => encoding.trim().toLowerCase() === "aws-chunked");
1728
+ }
1729
+ //#endregion
1730
+ //#region src/s3-mounts/gateway.ts
1731
+ const INSPECTION_VERSION_HEADER = "x-sandbox-s3-gateway-version";
1732
+ const INSPECTION_RESULT_HEADER = "x-sandbox-s3-inspection-result";
1733
+ const INSPECTION_DETAIL_HEADER = "x-sandbox-s3-inspection-detail";
1734
+ const MAX_INSPECTION_DETAIL_LENGTH = 1024;
1735
+ const MAX_S3_ERROR_BODY_BYTES = 65536;
1736
+ const FORWARDED_HEADERS = /* @__PURE__ */ new Set([
1737
+ "content-length",
1738
+ "content-md5",
1739
+ "content-type",
1740
+ "if-match",
1741
+ "if-modified-since",
1742
+ "if-none-match",
1743
+ "if-unmodified-since",
1744
+ "range",
1745
+ "x-amz-copy-source",
1746
+ "x-amz-copy-source-if-match",
1747
+ "x-amz-copy-source-if-modified-since",
1748
+ "x-amz-copy-source-if-none-match",
1749
+ "x-amz-copy-source-if-unmodified-since",
1750
+ "x-amz-copy-source-range",
1751
+ "x-amz-metadata-directive"
1752
+ ]);
1753
+ async function handleS3GatewayRequest(request, props) {
1754
+ if (props.protocolVersion !== 1) return inspectionResponse("gateway-protocol", "gateway protocol is incompatible", 500);
1755
+ if (props.mode === "deny") return inspectionResponse("rejected-access", "mount route has been revoked", 403);
1756
+ const authorization = await authorizeS3Request(request, props);
1757
+ if (!isAuthorizedS3Request(authorization)) return inspectionResponse("rejected-access", authorization.detail, 403);
1758
+ let credentials;
1759
+ try {
1760
+ credentials = await resolveS3Credentials(props.source.credentials, request.signal);
1761
+ } catch (error) {
1762
+ if (request.signal.aborted) throw request.signal.reason;
1763
+ return inspectionResponse("gateway-credential-provider", error instanceof CredentialProviderError ? error.message : "credential provider failed", 503);
1764
+ }
1765
+ const client = new AwsClient({
1766
+ accessKeyId: credentials.accessKeyId,
1767
+ secretAccessKey: credentials.secretAccessKey,
1768
+ sessionToken: credentials.sessionToken,
1769
+ service: "s3",
1770
+ region: props.source.region,
1771
+ retries: 0
1772
+ });
1773
+ let upstream;
1774
+ try {
1775
+ upstream = await client.fetch(authorization.upstreamUrl, {
1776
+ method: request.method,
1777
+ headers: forwardingHeaders(request.headers),
1778
+ body: request.body,
1779
+ signal: request.signal
1780
+ });
1781
+ } catch (error) {
1782
+ if (!authorization.inspection) throw error;
1783
+ return inspectionResponse("unavailable", error instanceof Error ? error.message : "upstream request failed", 503);
1784
+ }
1785
+ if (!authorization.inspection) return upstream;
1786
+ return classifyInspectionResponse(upstream);
1787
+ }
1788
+ function forwardingHeaders(incoming) {
1789
+ const headers = new Headers();
1790
+ for (const [name, value] of incoming) {
1791
+ const lowerName = name.toLowerCase();
1792
+ if (lowerName === "x-amz-content-sha256") {
1793
+ if (isPayloadHash(value)) headers.set(name, value);
1794
+ continue;
1795
+ }
1796
+ if (FORWARDED_HEADERS.has(lowerName) || lowerName.startsWith("x-amz-meta-")) headers.set(name, value);
1797
+ }
1798
+ if (!headers.has("x-amz-content-sha256")) headers.set("x-amz-content-sha256", "UNSIGNED-PAYLOAD");
1799
+ return headers;
1800
+ }
1801
+ function isPayloadHash(value) {
1802
+ return value === "UNSIGNED-PAYLOAD" || /^[a-fA-F0-9]{64}$/.test(value);
1803
+ }
1804
+ async function classifyInspectionResponse(response) {
1805
+ if (response.ok) return inspectionResponse("usable", "upstream list request succeeded", response.status);
1806
+ if (response.status === 401) return inspectionResponse("rejected-credentials", `upstream rejected credentials with HTTP ${response.status}`, response.status);
1807
+ if (response.status === 403) {
1808
+ const code = await readS3ErrorCode(response.body);
1809
+ return inspectionResponse(code !== void 0 && CREDENTIAL_ERROR_CODES.has(code) ? "rejected-credentials" : "rejected-access", code === void 0 ? "upstream denied the inspection request" : `upstream denied the inspection request with ${code}`, response.status);
1810
+ }
1811
+ if (response.status === 404) return inspectionResponse("rejected-not-found", "upstream bucket was not found", response.status);
1812
+ if (response.status === 429 || response.status >= 500) return inspectionResponse("unavailable", `upstream returned HTTP ${response.status}`, response.status);
1813
+ return inspectionResponse("rejected-other", `upstream rejected the request with HTTP ${response.status}`, response.status);
1814
+ }
1815
+ const CREDENTIAL_ERROR_CODES = /* @__PURE__ */ new Set([
1816
+ "AuthorizationHeaderMalformed",
1817
+ "ExpiredToken",
1818
+ "InvalidAccessKeyId",
1819
+ "InvalidToken",
1820
+ "RequestTimeTooSkewed",
1821
+ "SignatureDoesNotMatch",
1822
+ "TokenRefreshRequired"
1823
+ ]);
1824
+ async function readS3ErrorCode(body) {
1825
+ const result = await readBoundedBody(body, MAX_S3_ERROR_BODY_BYTES, "S3 error body exceeded the inspection limit");
1826
+ if (result.status !== "complete") return void 0;
1827
+ return /<Code>([A-Za-z0-9]+)<\/Code>/.exec(new TextDecoder().decode(result.bytes))?.[1];
1828
+ }
1829
+ function inspectionResponse(result, detail, status) {
1830
+ const boundedDetail = detail.slice(0, MAX_INSPECTION_DETAIL_LENGTH);
1831
+ return new Response(null, {
1832
+ status,
1833
+ headers: {
1834
+ [INSPECTION_VERSION_HEADER]: "1",
1835
+ [INSPECTION_RESULT_HEADER]: result,
1836
+ [INSPECTION_DETAIL_HEADER]: encodeURIComponent(boundedDetail)
1837
+ }
1838
+ });
1839
+ }
1840
+ //#endregion
1841
+ //#region src/s3-mounts/s3-gateway.ts
1842
+ var S3Gateway = class extends WorkerEntrypoint {
1843
+ fetch(request) {
1844
+ return handleS3GatewayRequest(request, this.ctx.props);
1845
+ }
1846
+ };
1847
+ //#endregion
1848
+ //#region src/s3-mounts/request.ts
1849
+ const objectSchema = z.object({});
1850
+ const stringSchema = z.string();
1851
+ const fetcherSchema = z.object({ fetch: z.function() });
1852
+ const finiteNumberSchema = z.number().check(z.refine(Number.isFinite));
1853
+ const RESERVED_GUEST_PATHS = [
1854
+ "/proc/self/mountinfo",
1855
+ "/run/sandbox/s3-mounts",
1856
+ "/usr/local/bin/sandbox-shim"
1857
+ ];
1858
+ const RESERVED_S3FS_OPTIONS = /* @__PURE__ */ new Set([
1859
+ "ahbe_conf",
1860
+ "allow_other",
1861
+ "compat_dir",
1862
+ "credlib",
1863
+ "ecs",
1864
+ "endpoint",
1865
+ "f",
1866
+ "fg",
1867
+ "foreground",
1868
+ "fsname",
1869
+ "host",
1870
+ "iam_role",
1871
+ "ibm_iam_auth",
1872
+ "logfile",
1873
+ "nomixupload",
1874
+ "noproxy",
1875
+ "passwd_file",
1876
+ "profile",
1877
+ "proxy",
1878
+ "proxy_cred_file",
1879
+ "public_bucket",
1880
+ "ro",
1881
+ "rw",
1882
+ "subtype",
1883
+ "use_path_request_style",
1884
+ "use_proxy",
1885
+ "use_session_token",
1886
+ "url"
1887
+ ]);
1888
+ function canonicalizeS3MountRequest(request) {
1889
+ if (!objectSchema.safeParse(request).success) throw new TypeError("request must be an object");
1890
+ const mountPath = canonicalizeMountPath(request.mountPath);
1891
+ if (RESERVED_GUEST_PATHS.some((reserved) => pathsOverlap(mountPath, reserved))) throw new TypeError("mountPath overlaps files required by @cloudflare/sandbox");
1892
+ return {
1893
+ mountPath,
1894
+ source: canonicalizeSource(request.source),
1895
+ keyPrefix: canonicalizeKeyPrefix(request.keyPrefix),
1896
+ access: canonicalizeAccess(request.access),
1897
+ s3fsOptions: canonicalizeS3fsOptions(request.s3fsOptions)
1898
+ };
1899
+ }
1900
+ function canonicalizeMountPath(value) {
1901
+ const parsed = stringSchema.safeParse(value);
1902
+ if (!parsed.success || !parsed.data.startsWith("/") || parsed.data.includes("\0")) throw new TypeError("mountPath must be an absolute path without NUL bytes");
1903
+ value = parsed.data;
1904
+ if (value === "/") throw new TypeError("mountPath must not be the filesystem root");
1905
+ const segments = value.split("/");
1906
+ if (value.endsWith("/") || segments.some((segment, index) => index > 0 && (segment === "" || segment === "." || segment === ".."))) throw new TypeError("mountPath must be a normalized non-root path");
1907
+ return value;
1908
+ }
1909
+ function observedConfiguration(request) {
1910
+ return {
1911
+ source: {
1912
+ type: "s3",
1913
+ endpoint: request.source.endpoint,
1914
+ region: request.source.region,
1915
+ bucket: request.source.bucket
1916
+ },
1917
+ keyPrefix: request.keyPrefix,
1918
+ access: request.access,
1919
+ s3fsOptions: request.s3fsOptions
1920
+ };
1921
+ }
1922
+ function pathsOverlap(left, right) {
1923
+ return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
1924
+ }
1925
+ function canonicalizeAccess(value) {
1926
+ if (value !== "read-only" && value !== "read-write") throw new TypeError("access must be \"read-only\" or \"read-write\"");
1927
+ return value;
1928
+ }
1929
+ function canonicalizeKeyPrefix(value) {
1930
+ if (value === void 0 || value === "") return void 0;
1931
+ const parsed = stringSchema.safeParse(value);
1932
+ if (!parsed.success || parsed.data.startsWith("/") || parsed.data.includes("\0")) throw new TypeError("keyPrefix must not start with '/' or contain NUL bytes");
1933
+ return `${parsed.data.replace(/\/+$/, "")}/`;
1934
+ }
1935
+ function canonicalizeSource(source) {
1936
+ if (!objectSchema.safeParse(source).success) throw new TypeError("source must be an object");
1937
+ if (source.type !== "s3") throw new TypeError("source.type must be \"s3\"");
1938
+ const bucket = requiredString(source.bucket, "source.bucket");
1939
+ if (bucket.startsWith("-") || bucket.includes("/") || bucket.includes(":")) throw new TypeError("source.bucket must not start with '-' or contain '/' or ':'");
1940
+ return {
1941
+ type: "s3",
1942
+ endpoint: canonicalizeEndpoint(source.endpoint),
1943
+ region: requiredString(source.region, "source.region"),
1944
+ bucket,
1945
+ credentials: canonicalizeCredentials(source.credentials)
1946
+ };
1947
+ }
1948
+ function canonicalizeEndpoint(value) {
1949
+ const endpoint = requiredString(value, "source.endpoint");
1950
+ let parsed;
1951
+ try {
1952
+ parsed = new URL(endpoint);
1953
+ } catch {
1954
+ throw new TypeError("source.endpoint must be a valid URL");
1955
+ }
1956
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new TypeError("source.endpoint must use HTTP or HTTPS");
1957
+ if (parsed.username !== "" || parsed.password !== "" || parsed.pathname !== "/" || parsed.search !== "" || parsed.hash !== "") throw new TypeError("source.endpoint must be an HTTP(S) origin without credentials, a path, query, or fragment");
1958
+ return parsed.toString();
1959
+ }
1960
+ function canonicalizeCredentials(credentials) {
1961
+ if (!objectSchema.safeParse(credentials).success) throw new TypeError("source.credentials must be an object");
1962
+ if (credentials.type === "static") return {
1963
+ type: "static",
1964
+ accessKeyId: requiredString(credentials.accessKeyId, "credentials.accessKeyId"),
1965
+ secretAccessKey: requiredString(credentials.secretAccessKey, "credentials.secretAccessKey"),
1966
+ sessionToken: credentials.sessionToken === void 0 ? void 0 : requiredString(credentials.sessionToken, "credentials.sessionToken")
1967
+ };
1968
+ if (credentials.type === "provider") {
1969
+ if (!fetcherSchema.safeParse(credentials.fetcher).success) throw new TypeError("provider credentials require a Fetcher");
1970
+ return {
1971
+ type: "provider",
1972
+ fetcher: credentials.fetcher
1973
+ };
1974
+ }
1975
+ throw new TypeError("credentials.type must be \"static\" or \"provider\"");
1976
+ }
1977
+ function canonicalizeS3fsOptions(options) {
1978
+ if (options === void 0) return [];
1979
+ if (!objectSchema.safeParse(options).success) throw new TypeError("s3fsOptions must be an object");
1980
+ const normalized = [];
1981
+ for (const [name, value] of Object.entries(options)) {
1982
+ if (name === "" || name.includes("\0") || name.includes(",") || name.includes("=")) throw new TypeError("s3fs option names must not be empty or contain NUL, ',' or '='");
1983
+ if (RESERVED_S3FS_OPTIONS.has(name.toLowerCase())) throw new TypeError(`s3fs option "${name}" is owned by @cloudflare/sandbox`);
1984
+ if (value === false) continue;
1985
+ if (value === true) {
1986
+ normalized.push({ name });
1987
+ continue;
1988
+ }
1989
+ const numberValue = finiteNumberSchema.safeParse(value);
1990
+ if (numberValue.success) {
1991
+ normalized.push({
1992
+ name,
1993
+ value: String(numberValue.data)
1994
+ });
1995
+ continue;
1996
+ }
1997
+ const stringValue = stringSchema.safeParse(value);
1998
+ if (!stringValue.success || stringValue.data.includes("\0") || stringValue.data.includes(",")) throw new TypeError(`s3fs option "${name}" has an invalid value`);
1999
+ normalized.push({
2000
+ name,
2001
+ value: stringValue.data
2002
+ });
2003
+ }
2004
+ return normalized.sort((left, right) => {
2005
+ if (left.name < right.name) return -1;
2006
+ if (left.name > right.name) return 1;
2007
+ return 0;
2008
+ });
2009
+ }
2010
+ function requiredString(value, name) {
2011
+ const parsed = stringSchema.safeParse(value);
2012
+ if (!parsed.success || parsed.data === "" || parsed.data.includes("\0")) throw new TypeError(`${name} must be a non-empty string without NUL bytes`);
2013
+ return parsed.data;
2014
+ }
2015
+ //#endregion
2016
+ //#region src/s3-mounts/protocol.ts
2017
+ const ROUTE_READY = new Uint8Array([1]);
2018
+ const routeIdSchema = z.string().check(z.regex(/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,58}[A-Za-z0-9])?$/));
2019
+ const keyPrefixSchema = z.optional(z.string().check(z.refine((value) => value.endsWith("/"))));
2020
+ const s3fsOptionSchema = z.object({
2021
+ name: z.string(),
2022
+ value: z.optional(z.string())
2023
+ });
2024
+ const observedConfigurationSchema = z.object({
2025
+ source: z.object({
2026
+ type: z.literal("s3"),
2027
+ endpoint: z.string(),
2028
+ region: z.string(),
2029
+ bucket: z.string()
2030
+ }),
2031
+ keyPrefix: keyPrefixSchema,
2032
+ access: z.union([z.literal("read-only"), z.literal("read-write")]),
2033
+ s3fsOptions: z.array(s3fsOptionSchema)
2034
+ });
2035
+ const markerSchema = z.object({
2036
+ protocolVersion: z.literal(1),
2037
+ routeId: routeIdSchema,
2038
+ mountPath: z.string(),
2039
+ configuration: observedConfigurationSchema
2040
+ });
2041
+ const fuseSchema = z.discriminatedUnion("status", [
2042
+ z.object({ status: z.literal("connected") }),
2043
+ z.object({ status: z.literal("disconnected") }),
2044
+ z.object({
2045
+ status: z.literal("indeterminate"),
2046
+ detail: z.string()
2047
+ })
2048
+ ]);
2049
+ const guestStateSchema = z.discriminatedUnion("kind", [
2050
+ z.object({ kind: z.literal("absent") }),
2051
+ z.object({
2052
+ kind: z.literal("unmanaged"),
2053
+ filesystemType: z.string()
2054
+ }),
2055
+ z.object({
2056
+ kind: z.literal("incompatible"),
2057
+ protocolVersion: z.number().check(z.int())
2058
+ }),
2059
+ z.object({
2060
+ kind: z.literal("stale"),
2061
+ marker: markerSchema
2062
+ }),
2063
+ z.object({
2064
+ kind: z.literal("managed"),
2065
+ marker: markerSchema,
2066
+ fuse: fuseSchema
2067
+ })
2068
+ ]);
2069
+ const gatewayStateSchema = z.discriminatedUnion("kind", [
2070
+ z.object({
2071
+ kind: z.literal("gatewayUnreachable"),
2072
+ detail: z.string()
2073
+ }),
2074
+ z.object({
2075
+ kind: z.literal("gatewayError"),
2076
+ reason: z.union([
2077
+ z.literal("credential-provider"),
2078
+ z.literal("protocol"),
2079
+ z.literal("internal")
2080
+ ]),
2081
+ detail: z.string()
2082
+ }),
2083
+ z.object({ kind: z.literal("usable") }),
2084
+ z.object({
2085
+ kind: z.literal("upstreamUnavailable"),
2086
+ detail: z.string()
2087
+ }),
2088
+ z.object({
2089
+ kind: z.literal("upstreamRejected"),
2090
+ reason: z.union([
2091
+ z.literal("credentials"),
2092
+ z.literal("access"),
2093
+ z.literal("not-found"),
2094
+ z.literal("other")
2095
+ ]),
2096
+ detail: z.string()
2097
+ })
2098
+ ]);
2099
+ const inspectionEvidenceSchema = z.object({
2100
+ state: guestStateSchema,
2101
+ gateway: z.optional(gatewayStateSchema)
2102
+ });
2103
+ const envelopeSchema = z.discriminatedUnion("ok", [z.object({
2104
+ ok: z.literal(true),
2105
+ value: z.json()
2106
+ }), z.object({
2107
+ ok: z.literal(false),
2108
+ error: z.object({
2109
+ kind: z.string(),
2110
+ detail: z.string()
2111
+ })
2112
+ })]);
2113
+ const routeSelectionSchema = z.object({
2114
+ kind: z.literal("route"),
2115
+ routeId: routeIdSchema
2116
+ });
2117
+ function mountGuest(container, request, options, installRoute) {
2118
+ return invokeInteractive(container, ["mount", JSON.stringify(request)], "mount", request.mountPath, options, false, installRoute);
2119
+ }
2120
+ function inspectGuestMount(container, mountPath, options) {
2121
+ return invokeOnce(container, ["inspect", mountPath], "inspect", mountPath, options, (value) => parseInspectionEvidence(value, mountPath));
2122
+ }
2123
+ function unmountGuest(container, mountPath, options, denyRoute) {
2124
+ return invokeInteractive(container, ["unmount", mountPath], "unmount", mountPath, options, true, denyRoute);
2125
+ }
2126
+ async function invokeInteractive(container, command, operation, mountPath, options, allowImmediateCompletion, handleRoute) {
2127
+ const session = await startSession(container, command, options, true);
2128
+ let control;
2129
+ let input;
2130
+ try {
2131
+ control = session.openStdoutControl();
2132
+ const first = await readEnvelope(control, operation, mountPath);
2133
+ if (first === null) {
2134
+ if (!allowImmediateCompletion) throw protocolError("sandbox-shim returned invalid S3 mount route selection");
2135
+ await finishSession(session, control);
2136
+ session.finish();
2137
+ return;
2138
+ }
2139
+ const routeId = parseRouteSelection(first);
2140
+ if (routeId === void 0) throw protocolError("sandbox-shim returned invalid S3 mount route selection");
2141
+ await session.waitFor(handleRoute(routeId));
2142
+ options.signal?.throwIfAborted();
2143
+ input = session.openStdinWriter();
2144
+ await session.waitFor(input.write(ROUTE_READY));
2145
+ await session.waitFor(input.close());
2146
+ input.releaseLock();
2147
+ input = void 0;
2148
+ let terminal;
2149
+ try {
2150
+ terminal = await readEnvelope(control, operation, mountPath);
2151
+ } catch (error) {
2152
+ if (!SandboxS3MountError.is(error)) throw error;
2153
+ await finishSession(session, control);
2154
+ session.finish();
2155
+ control = void 0;
2156
+ throw error;
2157
+ }
2158
+ if (terminal !== null) throw protocolError("sandbox-shim returned invalid S3 mount completion data");
2159
+ await finishSession(session, control);
2160
+ session.finish();
2161
+ } catch (error) {
2162
+ session.terminate();
2163
+ if (input !== void 0) input.abort(error).then(() => input?.releaseLock(), () => input?.releaseLock());
2164
+ control?.discard(error);
2165
+ throw error;
2166
+ }
2167
+ }
2168
+ async function invokeOnce(container, command, operation, mountPath, options, parseValue) {
2169
+ const session = await startSession(container, command, options, false);
2170
+ let control;
2171
+ try {
2172
+ control = session.openStdoutControl();
2173
+ const parsed = parseValue(await readEnvelope(control, operation, mountPath));
2174
+ if (parsed === void 0) throw protocolError("sandbox-shim returned invalid S3 mount command data");
2175
+ await finishSession(session, control);
2176
+ session.finish();
2177
+ return parsed;
2178
+ } catch (error) {
2179
+ session.terminate();
2180
+ control?.discard(error);
2181
+ throw error;
2182
+ }
2183
+ }
2184
+ function startSession(container, command, options, interactive) {
2185
+ const execOptions = {
2186
+ signal: options.signal,
2187
+ stdout: "pipe",
2188
+ stderr: "ignore"
2189
+ };
2190
+ if (interactive) execOptions.stdin = "pipe";
2191
+ return ShimSession.start(container, [
2192
+ SHIM_PATH,
2193
+ "s3-mount",
2194
+ ...command
2195
+ ], execOptions);
2196
+ }
2197
+ async function finishSession(session, control) {
2198
+ await control.expectEnd();
2199
+ const exitCode = await session.waitFor(session.process.exitCode);
2200
+ if (exitCode !== 0) throw protocolError(`sandbox-shim exited with code ${exitCode}`);
2201
+ control.releaseLock();
2202
+ }
2203
+ async function readEnvelope(control, operation, mountPath) {
2204
+ const frame = await control.readFrame();
2205
+ if (frame.kind !== "data") throw protocolError("sandbox-shim did not return S3 mount command data");
2206
+ return decodeEnvelope(parseJsonPayload(frame.payload, "sandbox-shim returned invalid S3 mount command data"), operation, mountPath);
2207
+ }
2208
+ function decodeEnvelope(value, operation, mountPath) {
2209
+ const parsed = envelopeSchema.safeParse(value);
2210
+ if (!parsed.success) throw protocolError("sandbox-shim returned an invalid S3 mount result");
2211
+ if (!parsed.data.ok) {
2212
+ const { detail, kind } = parsed.data.error;
2213
+ if (kind === "protocol") throw protocolError(detail);
2214
+ const code = errorCode(kind);
2215
+ if (code === void 0) throw protocolError(`sandbox-shim returned unknown S3 mount error kind "${kind}"`);
2216
+ throw s3MountError(code, operation, mountPath, detail);
2217
+ }
2218
+ return parsed.data.value;
2219
+ }
2220
+ function errorCode(kind) {
2221
+ switch (kind) {
2222
+ case "busy": return "S3_MOUNT_BUSY";
2223
+ case "conflict": return "S3_MOUNT_CONFLICT";
2224
+ case "failed": return "S3_MOUNT_FAILED";
2225
+ case "incompatible": return "S3_MOUNT_INCOMPATIBLE";
2226
+ default: return;
2227
+ }
2228
+ }
2229
+ function parseRouteSelection(value) {
2230
+ const selection = routeSelectionSchema.safeParse(value);
2231
+ return selection.success ? selection.data.routeId : void 0;
2232
+ }
2233
+ function parseInspectionEvidence(value, expectedMountPath) {
2234
+ const evidence = inspectionEvidenceSchema.safeParse(value);
2235
+ if (!evidence.success) return void 0;
2236
+ const state = parseGuestMountState(evidence.data.state, expectedMountPath);
2237
+ if (state === void 0) return void 0;
2238
+ if (state.kind !== "stale" && state.kind !== "managed") return evidence.data.gateway === void 0 ? { state } : void 0;
2239
+ return evidence.data.gateway === void 0 ? void 0 : {
2240
+ state,
2241
+ gateway: parseGatewayState(evidence.data.gateway)
2242
+ };
2243
+ }
2244
+ function parseGuestMountState(state, expectedMountPath) {
2245
+ switch (state.kind) {
2246
+ case "absent": return { kind: "absent" };
2247
+ case "unmanaged": return {
2248
+ kind: "unmanaged",
2249
+ filesystemType: state.filesystemType
2250
+ };
2251
+ case "incompatible": return { kind: "incompatible" };
2252
+ case "stale": {
2253
+ const marker = parseMarker(state.marker, expectedMountPath);
2254
+ return marker === void 0 ? void 0 : {
2255
+ kind: "stale",
2256
+ marker
2257
+ };
2258
+ }
2259
+ case "managed": {
2260
+ const marker = parseMarker(state.marker, expectedMountPath);
2261
+ const fuse = parseFuseState(state.fuse);
2262
+ return marker === void 0 || fuse === void 0 ? void 0 : {
2263
+ kind: "managed",
2264
+ marker,
2265
+ fuse
2266
+ };
2267
+ }
2268
+ default: return;
2269
+ }
2270
+ }
2271
+ function parseMarker(marker, expectedMountPath) {
2272
+ if (marker.mountPath !== expectedMountPath) return void 0;
2273
+ return {
2274
+ protocolVersion: 1,
2275
+ routeId: marker.routeId,
2276
+ mountPath: expectedMountPath,
2277
+ configuration: marker.configuration
2278
+ };
2279
+ }
2280
+ function parseFuseState(state) {
2281
+ return state;
2282
+ }
2283
+ function parseGatewayState(state) {
2284
+ switch (state.kind) {
2285
+ case "gatewayUnreachable": return {
2286
+ status: "unreachable",
2287
+ detail: state.detail
2288
+ };
2289
+ case "gatewayError": return {
2290
+ status: "error",
2291
+ reason: state.reason,
2292
+ detail: state.detail
2293
+ };
2294
+ case "usable": return {
2295
+ status: "reachable",
2296
+ upstream: { status: "usable" }
2297
+ };
2298
+ case "upstreamUnavailable": return {
2299
+ status: "reachable",
2300
+ upstream: {
2301
+ status: "unavailable",
2302
+ detail: state.detail
2303
+ }
2304
+ };
2305
+ case "upstreamRejected": return {
2306
+ status: "reachable",
2307
+ upstream: {
2308
+ status: "rejected",
2309
+ reason: state.reason,
2310
+ detail: state.detail
2311
+ }
2312
+ };
2313
+ }
2314
+ }
2315
+ //#endregion
2316
+ //#region src/s3-mounts/reconcile.ts
2317
+ function assembleInspection(mountPath, evidence) {
2318
+ const state = evidence.state;
2319
+ switch (state.kind) {
2320
+ case "absent": return {
2321
+ mountPath,
2322
+ attachment: { status: "absent" }
2323
+ };
2324
+ case "unmanaged": return {
2325
+ mountPath,
2326
+ attachment: {
2327
+ status: "unmanaged",
2328
+ filesystemType: state.filesystemType
2329
+ }
2330
+ };
2331
+ case "incompatible": return {
2332
+ mountPath,
2333
+ attachment: { status: "incompatible" }
2334
+ };
2335
+ case "stale": return {
2336
+ mountPath,
2337
+ attachment: {
2338
+ status: "stale",
2339
+ configuration: state.marker.configuration
2340
+ },
2341
+ gateway: gatewayFor(evidence)
2342
+ };
2343
+ case "managed": return {
2344
+ mountPath,
2345
+ attachment: {
2346
+ status: "managed",
2347
+ configuration: state.marker.configuration
2348
+ },
2349
+ fuse: state.fuse,
2350
+ gateway: gatewayFor(evidence)
2351
+ };
2352
+ }
2353
+ }
2354
+ function gatewayFor(evidence) {
2355
+ if ("gateway" in evidence) return evidence.gateway;
2356
+ throw protocolError("sandbox-shim omitted managed mount gateway evidence");
2357
+ }
2358
+ //#endregion
2359
+ //#region src/s3-mounts/s3-mounts.ts
2360
+ /**
2361
+ * Attaches an S3-compatible bucket or prefix to a running Container.
2362
+ *
2363
+ * Use this for a few long-lived mounts in one job or session. Calling `mount()`
2364
+ * again with the same settings reuses the existing mount. `unmount()` stops
2365
+ * access and unmounts the path. It does not fully clean up the Container's
2366
+ * intercept. For a new job or tenant, use a new sandbox name.
2367
+ *
2368
+ * Start the Container before calling `mount()`. This class never starts,
2369
+ * monitors, or replaces it. The mounted path is not a POSIX filesystem. Do not
2370
+ * use it for locking or atomic rename.
2371
+ */
2372
+ var S3Mounts = class {
2373
+ #container;
2374
+ #gateway;
2375
+ constructor(container, gateway) {
2376
+ this.#container = container;
2377
+ this.#gateway = gateway;
2378
+ }
2379
+ /**
2380
+ * Creates the mount, reuses a matching mount, or repairs leftover state.
2381
+ *
2382
+ * Reuse does not consume another Container intercept. Use `inspect()` to read
2383
+ * current state without changing it.
2384
+ */
2385
+ async mount(request, options = {}) {
2386
+ const canonical = canonicalizeS3MountRequest(request);
2387
+ options.signal?.throwIfAborted();
2388
+ let installedRouteId;
2389
+ try {
2390
+ await mountGuest(this.#container, {
2391
+ protocolVersion: 1,
2392
+ candidateRouteId: crypto.randomUUID(),
2393
+ mountPath: canonical.mountPath,
2394
+ configuration: observedConfiguration(canonical)
2395
+ }, options, async (routeId) => {
2396
+ installedRouteId = routeId;
2397
+ const gateway = this.#gateway({ props: {
2398
+ protocolVersion: 1,
2399
+ mode: "active",
2400
+ routeId,
2401
+ source: canonical.source,
2402
+ keyPrefix: canonical.keyPrefix,
2403
+ access: canonical.access
2404
+ } });
2405
+ try {
2406
+ await this.#container.interceptOutboundHttp(routeHost(routeId), gateway);
2407
+ } finally {
2408
+ if (options.signal?.aborted) try {
2409
+ await this.#denyRoute(routeId);
2410
+ } catch {}
2411
+ }
2412
+ });
2413
+ } catch (error) {
2414
+ if (installedRouteId !== void 0) try {
2415
+ await this.#denyRoute(installedRouteId);
2416
+ } catch {}
2417
+ throw error;
2418
+ }
2419
+ }
2420
+ /**
2421
+ * Reports the current path without changing it.
2422
+ *
2423
+ * Waits for an in-flight `mount()` or `unmount()` on the same path first.
2424
+ * Gateway evidence can be newer than the guest snapshot. Pass `signal` when
2425
+ * the application needs a deadline.
2426
+ */
2427
+ async inspect(mountPath, options = {}) {
2428
+ const canonicalPath = canonicalizeMountPath(mountPath);
2429
+ return assembleInspection(canonicalPath, await inspectGuestMount(this.#container, canonicalPath, options));
2430
+ }
2431
+ /**
2432
+ * Stops new access, then unmounts the path.
2433
+ *
2434
+ * This does not remove the Container intercept. If denying access fails, the
2435
+ * filesystem stays mounted. If the filesystem is busy, access stays denied
2436
+ * and you can retry. This never force-unmounts.
2437
+ */
2438
+ async unmount(mountPath, options = {}) {
2439
+ const canonicalPath = canonicalizeMountPath(mountPath);
2440
+ await unmountGuest(this.#container, canonicalPath, options, (routeId) => this.#denyRoute(routeId));
2441
+ }
2442
+ async #denyRoute(routeId) {
2443
+ const denyGateway = this.#gateway({ props: {
2444
+ protocolVersion: 1,
2445
+ mode: "deny",
2446
+ routeId
2447
+ } });
2448
+ await this.#container.interceptOutboundHttp(routeHost(routeId), denyGateway);
2449
+ }
2450
+ };
2451
+ //#endregion
2452
+ export { DirectoryBackupGateway, DirectoryBackups, Files, S3Gateway, S3Mounts, SandboxBackupError, SandboxFileError, SandboxProtocolError, SandboxS3MountError };