@hyperbrowser/sdk 1.2.0 → 1.2.2

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.
@@ -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
+ }
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SecretsSealer = exports.secretValueBytes = exports.MAX_SECRET_VALUE_BYTES = void 0;
7
+ const crypto_1 = __importDefault(require("crypto"));
8
+ const error_1 = require("./error");
9
+ /** The largest secret value the platform accepts. */
10
+ exports.MAX_SECRET_VALUE_BYTES = 32 * 1024;
11
+ const MIN_SECRETS_KEY_BITS = 3072;
12
+ const SECRETS_KEY_EXPONENT = 65537;
13
+ const invalid = (message) => new error_1.HyperbrowserError(message, { code: "invalid_secret_value", retryable: false });
14
+ const base64Url = (data) => Buffer.from(data).toString("base64url");
15
+ /**
16
+ * Apply the platform's value rules locally, so a bad value is reported before
17
+ * anything is created rather than when a sandbox starts.
18
+ */
19
+ const secretValueBytes = (value) => {
20
+ if (typeof value !== "string" && !(value instanceof Uint8Array)) {
21
+ throw invalid("Secret value must be a string or bytes");
22
+ }
23
+ const bytes = Buffer.from(value);
24
+ if (bytes.length === 0) {
25
+ throw invalid("Secret value is empty");
26
+ }
27
+ if (bytes.length > exports.MAX_SECRET_VALUE_BYTES) {
28
+ throw invalid(`Secret value is more than ${exports.MAX_SECRET_VALUE_BYTES} bytes`);
29
+ }
30
+ if (bytes.includes(0)) {
31
+ throw invalid("Secret value must not contain NUL bytes");
32
+ }
33
+ try {
34
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes);
35
+ }
36
+ catch {
37
+ throw invalid("Secret value must be valid UTF-8");
38
+ }
39
+ return bytes;
40
+ };
41
+ exports.secretValueBytes = secretValueBytes;
42
+ const minimalBytes = (value) => {
43
+ let start = 0;
44
+ while (start < value.length - 1 && value[start] === 0)
45
+ start += 1;
46
+ return value.subarray(start);
47
+ };
48
+ /** RFC 7638 JWK SHA-256 thumbprint of an RSA key. */
49
+ const rsaThumbprint = (n, e) => base64Url(crypto_1.default
50
+ .createHash("sha256")
51
+ .update(`{"e":"${base64Url(e)}","kty":"RSA","n":"${base64Url(n)}"}`)
52
+ .digest());
53
+ /** Encrypts values to a verified secrets public key. */
54
+ class SecretsSealer {
55
+ constructor(kid, publicKey) {
56
+ this.kid = kid;
57
+ this.publicKey = publicKey;
58
+ }
59
+ /**
60
+ * Check that `key` is a supported RSA key whose ID matches the key itself. A
61
+ * mismatch means a corrupted response or a misconfigured server; where the
62
+ * key comes from rests on TLS to the API.
63
+ */
64
+ static fromPublicKey(key) {
65
+ const unsupported = (message) => new error_1.HyperbrowserError(message, { code: "invalid_secrets_public_key", retryable: false });
66
+ if (key?.alg !== "RSA-OAEP-256" || key.enc !== "A256GCM" || key.jwk?.kty !== "RSA") {
67
+ throw unsupported(`Unsupported secrets public key (${key?.alg}/${key?.enc})`);
68
+ }
69
+ const n = minimalBytes(Buffer.from(key.jwk.n ?? "", "base64url"));
70
+ const e = minimalBytes(Buffer.from(key.jwk.e ?? "", "base64url"));
71
+ if (n.length === 0 || n[0] === 0 || e.length === 0 || e.length > 4) {
72
+ throw unsupported("Secrets public key is malformed");
73
+ }
74
+ // The same policy the server and vm-service enforce for their keys.
75
+ const bits = n.length * 8 - Math.clz32(n[0]) + 24;
76
+ if (bits < MIN_SECRETS_KEY_BITS || e.readUIntBE(0, e.length) !== SECRETS_KEY_EXPONENT) {
77
+ throw unsupported(`Secrets public key must be at least ${MIN_SECRETS_KEY_BITS}-bit RSA with exponent ${SECRETS_KEY_EXPONENT}`);
78
+ }
79
+ if (rsaThumbprint(n, e) !== key.kid) {
80
+ throw unsupported("Secrets public key does not match its key ID");
81
+ }
82
+ const publicKey = crypto_1.default.createPublicKey({
83
+ key: { kty: "RSA", n: base64Url(n), e: base64Url(e) },
84
+ format: "jwk",
85
+ });
86
+ return new SecretsSealer(key.kid, publicKey);
87
+ }
88
+ /**
89
+ * Encrypt `value` as a compact JWE (RSA-OAEP-256 + A256GCM) bound to a
90
+ * secret's encryption context.
91
+ */
92
+ seal(encryptionContext, value) {
93
+ const plaintext = (0, exports.secretValueBytes)(value);
94
+ const protectedHeader = base64Url(Buffer.from(JSON.stringify({
95
+ alg: "RSA-OAEP-256",
96
+ enc: "A256GCM",
97
+ kid: this.kid,
98
+ ctx: encryptionContext,
99
+ })));
100
+ const cek = crypto_1.default.randomBytes(32);
101
+ const iv = crypto_1.default.randomBytes(12);
102
+ try {
103
+ const encryptedKey = crypto_1.default.publicEncrypt({
104
+ key: this.publicKey,
105
+ padding: crypto_1.default.constants.RSA_PKCS1_OAEP_PADDING,
106
+ oaepHash: "sha256",
107
+ }, cek);
108
+ const cipher = crypto_1.default.createCipheriv("aes-256-gcm", cek, iv);
109
+ cipher.setAAD(Buffer.from(protectedHeader, "ascii"));
110
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
111
+ return [
112
+ protectedHeader,
113
+ base64Url(encryptedKey),
114
+ base64Url(iv),
115
+ base64Url(ciphertext),
116
+ base64Url(cipher.getAuthTag()),
117
+ ].join(".");
118
+ }
119
+ finally {
120
+ cek.fill(0);
121
+ }
122
+ }
123
+ }
124
+ exports.SecretsSealer = SecretsSealer;
@@ -10,6 +10,28 @@ const retry_1 = require("../retry");
10
10
  const getRequestId = (response) => {
11
11
  return response.headers.get("x-request-id") || response.headers.get("request-id") || undefined;
12
12
  };
13
+ /** The API's machine codes, such as SECRET_NOT_FOUND. */
14
+ const SERVER_ERROR_CODE_PATTERN = /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)+$/;
15
+ /** Codes whose retryable status default (such as 503) would be wrong. */
16
+ const NON_RETRYABLE_ERROR_CODES = new Set(["SECRETS_NOT_CONFIGURED"]);
17
+ /** The sandbox and secret an error is about, when the API names them. */
18
+ const errorMessageContext = (errorData) => {
19
+ let context = "";
20
+ // A failed create may leave a sandbox the caller has to stop.
21
+ if (typeof errorData.sandboxId === "string" && errorData.sandboxId.trim()) {
22
+ context += ` (sandbox ${errorData.sandboxId.trim()})`;
23
+ }
24
+ // Which of several secrets could not be used.
25
+ const secret = errorData.details;
26
+ if (secret && typeof secret.env === "string" && secret.env.trim()) {
27
+ const parts = [
28
+ typeof secret.secretId === "string" ? `secret ${secret.secretId}` : undefined,
29
+ typeof secret.version === "number" ? `version ${secret.version}` : undefined,
30
+ ].filter(Boolean);
31
+ context += ` (${secret.env.trim()}${parts.length > 0 ? `: ${parts.join(", ")}` : ""})`;
32
+ }
33
+ return context;
34
+ };
13
35
  class BaseService {
14
36
  constructor(apiKey, baseUrl, timeout = 30000) {
15
37
  this.apiKey = apiKey;
@@ -71,25 +93,51 @@ class BaseService {
71
93
  let errorMessage;
72
94
  let errorDetails;
73
95
  let errorCode;
96
+ let decodingFailed = false;
97
+ let decodingCause;
98
+ let retryable = retry_1.RETRYABLE_STATUS_CODES.has(response.status);
74
99
  try {
75
100
  const errorData = await response.json();
76
101
  errorDetails = errorData;
77
102
  errorCode = typeof errorData?.code === "string" ? errorData.code : undefined;
103
+ // Some routes send the code as "error" next to a message, for
104
+ // example API_KEY_SCOPE_DENIED.
105
+ if (errorCode === undefined &&
106
+ errorData?.message &&
107
+ typeof errorData.error === "string" &&
108
+ SERVER_ERROR_CODE_PATTERN.test(errorData.error)) {
109
+ errorCode = errorData.error;
110
+ }
111
+ // The server knows better when it says so: an ambiguous process
112
+ // start with secrets is a 502 that must not be retried blindly.
113
+ if (typeof errorData?.retryable === "boolean") {
114
+ retryable = errorData.retryable;
115
+ }
116
+ else if (errorCode !== undefined && NON_RETRYABLE_ERROR_CODES.has(errorCode)) {
117
+ retryable = false;
118
+ }
119
+ const message = errorData.message || errorData.error;
78
120
  errorMessage =
79
- errorData.message || errorData.error || `HTTP error! status: ${response.status}`;
121
+ typeof message === "string" && message
122
+ ? `${message}${errorMessageContext(errorData)}`
123
+ : message || `HTTP error! status: ${response.status}`;
80
124
  }
81
125
  catch (error) {
82
126
  if (init?.signal?.aborted)
83
127
  throw error;
128
+ decodingFailed = true;
129
+ decodingCause = error;
84
130
  errorMessage = `HTTP error! status: ${response.status}`;
85
131
  }
86
- throw new error_1.HyperbrowserError(errorMessage, {
132
+ const ErrorType = decodingFailed ? error_1.ResponseDecodingError : error_1.HyperbrowserError;
133
+ throw new ErrorType(errorMessage, {
87
134
  statusCode: response.status,
88
135
  code: errorCode,
89
136
  requestId: getRequestId(response),
90
- retryable: retry_1.RETRYABLE_STATUS_CODES.has(response.status),
137
+ retryable,
91
138
  service: "control",
92
139
  details: errorDetails,
140
+ cause: decodingCause,
93
141
  method: init?.method ?? "GET",
94
142
  path: (0, error_1.errorRequestPath)(path),
95
143
  });
@@ -1,5 +1,5 @@
1
1
  import { SandboxFilesApi } from "../sandbox/files";
2
- import { SandboxProcessHandle, SandboxProcessesApi } from "../sandbox/process";
2
+ import { SandboxProcessHandle, SandboxProcessesApi, SecretProcessStartPayload, SecretProcessStartResponse } from "../sandbox/process";
3
3
  import { SandboxTerminalApi } from "../sandbox/terminal";
4
4
  import { BasicResponse } from "../types/session";
5
5
  import { BuildSandboxImageFromDockerImageOptions, BuildSandboxImageFromDockerfileOptions, CompleteSandboxImageBuildParams, CreateSandboxImageBuildParams, CreateSandboxParams, StartSandboxFromSnapshotParams, SandboxRuntimeSession, SandboxRuntimeTarget, GetOrBuildSandboxImageOptions, ReuseSandboxDockerImageParams, SandboxDockerImageReuseResult, SandboxImageBuildResolution, SandboxImageBuildWaitOptions, SandboxImageSummary, SandboxDetail, SandboxExposeParams, SandboxExposeResult, SandboxExecParams, SandboxExecOptions, SandboxImageBuild, SandboxImageBuildCreateResult, SandboxImageBuildListParams, SandboxImageBuildListResponse, SandboxImageListParams, SandboxImageListResponse, SandboxListParams, SandboxListResponse, SandboxMemorySnapshotParams, SandboxMemorySnapshotResult, SandboxNetworkPolicyPatch, SandboxNetworkUpdateResult, SandboxProcessResult, SandboxImageDeleteResult, SandboxSnapshotDeleteResult, SandboxSnapshotListParams, SandboxSnapshotListResponse, SandboxSnapshotSummary, SandboxUnexposeResult } from "../types/sandbox";
@@ -110,7 +110,16 @@ export declare class SandboxesService extends BaseService {
110
110
  /** Create a build, upload requested artifacts, complete it, and optionally wait. */
111
111
  private submitImageBuild;
112
112
  private completeImageBuildResilient;
113
+ /**
114
+ * Start a process with secrets through the control API. Never retried: a
115
+ * failed start may still have run the command.
116
+ */
117
+ startSecretProcess(id: string, payload: SecretProcessStartPayload, signal?: AbortSignal): Promise<SecretProcessStartResponse>;
113
118
  updateNetwork(id: string, policy: SandboxNetworkPolicyPatch): Promise<SandboxNetworkUpdateResult>;
114
119
  unexpose(id: string, port: number): Promise<SandboxUnexposeResult>;
120
+ /**
121
+ * With secrets, a create whose result is unknown is never retried: the
122
+ * sandbox may exist and be running its startup command with the secrets.
123
+ */
115
124
  private createDetail;
116
125
  }
@@ -6,12 +6,15 @@ 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
+ const secret_references_1 = require("../sandbox/secret-references");
9
10
  const terminal_1 = require("../sandbox/terminal");
10
11
  const image_build_1 = require("../sandbox/image-build");
11
12
  const retry_1 = require("../retry");
12
13
  const normalize_1 = require("./normalize");
13
14
  const base_2 = require("./base");
14
15
  const RUNTIME_SESSION_REFRESH_BUFFER_MS = 60000;
16
+ /** Runtime waits for secret processes stay well under the request timeout. */
17
+ const SECRET_PROCESS_WAIT_CHUNK_MS = 20000;
15
18
  const normalizeSandbox = (sandbox) => {
16
19
  const { vcpus, memMiB, diskSizeMiB, ...rest } = sandbox;
17
20
  return {
@@ -27,6 +30,7 @@ const normalizeSandbox = (sandbox) => {
27
30
  timeoutMinutes: (0, normalize_1.optionalInteger)(rest.timeoutMinutes),
28
31
  creditsUsed: (0, normalize_1.optionalNumber)(rest.creditsUsed) ?? null,
29
32
  exposedPorts: rest.exposedPorts ?? [],
33
+ containsSecrets: rest.containsSecrets ?? false,
30
34
  network: rest.network ? { ...rest.network, allowOut: rest.network.allowOut ?? [], denyOut: rest.network.denyOut ?? [] } : rest.network,
31
35
  };
32
36
  };
@@ -66,6 +70,10 @@ const serializeCreateSandboxParams = (params) => {
66
70
  params.runtimeClass !== "gvisor-cpu")
67
71
  throw new error_1.HyperbrowserError("Unsupported sandbox runtimeClass");
68
72
  if (typeof params.imageName === "string") {
73
+ const secrets = (0, secret_references_1.serializeSecretReferences)(params.secrets);
74
+ // The startup command with secrets runs only in Firecracker sandboxes.
75
+ if (secrets !== undefined && params.runtimeClass === "gvisor-cpu")
76
+ throw new error_1.HyperbrowserError("secrets are not supported with runtimeClass gvisor-cpu");
69
77
  return {
70
78
  runtimeClass: params.runtimeClass,
71
79
  imageName: params.imageName,
@@ -81,9 +89,14 @@ const serializeCreateSandboxParams = (params) => {
81
89
  allowInternetAccess: params.allowInternetAccess,
82
90
  allowOut: params.allowOut,
83
91
  denyOut: params.denyOut,
92
+ secrets,
84
93
  };
85
94
  }
86
95
  const snapshotParams = params;
96
+ if (snapshotParams.secrets !== undefined) {
97
+ // Snapshot launches do not run a startup command.
98
+ throw new error_1.HyperbrowserError("secrets are only supported for image launches");
99
+ }
87
100
  if (snapshotParams.cpu !== undefined ||
88
101
  snapshotParams.memoryMiB !== undefined ||
89
102
  snapshotParams.diskMiB !== undefined) {
@@ -194,7 +207,11 @@ class SandboxHandle {
194
207
  this.detail = detail;
195
208
  this.runtimeSession = SandboxHandle.toRuntimeSession(detail);
196
209
  this.transport = new base_1.RuntimeTransport((forceRefresh) => this.resolveRuntimeConnection(forceRefresh), service.runtimeTimeout, service.runtimeProxyOverride);
197
- this.processes = new process_1.SandboxProcessesApi(this.transport);
210
+ this.processes = new process_1.SandboxProcessesApi(this.transport, {
211
+ sandboxId: detail.id,
212
+ waitChunkMs: Math.max(1, Math.floor(Math.min(SECRET_PROCESS_WAIT_CHUNK_MS, service.runtimeTimeout / 2))),
213
+ start: (payload, signal) => service.startSecretProcess(this.id, payload, signal),
214
+ });
198
215
  this.files = new files_1.SandboxFilesApi(this.transport, () => this.resolveRuntimeSocketConnectionInfo(), service.runtimeProxyOverride);
199
216
  this.terminal = new terminal_1.SandboxTerminalApi(this.transport, () => this.resolveRuntimeSocketConnectionInfo(), service.runtimeProxyOverride);
200
217
  this.pty = this.terminal;
@@ -1053,6 +1070,27 @@ class SandboxesService extends base_2.BaseService {
1053
1070
  return this.completeImageBuild(buildId, params);
1054
1071
  }
1055
1072
  }
1073
+ /**
1074
+ * Start a process with secrets through the control API. Never retried: a
1075
+ * failed start may still have run the command.
1076
+ */
1077
+ async startSecretProcess(id, payload, signal) {
1078
+ try {
1079
+ return await this.request(`/sandbox/${encodeURIComponent(id)}/processes`, {
1080
+ method: "POST",
1081
+ body: JSON.stringify(payload),
1082
+ signal,
1083
+ });
1084
+ }
1085
+ catch (error) {
1086
+ if (error instanceof error_1.HyperbrowserError) {
1087
+ throw error;
1088
+ }
1089
+ throw new error_1.HyperbrowserError(`Failed to start a process with secrets in sandbox ${id}`, {
1090
+ cause: error,
1091
+ });
1092
+ }
1093
+ }
1056
1094
  async updateNetwork(id, policy) {
1057
1095
  try {
1058
1096
  return await this.request(`/sandbox/${id}/network`, {
@@ -1081,15 +1119,36 @@ class SandboxesService extends base_2.BaseService {
1081
1119
  throw new error_1.HyperbrowserError(`Failed to unexpose port ${port} for sandbox ${id}`);
1082
1120
  }
1083
1121
  }
1122
+ /**
1123
+ * With secrets, a create whose result is unknown is never retried: the
1124
+ * sandbox may exist and be running its startup command with the secrets.
1125
+ */
1084
1126
  async createDetail(params) {
1127
+ const body = serializeCreateSandboxParams(params);
1085
1128
  try {
1086
1129
  const detail = await this.request("/sandbox", {
1087
1130
  method: "POST",
1088
- body: JSON.stringify(serializeCreateSandboxParams(params)),
1131
+ body: JSON.stringify(body),
1089
1132
  });
1133
+ if (body.secrets !== undefined &&
1134
+ (typeof detail?.id !== "string" || !detail.id.trim())) {
1135
+ throw new Error("The API accepted the create but returned no sandbox ID");
1136
+ }
1090
1137
  return normalizeSandboxDetail(detail);
1091
1138
  }
1092
1139
  catch (error) {
1140
+ if (body.secrets !== undefined && (0, process_1.requestMayHaveArrived)(error)) {
1141
+ throw new error_1.HyperbrowserError(`could not confirm the create result: ${error instanceof Error
1142
+ ? error.message.replace(/^\[Hyperbrowser\]: /, "")
1143
+ : String(error)}; a sandbox may have been created with the secrets, so list sandboxes before retrying`, {
1144
+ code: "secret_process_outcome_unknown",
1145
+ retryable: false,
1146
+ service: "control",
1147
+ cause: error,
1148
+ method: "POST",
1149
+ path: "/sandbox",
1150
+ });
1151
+ }
1093
1152
  if (error instanceof error_1.HyperbrowserError) {
1094
1153
  throw error;
1095
1154
  }
@@ -0,0 +1,39 @@
1
+ import { CreateSecretParams, Secret, SecretDeleteResult, SecretDetail, SecretListParams, SecretListResponse, SecretsPublicKey, SecretValue, SecretVersion, SecretVersionDeleteResult, UpdateSecretParams } from "../types/secret";
2
+ import { BaseService } from "./base";
3
+ /**
4
+ * Secrets are encrypted locally to the platform's public key; the API only
5
+ * ever receives envelopes, and values are never returned.
6
+ *
7
+ * Methods that take `secret` accept its ID or its exact name. A UUID-shaped
8
+ * argument is tried as an ID first and then as a name, since names may look
9
+ * like UUIDs.
10
+ */
11
+ export declare class SecretsService extends BaseService {
12
+ /** The key secret values are encrypted to. */
13
+ getPublicKey(): Promise<SecretsPublicKey>;
14
+ list(params?: SecretListParams): Promise<SecretListResponse>;
15
+ /**
16
+ * Create a secret, and store its first version when `value` is given. The
17
+ * value and the platform key are checked before the secret is created, so a
18
+ * bad value never leaves an empty secret behind.
19
+ */
20
+ create(params: CreateSecretParams): Promise<Secret>;
21
+ /** Metadata and versions; never the value. */
22
+ get(secret: string): Promise<SecretDetail>;
23
+ update(secret: string, params: UpdateSecretParams): Promise<Secret>;
24
+ /** Encrypt `value` locally and store it as the secret's next version. */
25
+ addVersion(secret: string, value: SecretValue): Promise<SecretVersion>;
26
+ /**
27
+ * Permanently delete a secret and its versions. Values already delivered to
28
+ * sandboxes are not revoked.
29
+ */
30
+ delete(secret: string): Promise<SecretDeleteResult>;
31
+ /** Delete one stored version. The latest version cannot be deleted. */
32
+ deleteVersion(secret: string, version: number): Promise<SecretVersionDeleteResult>;
33
+ private prepareSealer;
34
+ private uploadVersion;
35
+ private resolve;
36
+ private resolveDetail;
37
+ /** Found by ID, the result already includes the versions. */
38
+ private lookup;
39
+ }
@@ -0,0 +1,203 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SecretsService = void 0;
4
+ const error_1 = require("../error");
5
+ const secret_references_1 = require("../sandbox/secret-references");
6
+ const secrets_envelope_1 = require("../secrets-envelope");
7
+ const base_1 = require("./base");
8
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
9
+ /** The most secrets one list page can hold. */
10
+ const MAX_LIST_LIMIT = 100;
11
+ /** Teams hold at most 100 secrets, so one page of search results covers every match. */
12
+ const NAME_LOOKUP_LIMIT = MAX_LIST_LIMIT;
13
+ const secretPath = (id) => `/secrets/${encodeURIComponent(id)}`;
14
+ const rethrow = (error, message) => {
15
+ if (error instanceof error_1.HyperbrowserError) {
16
+ throw error;
17
+ }
18
+ throw new error_1.HyperbrowserError(message, { cause: error });
19
+ };
20
+ /**
21
+ * Secrets are encrypted locally to the platform's public key; the API only
22
+ * ever receives envelopes, and values are never returned.
23
+ *
24
+ * Methods that take `secret` accept its ID or its exact name. A UUID-shaped
25
+ * argument is tried as an ID first and then as a name, since names may look
26
+ * like UUIDs.
27
+ */
28
+ class SecretsService extends base_1.BaseService {
29
+ /** The key secret values are encrypted to. */
30
+ async getPublicKey() {
31
+ try {
32
+ return await this.request("/secrets/public-key");
33
+ }
34
+ catch (error) {
35
+ return rethrow(error, "Failed to get the secrets public key");
36
+ }
37
+ }
38
+ async list(params = {}) {
39
+ const { page, limit } = params;
40
+ if (page !== undefined && (!Number.isSafeInteger(page) || page < 1)) {
41
+ throw new error_1.HyperbrowserError("page must be a positive integer");
42
+ }
43
+ if (limit !== undefined &&
44
+ (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_LIST_LIMIT)) {
45
+ throw new error_1.HyperbrowserError(`limit must be an integer from 1 to ${MAX_LIST_LIMIT}`);
46
+ }
47
+ try {
48
+ return await this.request("/secrets", undefined, {
49
+ search: params.search,
50
+ page: params.page,
51
+ limit: params.limit,
52
+ });
53
+ }
54
+ catch (error) {
55
+ return rethrow(error, "Failed to list secrets");
56
+ }
57
+ }
58
+ /**
59
+ * Create a secret, and store its first version when `value` is given. The
60
+ * value and the platform key are checked before the secret is created, so a
61
+ * bad value never leaves an empty secret behind.
62
+ */
63
+ async create(params) {
64
+ const { name, description, value } = params ?? {};
65
+ if (typeof name !== "string" || !secret_references_1.SECRET_NAME_PATTERN.test(name)) {
66
+ throw new error_1.HyperbrowserError("Secret name must be 1-128 letters, digits, '_' or '-', starting with a letter or digit");
67
+ }
68
+ const sealer = value === undefined ? undefined : await this.prepareSealer(value);
69
+ let secret;
70
+ try {
71
+ secret = await this.request("/secrets", {
72
+ method: "POST",
73
+ body: JSON.stringify({ name, description }),
74
+ });
75
+ }
76
+ catch (error) {
77
+ return rethrow(error, `Failed to create secret ${name}`);
78
+ }
79
+ if (sealer === undefined) {
80
+ return secret;
81
+ }
82
+ try {
83
+ const version = await this.uploadVersion(secret, sealer, value);
84
+ return { ...secret, latestVersion: version.version };
85
+ }
86
+ catch (error) {
87
+ // Retrying create would fail with "name taken"; the value has to be
88
+ // stored on the secret that now exists.
89
+ throw new error_1.HyperbrowserError(`Secret ${secret.name} (${secret.id}) was created, but storing its value failed: ${error instanceof Error ? error.message.replace(/^\[Hyperbrowser\]: /, "") : String(error)}. Store it with secrets.addVersion(${JSON.stringify(secret.name)}, value).`, {
90
+ code: "secret_value_not_stored",
91
+ retryable: false,
92
+ service: "control",
93
+ details: { secret },
94
+ cause: error,
95
+ });
96
+ }
97
+ }
98
+ /** Metadata and versions; never the value. */
99
+ async get(secret) {
100
+ try {
101
+ return await this.resolveDetail(secret);
102
+ }
103
+ catch (error) {
104
+ return rethrow(error, `Failed to get secret ${secret}`);
105
+ }
106
+ }
107
+ async update(secret, params) {
108
+ try {
109
+ const { id } = await this.resolve(secret);
110
+ return await this.request(secretPath(id), {
111
+ method: "PATCH",
112
+ body: JSON.stringify({ description: params.description }),
113
+ });
114
+ }
115
+ catch (error) {
116
+ return rethrow(error, `Failed to update secret ${secret}`);
117
+ }
118
+ }
119
+ /** Encrypt `value` locally and store it as the secret's next version. */
120
+ async addVersion(secret, value) {
121
+ const sealer = await this.prepareSealer(value);
122
+ try {
123
+ return await this.uploadVersion(await this.resolve(secret), sealer, value);
124
+ }
125
+ catch (error) {
126
+ return rethrow(error, `Failed to add a version to secret ${secret}`);
127
+ }
128
+ }
129
+ /**
130
+ * Permanently delete a secret and its versions. Values already delivered to
131
+ * sandboxes are not revoked.
132
+ */
133
+ async delete(secret) {
134
+ try {
135
+ const { id } = await this.resolve(secret);
136
+ return await this.request(secretPath(id), { method: "DELETE" });
137
+ }
138
+ catch (error) {
139
+ return rethrow(error, `Failed to delete secret ${secret}`);
140
+ }
141
+ }
142
+ /** Delete one stored version. The latest version cannot be deleted. */
143
+ async deleteVersion(secret, version) {
144
+ if (!Number.isSafeInteger(version) || version < 1 || version > secret_references_1.MAX_SECRET_VERSION) {
145
+ throw new error_1.HyperbrowserError(`version must be an integer from 1 to ${secret_references_1.MAX_SECRET_VERSION}`);
146
+ }
147
+ try {
148
+ const { id } = await this.resolve(secret);
149
+ return await this.request(`${secretPath(id)}/versions/${version}`, {
150
+ method: "DELETE",
151
+ });
152
+ }
153
+ catch (error) {
154
+ return rethrow(error, `Failed to delete version ${version} of secret ${secret}`);
155
+ }
156
+ }
157
+ async prepareSealer(value) {
158
+ (0, secrets_envelope_1.secretValueBytes)(value);
159
+ return secrets_envelope_1.SecretsSealer.fromPublicKey(await this.getPublicKey());
160
+ }
161
+ uploadVersion(secret, sealer, value) {
162
+ return this.request(`${secretPath(secret.id)}/versions`, {
163
+ method: "POST",
164
+ body: JSON.stringify({ envelope: sealer.seal(secret.encryptionContext, value) }),
165
+ });
166
+ }
167
+ async resolve(secret) {
168
+ return (await this.lookup(secret)).secret;
169
+ }
170
+ async resolveDetail(secret) {
171
+ const found = await this.lookup(secret);
172
+ return found.detail ?? (await this.request(secretPath(found.secret.id)));
173
+ }
174
+ /** Found by ID, the result already includes the versions. */
175
+ async lookup(key) {
176
+ if (typeof key !== "string" || !key.trim()) {
177
+ throw new error_1.HyperbrowserError("A secret ID or name is required");
178
+ }
179
+ key = key.trim();
180
+ if (UUID_PATTERN.test(key)) {
181
+ try {
182
+ const detail = await this.request(secretPath(key));
183
+ return { secret: detail, detail };
184
+ }
185
+ catch (error) {
186
+ if (!(error instanceof error_1.HyperbrowserError) || error.statusCode !== 404) {
187
+ throw error;
188
+ }
189
+ }
190
+ }
191
+ const { secrets } = await this.list({ search: key, limit: NAME_LOOKUP_LIMIT });
192
+ const secret = secrets.find((candidate) => candidate.name === key);
193
+ if (!secret) {
194
+ throw new error_1.HyperbrowserError(`Secret ${JSON.stringify(key)} not found`, {
195
+ statusCode: 404,
196
+ code: "SECRET_NOT_FOUND",
197
+ service: "control",
198
+ });
199
+ }
200
+ return { secret };
201
+ }
202
+ }
203
+ exports.SecretsService = SecretsService;
@@ -1,4 +1,4 @@
1
- import { GeminiComputerUseLlm, GeminiComputerUseTaskStatus } from "../constants";
1
+ import { GeminiComputerUseLlm, GeminiComputerUseReasoningEffort, GeminiComputerUseTaskStatus } from "../constants";
2
2
  import { CreateSessionParams } from "../session";
3
3
  export interface GeminiComputerUseApiKeys {
4
4
  google?: string;
@@ -6,6 +6,7 @@ export interface GeminiComputerUseApiKeys {
6
6
  export interface StartGeminiComputerUseTaskParams {
7
7
  task: string;
8
8
  llm?: GeminiComputerUseLlm;
9
+ reasoningEffort?: GeminiComputerUseReasoningEffort;
9
10
  sessionId?: string;
10
11
  maxFailures?: number;
11
12
  maxSteps?: number;
@@ -26,10 +26,11 @@ export type CuaLlm = "computer-use-preview" | "gpt-6-astra" | "gpt-6.1-sol" | "g
26
26
  export type CuaReasoningEffort = "none" | "low" | "medium" | "high" | "xhigh" | "max";
27
27
  export type HyperAgentVersion = "0.8.0" | "1.1.0";
28
28
  export type HyperAgentLlm = "gpt-5.6-luna" | "gpt-5.5" | "gpt-5.2" | "gpt-5.1" | "gpt-5" | "gpt-5-mini" | "gpt-4o" | "gpt-4o-mini" | "gpt-4.1" | "gpt-4.1-mini" | "gpt-4.1-nano" | "claude-sonnet-5" | "claude-sonnet-4-5" | "claude-sonnet-4-6" | "claude-haiku-5-5" | "gemini-2.5-flash" | "gemini-3.8-flash" | "gemini-3.7-flash" | "gemini-3-flash-preview";
29
- export type GeminiComputerUseLlm = "gemini-3.8-flash" | "gemini-3.7-flash" | "gemini-3.6-flash" | "gemini-3.5-flash" | "gemini-3.5-flash-lite" | "gemini-3-flash-preview" | "gemini-2.5-computer-use-preview-10-2025";
29
+ export type GeminiComputerUseLlm = "gemini-4-argon" | "gemini-3.8-flash" | "gemini-3.7-flash" | "gemini-3.6-flash" | "gemini-3.5-flash" | "gemini-3.5-flash-lite" | "gemini-3-flash-preview" | "gemini-2.5-computer-use-preview-10-2025";
30
+ export type GeminiComputerUseReasoningEffort = "minimal" | "low" | "medium" | "high";
30
31
  export type GrokComputerUseLlm = "grok-4.7" | "grok-4.6" | "grok-4.5";
31
32
  export type GrokReasoningEffort = "low" | "medium" | "high";
32
- export type MetaComputerUseLlm = "muse-spark-1.1" | "muse-spark-1.2" | "muse-spark-1.3";
33
+ export type MetaComputerUseLlm = "muse-spark-1.1" | "muse-spark-1.2" | "muse-spark-1.3" | "muse-wonder-1.0";
33
34
  export type MetaReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
34
35
  export type JevComputerUseLlm = "jev-1.13.0" | "jev-latest";
35
36
  export type JevTextLlm = "gemini-3.5-flash-lite";