@hyperbrowser/sdk 1.2.0 → 1.2.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.
package/README.md CHANGED
@@ -375,6 +375,76 @@ await sandbox.stop();
375
375
  await client.volumes.delete(sameVolume.id);
376
376
  ```
377
377
 
378
+ Store secrets and deliver them to sandbox processes as environment variables:
379
+
380
+ ```typescript
381
+ // The value is encrypted locally to the platform key; the API only receives
382
+ // the envelope, and values are never returned.
383
+ await client.secrets.create({
384
+ name: "provider-key",
385
+ description: "Provider API key",
386
+ value: process.env.PROVIDER_KEY!,
387
+ });
388
+ await client.secrets.addVersion("provider-key", newValue); // next version
389
+ const secrets = await client.secrets.list({ search: "provider" });
390
+ const detail = await client.secrets.get("provider-key"); // metadata and versions
391
+
392
+ const sandbox = await client.sandboxes.create({ imageName: "node" });
393
+ const result = await sandbox.exec("node app.js", {
394
+ secrets: { API_KEY: "provider-key", DB_URL: { name: "db-url", version: 2 } },
395
+ });
396
+
397
+ // Long-running processes report the versions they received.
398
+ const worker = await sandbox.processes.start("node worker.js", {
399
+ secrets: { API_KEY: "provider-key" },
400
+ });
401
+ console.log(worker.secrets);
402
+
403
+ // Secrets for the image's startup command (image launches only).
404
+ await client.sandboxes.create({ imageName: "my-image", secrets: { API_KEY: "provider-key" } });
405
+
406
+ await client.secrets.deleteVersion("provider-key", 1);
407
+ await client.secrets.delete("provider-key");
408
+ ```
409
+
410
+ - Secret methods take a secret's ID or exact name. Values are strings or bytes,
411
+ used byte-for-byte: 1 to 32,768 bytes of UTF-8 without NUL.
412
+ - A secret is delivered only to the process it is passed to, but anything
413
+ running as the same user (or root) in the sandbox can read that process's
414
+ environment. The sandbox and its later snapshots are marked
415
+ `containsSecrets`.
416
+ - Deleting a secret does not revoke a value already delivered to a sandbox.
417
+ - Limits: 100 secrets per team, 10 stored versions per secret, 32 secrets per
418
+ process or sandbox. The API key needs the `secrets.read`, `secrets.manage`
419
+ or `secrets.use` scope for reading, managing or delivering secrets.
420
+ SDK updates, version additions, and deletions also require `secrets.read`
421
+ alongside `secrets.manage`: the SDK resolves the existing secret before
422
+ changing it, even when given an ID. Encryption also needs its encryption
423
+ context. Creating a new secret needs only `secrets.manage`.
424
+ - A process start with secrets is never retried. If it times out, its
425
+ response is lost, or the API reports that the sandbox host did not confirm
426
+ the start (`SECRET_PROCESS_OUTCOME_UNKNOWN`), the error code is
427
+ `secret_process_outcome_unknown` and the API's response, if any, is in
428
+ `details`: check the sandbox's processes before starting it again.
429
+ - The same applies to `sandboxes.create` with `secrets`: if the create result
430
+ can't be confirmed, the error code is `secret_process_outcome_unknown`, since
431
+ a sandbox may already be running its startup command with the secrets. List
432
+ sandboxes before creating it again.
433
+ - Startup secrets need an image with a startup command and a Firecracker
434
+ sandbox. Otherwise the create fails with `SECRET_STARTUP_UNSUPPORTED`; create
435
+ the sandbox without `secrets` and pass them to `processes.exec` or
436
+ `processes.start` instead.
437
+ - Secrets routes return 403 `SECRETS_FEATURE_DISABLED` when the team doesn't
438
+ have secrets enabled, and 503 `SECRETS_NOT_CONFIGURED` (not retryable) when
439
+ the server has no secrets encryption key.
440
+ - A command timeout (`timeoutSec` or `timeoutMs`) for a process started with
441
+ secrets is sent in whole milliseconds and can be at most 24 hours.
442
+ - Secret-started handles support `wait()` and `result()` without a local
443
+ deadline. An explicit wait timeout limits the whole local wait, leaving the
444
+ command running; the same handle can be awaited again. Runtime waits use
445
+ bounded requests below the client's request timeout. Output still comes from
446
+ the receiver's retained output, so `maxOutputBytes` does not apply.
447
+
378
448
  List sandboxes with time-range and search filters:
379
449
 
380
450
  ```typescript
package/dist/client.d.ts CHANGED
@@ -18,6 +18,7 @@ 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
+ import { SecretsService } from "./services/secrets";
21
22
  export { HyperbrowserError } from "./error";
22
23
  export type { HyperbrowserErrorOptions, HyperbrowserService } from "./error";
23
24
  export declare class HyperbrowserClient {
@@ -42,5 +43,6 @@ export declare class HyperbrowserClient {
42
43
  readonly computerAction: ComputerActionService;
43
44
  readonly sandboxes: SandboxesService;
44
45
  readonly volumes: VolumesService;
46
+ readonly secrets: SecretsService;
45
47
  constructor(config?: HyperbrowserConfig);
46
48
  }
package/dist/client.js CHANGED
@@ -20,6 +20,7 @@ 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
+ const secrets_1 = require("./services/secrets");
23
24
  const error_1 = require("./error");
24
25
  var error_2 = require("./error");
25
26
  Object.defineProperty(exports, "HyperbrowserError", { enumerable: true, get: function () { return error_2.HyperbrowserError; } });
@@ -43,6 +44,7 @@ class HyperbrowserClient {
43
44
  this.computerAction = new computer_action_1.ComputerActionService(apiKey, baseUrl, timeout);
44
45
  this.sandboxes = new sandboxes_1.SandboxesService(apiKey, baseUrl, timeout, runtimeProxyOverride);
45
46
  this.volumes = new volumes_1.VolumesService(apiKey, baseUrl, timeout);
47
+ this.secrets = new secrets_1.SecretsService(apiKey, baseUrl, timeout);
46
48
  this.agents = {
47
49
  browserUse: new browser_use_1.BrowserUseService(apiKey, baseUrl, timeout),
48
50
  claudeComputerUse: new claude_computer_use_1.ClaudeComputerUseService(apiKey, baseUrl, timeout),
package/dist/error.d.ts CHANGED
@@ -25,3 +25,6 @@ export declare class HyperbrowserError extends Error {
25
25
  readonly path?: string;
26
26
  constructor(message: string, options?: number | HyperbrowserErrorOptions);
27
27
  }
28
+ /** @internal The response body could not be read or decoded as JSON. */
29
+ export declare class ResponseDecodingError extends HyperbrowserError {
30
+ }
package/dist/error.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HyperbrowserError = exports.networkErrorMessage = exports.errorRequestPath = void 0;
3
+ exports.ResponseDecodingError = exports.HyperbrowserError = exports.networkErrorMessage = exports.errorRequestPath = void 0;
4
4
  /** Keep diagnostics useful without retaining query credentials or URL hosts. */
5
5
  const errorRequestPath = (target) => {
6
6
  try {
@@ -30,3 +30,7 @@ class HyperbrowserError extends Error {
30
30
  }
31
31
  }
32
32
  exports.HyperbrowserError = HyperbrowserError;
33
+ /** @internal The response body could not be read or decoded as JSON. */
34
+ class ResponseDecodingError extends HyperbrowserError {
35
+ }
36
+ exports.ResponseDecodingError = ResponseDecodingError;
@@ -1,13 +1,59 @@
1
1
  import { RuntimeSSEStream, RuntimeTransport } from "./base";
2
- import { SandboxExecParams, SandboxExecOptions, SandboxProcessListParams, SandboxProcessListResponse, SandboxProcessResult, SandboxProcessSignal, SandboxProcessStdinParams, SandboxProcessStreamEvent, SandboxProcessSummary, SandboxProcessWaitParams } from "../types/sandbox";
2
+ import { WireSecretReference } from "./secret-references";
3
+ import { SandboxExecParams, SandboxExecOptions, SandboxResolvedSecret, SandboxProcessListParams, SandboxProcessListResponse, SandboxProcessResult, SandboxProcessSignal, SandboxProcessStdinParams, SandboxProcessStreamEvent, SandboxProcessSummary, SandboxProcessWaitParams } from "../types/sandbox";
4
+ export interface RawProcessSummary extends Partial<SandboxProcessSummary> {
5
+ id: string;
6
+ status: SandboxProcessSummary["status"];
7
+ command: string;
8
+ args?: string[] | null;
9
+ cwd: string;
10
+ pid?: number | null;
11
+ exit_code?: number | null;
12
+ started_at: number;
13
+ completed_at?: number | null;
14
+ }
15
+ /** The body of a control API process start with secrets. */
16
+ export interface SecretProcessStartPayload {
17
+ command: string;
18
+ cwd?: string;
19
+ env?: Record<string, string>;
20
+ timeoutMs?: number;
21
+ runAs?: string;
22
+ secrets: Record<string, WireSecretReference>;
23
+ }
24
+ export interface SecretProcessStartResponse {
25
+ /** The sandbox agent's summary, passed through unchanged. */
26
+ process?: RawProcessSummary;
27
+ secrets?: SandboxResolvedSecret[];
28
+ }
29
+ /** @internal Starts processes with secrets through the control API. */
30
+ export interface SecretProcessStarter {
31
+ sandboxId: string;
32
+ /** Each runtime wait stays under the runtime request timeout. */
33
+ waitChunkMs: number;
34
+ start(payload: SecretProcessStartPayload, signal?: AbortSignal): Promise<SecretProcessStartResponse>;
35
+ }
36
+ interface SecretProcessWaitContext {
37
+ chunkMs: number;
38
+ signal?: AbortSignal;
39
+ }
40
+ /**
41
+ * @internal Whether a failed control API request may still have reached the
42
+ * API: it timed out or its response couldn't be read. API errors and failures
43
+ * to connect are excluded.
44
+ */
45
+ export declare const requestMayHaveArrived: (error: unknown) => boolean;
3
46
  export declare class SandboxProcessHandle {
4
47
  private readonly transport;
5
48
  private summary;
49
+ private readonly secretWait?;
6
50
  private output;
7
51
  private collector;
8
52
  private closeStream;
9
53
  private readonly changed;
10
- constructor(transport: RuntimeTransport, summary: SandboxProcessSummary);
54
+ /** The secret versions delivered to a process started with `secrets`. */
55
+ readonly secrets?: SandboxResolvedSecret[];
56
+ constructor(transport: RuntimeTransport, summary: SandboxProcessSummary, secrets?: SandboxResolvedSecret[], secretWait?: SecretProcessWaitContext | undefined);
11
57
  /** @internal Collect output from the start-stream that created this process. */
12
58
  attachCollector(stream: RuntimeSSEStream, maxOutputBytes: number): void;
13
59
  private collect;
@@ -23,6 +69,9 @@ export declare class SandboxProcessHandle {
23
69
  * collected stream; a local timeout rejects without stopping collection.
24
70
  */
25
71
  wait(params?: SandboxProcessWaitParams): Promise<SandboxProcessResult>;
72
+ /** Secret starts retain output on the receiver, so poll until the local deadline. */
73
+ private waitWithSecrets;
74
+ private waitRetained;
26
75
  signal(signal: SandboxProcessSignal): Promise<void>;
27
76
  kill(params?: SandboxProcessWaitParams): Promise<SandboxProcessResult>;
28
77
  writeStdin(input: string | Uint8Array | SandboxProcessStdinParams): Promise<void>;
@@ -35,7 +84,8 @@ export declare class SandboxProcessHandle {
35
84
  }
36
85
  export declare class SandboxProcessesApi {
37
86
  private readonly transport;
38
- constructor(transport: RuntimeTransport);
87
+ private readonly secretStarter?;
88
+ constructor(transport: RuntimeTransport, secretStarter?: SecretProcessStarter | undefined);
39
89
  /** Run a command to completion, collecting its full output from process start. */
40
90
  exec(command: string, options?: SandboxExecOptions): Promise<SandboxProcessResult>;
41
91
  exec(input: SandboxExecParams): Promise<SandboxProcessResult>;
@@ -46,6 +96,18 @@ export declare class SandboxProcessesApi {
46
96
  */
47
97
  start(command: string, options?: SandboxExecOptions): Promise<SandboxProcessHandle>;
48
98
  start(input: SandboxExecParams): Promise<SandboxProcessHandle>;
99
+ /**
100
+ * Start through the control API so the sandbox service decrypts the
101
+ * secrets. Output is then read through the runtime's retained output.
102
+ */
103
+ private startWithSecrets;
104
+ /**
105
+ * Like one-shot exec: stdin is closed, then the command's final result is
106
+ * awaited. The command's own timeout is enforced in the sandbox, so an
107
+ * expired wait (408) only means it is still running.
108
+ */
109
+ private execWithSecrets;
49
110
  get(processId: string): Promise<SandboxProcessHandle>;
50
111
  list(params?: SandboxProcessListParams): Promise<SandboxProcessListResponse>;
51
112
  }
113
+ export {};
@@ -1,8 +1,40 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SandboxProcessesApi = exports.SandboxProcessHandle = void 0;
3
+ exports.SandboxProcessesApi = exports.SandboxProcessHandle = exports.requestMayHaveArrived = void 0;
4
+ const perf_hooks_1 = require("perf_hooks");
4
5
  const error_1 = require("../error");
5
6
  const process_output_1 = require("./process-output");
7
+ const secret_references_1 = require("./secret-references");
8
+ /** Errors from connecting at all; anything else may have reached the API. */
9
+ const CONNECT_ERROR_CODES = new Set(["ECONNREFUSED", "ENOTFOUND", "EAI_AGAIN", "EHOSTUNREACH"]);
10
+ /** The control API's code for a secret process start it could not confirm. */
11
+ const SERVER_OUTCOME_UNKNOWN_CODE = "SECRET_PROCESS_OUTCOME_UNKNOWN";
12
+ /** The control API's limit on a secret process's `timeoutMs`. */
13
+ const MAX_SECRET_PROCESS_TIMEOUT_MS = 86400000;
14
+ /**
15
+ * @internal Whether a failed control API request may still have reached the
16
+ * API: it timed out or its response couldn't be read. API errors and failures
17
+ * to connect are excluded.
18
+ */
19
+ const requestMayHaveArrived = (error) => {
20
+ if (error instanceof error_1.ResponseDecodingError) {
21
+ return true;
22
+ }
23
+ if (!(error instanceof error_1.HyperbrowserError)) {
24
+ return true;
25
+ }
26
+ // Pre-aborted calls are rejected before sending. Cancellation here may
27
+ // interrupt an error body after the API has already accepted the start.
28
+ if (error.code === "request_aborted") {
29
+ return true;
30
+ }
31
+ if (error.statusCode !== undefined && error.statusCode >= 400) {
32
+ return false;
33
+ }
34
+ const code = error.cause?.code;
35
+ return !(code && CONNECT_ERROR_CODES.has(code));
36
+ };
37
+ exports.requestMayHaveArrived = requestMayHaveArrived;
6
38
  const DEFAULT_PROCESS_KILL_WAIT_MS = 5000;
7
39
  const SHELL_SAFE_TOKEN_PATTERN = /^[A-Za-z0-9_@%+=:,./-]+$/;
8
40
  const normalizeProcessSummary = (process) => ({
@@ -99,6 +131,40 @@ const localWaitTimeoutMs = (params) => {
99
131
  }
100
132
  return undefined;
101
133
  };
134
+ /**
135
+ * The command timeout for a secret process start, as the whole milliseconds
136
+ * the control API accepts. Rounded, but a positive timeout stays at least 1ms
137
+ * rather than becoming 0 (no timeout).
138
+ */
139
+ const secretProcessTimeoutMs = (params) => {
140
+ for (const name of ["timeoutSec", "timeoutMs"]) {
141
+ const value = params[name];
142
+ if (value !== undefined &&
143
+ (typeof value !== "number" || !Number.isFinite(value) || value < 0)) {
144
+ throw new error_1.HyperbrowserError(`${name} must be a non-negative number`);
145
+ }
146
+ }
147
+ // Match the receiver: positive seconds take precedence over milliseconds.
148
+ const timeoutMs = localWaitTimeoutMs(params);
149
+ if (timeoutMs === undefined) {
150
+ return undefined;
151
+ }
152
+ const wholeMs = Math.max(1, Math.round(timeoutMs));
153
+ if (wholeMs > MAX_SECRET_PROCESS_TIMEOUT_MS) {
154
+ throw new error_1.HyperbrowserError(`A process started with secrets can run for at most ${MAX_SECRET_PROCESS_TIMEOUT_MS / 1000} seconds`);
155
+ }
156
+ return wholeMs;
157
+ };
158
+ const waitTimeoutError = () => new error_1.HyperbrowserError("Timed out waiting for command output", {
159
+ code: "wait_timeout",
160
+ service: "runtime",
161
+ retryable: false,
162
+ });
163
+ const requestAbortedError = () => new error_1.HyperbrowserError("Request canceled", {
164
+ code: "request_aborted",
165
+ retryable: false,
166
+ service: "runtime",
167
+ });
102
168
  const buildProcessPayload = (input) => ({
103
169
  command: input.command,
104
170
  cwd: input.cwd,
@@ -133,13 +199,15 @@ const encodeStdinPayload = (input) => {
133
199
  };
134
200
  };
135
201
  class SandboxProcessHandle {
136
- constructor(transport, summary) {
202
+ constructor(transport, summary, secrets, secretWait) {
137
203
  this.transport = transport;
138
204
  this.summary = summary;
205
+ this.secretWait = secretWait;
139
206
  this.output = null;
140
207
  this.collector = null;
141
208
  this.closeStream = null;
142
209
  this.changed = new ChangeSignal();
210
+ this.secrets = secrets;
143
211
  }
144
212
  /** @internal Collect output from the start-stream that created this process. */
145
213
  attachCollector(stream, maxOutputBytes) {
@@ -246,6 +314,70 @@ class SandboxProcessHandle {
246
314
  }
247
315
  return this.collectedResult();
248
316
  }
317
+ if (this.secretWait) {
318
+ return this.waitWithSecrets(params);
319
+ }
320
+ return this.waitRetained(params);
321
+ }
322
+ /** Secret starts retain output on the receiver, so poll until the local deadline. */
323
+ async waitWithSecrets(params) {
324
+ const context = this.secretWait;
325
+ const timeoutMs = localWaitTimeoutMs(params);
326
+ const deadline = timeoutMs === undefined ? undefined : perf_hooks_1.performance.now() + timeoutMs;
327
+ for (;;) {
328
+ if (context.signal?.aborted)
329
+ throw requestAbortedError();
330
+ const remaining = deadline === undefined ? undefined : deadline - perf_hooks_1.performance.now();
331
+ if (remaining !== undefined && remaining <= 0)
332
+ throw waitTimeoutError();
333
+ const controller = new AbortController();
334
+ let deadlineExpired = false;
335
+ let cancelWait;
336
+ const canceled = new Promise((_, reject) => {
337
+ cancelWait = reject;
338
+ });
339
+ const abort = () => {
340
+ controller.abort();
341
+ cancelWait(requestAbortedError());
342
+ };
343
+ context.signal?.addEventListener("abort", abort, { once: true });
344
+ // Node timers cannot represent intervals above 2^31-1 ms. Long local
345
+ // deadlines are checked again after every bounded request instead.
346
+ const timer = remaining !== undefined && remaining <= 2147483647
347
+ ? setTimeout(() => {
348
+ deadlineExpired = true;
349
+ controller.abort();
350
+ cancelWait(waitTimeoutError());
351
+ }, remaining)
352
+ : undefined;
353
+ try {
354
+ const result = await Promise.race([
355
+ this.waitRetained({ timeoutMs: Math.max(1, Math.floor(Math.min(context.chunkMs, remaining ?? Infinity))) }, controller.signal),
356
+ canceled,
357
+ ]);
358
+ if (context.signal?.aborted)
359
+ throw requestAbortedError();
360
+ if (deadlineExpired || (deadline !== undefined && perf_hooks_1.performance.now() >= deadline)) {
361
+ throw waitTimeoutError();
362
+ }
363
+ return result;
364
+ }
365
+ catch (error) {
366
+ if (context.signal?.aborted)
367
+ throw requestAbortedError();
368
+ if (deadlineExpired || (deadline !== undefined && perf_hooks_1.performance.now() >= deadline)) {
369
+ throw waitTimeoutError();
370
+ }
371
+ if (!(error instanceof error_1.HyperbrowserError) || error.statusCode !== 408)
372
+ throw error;
373
+ }
374
+ finally {
375
+ clearTimeout(timer);
376
+ context.signal?.removeEventListener("abort", abort);
377
+ }
378
+ }
379
+ }
380
+ async waitRetained(params, signal) {
249
381
  const response = await this.transport.requestJSON(`/sandbox/processes/${this.id}/wait`, {
250
382
  method: "POST",
251
383
  body: JSON.stringify({
@@ -255,6 +387,7 @@ class SandboxProcessHandle {
255
387
  headers: {
256
388
  "content-type": "application/json",
257
389
  },
390
+ signal,
258
391
  });
259
392
  const result = (0, process_output_1.normalizeProcessResult)(response.result);
260
393
  if (result.outputTruncated) {
@@ -358,11 +491,16 @@ class SandboxProcessHandle {
358
491
  }
359
492
  exports.SandboxProcessHandle = SandboxProcessHandle;
360
493
  class SandboxProcessesApi {
361
- constructor(transport) {
494
+ constructor(transport, secretStarter) {
362
495
  this.transport = transport;
496
+ this.secretStarter = secretStarter;
363
497
  }
364
498
  async exec(input, options) {
365
- const handle = typeof input === "string" ? await this.start(input, options) : await this.start(input);
499
+ const params = normalizeExecParams(input, options);
500
+ if (params.secrets && Object.keys(params.secrets).length > 0) {
501
+ return this.execWithSecrets(params);
502
+ }
503
+ const handle = await this.start(params);
366
504
  try {
367
505
  return await handle.wait();
368
506
  }
@@ -372,6 +510,9 @@ class SandboxProcessesApi {
372
510
  }
373
511
  async start(input, options) {
374
512
  const params = normalizeExecParams(input, options);
513
+ if (params.secrets && Object.keys(params.secrets).length > 0) {
514
+ return this.startWithSecrets(params);
515
+ }
375
516
  const maxOutputBytes = params.maxOutputBytes ?? process_output_1.DEFAULT_MAX_PROCESS_OUTPUT_BYTES;
376
517
  (0, process_output_1.validateOutputLimit)(maxOutputBytes);
377
518
  const stream = await this.transport.openSSE("/sandbox/processes", undefined, {
@@ -398,6 +539,109 @@ class SandboxProcessesApi {
398
539
  handle.attachCollector(stream, maxOutputBytes);
399
540
  return handle;
400
541
  }
542
+ /**
543
+ * Start through the control API so the sandbox service decrypts the
544
+ * secrets. Output is then read through the runtime's retained output.
545
+ */
546
+ async startWithSecrets(params) {
547
+ if (params.signal?.aborted)
548
+ throw requestAbortedError();
549
+ const starter = this.secretStarter;
550
+ if (!starter) {
551
+ throw new error_1.HyperbrowserError("Starting a process with secrets requires a sandbox handle");
552
+ }
553
+ const secrets = (0, secret_references_1.serializeSecretReferences)(params.secrets);
554
+ (0, secret_references_1.assertNoEnvSecretOverlap)(params.env, secrets);
555
+ const timeoutMs = secretProcessTimeoutMs(params);
556
+ let response;
557
+ try {
558
+ response = await starter.start({
559
+ command: params.command,
560
+ cwd: params.cwd,
561
+ env: params.env,
562
+ timeoutMs,
563
+ runAs: params.runAs,
564
+ secrets,
565
+ }, params.signal);
566
+ }
567
+ catch (error) {
568
+ if (error instanceof error_1.HyperbrowserError && error.code === SERVER_OUTCOME_UNKNOWN_CODE) {
569
+ // The API reached the sandbox host but could not confirm the start.
570
+ throw new error_1.HyperbrowserError(error.message.replace(/^\[Hyperbrowser\]: /, ""), {
571
+ statusCode: error.statusCode,
572
+ code: "secret_process_outcome_unknown",
573
+ requestId: error.requestId,
574
+ retryable: false,
575
+ service: "control",
576
+ details: {
577
+ ...(typeof error.details === "object" ? error.details : undefined),
578
+ sandboxId: starter.sandboxId,
579
+ },
580
+ cause: error,
581
+ method: error.method,
582
+ path: error.path,
583
+ });
584
+ }
585
+ if (!(0, exports.requestMayHaveArrived)(error)) {
586
+ throw error;
587
+ }
588
+ throw new error_1.HyperbrowserError(`The process start may have reached the API, but no result came back; the command may be running, so check the sandbox's processes before retrying`, {
589
+ code: "secret_process_outcome_unknown",
590
+ retryable: false,
591
+ service: "control",
592
+ details: { sandboxId: starter.sandboxId },
593
+ cause: error,
594
+ });
595
+ }
596
+ const process = response?.process;
597
+ const processId = process?.id;
598
+ if (!process || typeof processId !== "string" || !processId.trim()) {
599
+ // The start succeeded, so the command may be running with the secrets.
600
+ throw new error_1.HyperbrowserError("The API accepted the process start but returned no process ID; the command may be running, so check the sandbox's processes before retrying", {
601
+ code: "secret_process_outcome_unknown",
602
+ retryable: false,
603
+ service: "control",
604
+ details: { sandboxId: starter.sandboxId },
605
+ });
606
+ }
607
+ return new SandboxProcessHandle(this.transport, normalizeProcessSummary({ ...process, id: processId.trim() }), response.secrets ?? [], { chunkMs: starter.waitChunkMs, signal: params.signal });
608
+ }
609
+ /**
610
+ * Like one-shot exec: stdin is closed, then the command's final result is
611
+ * awaited. The command's own timeout is enforced in the sandbox, so an
612
+ * expired wait (408) only means it is still running.
613
+ */
614
+ async execWithSecrets(params) {
615
+ const handle = await this.startWithSecrets(params);
616
+ const starter = this.secretStarter;
617
+ try {
618
+ try {
619
+ await this.transport.requestJSON(`/sandbox/processes/${handle.id}/stdin`, {
620
+ method: "POST",
621
+ body: JSON.stringify({ eof: true }),
622
+ headers: { "content-type": "application/json" },
623
+ signal: params.signal,
624
+ });
625
+ }
626
+ catch (error) {
627
+ // 409: the command already exited; its result is still reported.
628
+ if (!(error instanceof error_1.HyperbrowserError) || error.statusCode !== 409) {
629
+ throw error;
630
+ }
631
+ }
632
+ return await handle.wait();
633
+ }
634
+ catch (error) {
635
+ // Running exec again would start a second process with the secrets.
636
+ throw new error_1.HyperbrowserError(`Process ${handle.id} started, but waiting for its result failed: ${error instanceof Error ? error.message.replace(/^\[Hyperbrowser\]: /, "") : String(error)}`, {
637
+ code: "secret_process_failed",
638
+ retryable: false,
639
+ service: "runtime",
640
+ details: { sandboxId: starter.sandboxId, processId: handle.id },
641
+ cause: error,
642
+ });
643
+ }
644
+ }
401
645
  async get(processId) {
402
646
  const response = await this.transport.requestJSON(`/sandbox/processes/${processId}`);
403
647
  return new SandboxProcessHandle(this.transport, normalizeProcessSummary(response.process));
@@ -0,0 +1,15 @@
1
+ import { SandboxSecrets } from "../types/sandbox";
2
+ export declare const SECRET_NAME_PATTERN: RegExp;
3
+ /** The largest version number the API stores. */
4
+ export declare const MAX_SECRET_VERSION = 2147483647;
5
+ export interface WireSecretReference {
6
+ name: string;
7
+ version?: number | "latest";
8
+ }
9
+ /**
10
+ * Check secret references locally and convert them to the API's
11
+ * `{ name, version? }` shape. Returns undefined when there are none.
12
+ */
13
+ export declare const serializeSecretReferences: (secrets: SandboxSecrets | undefined) => Record<string, WireSecretReference> | undefined;
14
+ /** An environment variable must come from either `env` or `secrets`, not both. */
15
+ export declare const assertNoEnvSecretOverlap: (env: Record<string, string> | undefined, secrets: Record<string, WireSecretReference>) => void;
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assertNoEnvSecretOverlap = exports.serializeSecretReferences = exports.MAX_SECRET_VERSION = exports.SECRET_NAME_PATTERN = void 0;
4
+ const error_1 = require("../error");
5
+ const ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
6
+ exports.SECRET_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
7
+ /** The largest version number the API stores. */
8
+ exports.MAX_SECRET_VERSION = 2147483647;
9
+ const MAX_SECRETS = 32;
10
+ /**
11
+ * Check secret references locally and convert them to the API's
12
+ * `{ name, version? }` shape. Returns undefined when there are none.
13
+ */
14
+ const serializeSecretReferences = (secrets) => {
15
+ if (secrets === undefined || secrets === null) {
16
+ return undefined;
17
+ }
18
+ if (typeof secrets !== "object" || Array.isArray(secrets)) {
19
+ throw new error_1.HyperbrowserError("secrets must map environment variable names to secrets");
20
+ }
21
+ const entries = Object.entries(secrets);
22
+ if (entries.length === 0) {
23
+ return undefined;
24
+ }
25
+ if (entries.length > MAX_SECRETS) {
26
+ throw new error_1.HyperbrowserError(`At most ${MAX_SECRETS} secrets can be delivered at once`);
27
+ }
28
+ const wire = {};
29
+ for (const [env, reference] of entries) {
30
+ if (!ENV_NAME_PATTERN.test(env) || env === "__proto__") {
31
+ throw new error_1.HyperbrowserError(`Invalid environment variable name for a secret: ${env}`);
32
+ }
33
+ const { name, version } = typeof reference === "string" ? { name: reference, version: undefined } : (reference ?? {});
34
+ if (typeof name !== "string" || !exports.SECRET_NAME_PATTERN.test(name)) {
35
+ throw new error_1.HyperbrowserError(`Invalid secret name for ${env}: ${String(name)}`);
36
+ }
37
+ if (version !== undefined &&
38
+ version !== "latest" &&
39
+ (!Number.isSafeInteger(version) ||
40
+ version < 1 ||
41
+ version > exports.MAX_SECRET_VERSION)) {
42
+ throw new error_1.HyperbrowserError(`Secret version for ${env} must be an integer from 1 to ${exports.MAX_SECRET_VERSION} or "latest"`);
43
+ }
44
+ wire[env] = version === undefined ? { name } : { name, version };
45
+ }
46
+ return wire;
47
+ };
48
+ exports.serializeSecretReferences = serializeSecretReferences;
49
+ /** An environment variable must come from either `env` or `secrets`, not both. */
50
+ const assertNoEnvSecretOverlap = (env, secrets) => {
51
+ const overlap = Object.keys(env ?? {}).filter((name) => Object.prototype.hasOwnProperty.call(secrets, name));
52
+ if (overlap.length > 0) {
53
+ throw new error_1.HyperbrowserError(`Environment variables set by both env and secrets: ${overlap.sort().join(", ")}`);
54
+ }
55
+ };
56
+ exports.assertNoEnvSecretOverlap = assertNoEnvSecretOverlap;
@@ -0,0 +1,25 @@
1
+ import { SecretsPublicKey, SecretValue } from "./types/secret";
2
+ /** The largest secret value the platform accepts. */
3
+ export declare const MAX_SECRET_VALUE_BYTES: number;
4
+ /**
5
+ * Apply the platform's value rules locally, so a bad value is reported before
6
+ * anything is created rather than when a sandbox starts.
7
+ */
8
+ export declare const secretValueBytes: (value: SecretValue) => Buffer;
9
+ /** Encrypts values to a verified secrets public key. */
10
+ export declare class SecretsSealer {
11
+ private readonly kid;
12
+ private readonly publicKey;
13
+ private constructor();
14
+ /**
15
+ * Check that `key` is a supported RSA key whose ID matches the key itself. A
16
+ * mismatch means a corrupted response or a misconfigured server; where the
17
+ * key comes from rests on TLS to the API.
18
+ */
19
+ static fromPublicKey(key: SecretsPublicKey): SecretsSealer;
20
+ /**
21
+ * Encrypt `value` as a compact JWE (RSA-OAEP-256 + A256GCM) bound to a
22
+ * secret's encryption context.
23
+ */
24
+ seal(encryptionContext: string, value: SecretValue): string;
25
+ }