@hyperbrowser/sdk 0.93.0 → 1.0.1

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 (64) hide show
  1. package/README.md +190 -0
  2. package/dist/client.d.ts +2 -20
  3. package/dist/client.js +5 -17
  4. package/dist/error.d.ts +27 -0
  5. package/dist/error.js +32 -0
  6. package/dist/image-builds.d.ts +6 -0
  7. package/dist/image-builds.js +12 -0
  8. package/dist/index.d.ts +3 -0
  9. package/dist/index.js +12 -1
  10. package/dist/retry.d.ts +14 -0
  11. package/dist/retry.js +49 -0
  12. package/dist/sandbox/base.d.ts +27 -0
  13. package/dist/sandbox/base.js +298 -99
  14. package/dist/sandbox/files.d.ts +24 -25
  15. package/dist/sandbox/files.js +146 -37
  16. package/dist/sandbox/image-build/artifacts.d.ts +16 -0
  17. package/dist/sandbox/image-build/artifacts.js +16 -0
  18. package/dist/sandbox/image-build/blake2b.d.ts +2 -0
  19. package/dist/sandbox/image-build/blake2b.js +89 -0
  20. package/dist/sandbox/image-build/common.d.ts +19 -0
  21. package/dist/sandbox/image-build/common.js +109 -0
  22. package/dist/sandbox/image-build/context.d.ts +43 -0
  23. package/dist/sandbox/image-build/context.js +599 -0
  24. package/dist/sandbox/image-build/docker-image.d.ts +34 -0
  25. package/dist/sandbox/image-build/docker-image.js +472 -0
  26. package/dist/sandbox/image-build/dockerfile-analysis.d.ts +17 -0
  27. package/dist/sandbox/image-build/dockerfile-analysis.js +409 -0
  28. package/dist/sandbox/image-build/dockerignore.d.ts +18 -0
  29. package/dist/sandbox/image-build/dockerignore.js +275 -0
  30. package/dist/sandbox/image-build/gzip.d.ts +11 -0
  31. package/dist/sandbox/image-build/gzip.js +91 -0
  32. package/dist/sandbox/image-build/image-init.d.ts +5 -0
  33. package/dist/sandbox/image-build/image-init.js +97 -0
  34. package/dist/sandbox/image-build/index.d.ts +13 -0
  35. package/dist/sandbox/image-build/index.js +36 -0
  36. package/dist/sandbox/image-build/local-docker.d.ts +11 -0
  37. package/dist/sandbox/image-build/local-docker.js +72 -0
  38. package/dist/sandbox/image-build/resolution.d.ts +27 -0
  39. package/dist/sandbox/image-build/resolution.js +137 -0
  40. package/dist/sandbox/image-build/tar.d.ts +41 -0
  41. package/dist/sandbox/image-build/tar.js +378 -0
  42. package/dist/sandbox/image-build/upload.d.ts +10 -0
  43. package/dist/sandbox/image-build/upload.js +143 -0
  44. package/dist/sandbox/index.d.ts +2 -1
  45. package/dist/sandbox/index.js +4 -1
  46. package/dist/sandbox/process-output.d.ts +36 -0
  47. package/dist/sandbox/process-output.js +117 -0
  48. package/dist/sandbox/process.d.ts +25 -1
  49. package/dist/sandbox/process.js +197 -29
  50. package/dist/sandbox/terminal.js +9 -5
  51. package/dist/sandbox/ws.d.ts +4 -1
  52. package/dist/sandbox/ws.js +33 -7
  53. package/dist/services/base.d.ts +2 -0
  54. package/dist/services/base.js +46 -30
  55. package/dist/services/normalize.d.ts +3 -0
  56. package/dist/services/normalize.js +21 -0
  57. package/dist/services/sandboxes.d.ts +40 -3
  58. package/dist/services/sandboxes.js +569 -53
  59. package/dist/services/volumes.js +29 -14
  60. package/dist/types/constants.d.ts +1 -1
  61. package/dist/types/index.d.ts +2 -1
  62. package/dist/types/sandbox.d.ts +246 -30
  63. package/dist/types/volume.d.ts +7 -7
  64. package/package.json +24 -5
@@ -1,29 +1,41 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SandboxesService = exports.SandboxHandle = void 0;
4
- const client_1 = require("../client");
4
+ const error_1 = require("../error");
5
5
  const files_1 = require("../sandbox/files");
6
6
  const base_1 = require("../sandbox/base");
7
7
  const runtime_path_1 = require("../sandbox/runtime-path");
8
8
  const process_1 = require("../sandbox/process");
9
9
  const terminal_1 = require("../sandbox/terminal");
10
+ const image_build_1 = require("../sandbox/image-build");
11
+ const retry_1 = require("../retry");
12
+ const normalize_1 = require("./normalize");
10
13
  const base_2 = require("./base");
11
14
  const RUNTIME_SESSION_REFRESH_BUFFER_MS = 60000;
12
15
  const normalizeSandbox = (sandbox) => {
13
16
  const { vcpus, memMiB, diskSizeMiB, ...rest } = sandbox;
14
17
  return {
15
18
  ...rest,
16
- cpu: vcpus,
17
- memoryMiB: memMiB,
18
- diskMiB: diskSizeMiB,
19
+ cpu: (0, normalize_1.optionalInteger)(vcpus),
20
+ memoryMiB: (0, normalize_1.optionalInteger)(memMiB),
21
+ diskMiB: (0, normalize_1.optionalInteger)(diskSizeMiB),
22
+ endTime: (0, normalize_1.optionalInteger)(rest.endTime),
23
+ startTime: (0, normalize_1.optionalInteger)(rest.startTime),
24
+ dataConsumed: (0, normalize_1.optionalInteger)(rest.dataConsumed),
25
+ proxyDataConsumed: (0, normalize_1.optionalInteger)(rest.proxyDataConsumed),
26
+ proxyBytesUsed: (0, normalize_1.optionalInteger)(rest.proxyBytesUsed),
27
+ timeoutMinutes: (0, normalize_1.optionalInteger)(rest.timeoutMinutes),
28
+ creditsUsed: (0, normalize_1.optionalNumber)(rest.creditsUsed) ?? null,
29
+ exposedPorts: rest.exposedPorts ?? [],
30
+ network: rest.network ? { ...rest.network, allowOut: rest.network.allowOut ?? [], denyOut: rest.network.denyOut ?? [] } : rest.network,
19
31
  };
20
32
  };
21
33
  const normalizeSandboxDetail = (detail) => {
22
34
  const { token, tokenExpiresAt, ...sandbox } = detail;
23
35
  return {
24
36
  ...normalizeSandbox(sandbox),
25
- token,
26
- tokenExpiresAt,
37
+ token: token || null,
38
+ tokenExpiresAt: tokenExpiresAt || null,
27
39
  };
28
40
  };
29
41
  const normalizeSandboxListResponse = (response) => ({
@@ -31,8 +43,31 @@ const normalizeSandboxListResponse = (response) => ({
31
43
  sandboxes: response.sandboxes.map(normalizeSandbox),
32
44
  });
33
45
  const serializeCreateSandboxParams = (params) => {
46
+ if (!params || typeof params !== "object")
47
+ throw new error_1.HyperbrowserError("Sandbox launch parameters are required");
48
+ for (const name of ["imageName", "snapshotName", "imageId", "snapshotId"]) {
49
+ const value = params[name];
50
+ if (value !== undefined && (typeof value !== "string" || !value.trim()))
51
+ throw new error_1.HyperbrowserError(`${name} must be a nonempty string`);
52
+ }
53
+ if (params.imageId !== undefined && !params.imageName)
54
+ throw new error_1.HyperbrowserError("imageId requires imageName");
55
+ if (params.snapshotId !== undefined && !params.snapshotName)
56
+ throw new error_1.HyperbrowserError("snapshotId requires snapshotName");
57
+ if (Boolean(params.imageName) === Boolean(params.snapshotName))
58
+ throw new error_1.HyperbrowserError("Provide exactly one start source: snapshotName or imageName");
59
+ for (const name of ["cpu", "memoryMiB", "diskMiB"]) {
60
+ const value = params[name];
61
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < 1))
62
+ throw new error_1.HyperbrowserError(`${name} must be a positive integer`);
63
+ }
64
+ if (params.runtimeClass !== undefined &&
65
+ params.runtimeClass !== "firecracker" &&
66
+ params.runtimeClass !== "gvisor-cpu")
67
+ throw new error_1.HyperbrowserError("Unsupported sandbox runtimeClass");
34
68
  if (typeof params.imageName === "string") {
35
69
  return {
70
+ runtimeClass: params.runtimeClass,
36
71
  imageName: params.imageName,
37
72
  imageId: params.imageId,
38
73
  region: params.region,
@@ -52,9 +87,10 @@ const serializeCreateSandboxParams = (params) => {
52
87
  if (snapshotParams.cpu !== undefined ||
53
88
  snapshotParams.memoryMiB !== undefined ||
54
89
  snapshotParams.diskMiB !== undefined) {
55
- throw new client_1.HyperbrowserError("cpu, memoryMiB, and diskMiB are only supported for image launches", undefined);
90
+ throw new error_1.HyperbrowserError("cpu, memoryMiB, and diskMiB are only supported for image launches", undefined);
56
91
  }
57
92
  return {
93
+ runtimeClass: snapshotParams.runtimeClass,
58
94
  snapshotName: snapshotParams.snapshotName,
59
95
  snapshotId: snapshotParams.snapshotId,
60
96
  region: snapshotParams.region,
@@ -67,6 +103,59 @@ const serializeCreateSandboxParams = (params) => {
67
103
  denyOut: snapshotParams.denyOut,
68
104
  };
69
105
  };
106
+ const IMAGE_BUILD_DEFAULT_POLL_INTERVAL_SECONDS = 3;
107
+ const IMAGE_BUILD_DEFAULT_WAIT_TIMEOUT_SECONDS = 35 * 60;
108
+ const IMAGE_BUILD_COMPLETE_RETRY_DELAY_MS = 2000;
109
+ const READY_IMAGE_PAGE_SIZE = 100;
110
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
111
+ const validatePagination = (params, maxLimit) => {
112
+ for (const key of ["page", "limit"]) {
113
+ const value = params[key];
114
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < 1))
115
+ throw new error_1.HyperbrowserError(`${key} must be a positive integer`);
116
+ }
117
+ if (maxLimit !== undefined && params.limit !== undefined && params.limit > maxLimit)
118
+ throw new error_1.HyperbrowserError(`limit must be at most ${maxLimit}`);
119
+ };
120
+ const validateImageBuildFormat = (params) => {
121
+ if (params.sourcePlatform !== undefined && params.sourcePlatform !== "linux/amd64")
122
+ throw new error_1.HyperbrowserError("sourcePlatform must be linux/amd64");
123
+ if (params.inputFormat !== undefined && ![
124
+ "rootfs_export_tar_gz", "dockerfile_context_tar_gz",
125
+ "dockerfile_context_manifest_v1", "docker_image_manifest_v1",
126
+ ].includes(params.inputFormat))
127
+ throw new error_1.HyperbrowserError("Unsupported image build inputFormat");
128
+ };
129
+ const serializeCreateImageBuildParams = (params) => {
130
+ validateImageBuildFormat(params);
131
+ for (const key of ["builderCpus", "builderMemoryMiB", "builderScratchMiB"]) {
132
+ const value = params[key];
133
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < 1))
134
+ throw new error_1.HyperbrowserError(`${key} must be a positive integer`);
135
+ }
136
+ return ({
137
+ imageName: params.imageName,
138
+ inputSha256: params.inputSha256,
139
+ inputSizeBytes: params.inputSizeBytes,
140
+ vcpus: params.builderCpus,
141
+ memMiB: params.builderMemoryMiB,
142
+ scratchMiB: params.builderScratchMiB,
143
+ inputFormat: params.inputFormat,
144
+ sourcePlatform: params.sourcePlatform,
145
+ imageConfigUser: params.imageConfigUser,
146
+ imageInit: params.imageInit,
147
+ dockerfilePath: params.dockerfilePath,
148
+ contextManifest: params.contextManifest,
149
+ dockerImageManifest: params.dockerImageManifest,
150
+ });
151
+ };
152
+ const normalizeImageBuildPlatform = (platform) => {
153
+ const normalized = (platform ?? image_build_1.IMAGE_BUILD_SOURCE_PLATFORM).trim().toLowerCase();
154
+ if (normalized !== image_build_1.IMAGE_BUILD_SOURCE_PLATFORM) {
155
+ throw new error_1.HyperbrowserError(`Image builds require platform '${image_build_1.IMAGE_BUILD_SOURCE_PLATFORM}'`);
156
+ }
157
+ return normalized;
158
+ };
70
159
  const resolveSandboxRuntimeSessionHost = (runtime, baseUrl) => {
71
160
  const sessionIdFromBasePath = (0, runtime_path_1.runtimeSessionIdFromPath)(baseUrl.pathname);
72
161
  if (sessionIdFromBasePath && baseUrl.hostname) {
@@ -168,7 +257,7 @@ class SandboxHandle {
168
257
  return this.service.createMemorySnapshot(this.id, params);
169
258
  }
170
259
  async expose(params) {
171
- const exposure = await this.service.expose(this.id, params);
260
+ const exposure = await this.service.expose(this.id, params, this.runtime);
172
261
  this.detail = {
173
262
  ...this.detail,
174
263
  exposedPorts: upsertExposedPort(this.detail.exposedPorts ?? [], exposure),
@@ -243,6 +332,10 @@ class SandboxHandle {
243
332
  }
244
333
  return expiresAt - Date.now() <= RUNTIME_SESSION_REFRESH_BUFFER_MS;
245
334
  }
335
+ async createRuntimeSession(options = {}) {
336
+ const session = await this.ensureRuntimeSession(options.forceRefresh);
337
+ return { ...session, runtime: { ...session.runtime } };
338
+ }
246
339
  async ensureRuntimeSession(forceRefresh = false) {
247
340
  this.assertRuntimeAvailable();
248
341
  if (!forceRefresh && this.runtimeSession && !this.isRuntimeSessionExpiring()) {
@@ -251,7 +344,7 @@ class SandboxHandle {
251
344
  const detail = await this.service.getDetail(this.id);
252
345
  this.hydrate(detail);
253
346
  if (!this.runtimeSession) {
254
- throw new client_1.HyperbrowserError(`Sandbox ${this.id} is not running`, {
347
+ throw new error_1.HyperbrowserError(`Sandbox ${this.id} is not running`, {
255
348
  statusCode: 409,
256
349
  code: "sandbox_not_running",
257
350
  retryable: false,
@@ -284,7 +377,7 @@ class SandboxHandle {
284
377
  if (this.detail.status === "closed" ||
285
378
  this.detail.status === "close-error" ||
286
379
  this.detail.status === "error") {
287
- throw new client_1.HyperbrowserError(`Sandbox ${this.id} is not running`, {
380
+ throw new error_1.HyperbrowserError(`Sandbox ${this.id} is not running`, {
288
381
  statusCode: 409,
289
382
  code: "sandbox_not_running",
290
383
  retryable: false,
@@ -317,6 +410,29 @@ class SandboxesService extends base_2.BaseService {
317
410
  const detail = await this.createDetail(params);
318
411
  return this.attach(detail);
319
412
  }
413
+ async startFromSnapshot(params) {
414
+ if (!params?.snapshotName)
415
+ throw new error_1.HyperbrowserError("snapshotName is required");
416
+ return this.create(params);
417
+ }
418
+ async getRuntimeSession(id) {
419
+ const detail = await this.getDetail(id);
420
+ if (!detail.token || ["closed", "close-error", "error"].includes(detail.status)) {
421
+ throw new error_1.HyperbrowserError(`Sandbox ${id} is not running`, {
422
+ statusCode: 409,
423
+ code: "sandbox_not_running",
424
+ service: "runtime",
425
+ });
426
+ }
427
+ return {
428
+ sandboxId: id,
429
+ status: detail.status,
430
+ region: detail.region,
431
+ token: detail.token,
432
+ tokenExpiresAt: detail.tokenExpiresAt,
433
+ runtime: { ...detail.runtime },
434
+ };
435
+ }
320
436
  async get(id) {
321
437
  const detail = await this.getDetail(id);
322
438
  return this.attach(detail);
@@ -327,22 +443,23 @@ class SandboxesService extends base_2.BaseService {
327
443
  return handle;
328
444
  }
329
445
  async list(params = {}) {
446
+ validatePagination(params);
330
447
  try {
331
448
  const response = await this.request("/sandboxes", undefined, {
332
449
  status: params.status,
333
450
  start: params.start,
334
451
  end: params.end,
335
452
  search: params.search,
336
- page: params.page,
337
- limit: params.limit,
453
+ page: params.page ?? 1,
454
+ limit: params.limit ?? 10,
338
455
  });
339
456
  return normalizeSandboxListResponse(response);
340
457
  }
341
458
  catch (error) {
342
- if (error instanceof client_1.HyperbrowserError) {
459
+ if (error instanceof error_1.HyperbrowserError) {
343
460
  throw error;
344
461
  }
345
- throw new client_1.HyperbrowserError("Failed to list sandboxes", undefined);
462
+ throw new error_1.HyperbrowserError("Failed to list sandboxes", undefined);
346
463
  }
347
464
  }
348
465
  async deleteImage(image) {
@@ -352,13 +469,14 @@ class SandboxesService extends base_2.BaseService {
352
469
  });
353
470
  }
354
471
  catch (error) {
355
- if (error instanceof client_1.HyperbrowserError) {
472
+ if (error instanceof error_1.HyperbrowserError) {
356
473
  throw error;
357
474
  }
358
- throw new client_1.HyperbrowserError(`Failed to delete image ${image}`);
475
+ throw new error_1.HyperbrowserError(`Failed to delete image ${image}`);
359
476
  }
360
477
  }
361
478
  async listImages(params = {}) {
479
+ validatePagination(params, 100);
362
480
  try {
363
481
  return await this.request("/images", undefined, {
364
482
  source: params.source,
@@ -368,13 +486,14 @@ class SandboxesService extends base_2.BaseService {
368
486
  });
369
487
  }
370
488
  catch (error) {
371
- if (error instanceof client_1.HyperbrowserError) {
489
+ if (error instanceof error_1.HyperbrowserError) {
372
490
  throw error;
373
491
  }
374
- throw new client_1.HyperbrowserError("Failed to list sandbox images", undefined);
492
+ throw new error_1.HyperbrowserError("Failed to list sandbox images", undefined);
375
493
  }
376
494
  }
377
495
  async listSnapshots(params = {}) {
496
+ validatePagination(params, 100);
378
497
  try {
379
498
  return await this.request("/snapshots", undefined, {
380
499
  status: params.status,
@@ -385,10 +504,10 @@ class SandboxesService extends base_2.BaseService {
385
504
  });
386
505
  }
387
506
  catch (error) {
388
- if (error instanceof client_1.HyperbrowserError) {
507
+ if (error instanceof error_1.HyperbrowserError) {
389
508
  throw error;
390
509
  }
391
- throw new client_1.HyperbrowserError("Failed to list sandbox snapshots", undefined);
510
+ throw new error_1.HyperbrowserError("Failed to list sandbox snapshots", undefined);
392
511
  }
393
512
  }
394
513
  async stop(id) {
@@ -398,10 +517,10 @@ class SandboxesService extends base_2.BaseService {
398
517
  });
399
518
  }
400
519
  catch (error) {
401
- if (error instanceof client_1.HyperbrowserError) {
520
+ if (error instanceof error_1.HyperbrowserError) {
402
521
  throw error;
403
522
  }
404
- throw new client_1.HyperbrowserError(`Failed to stop sandbox ${id}`, undefined);
523
+ throw new error_1.HyperbrowserError(`Failed to stop sandbox ${id}`, undefined);
405
524
  }
406
525
  }
407
526
  async getDetail(id) {
@@ -410,10 +529,10 @@ class SandboxesService extends base_2.BaseService {
410
529
  return normalizeSandboxDetail(detail);
411
530
  }
412
531
  catch (error) {
413
- if (error instanceof client_1.HyperbrowserError) {
532
+ if (error instanceof error_1.HyperbrowserError) {
414
533
  throw error;
415
534
  }
416
- throw new client_1.HyperbrowserError(`Failed to get sandbox ${id}`, undefined);
535
+ throw new error_1.HyperbrowserError(`Failed to get sandbox ${id}`, undefined);
417
536
  }
418
537
  }
419
538
  attach(detail) {
@@ -427,24 +546,27 @@ class SandboxesService extends base_2.BaseService {
427
546
  });
428
547
  }
429
548
  catch (error) {
430
- if (error instanceof client_1.HyperbrowserError) {
549
+ if (error instanceof error_1.HyperbrowserError) {
431
550
  throw error;
432
551
  }
433
- throw new client_1.HyperbrowserError(`Failed to create memory snapshot for sandbox ${id}`, undefined);
552
+ throw new error_1.HyperbrowserError(`Failed to create memory snapshot for sandbox ${id}`, undefined);
434
553
  }
435
554
  }
436
- async expose(id, params) {
555
+ async expose(id, params, runtime) {
437
556
  try {
438
- return await this.request(`/sandbox/${id}/expose`, {
557
+ const exposure = await this.request(`/sandbox/${id}/expose`, {
439
558
  method: "POST",
440
559
  body: JSON.stringify(params),
441
560
  });
561
+ if (!exposure.url)
562
+ exposure.url = buildSandboxExposedUrl(runtime ?? (await this.getDetail(id)).runtime, exposure.port);
563
+ return exposure;
442
564
  }
443
565
  catch (error) {
444
- if (error instanceof client_1.HyperbrowserError) {
566
+ if (error instanceof error_1.HyperbrowserError) {
445
567
  throw error;
446
568
  }
447
- throw new client_1.HyperbrowserError(`Failed to expose port ${params.port} for sandbox ${id}`);
569
+ throw new error_1.HyperbrowserError(`Failed to expose port ${params.port} for sandbox ${id}`);
448
570
  }
449
571
  }
450
572
  async getSnapshot(snapshot) {
@@ -453,10 +575,10 @@ class SandboxesService extends base_2.BaseService {
453
575
  return response.snapshot;
454
576
  }
455
577
  catch (error) {
456
- if (error instanceof client_1.HyperbrowserError) {
578
+ if (error instanceof error_1.HyperbrowserError) {
457
579
  throw error;
458
580
  }
459
- throw new client_1.HyperbrowserError(`Failed to get snapshot ${snapshot}`);
581
+ throw new error_1.HyperbrowserError(`Failed to get snapshot ${snapshot}`);
460
582
  }
461
583
  }
462
584
  async deleteSnapshot(snapshot) {
@@ -464,36 +586,36 @@ class SandboxesService extends base_2.BaseService {
464
586
  return await this.request(`/snapshots/${encodeURIComponent(snapshot)}`, { method: "DELETE" });
465
587
  }
466
588
  catch (error) {
467
- if (error instanceof client_1.HyperbrowserError) {
589
+ if (error instanceof error_1.HyperbrowserError) {
468
590
  throw error;
469
591
  }
470
- throw new client_1.HyperbrowserError(`Failed to delete snapshot ${snapshot}`);
592
+ throw new error_1.HyperbrowserError(`Failed to delete snapshot ${snapshot}`);
471
593
  }
472
594
  }
473
595
  async createImageBuild(params) {
474
596
  try {
475
597
  return await this.request("/images/builds", {
476
598
  method: "POST",
477
- body: JSON.stringify(params),
599
+ body: JSON.stringify(serializeCreateImageBuildParams(params)),
478
600
  });
479
601
  }
480
602
  catch (error) {
481
- if (error instanceof client_1.HyperbrowserError) {
603
+ if (error instanceof error_1.HyperbrowserError) {
482
604
  throw error;
483
605
  }
484
- throw new client_1.HyperbrowserError("Failed to create image build");
606
+ throw new error_1.HyperbrowserError("Failed to create image build");
485
607
  }
486
608
  }
487
- async getImageBuild(buildId) {
609
+ async getImageBuild(buildId, options = {}) {
488
610
  try {
489
- const response = await this.request(`/images/builds/${encodeURIComponent(buildId)}`);
611
+ const response = await this.request(`/images/builds/${encodeURIComponent(buildId)}`, { signal: options.signal });
490
612
  return response.build;
491
613
  }
492
614
  catch (error) {
493
- if (error instanceof client_1.HyperbrowserError) {
615
+ if (error instanceof error_1.HyperbrowserError) {
494
616
  throw error;
495
617
  }
496
- throw new client_1.HyperbrowserError(`Failed to get image build ${buildId}`);
618
+ throw new error_1.HyperbrowserError(`Failed to get image build ${buildId}`);
497
619
  }
498
620
  }
499
621
  async listImageBuilds(params = {}) {
@@ -504,13 +626,14 @@ class SandboxesService extends base_2.BaseService {
504
626
  });
505
627
  }
506
628
  catch (error) {
507
- if (error instanceof client_1.HyperbrowserError) {
629
+ if (error instanceof error_1.HyperbrowserError) {
508
630
  throw error;
509
631
  }
510
- throw new client_1.HyperbrowserError("Failed to list image builds");
632
+ throw new error_1.HyperbrowserError("Failed to list image builds");
511
633
  }
512
634
  }
513
635
  async completeImageBuild(buildId, params) {
636
+ validateImageBuildFormat(params);
514
637
  try {
515
638
  const response = await this.request(`/images/builds/${encodeURIComponent(buildId)}/complete`, {
516
639
  method: "POST",
@@ -519,10 +642,10 @@ class SandboxesService extends base_2.BaseService {
519
642
  return response.build;
520
643
  }
521
644
  catch (error) {
522
- if (error instanceof client_1.HyperbrowserError) {
645
+ if (error instanceof error_1.HyperbrowserError) {
523
646
  throw error;
524
647
  }
525
- throw new client_1.HyperbrowserError(`Failed to complete image build ${buildId}`);
648
+ throw new error_1.HyperbrowserError(`Failed to complete image build ${buildId}`);
526
649
  }
527
650
  }
528
651
  async cancelImageBuild(buildId) {
@@ -531,10 +654,403 @@ class SandboxesService extends base_2.BaseService {
531
654
  return response.build;
532
655
  }
533
656
  catch (error) {
534
- if (error instanceof client_1.HyperbrowserError) {
657
+ if (error instanceof error_1.HyperbrowserError) {
658
+ throw error;
659
+ }
660
+ throw new error_1.HyperbrowserError(`Failed to cancel image build ${buildId}`);
661
+ }
662
+ }
663
+ async reuseDockerImage(params) {
664
+ validateImageBuildFormat(params);
665
+ try {
666
+ return await this.request("/images/builds/reuse", {
667
+ method: "POST",
668
+ body: JSON.stringify(params),
669
+ });
670
+ }
671
+ catch (error) {
672
+ if (error instanceof error_1.HyperbrowserError) {
673
+ throw error;
674
+ }
675
+ throw new error_1.HyperbrowserError("Failed to reuse Docker image");
676
+ }
677
+ }
678
+ /** Find an exact ready team image, including revisions awaiting backup. */
679
+ async findReadyImage(imageName) {
680
+ let page = 1;
681
+ for (;;) {
682
+ const response = await this.listImages({
683
+ search: imageName,
684
+ source: ["team"],
685
+ page,
686
+ limit: READY_IMAGE_PAGE_SIZE,
687
+ });
688
+ for (const image of response.images) {
689
+ if (image.imageName === imageName && (image.uploaded || image.ready === true)) {
690
+ return image;
691
+ }
692
+ }
693
+ if (response.images.length < READY_IMAGE_PAGE_SIZE) {
694
+ return null;
695
+ }
696
+ if (typeof response.totalCount === "number" &&
697
+ page * READY_IMAGE_PAGE_SIZE >= response.totalCount) {
698
+ return null;
699
+ }
700
+ page += 1;
701
+ }
702
+ }
703
+ /** Poll an image build until it completes, rejecting on failure or timeout. */
704
+ async waitForImageBuild(buildId, options = {}) {
705
+ const pollInterval = options.pollInterval ?? IMAGE_BUILD_DEFAULT_POLL_INTERVAL_SECONDS;
706
+ const timeout = options.timeout === undefined ? IMAGE_BUILD_DEFAULT_WAIT_TIMEOUT_SECONDS : options.timeout;
707
+ if (!Number.isFinite(pollInterval) ||
708
+ pollInterval < 0 ||
709
+ (timeout !== null && (!Number.isFinite(timeout) || timeout < 0))) {
710
+ throw new error_1.HyperbrowserError("Image build wait timeout and interval must be nonnegative finite seconds");
711
+ }
712
+ const controller = new AbortController();
713
+ let timedOut = false;
714
+ const deadlineTimer = timeout === null
715
+ ? undefined
716
+ : setTimeout(() => {
717
+ timedOut = true;
718
+ controller.abort();
719
+ }, timeout * 1000);
720
+ const cancel = () => controller.abort();
721
+ options.signal?.addEventListener("abort", cancel, { once: true });
722
+ if (options.signal?.aborted)
723
+ cancel();
724
+ const waitFailure = (error) => {
725
+ if (timedOut)
726
+ throw new error_1.HyperbrowserError(`Timed out waiting for image build ${buildId}`, {
727
+ code: "wait_timeout",
728
+ service: "control",
729
+ cause: error,
730
+ });
731
+ throw error;
732
+ };
733
+ try {
734
+ for (;;) {
735
+ const build = await this.getImageBuild(buildId, { signal: controller.signal }).catch(waitFailure);
736
+ if (build.status === "completed")
737
+ return build;
738
+ if (build.status === "failed" || build.status === "canceled") {
739
+ throw new error_1.HyperbrowserError(build.errorMessage || `Image build ${buildId} ${build.status}`, { code: build.errorCode || "image_build_failed", service: "control", details: build });
740
+ }
741
+ await (0, retry_1.retryDelay)(pollInterval * 1000, controller.signal).catch(waitFailure);
742
+ }
743
+ }
744
+ finally {
745
+ clearTimeout(deadlineTimer);
746
+ options.signal?.removeEventListener("abort", cancel);
747
+ }
748
+ }
749
+ /** Import a local Docker image as reusable layers, reusing exact cached imports. */
750
+ async buildImageFromDockerImage(options) {
751
+ const platform = normalizeImageBuildPlatform(options.platform);
752
+ const source = await (0, image_build_1.prepareDockerImageManifestSource)(options.dockerImage, { platform });
753
+ try {
754
+ if (options.expectedImageDigest !== undefined &&
755
+ source.imageDigest !== options.expectedImageDigest.toLowerCase()) {
756
+ throw new error_1.HyperbrowserError("Docker image changed after its cache identity was computed. " +
757
+ "Retry with a fresh image digest.");
758
+ }
759
+ const imageInit = (0, image_build_1.mergeImageInit)(source.imageInit, options.imageInit);
760
+ const imageConfigUser = options.imageConfigUser ?? source.imageConfigUser;
761
+ let reused = null;
762
+ try {
763
+ reused = await this.reuseDockerImage({
764
+ imageName: options.imageName,
765
+ sourceImageDigest: source.imageDigest,
766
+ sourcePlatform: "linux/amd64",
767
+ imageConfigUser,
768
+ imageInit,
769
+ });
770
+ }
771
+ catch (error) {
772
+ if (!(error instanceof error_1.HyperbrowserError) || error.statusCode !== 404) {
773
+ throw error;
774
+ }
775
+ }
776
+ if (reused?.hit) {
777
+ if (!reused.build) {
778
+ throw new error_1.HyperbrowserError("exact image cache response is missing its completed build");
779
+ }
780
+ return reused.build;
781
+ }
782
+ const packaged = await (0, image_build_1.packageDockerImageManifest)(options.dockerImage, source.imageDigest, source.config, { platform, tempDir: options.tempDir });
783
+ try {
784
+ return await this.submitImageBuild({
785
+ imageName: options.imageName,
786
+ inputSha256: packaged.artifact.sha256Hex,
787
+ inputSizeBytes: packaged.artifact.sizeBytes,
788
+ inputFormat: packaged.artifact.inputFormat,
789
+ sourcePlatform: "linux/amd64",
790
+ imageConfigUser,
791
+ imageInit,
792
+ dockerImageManifest: packaged.manifest,
793
+ builderCpus: options.builderCpus,
794
+ builderMemoryMiB: options.builderMemoryMiB,
795
+ builderScratchMiB: options.builderScratchMiB,
796
+ }, packaged.artifact, packaged.layers, "Docker image layer", options);
797
+ }
798
+ finally {
799
+ packaged.cleanup();
800
+ }
801
+ }
802
+ finally {
803
+ await source.cleanup();
804
+ }
805
+ }
806
+ /**
807
+ * Build a Dockerfile into a sandbox image.
808
+ *
809
+ * Remote builds (default) upload only the effective build context and build
810
+ * on Hyperbrowser. `remote: false` builds with local Docker and imports the
811
+ * resulting image instead.
812
+ */
813
+ async buildImageFromDockerfile(options) {
814
+ options = { ...options, platform: normalizeImageBuildPlatform(options.platform) };
815
+ const remote = options.remote ?? true;
816
+ if (remote) {
817
+ if (options.dockerTag !== undefined ||
818
+ (options.buildArgs && Object.keys(options.buildArgs).length > 0)) {
819
+ throw new error_1.HyperbrowserError("dockerTag and buildArgs require remote: false; remote Dockerfile builds " +
820
+ "send the build context to Hyperbrowser");
821
+ }
822
+ return this.buildImageFromRemoteDockerfile(options);
823
+ }
824
+ if (options.expectedContextFingerprint !== undefined) {
825
+ throw new error_1.HyperbrowserError("expectedContextFingerprint requires remote: true");
826
+ }
827
+ const tag = options.dockerTag ?? (0, image_build_1.makeTempDockerTag)();
828
+ try {
829
+ await (0, image_build_1.buildDockerImageFromDockerfile)({
830
+ contextPath: options.contextPath,
831
+ dockerfile: options.dockerfile,
832
+ tag,
833
+ platform: options.platform,
834
+ buildArgs: options.buildArgs,
835
+ });
836
+ return await this.buildImageFromDockerImage({
837
+ dockerImage: tag,
838
+ imageName: options.imageName,
839
+ platform: options.platform,
840
+ imageInit: options.imageInit,
841
+ imageConfigUser: options.imageConfigUser,
842
+ builderCpus: options.builderCpus,
843
+ builderMemoryMiB: options.builderMemoryMiB,
844
+ builderScratchMiB: options.builderScratchMiB,
845
+ wait: options.wait,
846
+ pollInterval: options.pollInterval,
847
+ waitTimeout: options.waitTimeout,
848
+ signal: options.signal,
849
+ tempDir: options.tempDir,
850
+ uploadTimeout: options.uploadTimeout,
851
+ });
852
+ }
853
+ finally {
854
+ if (options.dockerTag === undefined) {
855
+ await (0, image_build_1.removeDockerImage)(tag);
856
+ }
857
+ }
858
+ }
859
+ /**
860
+ * Reuse, join, or build content-derived remote Dockerfile/image inputs.
861
+ *
862
+ * Supply exactly one of `contextPath` or `dockerImage`. Names include source
863
+ * contents, platform and image initialization overrides. `forceBuild` skips
864
+ * ready-image lookup, but joins matching active builds and retains builder
865
+ * layer/artifact caches. Canceling polling never cancels the backend build.
866
+ * `waitTimeout` applies to this caller's polling, independently of uploads.
867
+ * This composes existing APIs; lookup plus creation is not server-atomic.
868
+ */
869
+ async getOrBuildImage(options) {
870
+ const platform = normalizeImageBuildPlatform(options.platform);
871
+ const dockerfile = options.dockerfile ?? "Dockerfile";
872
+ const remoteFullContext = options.remoteFullContext ?? false;
873
+ const { contextPath, dockerImage } = options;
874
+ if ((contextPath === undefined) === (dockerImage === undefined)) {
875
+ throw new error_1.HyperbrowserError("Supply exactly one of contextPath or dockerImage");
876
+ }
877
+ let fingerprint;
878
+ let source;
879
+ let inputFormat;
880
+ if (contextPath !== undefined) {
881
+ if (options.expectedImageDigest !== undefined) {
882
+ throw new error_1.HyperbrowserError("expectedImageDigest requires dockerImage");
883
+ }
884
+ fingerprint =
885
+ options.expectedContextFingerprint ??
886
+ (await (0, image_build_1.dockerBuildContextFingerprint)(contextPath, {
887
+ dockerfile,
888
+ forceFullContext: remoteFullContext,
889
+ }));
890
+ source = "dockerfile";
891
+ inputFormat = "dockerfile_context_manifest_v1";
892
+ }
893
+ else {
894
+ if (options.expectedContextFingerprint !== undefined ||
895
+ remoteFullContext ||
896
+ dockerfile !== "Dockerfile") {
897
+ throw new error_1.HyperbrowserError("Dockerfile context options require contextPath");
898
+ }
899
+ fingerprint =
900
+ options.expectedImageDigest ??
901
+ (await (0, image_build_1.dockerImageDigest)(dockerImage, { platform }));
902
+ source = "prebuilt";
903
+ inputFormat = "docker_image_manifest_v1";
904
+ }
905
+ const imageName = (0, image_build_1.imageBuildName)({
906
+ source,
907
+ fingerprint,
908
+ namePrefix: options.imageNamePrefix,
909
+ platform,
910
+ imageInit: options.imageInit,
911
+ imageConfigUser: options.imageConfigUser,
912
+ });
913
+ if (!options.forceBuild) {
914
+ const image = await this.findReadyImage(imageName);
915
+ if (image !== null) {
916
+ return { outcome: "reused", imageName, imageId: image.id };
917
+ }
918
+ }
919
+ const common = {
920
+ imageName,
921
+ platform,
922
+ imageInit: options.imageInit,
923
+ imageConfigUser: options.imageConfigUser,
924
+ builderCpus: options.builderCpus,
925
+ builderMemoryMiB: options.builderMemoryMiB,
926
+ builderScratchMiB: options.builderScratchMiB,
927
+ wait: false,
928
+ uploadTimeout: options.uploadTimeout === undefined ? 600 : options.uploadTimeout,
929
+ tempDir: options.tempDir,
930
+ };
931
+ let outcome = "created";
932
+ let build;
933
+ try {
934
+ build =
935
+ contextPath !== undefined
936
+ ? await this.buildImageFromDockerfile({
937
+ ...common,
938
+ contextPath,
939
+ dockerfile,
940
+ remote: true,
941
+ remoteFullContext,
942
+ expectedContextFingerprint: fingerprint,
943
+ })
944
+ : await this.buildImageFromDockerImage({
945
+ ...common,
946
+ dockerImage: dockerImage,
947
+ expectedImageDigest: fingerprint,
948
+ });
949
+ }
950
+ catch (error) {
951
+ if (!(error instanceof error_1.HyperbrowserError)) {
952
+ throw error;
953
+ }
954
+ const existing = (0, image_build_1.matchingImageBuild)(error, imageName, inputFormat);
955
+ if (existing === null) {
956
+ throw error;
957
+ }
958
+ build = existing;
959
+ outcome = "joined";
960
+ }
961
+ const wait = options.wait ?? true;
962
+ if (wait && build.status !== "completed") {
963
+ build = await this.waitForImageBuild(build.id, {
964
+ pollInterval: options.pollInterval,
965
+ timeout: options.waitTimeout,
966
+ signal: options.signal,
967
+ });
968
+ }
969
+ return { outcome, imageName, imageId: (0, image_build_1.completedImageId)(build), build };
970
+ }
971
+ async buildImageFromRemoteDockerfile(options) {
972
+ const packaged = await (0, image_build_1.packageDockerBuildContextManifest)(options.contextPath, {
973
+ dockerfile: options.dockerfile,
974
+ forceFullContext: options.remoteFullContext,
975
+ expectedContextFingerprint: options.expectedContextFingerprint,
976
+ tempDir: options.tempDir,
977
+ });
978
+ try {
979
+ return await this.submitImageBuild({
980
+ imageName: options.imageName,
981
+ inputSha256: packaged.artifact.sha256Hex,
982
+ inputSizeBytes: packaged.artifact.sizeBytes,
983
+ inputFormat: packaged.artifact.inputFormat,
984
+ sourcePlatform: "linux/amd64",
985
+ dockerfilePath: packaged.manifest.dockerfilePath,
986
+ imageConfigUser: options.imageConfigUser,
987
+ imageInit: options.imageInit,
988
+ contextManifest: packaged.manifest,
989
+ builderCpus: options.builderCpus,
990
+ builderMemoryMiB: options.builderMemoryMiB,
991
+ builderScratchMiB: options.builderScratchMiB,
992
+ }, packaged.artifact, packaged.bundles, "build context bundle", options);
993
+ }
994
+ finally {
995
+ packaged.cleanup();
996
+ }
997
+ }
998
+ /** Create a build, upload requested artifacts, complete it, and optionally wait. */
999
+ async submitImageBuild(params, artifact, artifacts, label, options) {
1000
+ let buildId = null;
1001
+ let buildStarted = false;
1002
+ try {
1003
+ const createResult = await this.createImageBuild(params);
1004
+ buildId = createResult.build.id;
1005
+ await (0, image_build_1.uploadMissingImageBuildArtifacts)(createResult.uploads, artifacts, {
1006
+ label,
1007
+ timeout: options.uploadTimeout,
1008
+ });
1009
+ const build = await this.completeImageBuildResilient(buildId, artifact);
1010
+ buildStarted = true;
1011
+ if (options.wait ?? true) {
1012
+ return await this.waitForImageBuild(build.id, {
1013
+ pollInterval: options.pollInterval,
1014
+ timeout: options.waitTimeout,
1015
+ signal: options.signal,
1016
+ });
1017
+ }
1018
+ return build;
1019
+ }
1020
+ catch (error) {
1021
+ if (buildId !== null && !buildStarted) {
1022
+ try {
1023
+ await this.cancelImageBuild(buildId);
1024
+ }
1025
+ catch {
1026
+ // Best-effort cancellation; surface the original error.
1027
+ }
1028
+ }
1029
+ throw error;
1030
+ }
1031
+ }
1032
+ async completeImageBuildResilient(buildId, artifact) {
1033
+ const params = {
1034
+ inputSha256: artifact.sha256Hex,
1035
+ inputSizeBytes: artifact.sizeBytes,
1036
+ inputFormat: artifact.inputFormat,
1037
+ };
1038
+ try {
1039
+ return await this.completeImageBuild(buildId, params);
1040
+ }
1041
+ catch (error) {
1042
+ if (!(error instanceof error_1.HyperbrowserError) || error.statusCode !== 409) {
1043
+ throw error;
1044
+ }
1045
+ if (error.message.toLowerCase().includes("already in progress")) {
1046
+ return this.getImageBuild(buildId);
1047
+ }
1048
+ const current = await this.getImageBuild(buildId);
1049
+ if (current.status !== "awaiting_upload" && current.status !== "upload_verified") {
535
1050
  throw error;
536
1051
  }
537
- throw new client_1.HyperbrowserError(`Failed to cancel image build ${buildId}`);
1052
+ await sleep(IMAGE_BUILD_COMPLETE_RETRY_DELAY_MS);
1053
+ return this.completeImageBuild(buildId, params);
538
1054
  }
539
1055
  }
540
1056
  async updateNetwork(id, policy) {
@@ -545,10 +1061,10 @@ class SandboxesService extends base_2.BaseService {
545
1061
  });
546
1062
  }
547
1063
  catch (error) {
548
- if (error instanceof client_1.HyperbrowserError) {
1064
+ if (error instanceof error_1.HyperbrowserError) {
549
1065
  throw error;
550
1066
  }
551
- throw new client_1.HyperbrowserError(`Failed to update network policy for sandbox ${id}`);
1067
+ throw new error_1.HyperbrowserError(`Failed to update network policy for sandbox ${id}`);
552
1068
  }
553
1069
  }
554
1070
  async unexpose(id, port) {
@@ -559,10 +1075,10 @@ class SandboxesService extends base_2.BaseService {
559
1075
  });
560
1076
  }
561
1077
  catch (error) {
562
- if (error instanceof client_1.HyperbrowserError) {
1078
+ if (error instanceof error_1.HyperbrowserError) {
563
1079
  throw error;
564
1080
  }
565
- throw new client_1.HyperbrowserError(`Failed to unexpose port ${port} for sandbox ${id}`);
1081
+ throw new error_1.HyperbrowserError(`Failed to unexpose port ${port} for sandbox ${id}`);
566
1082
  }
567
1083
  }
568
1084
  async createDetail(params) {
@@ -574,10 +1090,10 @@ class SandboxesService extends base_2.BaseService {
574
1090
  return normalizeSandboxDetail(detail);
575
1091
  }
576
1092
  catch (error) {
577
- if (error instanceof client_1.HyperbrowserError) {
1093
+ if (error instanceof error_1.HyperbrowserError) {
578
1094
  throw error;
579
1095
  }
580
- throw new client_1.HyperbrowserError("Failed to create sandbox", undefined);
1096
+ throw new error_1.HyperbrowserError("Failed to create sandbox", undefined);
581
1097
  }
582
1098
  }
583
1099
  }