@claxedo/sandbox-manager 0.7.0 → 0.10.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 (70) hide show
  1. package/README.md +245 -42
  2. package/dist/checkpoint-manager.d.ts +29 -4
  3. package/dist/checkpoint-manager.mjs +133 -24
  4. package/dist/command.d.ts +9 -2
  5. package/dist/command.mjs +8 -8
  6. package/dist/contract.d.ts +687 -0
  7. package/dist/driver-billing.d.ts +29 -0
  8. package/dist/driver-catalog.d.ts +43 -58
  9. package/dist/driver-catalog.mjs +192 -229
  10. package/dist/drivers/boat-client.d.ts +95 -0
  11. package/dist/drivers/boat-named-snapshots.d.ts +31 -0
  12. package/dist/drivers/boat-template.d.ts +49 -0
  13. package/dist/drivers/boat-template.mjs +1196 -0
  14. package/dist/drivers/boat.d.ts +64 -0
  15. package/dist/drivers/boat.mjs +990 -0
  16. package/dist/drivers/cloudflare.d.ts +5 -8
  17. package/dist/drivers/cloudflare.mjs +364 -213
  18. package/dist/drivers/docker.d.ts +4 -3
  19. package/dist/drivers/docker.mjs +244 -165
  20. package/dist/drivers/local-brokering.d.ts +14 -0
  21. package/dist/drivers/local-brokering.mjs +1029 -0
  22. package/dist/drivers/modal.d.ts +4 -7
  23. package/dist/drivers/modal.mjs +220 -155
  24. package/dist/drivers/transient-error.d.ts +13 -0
  25. package/dist/drivers/vercel.d.ts +21 -23
  26. package/dist/drivers/vercel.mjs +279 -169
  27. package/dist/egress-policy.d.ts +134 -0
  28. package/dist/egress-warnings.d.ts +22 -0
  29. package/dist/garbage-collect.d.ts +14 -0
  30. package/dist/hosted-network-policy.d.ts +11 -4
  31. package/dist/hosted-network-policy.mjs +221 -4
  32. package/dist/image-name.d.ts +12 -2
  33. package/dist/image-name.mjs +43 -0
  34. package/dist/image.d.ts +1 -20
  35. package/dist/image.mjs +11 -66
  36. package/dist/index.d.ts +8 -533
  37. package/dist/index.mjs +1420 -315
  38. package/dist/json.d.ts +13 -0
  39. package/dist/lease-failure.d.ts +173 -0
  40. package/dist/lease-lifecycle.d.ts +65 -0
  41. package/dist/lease-retry-wait.d.ts +8 -0
  42. package/dist/lease-start.d.ts +21 -0
  43. package/dist/lease-types.d.ts +15 -5
  44. package/dist/manager.d.ts +2 -0
  45. package/dist/runtime-env.d.ts +15 -2
  46. package/dist/runtime-env.mjs +50 -1
  47. package/dist/runtime-snapshot.d.ts +49 -0
  48. package/dist/runtime-version.d.ts +9 -0
  49. package/dist/runtime-version.mjs +2 -0
  50. package/dist/service-url-exposure.d.ts +1 -3
  51. package/dist/service-url-exposure.mjs +1 -26
  52. package/dist/start-fresh.d.ts +17 -0
  53. package/dist/start-telemetry.d.ts +37 -0
  54. package/dist/stores/memory.d.ts +1 -1
  55. package/dist/stores/memory.mjs +65 -54
  56. package/package.json +67 -81
  57. package/dist/daytona-allow-list.d.ts +0 -16
  58. package/dist/daytona-allow-list.mjs +0 -11
  59. package/dist/drivers/box.d.ts +0 -52
  60. package/dist/drivers/box.mjs +0 -499
  61. package/dist/drivers/cloudflare-egress.d.ts +0 -40
  62. package/dist/drivers/cloudflare-egress.mjs +0 -113
  63. package/dist/drivers/daytona.d.ts +0 -127
  64. package/dist/drivers/daytona.mjs +0 -541
  65. package/dist/drivers/exe.d.ts +0 -26
  66. package/dist/drivers/exe.mjs +0 -469
  67. package/dist/drivers/fetch-bridge.d.ts +0 -10
  68. package/dist/drivers/fetch-bridge.mjs +0 -111
  69. package/dist/lease-policy.d.ts +0 -48
  70. package/dist/lease-policy.mjs +0 -280
@@ -1,499 +0,0 @@
1
- // src/runtime-env.ts
2
- function workspaceRuntimeTargetEnv(input) {
3
- return {
4
- WORKSPACE_RUNTIME_WORKSPACE_ID: input.workspaceId,
5
- ...input.hostId ? { WORKSPACE_RUNTIME_HOST_ID: input.hostId } : {},
6
- WORKSPACE_RUNTIME_DIRECTORY: input.directory,
7
- WORKSPACE_RUNTIME_PORT: String(input.port),
8
- ...input.host ? { WORKSPACE_RUNTIME_HOST: input.host } : {}
9
- };
10
- }
11
- function workspaceRuntimeSourceEnv(input) {
12
- if (!input.source || input.source.kind === "empty") return { WORKSPACE_RUNTIME_SOURCE_KIND: "empty" };
13
- return {
14
- WORKSPACE_RUNTIME_SOURCE_KIND: "git",
15
- WORKSPACE_RUNTIME_GIT_REPO_URL: input.source.repoUrl,
16
- ...input.source.branch ? { WORKSPACE_RUNTIME_GIT_BRANCH: input.source.branch } : {}
17
- };
18
- }
19
-
20
- // src/command.ts
21
- function shell(input) {
22
- return `'${input.replace(/'/g, `'"'"'`)}'`;
23
- }
24
-
25
- // src/constants.ts
26
- var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
27
-
28
- // src/image.ts
29
- import { Image, DaytonaNotFoundError } from "@daytona/sdk";
30
-
31
- // src/runtime-version.ts
32
- var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
33
- function workspaceRuntimeVersion() {
34
- return process.env.WORKSPACE_RUNTIME_VERSION || DEFAULT_WORKSPACE_RUNTIME_VERSION;
35
- }
36
-
37
- // src/image-name.ts
38
- var SNAPSHOT_SCHEMA_VERSION = 8;
39
- var DEFAULT_SANDBOX_IMAGE_REPOSITORY = "ghcr.io/kyashrathore/claxedo-sandbox";
40
- function snapshotVersion(value) {
41
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
42
- }
43
- function sandboxImageRepository(env = process.env) {
44
- const repository = env.CLAXEDO_SANDBOX_IMAGE_REPOSITORY?.trim();
45
- return repository ? repository : DEFAULT_SANDBOX_IMAGE_REPOSITORY;
46
- }
47
- function buildIdSuffix(buildId, env) {
48
- const id = (buildId ?? env.CLAXEDO_SANDBOX_BUILD_ID)?.trim();
49
- return id ? `-${snapshotVersion(id)}` : "";
50
- }
51
- function defaultSandboxImage(version = workspaceRuntimeVersion(), buildId, env = process.env) {
52
- return `${sandboxImageRepository(env)}:workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
53
- }
54
- function defaultSnapshotName(version = workspaceRuntimeVersion(), buildId, env = process.env) {
55
- return `claxedo-workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
56
- }
57
-
58
- // src/image.ts
59
- var SANDBOX_IMAGE_REPOSITORY = sandboxImageRepository();
60
- var SANDBOX_IMAGE = process.env.CLAXEDO_SANDBOX_IMAGE || defaultSandboxImage();
61
- var SNAPSHOT_NAME = process.env.CLAXEDO_SNAPSHOT_NAME || defaultSnapshotName();
62
-
63
- // src/hosted-network-policy.ts
64
- var SANDBOX_MODEL_PROVIDER_HOSTS = [
65
- "api.anthropic.com",
66
- "api.openai.com",
67
- "generativelanguage.googleapis.com",
68
- "openrouter.ai",
69
- // Model catalog + engine config, fetched on runner startup.
70
- "models.dev",
71
- "opencode.ai"
72
- ];
73
- var SANDBOX_PACKAGE_REGISTRY_HOSTS = [
74
- "registry.npmjs.org",
75
- "nodejs.org",
76
- "pypi.org",
77
- "files.pythonhosted.org",
78
- "crates.io",
79
- "static.crates.io",
80
- "index.crates.io",
81
- "proxy.golang.org",
82
- "sum.golang.org"
83
- ];
84
- var SANDBOX_BASELINE_EGRESS_HOSTS = [
85
- ...SANDBOX_MODEL_PROVIDER_HOSTS,
86
- ...SANDBOX_PACKAGE_REGISTRY_HOSTS
87
- ];
88
-
89
- // src/driver-catalog.ts
90
- var sandboxDriverCatalog = {
91
- exe: {
92
- id: "exe",
93
- label: "exe.dev",
94
- credentialFields: [{ key: "api_token", label: "API Token", secret: true }],
95
- metadata: {
96
- driverRunsIn: ["worker", "node"],
97
- hostStopBehavior: "not-supported",
98
- hostResumeBehavior: "same-host",
99
- targetAccess: "relay",
100
- secretBrokering: "none",
101
- egressControl: "none",
102
- persistence: {
103
- resume: "same-sandbox",
104
- capture: "none",
105
- clone: true,
106
- captureSource: "not-applicable",
107
- retention: "provider-managed",
108
- restoreMount: "same-resource"
109
- }
110
- }
111
- },
112
- daytona: {
113
- id: "daytona",
114
- label: "Daytona",
115
- credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
116
- metadata: {
117
- driverRunsIn: ["worker", "node"],
118
- hostStopBehavior: "suspends-host",
119
- hostResumeBehavior: "same-host",
120
- targetAccess: "relay",
121
- secretBrokering: "native",
122
- // Daytona is the only driver that can filter egress by NAME as well as
123
- // by address: `domainAllowList` (names) alongside `networkAllowList`
124
- // (CIDRs), with `networkBlockAll` as the deny-all floor.
125
- egressControl: "hosts-and-cidrs",
126
- persistence: {
127
- resume: "same-sandbox",
128
- capture: "filesystem",
129
- clone: false,
130
- captureSource: "preserved",
131
- retention: "provider-managed",
132
- restoreMount: "new-resource"
133
- }
134
- }
135
- },
136
- modal: {
137
- id: "modal",
138
- label: "Modal",
139
- credentialFields: [
140
- { key: "token_id", label: "Token ID", secret: true },
141
- { key: "token_secret", label: "Token Secret", secret: true }
142
- ],
143
- metadata: {
144
- driverRunsIn: ["node"],
145
- hostStopBehavior: "terminates-host",
146
- hostResumeBehavior: "replacement-host",
147
- targetAccess: "relay",
148
- secretBrokering: "none",
149
- // Modal can cut the network entirely (`blockNetwork`) but cannot express
150
- // an allowlist, and its driver throws on a host policy. A blackout is
151
- // not containment for a workspace that has to clone and reach a model.
152
- egressControl: "none",
153
- persistence: {
154
- resume: "replacement-restore",
155
- capture: "filesystem",
156
- clone: false,
157
- captureSource: "preserved",
158
- retention: "provider-managed",
159
- restoreMount: "new-resource"
160
- }
161
- }
162
- },
163
- vercel: {
164
- id: "vercel",
165
- label: "Vercel",
166
- credentialFields: [
167
- { key: "access_token", label: "Access Token", secret: true },
168
- { key: "team_id", label: "Team ID" },
169
- { key: "project_id", label: "Project ID" }
170
- ],
171
- metadata: {
172
- driverRunsIn: ["node"],
173
- hostStopBehavior: "terminates-host",
174
- hostResumeBehavior: "replacement-host",
175
- targetAccess: "relay",
176
- secretBrokering: "native",
177
- // Vercel's sandbox firewall takes a hostname allow list (or "deny-all"),
178
- // never CIDRs.
179
- egressControl: "hosts",
180
- persistence: {
181
- resume: "replacement-restore",
182
- capture: "filesystem",
183
- clone: false,
184
- captureSource: "stopped",
185
- retention: "explicit",
186
- restoreMount: "new-resource"
187
- }
188
- }
189
- },
190
- cloudflare: {
191
- id: "cloudflare",
192
- label: "Cloudflare",
193
- credentialFields: [
194
- { key: "api_token", label: "API Token", secret: true },
195
- { key: "worker_url", label: "Worker URL" }
196
- ],
197
- metadata: {
198
- driverRunsIn: ["worker"],
199
- hostStopBehavior: "not-supported",
200
- hostResumeBehavior: "same-host",
201
- targetAccess: "relay",
202
- secretBrokering: "proxy",
203
- // The Cloudflare Sandbox Worker exposes no egress filter: its broker
204
- // (drivers/cloudflare-egress.ts) is an OPT-IN proxy the sandbox chooses
205
- // to route brokered-credential requests through, not a boundary. The
206
- // driver therefore drops `net` entirely — which is exactly why this
207
- // must be declared, so the manager refuses instead of provisioning a
208
- // sandbox the caller believes is contained.
209
- egressControl: "none",
210
- persistence: {
211
- resume: "replacement-restore",
212
- capture: "directories",
213
- clone: false,
214
- captureSource: "preserved",
215
- retention: "provider-managed",
216
- restoreMount: "copy-on-write"
217
- }
218
- }
219
- },
220
- box: {
221
- id: "box",
222
- label: "Box",
223
- credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
224
- metadata: {
225
- driverRunsIn: ["node"],
226
- hostStopBehavior: "suspends-host",
227
- hostResumeBehavior: "same-host",
228
- targetAccess: "relay",
229
- secretBrokering: "none",
230
- egressControl: "none",
231
- persistence: {
232
- resume: "same-sandbox",
233
- capture: "none",
234
- clone: false,
235
- captureSource: "not-applicable",
236
- retention: "provider-managed",
237
- restoreMount: "same-resource"
238
- }
239
- }
240
- },
241
- docker: {
242
- id: "docker",
243
- label: "Docker",
244
- credentialFields: [{ key: "image", label: "Image" }],
245
- metadata: {
246
- driverRunsIn: ["local"],
247
- hostStopBehavior: "terminates-host",
248
- hostResumeBehavior: "same-host",
249
- targetAccess: "loopback",
250
- secretBrokering: "none",
251
- egressControl: "none",
252
- persistence: {
253
- resume: "replacement-restore",
254
- capture: "same-resource",
255
- clone: false,
256
- captureSource: "preserved",
257
- retention: "provider-managed",
258
- restoreMount: "same-resource"
259
- }
260
- }
261
- }
262
- };
263
-
264
- // src/drivers/box.ts
265
- var DEFAULT_BASE_URL = "https://ascii.dev/api/box/v1";
266
- var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
267
- var DEFAULT_WORKSPACE_DIR = "/workspace";
268
- var DEFAULT_CONTAINER_NAME = "claxedo-runtime";
269
- var DEFAULT_PROVISION_TIMEOUT_MS = 12e4;
270
- var DEFAULT_PROVISION_INTERVAL_MS = 2e3;
271
- var DEFAULT_HEALTH_TIMEOUT_MS = 6e4;
272
- var DEFAULT_HEALTH_INTERVAL_MS = 1e3;
273
- var DEFAULT_OPERATION_TIMEOUT_MS = 12e4;
274
- var READY_STATES = /* @__PURE__ */ new Set(["ready", "idle", "running"]);
275
- var PENDING_STATES = /* @__PURE__ */ new Set(["init", "provisioning", "provisioned", "cloning"]);
276
- function sleep(ms) {
277
- return new Promise((resolve) => setTimeout(resolve, ms));
278
- }
279
- function clean(input) {
280
- const txt = input?.trim();
281
- return txt ? txt : void 0;
282
- }
283
- function transientDriverError(err) {
284
- const shaped = err;
285
- if (typeof shaped.status === "number" && shaped.status >= 500) return true;
286
- const text = `${shaped.code ?? ""} ${shaped.name ?? ""} ${shaped.message ?? ""}`.toLowerCase();
287
- return text.includes("timeout") || text.includes("unavailable") || text.includes("econnreset") || text.includes("network");
288
- }
289
- var BoxApiError = class extends Error {
290
- status;
291
- constructor(message, status) {
292
- super(message);
293
- this.name = "BoxApiError";
294
- this.status = status;
295
- }
296
- };
297
- function createBoxSandboxDriver(options) {
298
- const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
299
- const image = options.image ?? SANDBOX_IMAGE;
300
- const runtimeCommand = options.runtimeCommand ?? DEFAULT_RUNTIME_COMMAND;
301
- const workspaceDir = options.workspaceDir ?? DEFAULT_WORKSPACE_DIR;
302
- const containerName = options.containerName ?? DEFAULT_CONTAINER_NAME;
303
- const ttlSeconds = options.ttlSeconds === void 0 ? null : options.ttlSeconds;
304
- const provisionTimeoutMs = options.provisionTimeoutMs ?? DEFAULT_PROVISION_TIMEOUT_MS;
305
- const provisionIntervalMs = options.provisionIntervalMs ?? DEFAULT_PROVISION_INTERVAL_MS;
306
- const healthTimeoutMs = options.healthTimeoutMs ?? DEFAULT_HEALTH_TIMEOUT_MS;
307
- const healthIntervalMs = options.healthIntervalMs ?? DEFAULT_HEALTH_INTERVAL_MS;
308
- const operationTimeoutMs = options.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS;
309
- const fetchImpl = options.fetchImpl ?? fetch;
310
- async function api(path, init) {
311
- let res;
312
- try {
313
- res = await fetchImpl(`${baseUrl}${path}`, {
314
- method: init?.method ?? "GET",
315
- headers: {
316
- Authorization: `Bearer ${options.apiKey}`,
317
- ...init?.body !== void 0 ? { "Content-Type": "application/json" } : {}
318
- },
319
- ...init?.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
320
- signal: AbortSignal.timeout(operationTimeoutMs)
321
- });
322
- } catch (err) {
323
- throw new BoxApiError(err instanceof Error ? err.message : String(err));
324
- }
325
- const text = await res.text();
326
- const body = text ? JSON.parse(text) : {};
327
- if (!res.ok || body.ok === false) {
328
- const detail = clean(body.error) ?? clean(body.message) ?? res.statusText;
329
- throw new BoxApiError(`Box API ${init?.method ?? "GET"} ${path} failed: ${detail}`, res.status);
330
- }
331
- return body;
332
- }
333
- function runtimePort(input) {
334
- return input.workspaceRuntimePort ?? options.runtimePort ?? DEFAULT_WORKSPACE_RUNTIME_PORT;
335
- }
336
- function workspaceDirectory(input) {
337
- return input.workspaceRoot ?? workspaceDir;
338
- }
339
- function resolveImage(input) {
340
- if (input.bootSource?.kind === "image") return input.bootSource.image;
341
- return input.snapshot ?? image;
342
- }
343
- function bootEnv(input, hostId) {
344
- const env = {
345
- ...workspaceRuntimeTargetEnv({
346
- workspaceId: input.workspaceId,
347
- hostId,
348
- directory: workspaceDirectory(input),
349
- port: runtimePort(input),
350
- host: "0.0.0.0"
351
- }),
352
- ...workspaceRuntimeSourceEnv({ source: input.source }),
353
- ...input.env
354
- };
355
- if (options.runner) env.WORKSPACE_RUNTIME_RUNNER = options.runner;
356
- return env;
357
- }
358
- async function exec(boxId, command) {
359
- const body = await api(`/boxes/${boxId}/commands`, {
360
- method: "POST",
361
- body: { command }
362
- });
363
- const result = body.result ?? body;
364
- return {
365
- stdout: typeof result.stdout === "string" ? result.stdout : "",
366
- stderr: typeof result.stderr === "string" ? result.stderr : "",
367
- exitCode: typeof result.exitCode === "number" ? result.exitCode : 0
368
- };
369
- }
370
- async function execOrThrow(boxId, command, label) {
371
- const result = await exec(boxId, command);
372
- if ((result.exitCode ?? 0) !== 0) {
373
- throw new BoxApiError(`Box ${boxId} ${label} failed (exit ${result.exitCode}): ${clean(result.stderr) ?? clean(result.stdout) ?? ""}`);
374
- }
375
- return result;
376
- }
377
- async function waitUntilReady(boxId) {
378
- const until = Date.now() + provisionTimeoutMs;
379
- for (; ; ) {
380
- const { box } = await api(`/boxes/${boxId}`);
381
- if (READY_STATES.has(box.state)) return true;
382
- if (!PENDING_STATES.has(box.state)) {
383
- throw new BoxApiError(`Box ${boxId} entered non-ready state: ${box.state}`);
384
- }
385
- if (Date.now() >= until) return false;
386
- await sleep(provisionIntervalMs);
387
- }
388
- }
389
- function dockerRunScript(input, env) {
390
- const port = runtimePort(input);
391
- const directory = workspaceDirectory(input);
392
- const bootScript = [`mkdir -p ${shell(directory)}`, `cd ${shell(directory)}`, `exec ${runtimeCommand}`].join(" && ");
393
- const envArgs = Object.entries(env).map(([key, value]) => `--env ${shell(`${key}=${value}`)}`).join(" ");
394
- const login = options.registryAuth ? `echo ${shell(options.registryAuth.password)} | docker login ${shell(options.registryAuth.server)} --username ${shell(options.registryAuth.username)} --password-stdin && ` : "";
395
- return `${login}docker rm -f ${containerName} >/dev/null 2>&1 || true; docker run -d --name ${containerName} -p ${port}:${port} ${envArgs} --entrypoint sh ${shell(resolveImage(input))} -lc ${shell(bootScript)}`;
396
- }
397
- async function waitForHealth(boxId, input) {
398
- const port = runtimePort(input);
399
- const until = Date.now() + healthTimeoutMs;
400
- let last = "workspace runtime not ready";
401
- while (Date.now() < until) {
402
- const probe = await exec(
403
- boxId,
404
- `curl -sf -o /dev/null -w '%{http_code}' http://127.0.0.1:${port}/global/health || true`
405
- );
406
- const code = clean(probe.stdout);
407
- if (code === "200") return;
408
- last = `health probe returned ${code ?? "no response"}`;
409
- await sleep(healthIntervalMs);
410
- }
411
- throw new BoxApiError(`Box ${boxId} runtime did not become healthy: ${last}`);
412
- }
413
- async function boot(boxId, input, hostId) {
414
- const env = {
415
- ...bootEnv(input, hostId),
416
- ...await options.env?.(input, { id: hostId })
417
- };
418
- const port = runtimePort(input);
419
- await execOrThrow(boxId, dockerRunScript(input, env), "docker run");
420
- await execOrThrow(boxId, `host ${port}`, "host publish");
421
- await waitForHealth(boxId, input);
422
- const urlResult = await execOrThrow(boxId, `host url ${port}`, "host url");
423
- const url = clean(urlResult.stdout?.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).pop());
424
- if (!url) throw new BoxApiError(`Box ${boxId} did not return a public URL for port ${port}`);
425
- return {
426
- workspaceId: input.workspaceId,
427
- sandboxId: boxId,
428
- url,
429
- hostId,
430
- driverResourceId: boxId,
431
- driver: { id: "box", resourceId: boxId },
432
- labels: input.labels
433
- };
434
- }
435
- function assertNetwork(input) {
436
- if (input.net && input.net.mode !== "allow-all") {
437
- throw new BoxApiError("Box SandboxDriver cannot enforce host-based network policy");
438
- }
439
- }
440
- async function ensureHost(input) {
441
- assertNetwork(input);
442
- const hostId = input.hostId ?? `box-${input.workspaceId}`;
443
- const created = await api("/boxes", {
444
- method: "POST",
445
- body: { ttlSeconds }
446
- }).catch((err) => {
447
- if (transientDriverError(err)) return void 0;
448
- throw err;
449
- });
450
- if (!created) return { provisioning: true, retryAfterMs: provisionIntervalMs };
451
- const boxId = created.box.id;
452
- const ready = await waitUntilReady(boxId);
453
- if (!ready) return { provisioning: true, retryAfterMs: provisionIntervalMs };
454
- return boot(boxId, input, hostId);
455
- }
456
- async function resumeHost(input) {
457
- const boxId = input.lease.sandboxId;
458
- if (!boxId) return ensureHost(input.ensure);
459
- assertNetwork(input.ensure);
460
- const hostId = input.ensure.hostId ?? input.lease.hostId ?? `box-${input.ensure.workspaceId}`;
461
- try {
462
- await api(`/boxes/${boxId}/resume`, { method: "POST" });
463
- const ready = await waitUntilReady(boxId);
464
- if (!ready) return { provisioning: true, retryAfterMs: provisionIntervalMs };
465
- return await boot(boxId, input.ensure, hostId);
466
- } catch (err) {
467
- if (transientDriverError(err)) return { provisioning: true, retryAfterMs: provisionIntervalMs };
468
- return ensureHost(input.ensure);
469
- }
470
- }
471
- async function stop(target) {
472
- await api(`/boxes/${target.sandboxId}/stop`, { method: "POST" });
473
- }
474
- async function destroy(target) {
475
- await api(`/boxes/${target.sandboxId}`, { method: "DELETE" });
476
- }
477
- return {
478
- id: "box",
479
- metadata: {
480
- driverRunsIn: ["node"],
481
- hostStopBehavior: "suspends-host",
482
- hostResumeBehavior: "same-host",
483
- targetAccess: "relay",
484
- secretBrokering: "none",
485
- egressControl: "none",
486
- persistence: sandboxDriverCatalog.box.metadata.persistence
487
- },
488
- ensureHost,
489
- resumeHost,
490
- async touch() {
491
- },
492
- suspend: stop,
493
- stop,
494
- destroy
495
- };
496
- }
497
- export {
498
- createBoxSandboxDriver
499
- };
@@ -1,40 +0,0 @@
1
- export type EgressTokenClaims = {
2
- /** Sandbox the token is bound to. */
3
- sub: string;
4
- /** Hosts the sandbox may reach through the broker. */
5
- hosts: string[];
6
- /** Expiry (epoch seconds). */
7
- exp: number;
8
- };
9
- /** Mint a short-lived HS256 token binding a sandbox to its brokered hosts. Carries NO secret values. */
10
- export declare function mintEgressToken(input: {
11
- sandboxId: string;
12
- hosts: string[];
13
- signingSecret: string;
14
- ttlSeconds?: number;
15
- now?: () => number;
16
- }): Promise<string>;
17
- /** Verify a token and return its claims, or null if invalid/expired. */
18
- export declare function verifyEgressToken(token: string, signingSecret: string, now?: () => number): Promise<EgressTokenClaims | null>;
19
- /** How the sandbox names its egress target: header carries the absolute upstream URL. */
20
- export declare const EGRESS_TARGET_HEADER = "x-claxedo-egress-target";
21
- /** Resolves the brokered credential for a (sandbox, host) pair — Worker-side only, never the sandbox. */
22
- export type EgressSecretResolver = (sandboxId: string, host: string) => Promise<{
23
- header: string;
24
- value: string;
25
- } | undefined> | {
26
- header: string;
27
- value: string;
28
- } | undefined;
29
- /**
30
- * Handle one egress-proxy request from a sandbox: validate the JWT, confirm
31
- * the target host is allowed, inject the brokered credential as its header,
32
- * and forward. The credential is read from `resolveSecret` (Worker store) and
33
- * never round-trips through the sandbox.
34
- */
35
- export declare function handleEgressRequest(request: Request, options: {
36
- signingSecret: string;
37
- resolveSecret: EgressSecretResolver;
38
- fetchImpl?: typeof fetch;
39
- now?: () => number;
40
- }): Promise<Response>;
@@ -1,113 +0,0 @@
1
- // src/drivers/cloudflare-egress.ts
2
- function b64urlEncode(bytes) {
3
- let binary = "";
4
- for (const byte of bytes) binary += String.fromCharCode(byte);
5
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
6
- }
7
- function b64urlDecode(input) {
8
- const padded = input.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - input.length % 4) % 4);
9
- const binary = atob(padded);
10
- const bytes = new Uint8Array(binary.length);
11
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
12
- return bytes;
13
- }
14
- function encodeJson(value) {
15
- return b64urlEncode(new TextEncoder().encode(JSON.stringify(value)));
16
- }
17
- async function hmacKey(secret) {
18
- return crypto.subtle.importKey(
19
- "raw",
20
- new TextEncoder().encode(secret),
21
- { name: "HMAC", hash: "SHA-256" },
22
- false,
23
- ["sign", "verify"]
24
- );
25
- }
26
- function timingSafeEqual(a, b) {
27
- if (a.length !== b.length) return false;
28
- let diff = 0;
29
- for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
30
- return diff === 0;
31
- }
32
- async function mintEgressToken(input) {
33
- const now = input.now ?? Date.now;
34
- const header = encodeJson({ alg: "HS256", typ: "JWT" });
35
- const payload = encodeJson({
36
- sub: input.sandboxId,
37
- hosts: input.hosts,
38
- exp: Math.floor(now() / 1e3) + (input.ttlSeconds ?? 15 * 60)
39
- });
40
- const key = await hmacKey(input.signingSecret);
41
- const sig = new Uint8Array(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${header}.${payload}`)));
42
- return `${header}.${payload}.${b64urlEncode(sig)}`;
43
- }
44
- async function verifyEgressToken(token, signingSecret, now = Date.now) {
45
- const parts = token.split(".");
46
- if (parts.length !== 3) return null;
47
- const [header, payload, sig] = parts;
48
- const key = await hmacKey(signingSecret);
49
- const expected = b64urlEncode(
50
- new Uint8Array(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${header}.${payload}`)))
51
- );
52
- if (!timingSafeEqual(sig, expected)) return null;
53
- let claims;
54
- try {
55
- claims = JSON.parse(new TextDecoder().decode(b64urlDecode(payload)));
56
- } catch {
57
- return null;
58
- }
59
- if (typeof claims.sub !== "string" || !Array.isArray(claims.hosts) || typeof claims.exp !== "number") return null;
60
- if (claims.exp <= Math.floor(now() / 1e3)) return null;
61
- return claims;
62
- }
63
- var EGRESS_TARGET_HEADER = "x-claxedo-egress-target";
64
- async function handleEgressRequest(request, options) {
65
- const fetchImpl = options.fetchImpl ?? fetch;
66
- const auth = request.headers.get("authorization");
67
- const token = auth?.toLowerCase().startsWith("bearer ") ? auth.slice(7) : void 0;
68
- if (!token) return egressError(401, "missing egress token");
69
- const claims = await verifyEgressToken(token, options.signingSecret, options.now);
70
- if (!claims) return egressError(401, "invalid or expired egress token");
71
- const targetRaw = request.headers.get(EGRESS_TARGET_HEADER);
72
- if (!targetRaw) return egressError(400, "missing egress target");
73
- let target;
74
- try {
75
- target = new URL(targetRaw);
76
- } catch {
77
- return egressError(400, "malformed egress target");
78
- }
79
- if (target.protocol !== "https:") return egressError(400, "egress target must be https");
80
- if (!claims.hosts.includes(target.host)) return egressError(403, "host not permitted for this sandbox");
81
- const secret = await options.resolveSecret(claims.sub, target.host);
82
- if (!secret) return egressError(403, "no brokered credential for host");
83
- const headers = new Headers(request.headers);
84
- headers.delete("authorization");
85
- headers.delete(EGRESS_TARGET_HEADER);
86
- headers.set(secret.header, secret.value);
87
- headers.set("host", target.host);
88
- try {
89
- const upstream = await fetchImpl(target.toString(), {
90
- method: request.method,
91
- headers,
92
- body: request.method === "GET" || request.method === "HEAD" ? void 0 : request.body,
93
- redirect: "manual"
94
- });
95
- const outHeaders = new Headers(upstream.headers);
96
- outHeaders.delete(secret.header);
97
- return new Response(upstream.body, { status: upstream.status, statusText: upstream.statusText, headers: outHeaders });
98
- } catch {
99
- return egressError(502, "upstream request failed");
100
- }
101
- }
102
- function egressError(status, message) {
103
- return new Response(JSON.stringify({ error: message }), {
104
- status,
105
- headers: { "content-type": "application/json" }
106
- });
107
- }
108
- export {
109
- EGRESS_TARGET_HEADER,
110
- handleEgressRequest,
111
- mintEgressToken,
112
- verifyEgressToken
113
- };