@hyperbrowser/sdk 0.93.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) 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/index.d.ts +2 -1
  61. package/dist/types/sandbox.d.ts +246 -30
  62. package/dist/types/volume.d.ts +7 -7
  63. package/package.json +24 -5
package/README.md CHANGED
@@ -241,6 +241,70 @@ await sandbox.stop();
241
241
 
242
242
  `connect()` refreshes runtime auth and throws if the sandbox is no longer running.
243
243
 
244
+ Run commands and stream complete output. `exec()` and `processes.start()` open a
245
+ single streamed request and collect stdout/stderr from process start, so output is
246
+ complete even beyond the receiver's replay window. `wait()` timeouts are local and
247
+ keep collecting; `disconnect()` stops collecting without killing the process.
248
+
249
+ ```typescript
250
+ const result = await sandbox.exec("npm test", {
251
+ cwd: "/workspace",
252
+ maxOutputBytes: 128 * 1024 * 1024, // default 64 MiB; exceeding it fails, never truncates
253
+ });
254
+ console.log(result.exitCode, result.stdout);
255
+
256
+ const proc = await sandbox.processes.start("tail -f /var/log/app.log");
257
+ try {
258
+ await proc.wait({ timeoutSec: 5 });
259
+ } catch (error) {
260
+ // Local wait timeout: the process is still running and output is still collected.
261
+ }
262
+ for await (const event of proc.stream()) {
263
+ if (event.type === "stdout") process.stdout.write(event.data);
264
+ if (event.type === "exit") console.log(event.result.exitCode);
265
+ }
266
+ proc.disconnect();
267
+ ```
268
+
269
+ Build a custom sandbox image from a Dockerfile or a local Docker image. `getOrBuildImage()`
270
+ derives a content-based image name, reuses a ready image with the same identity, joins a
271
+ matching in-progress build, or creates a new one. Dockerfile builds package the effective
272
+ build context (Dockerfile sources, `.dockerignore`) and build remotely; no local Docker is
273
+ required. `dockerImage` imports a local `linux/amd64` image via the Docker CLI.
274
+
275
+ ```typescript
276
+ const resolved = await client.sandboxes.getOrBuildImage({
277
+ contextPath: "./services/api",
278
+ dockerfile: "Dockerfile", // relative to contextPath
279
+ imageInit: { env: { NODE_ENV: "production" }, workingDir: "/app" },
280
+ builderCpus: 4,
281
+ builderMemoryMiB: 8192,
282
+ builderScratchMiB: 20480,
283
+ });
284
+ console.log(resolved.outcome, resolved.imageName, resolved.imageId); // "reused" | "joined" | "created"
285
+
286
+ const sandbox = await client.sandboxes.create({ imageName: resolved.imageName });
287
+
288
+ // Import a local Docker image instead (requires docker CLI, linux/amd64 image):
289
+ const imported = await client.sandboxes.getOrBuildImage({ dockerImage: "myorg/app:1.2.3" });
290
+
291
+ // Detached build: return immediately and poll later.
292
+ const pending = await client.sandboxes.getOrBuildImage({ contextPath: ".", wait: false });
293
+ if (pending.build) {
294
+ const build = await client.sandboxes.waitForImageBuild(pending.build.id, {
295
+ pollInterval: 3,
296
+ timeout: 35 * 60,
297
+ });
298
+ console.log(build.status, build.imageId);
299
+ }
300
+ ```
301
+
302
+ Lower-level helpers are also available: `buildImageFromDockerfile()`,
303
+ `buildImageFromDockerImage()`, `findReadyImage()`, `reuseDockerImage()`, plus the raw
304
+ `createImageBuild()` / `completeImageBuild()` / `getImageBuild()` / `listImageBuilds()` /
305
+ `cancelImageBuild()` APIs. Control-plane `GET` requests retry transient failures
306
+ (429/502/503/504 and network errors) up to three times with jittered backoff.
307
+
244
308
  Create a sandbox with pre-exposed ports:
245
309
 
246
310
  ```typescript
@@ -339,3 +403,129 @@ const terminal = await sandbox.terminal.create({
339
403
 
340
404
  const connection = await terminal.attach(10);
341
405
  ```
406
+
407
+ ### Streaming file transfers and watches
408
+
409
+ `read({ format: "stream" })` retains its buffered behavior. Use `uploadStream()`
410
+ and `downloadStream()` for large transfers with backpressure and bounded memory:
411
+
412
+ ```typescript
413
+ import { createReadStream, createWriteStream } from "node:fs";
414
+ import { pipeline } from "node:stream/promises";
415
+
416
+ await sandbox.files.withRunAs("root").uploadStream(
417
+ "/tmp/archive.tar", createReadStream("./archive.tar"),
418
+ );
419
+ await pipeline(sandbox.files.downloadStream("/tmp/archive.tar"), createWriteStream("./copy.tar"));
420
+ ```
421
+
422
+ Uploads accept a Node readable or iterable of string/byte chunks and optional
423
+ `{ contentLength, signal }`. Downloads return an async generator of buffers and
424
+ accept `{ signal }`. Breaking download iteration closes the request. After an
425
+ authentication failure, a consumed upload is rejected with `stream_not_replayable`;
426
+ open a fresh source to retry it. `files.stat`, `mkdir`, `move`, and `delete` are
427
+ aliases; `rename(oldPath, newPath, { overwrite: false })` forwards overwrite policy.
428
+
429
+ ```typescript
430
+ const watch = await sandbox.files.watch("/workspace", { recursive: true });
431
+ try {
432
+ for await (const message of watch.events({ cursor: 0, route: "ws" })) {
433
+ if (message.type === "done") break;
434
+ console.log(message.event.seq, message.event.path, message.event.op);
435
+ break; // Close this connection; the remote watch remains active.
436
+ }
437
+ // Persist watch.id and the last sequence to resume an active watch:
438
+ const resumed = await sandbox.files.getWatch(watch.id, true);
439
+ await resumed.refresh(true);
440
+ console.log(resumed.current, resumed.toJSON());
441
+ } finally {
442
+ await watch.stop();
443
+ }
444
+ ```
445
+
446
+ Both watch routes (`ws` and `stream`) use WebSocket transport. Watch events include
447
+ an explicit `done` envelope. `watchDir()` remains the callback convenience API.
448
+ Stopping a watcher stops the remote watch; breaking event iteration only closes
449
+ that connection. Watch timestamps remain milliseconds; existing file metadata
450
+ continues to expose `modifiedTime` as a `Date`.
451
+
452
+ ### Image identities, snapshots, and runtime sessions
453
+
454
+ Offline identity helpers are available from the package root and
455
+ `@hyperbrowser/sdk/image-builds`:
456
+
457
+ ```typescript
458
+ import {
459
+ dockerBuildContextFingerprint, imageBuildName, DockerBuildContextChangedError,
460
+ } from "@hyperbrowser/sdk/image-builds";
461
+
462
+ const fingerprint = await dockerBuildContextFingerprint("./app");
463
+ const imageName = imageBuildName({ source: "dockerfile", fingerprint });
464
+ try {
465
+ await client.sandboxes.buildImageFromDockerfile({
466
+ contextPath: "./app", imageName, expectedContextFingerprint: fingerprint,
467
+ builderCpus: 4, builderMemoryMiB: 8192, builderScratchMiB: 20480,
468
+ });
469
+ } catch (error) {
470
+ if (error instanceof DockerBuildContextChangedError) {
471
+ // Recompute identity after a local edit, then retry explicitly.
472
+ } else throw error;
473
+ }
474
+
475
+ const restored = await client.sandboxes.startFromSnapshot({ snapshotName: "saved" });
476
+ const session = await restored.createRuntimeSession({ forceRefresh: true });
477
+ // Also available without keeping a handle:
478
+ await client.sandboxes.getRuntimeSession(restored.id);
479
+ ```
480
+
481
+ Remote Dockerfile builds need no local Docker. Local builds/imports require Docker;
482
+ platform-specific digest lookup requires Docker API 1.49+ (Docker 28.1+). Imports
483
+ preserve image `PATH`, entrypoint/CMD, working directory, and supported environment
484
+ variables; explicit image initialization overrides take precedence. Names include
485
+ context/digest, platform, and initialization overrides. Mutable base tags and
486
+ network downloads are not resolved by fingerprinting; use `forceBuild` when needed.
487
+
488
+ Only `linux/amd64` image builds are supported. Sandbox creation preserves
489
+ `runtimeClass: "firecracker" | "gvisor-cpu"`; use response capabilities when choosing
490
+ snapshot, volume, or exposure operations. Snapshot launches cannot override image
491
+ resources. Invalid or conflicting launch sources fail before a network request.
492
+
493
+ ### Timeouts, cancellation, and compatibility
494
+
495
+ - Client `timeout` is milliseconds. Normal runtime requests have separate header
496
+ and body budgets. Streaming transfers reset inactivity budgets as data moves.
497
+ - Process `timeoutMs` / `timeoutSec` limit remote execution. `wait({ timeoutMs })`
498
+ only limits local waiting and leaves collection running.
499
+ - `exec()` / `processes.start()` accept an `AbortSignal`. Aborting stops local
500
+ collection and closes its connection; the detached remote process continues.
501
+ Use `signal()` or `kill()` when you intend to stop the command.
502
+ - Process stream inactivity is limited to 60 seconds; receiver keepalives reset it.
503
+ Incomplete output, exceeded output limits, and unsupported receivers produce
504
+ structured errors. A failed streaming start is never automatically re-executed.
505
+ - Image polling `pollInterval`, `waitTimeout`, and `uploadTimeout` are seconds.
506
+ `getOrBuildImage()` defaults uploads to 600 seconds of inactivity and polling to
507
+ 35 minutes. Explicit `null` disables the corresponding timeout. Polling deadlines
508
+ include control GET retries. A polling `signal` or timeout leaves accepted builds
509
+ running for other callers. Lower-level build helpers leave upload timeouts unset
510
+ unless explicitly supplied.
511
+ - `HYPERBROWSER_BASE_URL` supplies the API base URL when `baseUrl` is not provided.
512
+
513
+ Public handle classes are available from `@hyperbrowser/sdk/sandbox`; request and
514
+ response types remain under `@hyperbrowser/sdk/types`.
515
+
516
+ ### Development checks
517
+
518
+ The supported Node baseline is 20.20.2. Run `yarn build`, `yarn typecheck`,
519
+ `yarn lint`, and `yarn test` for local verification. Tests share `vitest.config.ts`:
520
+
521
+ - `tests/unit`: isolated logic and mocked contracts (`yarn test:unit`).
522
+ - `tests/integration`: local HTTP, WebSocket, filesystem, and subprocess tests
523
+ (`yarn test:integration`).
524
+ - `tests/e2e`: live Hyperbrowser API tests (`yarn test:e2e`).
525
+
526
+ `yarn test` and `yarn test:watch` select unit and integration tests by default;
527
+ neither requires API credentials or a Docker daemon.
528
+
529
+ Live tests are opt-in: set `HYPERBROWSER_API_KEY` and `HYPERBROWSER_BASE_URL`, then
530
+ run `yarn test:e2e`. They create remote resources and require a receiver supporting
531
+ streamed process starts.
package/dist/client.d.ts CHANGED
@@ -18,26 +18,8 @@ import { JevComputerUseService } from "./services/agents/jev-computer-use";
18
18
  import { WebService } from "./services/web";
19
19
  import { SandboxesService } from "./services/sandboxes";
20
20
  import { VolumesService } from "./services/volumes";
21
- export type HyperbrowserService = "control" | "runtime";
22
- export interface HyperbrowserErrorOptions {
23
- statusCode?: number;
24
- code?: string;
25
- requestId?: string;
26
- retryable?: boolean;
27
- service?: HyperbrowserService;
28
- details?: unknown;
29
- cause?: unknown;
30
- }
31
- export declare class HyperbrowserError extends Error {
32
- readonly statusCode?: number;
33
- readonly code?: string;
34
- readonly requestId?: string;
35
- readonly retryable: boolean;
36
- readonly service?: HyperbrowserService;
37
- readonly details?: unknown;
38
- readonly cause?: unknown;
39
- constructor(message: string, options?: number | HyperbrowserErrorOptions);
40
- }
21
+ export { HyperbrowserError } from "./error";
22
+ export type { HyperbrowserErrorOptions, HyperbrowserService } from "./error";
41
23
  export declare class HyperbrowserClient {
42
24
  readonly sessions: SessionsService;
43
25
  readonly scrape: ScrapeService;
package/dist/client.js CHANGED
@@ -20,29 +20,17 @@ const jev_computer_use_1 = require("./services/agents/jev-computer-use");
20
20
  const web_1 = require("./services/web");
21
21
  const sandboxes_1 = require("./services/sandboxes");
22
22
  const volumes_1 = require("./services/volumes");
23
- class HyperbrowserError extends Error {
24
- constructor(message, options = {}) {
25
- super(`[Hyperbrowser]: ${message}`);
26
- this.name = "HyperbrowserError";
27
- const normalized = typeof options === "number" ? { statusCode: options } : options;
28
- this.statusCode = normalized.statusCode;
29
- this.code = normalized.code;
30
- this.requestId = normalized.requestId;
31
- this.retryable = normalized.retryable ?? false;
32
- this.service = normalized.service;
33
- this.details = normalized.details;
34
- this.cause = normalized.cause;
35
- }
36
- }
37
- exports.HyperbrowserError = HyperbrowserError;
23
+ const error_1 = require("./error");
24
+ var error_2 = require("./error");
25
+ Object.defineProperty(exports, "HyperbrowserError", { enumerable: true, get: function () { return error_2.HyperbrowserError; } });
38
26
  class HyperbrowserClient {
39
27
  constructor(config = {}) {
40
28
  const apiKey = config.apiKey || process.env["HYPERBROWSER_API_KEY"];
41
- const baseUrl = config.baseUrl || "https://api.hyperbrowser.ai";
29
+ const baseUrl = config.baseUrl || process.env["HYPERBROWSER_BASE_URL"] || "https://api.hyperbrowser.ai";
42
30
  const timeout = config.timeout || 30000;
43
31
  const runtimeProxyOverride = config.runtimeProxyOverride?.trim() || undefined;
44
32
  if (!apiKey) {
45
- throw new HyperbrowserError("API key is required - either pass it in config or set HYPERBROWSER_API_KEY environment variable");
33
+ throw new error_1.HyperbrowserError("API key is required - either pass it in config or set HYPERBROWSER_API_KEY environment variable");
46
34
  }
47
35
  this.sessions = new sessions_1.SessionsService(apiKey, baseUrl, timeout);
48
36
  this.scrape = new scrape_1.ScrapeService(apiKey, baseUrl, timeout);
@@ -0,0 +1,27 @@
1
+ export type HyperbrowserService = "control" | "runtime";
2
+ /** Keep diagnostics useful without retaining query credentials or URL hosts. */
3
+ export declare const errorRequestPath: (target: string) => string;
4
+ export declare const networkErrorMessage: (error: unknown, fallback: string) => string;
5
+ export interface HyperbrowserErrorOptions {
6
+ statusCode?: number;
7
+ code?: string;
8
+ requestId?: string;
9
+ retryable?: boolean;
10
+ service?: HyperbrowserService;
11
+ details?: unknown;
12
+ cause?: unknown;
13
+ method?: string;
14
+ path?: string;
15
+ }
16
+ export declare class HyperbrowserError extends Error {
17
+ readonly statusCode?: number;
18
+ readonly code?: string;
19
+ readonly requestId?: string;
20
+ readonly retryable: boolean;
21
+ readonly service?: HyperbrowserService;
22
+ readonly details?: unknown;
23
+ readonly cause?: unknown;
24
+ readonly method?: string;
25
+ readonly path?: string;
26
+ constructor(message: string, options?: number | HyperbrowserErrorOptions);
27
+ }
package/dist/error.js ADDED
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HyperbrowserError = exports.networkErrorMessage = exports.errorRequestPath = void 0;
4
+ /** Keep diagnostics useful without retaining query credentials or URL hosts. */
5
+ const errorRequestPath = (target) => {
6
+ try {
7
+ return new URL(target, "http://sdk.invalid").pathname;
8
+ }
9
+ catch {
10
+ return target.split("?", 1)[0];
11
+ }
12
+ };
13
+ exports.errorRequestPath = errorRequestPath;
14
+ const networkErrorMessage = (error, fallback) => error instanceof Error ? error.message || `${fallback} (${error.name || "Error"})` : fallback;
15
+ exports.networkErrorMessage = networkErrorMessage;
16
+ class HyperbrowserError extends Error {
17
+ constructor(message, options = {}) {
18
+ super(`[Hyperbrowser]: ${message}`);
19
+ this.name = "HyperbrowserError";
20
+ const normalized = typeof options === "number" ? { statusCode: options } : options;
21
+ this.statusCode = normalized.statusCode;
22
+ this.code = normalized.code;
23
+ this.requestId = normalized.requestId;
24
+ this.retryable = normalized.retryable ?? false;
25
+ this.service = normalized.service;
26
+ this.details = normalized.details;
27
+ this.cause = normalized.cause;
28
+ this.method = normalized.method;
29
+ this.path = normalized.path;
30
+ }
31
+ }
32
+ exports.HyperbrowserError = HyperbrowserError;
@@ -0,0 +1,6 @@
1
+ /** Public, offline image identity helpers. */
2
+ export { dockerBuildContextFingerprint, DockerBuildContextChangedError, } from "./sandbox/image-build/context";
3
+ export type { DockerBuildContextOptions } from "./sandbox/image-build/context";
4
+ export { imageBuildName } from "./sandbox/image-build/resolution";
5
+ export type { ImageBuildNameOptions, ImageBuildSource } from "./sandbox/image-build/resolution";
6
+ export { dockerImageDigest, DockerCommandError } from "./sandbox/image-build/docker-image";
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DockerCommandError = exports.dockerImageDigest = exports.imageBuildName = exports.DockerBuildContextChangedError = exports.dockerBuildContextFingerprint = void 0;
4
+ /** Public, offline image identity helpers. */
5
+ var context_1 = require("./sandbox/image-build/context");
6
+ Object.defineProperty(exports, "dockerBuildContextFingerprint", { enumerable: true, get: function () { return context_1.dockerBuildContextFingerprint; } });
7
+ Object.defineProperty(exports, "DockerBuildContextChangedError", { enumerable: true, get: function () { return context_1.DockerBuildContextChangedError; } });
8
+ var resolution_1 = require("./sandbox/image-build/resolution");
9
+ Object.defineProperty(exports, "imageBuildName", { enumerable: true, get: function () { return resolution_1.imageBuildName; } });
10
+ var docker_image_1 = require("./sandbox/image-build/docker-image");
11
+ Object.defineProperty(exports, "dockerImageDigest", { enumerable: true, get: function () { return docker_image_1.dockerImageDigest; } });
12
+ Object.defineProperty(exports, "DockerCommandError", { enumerable: true, get: function () { return docker_image_1.DockerCommandError; } });
package/dist/index.d.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import { dockerBuildContextFingerprint, DockerBuildContextChangedError, imageBuildName, dockerImageDigest, DockerCommandError } from "./image-builds";
2
+ export { dockerBuildContextFingerprint, DockerBuildContextChangedError, imageBuildName, dockerImageDigest, DockerCommandError, };
3
+ export type { DockerBuildContextOptions, ImageBuildNameOptions, ImageBuildSource, } from "./image-builds";
1
4
  import { HyperbrowserClient, HyperbrowserError } from "./client";
2
5
  export declare const Hyperbrowser: typeof HyperbrowserClient;
3
6
  export type Hyperbrowser = HyperbrowserClient;
package/dist/index.js CHANGED
@@ -1,6 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HyperbrowserError = exports.HyperbrowserClient = exports.Hyperbrowser = void 0;
3
+ exports.HyperbrowserError = exports.HyperbrowserClient = exports.Hyperbrowser = exports.DockerCommandError = exports.dockerImageDigest = exports.imageBuildName = exports.DockerBuildContextChangedError = exports.dockerBuildContextFingerprint = void 0;
4
+ const image_builds_1 = require("./image-builds");
5
+ Object.defineProperty(exports, "dockerBuildContextFingerprint", { enumerable: true, get: function () { return image_builds_1.dockerBuildContextFingerprint; } });
6
+ Object.defineProperty(exports, "DockerBuildContextChangedError", { enumerable: true, get: function () { return image_builds_1.DockerBuildContextChangedError; } });
7
+ Object.defineProperty(exports, "imageBuildName", { enumerable: true, get: function () { return image_builds_1.imageBuildName; } });
8
+ Object.defineProperty(exports, "dockerImageDigest", { enumerable: true, get: function () { return image_builds_1.dockerImageDigest; } });
9
+ Object.defineProperty(exports, "DockerCommandError", { enumerable: true, get: function () { return image_builds_1.DockerCommandError; } });
4
10
  const client_1 = require("./client");
5
11
  Object.defineProperty(exports, "HyperbrowserClient", { enumerable: true, get: function () { return client_1.HyperbrowserClient; } });
6
12
  Object.defineProperty(exports, "HyperbrowserError", { enumerable: true, get: function () { return client_1.HyperbrowserError; } });
@@ -13,5 +19,10 @@ if (typeof module !== "undefined" && module.exports) {
13
19
  module.exports.Hyperbrowser = client_1.HyperbrowserClient;
14
20
  module.exports.HyperbrowserClient = client_1.HyperbrowserClient;
15
21
  module.exports.HyperbrowserError = client_1.HyperbrowserError;
22
+ module.exports.dockerBuildContextFingerprint = image_builds_1.dockerBuildContextFingerprint;
23
+ module.exports.DockerBuildContextChangedError = image_builds_1.DockerBuildContextChangedError;
24
+ module.exports.imageBuildName = image_builds_1.imageBuildName;
25
+ module.exports.dockerImageDigest = image_builds_1.dockerImageDigest;
26
+ module.exports.DockerCommandError = image_builds_1.DockerCommandError;
16
27
  module.exports.default = client_1.HyperbrowserClient;
17
28
  }
@@ -0,0 +1,14 @@
1
+ import { HyperbrowserError } from "./error";
2
+ export declare const RETRYABLE_STATUS_CODES: Set<number>;
3
+ export declare const GET_RETRY_MAX_ATTEMPTS = 3;
4
+ export declare const GET_RETRY_INITIAL_DELAY_MS = 250;
5
+ export declare const GET_RETRY_MAX_DELAY_MS = 1000;
6
+ export declare const isRetryableNetworkError: (error: unknown) => boolean;
7
+ export declare const shouldRetryGet: (method: string, error: HyperbrowserError, failedAttempt: number) => boolean;
8
+ /** Exponential backoff with jitter, capped at one second. */
9
+ export declare const getRetryDelayMs: (failedAttempt: number) => number;
10
+ export declare const retryDelay: (ms: number, signal?: {
11
+ readonly aborted: boolean;
12
+ addEventListener: AbortSignal["addEventListener"];
13
+ removeEventListener: AbortSignal["removeEventListener"];
14
+ }) => Promise<void>;
package/dist/retry.js ADDED
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.retryDelay = exports.getRetryDelayMs = exports.shouldRetryGet = exports.isRetryableNetworkError = exports.GET_RETRY_MAX_DELAY_MS = exports.GET_RETRY_INITIAL_DELAY_MS = exports.GET_RETRY_MAX_ATTEMPTS = exports.RETRYABLE_STATUS_CODES = void 0;
4
+ const error_1 = require("./error");
5
+ exports.RETRYABLE_STATUS_CODES = new Set([429, 502, 503, 504]);
6
+ exports.GET_RETRY_MAX_ATTEMPTS = 3;
7
+ exports.GET_RETRY_INITIAL_DELAY_MS = 250;
8
+ exports.GET_RETRY_MAX_DELAY_MS = 1000;
9
+ const RETRYABLE_NETWORK_CODES = new Set([
10
+ "ECONNRESET",
11
+ "ECONNREFUSED",
12
+ "EAI_AGAIN",
13
+ "ETIMEDOUT",
14
+ "ESOCKETTIMEDOUT",
15
+ ]);
16
+ const isRetryableNetworkError = (error) => {
17
+ if (!(error instanceof Error)) {
18
+ return false;
19
+ }
20
+ const networkError = error;
21
+ return (networkError.name === "AbortError" ||
22
+ networkError.type === "request-timeout" ||
23
+ (networkError.code ? RETRYABLE_NETWORK_CODES.has(networkError.code) : false));
24
+ };
25
+ exports.isRetryableNetworkError = isRetryableNetworkError;
26
+ const shouldRetryGet = (method, error, failedAttempt) => method.toUpperCase() === "GET" && error.retryable && failedAttempt < exports.GET_RETRY_MAX_ATTEMPTS;
27
+ exports.shouldRetryGet = shouldRetryGet;
28
+ /** Exponential backoff with jitter, capped at one second. */
29
+ const getRetryDelayMs = (failedAttempt) => {
30
+ const maximumDelay = Math.min(exports.GET_RETRY_INITIAL_DELAY_MS * 2 ** (failedAttempt - 1), exports.GET_RETRY_MAX_DELAY_MS);
31
+ return maximumDelay / 2 + Math.random() * (maximumDelay / 2);
32
+ };
33
+ exports.getRetryDelayMs = getRetryDelayMs;
34
+ const retryDelay = (ms, signal) => new Promise((resolve, reject) => {
35
+ const aborted = () => {
36
+ clearTimeout(timer);
37
+ signal?.removeEventListener("abort", aborted);
38
+ reject(new error_1.HyperbrowserError("Request canceled", { code: "request_aborted", service: "control" }));
39
+ };
40
+ const timer = setTimeout(() => {
41
+ signal?.removeEventListener("abort", aborted);
42
+ resolve();
43
+ }, ms);
44
+ if (signal?.aborted)
45
+ aborted();
46
+ else
47
+ signal?.addEventListener("abort", aborted, { once: true });
48
+ });
49
+ exports.retryDelay = retryDelay;
@@ -11,14 +11,41 @@ export interface RuntimeSSEEvent {
11
11
  id?: string;
12
12
  }
13
13
  type RuntimeParams = Record<string, string | number | boolean | undefined>;
14
+ export declare const PROCESS_STREAM_IDLE_TIMEOUT_MS = 60000;
15
+ export interface RuntimeSSEInit {
16
+ method?: "GET" | "POST";
17
+ body?: string;
18
+ headers?: Record<string, string>;
19
+ /** Milliseconds without any bytes before the stream fails. Defaults to 60s. */
20
+ idleTimeoutMs?: number;
21
+ signal?: AbortSignal;
22
+ }
23
+ export interface RuntimeSSEStream {
24
+ events: AsyncGenerator<RuntimeSSEEvent>;
25
+ /** Close the underlying connection; the remote process keeps running. */
26
+ close(): void;
27
+ }
14
28
  export declare class RuntimeTransport {
15
29
  private readonly resolveConnection;
16
30
  private readonly timeout;
17
31
  private readonly runtimeProxyOverride?;
32
+ private readonly responseCleanup;
33
+ private closeResponse;
34
+ private failure;
18
35
  constructor(resolveConnection: (forceRefresh?: boolean) => Promise<RuntimeConnection>, timeout?: number, runtimeProxyOverride?: string | undefined);
19
36
  requestJSON<T>(path: string, init?: RequestInit, params?: RuntimeParams): Promise<T>;
20
37
  requestBuffer(path: string, init?: RequestInit, params?: RuntimeParams): Promise<Buffer>;
38
+ /** Pull-based binary transfer. Breaking iteration closes the HTTP connection. */
39
+ streamBytes(path: string, init?: RequestInit, params?: RuntimeParams): AsyncGenerator<Buffer>;
40
+ private readChunk;
21
41
  streamSSE(path: string, params?: RuntimeParams): AsyncGenerator<RuntimeSSEEvent>;
42
+ /**
43
+ * Open a server-sent event stream. POST streams carry a JSON body and
44
+ * require an event-stream response, since the request may have side effects
45
+ * that must not be retried blindly.
46
+ */
47
+ openSSE(path: string, params?: RuntimeParams, init?: RuntimeSSEInit): Promise<RuntimeSSEStream>;
48
+ private parseSSE;
22
49
  private fetchWithAuth;
23
50
  private fetchForConnection;
24
51
  private assertResponse;