@declaw/sdk 1.2.3 → 1.4.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,40 @@ All notable changes to the Declaw TypeScript / JavaScript SDK are documented in
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.4.0]
9
+
10
+ _2026-08 train: idempotent sandbox creation._
11
+
12
+ ### Added
13
+
14
+ - `Sandbox.create` now sends an `Idempotency-Key`. A create that times out or is
15
+ retried no longer risks leaving a second running, billable sandbox the caller
16
+ has no handle for. The key is generated once per logical create and reused
17
+ across that call's retries.
18
+ - A `409` carrying `idempotency_in_progress` is retried automatically, honoring
19
+ `Retry-After`.
20
+ - `SandboxError.code` exposes the API's machine-readable error code, with the
21
+ `CODE_IDEMPOTENCY_IN_PROGRESS`, `CODE_IDEMPOTENCY_KEY_REUSED` and
22
+ `CODE_TEMPLATE_NOT_READY` constants exported. Branch on the code, never the
23
+ message.
24
+
25
+ _(Retry backoff already carried jitter in this SDK; unlike the Go and Python
26
+ clients it needed no change.)_
27
+
28
+ ## [1.3.0]
29
+
30
+ _2026-07 train: credential vault client + injection domain scoping._
31
+
32
+ ### Added
33
+
34
+ - Credential vault client — the `Vault` API for managing secrets **by name**:
35
+ `Vault.createSecret()`, `listSecrets()`, `rotateSecret()`, `deleteSecret()`,
36
+ `updateScopes()`, and `listPresets()`. Secret values are write-only (never
37
+ returned after create). Attach secrets to a sandbox with
38
+ `vaultRefs: { ENV_VAR: "secret-name" }`; the value is injected at the egress
39
+ proxy and never enters the sandbox (#386, #399, #408, #456).
40
+ - Opt-in domain scoping for full injection defense via the `domains` option.
41
+
8
42
  ## [1.2.3]
9
43
 
10
44
  _Restore the conservative default connection ceiling._
package/dist/index.cjs CHANGED
@@ -34,6 +34,9 @@ __export(index_exports, {
34
34
  ApiClient: () => ApiClient,
35
35
  AuthenticationError: () => AuthenticationError,
36
36
  BuildError: () => BuildError,
37
+ CODE_IDEMPOTENCY_IN_PROGRESS: () => CODE_IDEMPOTENCY_IN_PROGRESS,
38
+ CODE_IDEMPOTENCY_KEY_REUSED: () => CODE_IDEMPOTENCY_KEY_REUSED,
39
+ CODE_TEMPLATE_NOT_READY: () => CODE_TEMPLATE_NOT_READY,
37
40
  CommandExitError: () => CommandExitError,
38
41
  CommandHandle: () => CommandHandle,
39
42
  Commands: () => Commands,
@@ -68,6 +71,7 @@ __export(index_exports, {
68
71
  TemplateError: () => TemplateError,
69
72
  TimeoutError: () => TimeoutError,
70
73
  TransformDirection: () => TransformDirection,
74
+ Vault: () => Vault,
71
75
  VolumeFiles: () => VolumeFiles,
72
76
  VolumeLocks: () => VolumeLocks,
73
77
  Volumes: () => Volumes,
@@ -119,6 +123,9 @@ __export(index_exports, {
119
123
  parseSnapshotInfo: () => parseSnapshotInfo,
120
124
  parseTemplateBuildStatus: () => parseTemplateBuildStatus,
121
125
  parseToxicityConfig: () => parseToxicityConfig,
126
+ parseVaultPreset: () => parseVaultPreset,
127
+ parseVaultScope: () => parseVaultScope,
128
+ parseVaultSecret: () => parseVaultSecret,
122
129
  parseVolumeInfo: () => parseVolumeInfo,
123
130
  parseWriteInfo: () => parseWriteInfo,
124
131
  requiresTlsInterception: () => requiresTlsInterception,
@@ -126,6 +133,7 @@ __export(index_exports, {
126
133
  securityPolicyToJSON: () => securityPolicyToJSON,
127
134
  toxicityConfigToJSON: () => toxicityConfigToJSON,
128
135
  validateNetworkEntry: () => validateNetworkEntry,
136
+ vaultScopeToJSON: () => vaultScopeToJSON,
129
137
  volumeAttachmentToJSON: () => volumeAttachmentToJSON
130
138
  });
131
139
  module.exports = __toCommonJS(index_exports);
@@ -165,13 +173,51 @@ var ConnectionConfig = class {
165
173
  }
166
174
  };
167
175
 
176
+ // src/api/idempotency.ts
177
+ var CODE_IDEMPOTENCY_IN_PROGRESS = "idempotency_in_progress";
178
+ var CODE_IDEMPOTENCY_KEY_REUSED = "idempotency_key_reused";
179
+ var CODE_TEMPLATE_NOT_READY = "template_not_ready";
180
+ var MAX_RETRY_AFTER_MS = 6e4;
181
+ function newIdempotencyKey() {
182
+ const c = globalThis.crypto;
183
+ if (c?.randomUUID) {
184
+ return c.randomUUID();
185
+ }
186
+ if (c?.getRandomValues) {
187
+ const b = c.getRandomValues(new Uint8Array(16));
188
+ b[6] = b[6] & 15 | 64;
189
+ b[8] = b[8] & 63 | 128;
190
+ const hex = Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
191
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
192
+ }
193
+ return "";
194
+ }
195
+ function retryAfterMs(response) {
196
+ const raw = response.headers.get("Retry-After");
197
+ if (raw === null) return void 0;
198
+ const secs = Number(raw);
199
+ if (!Number.isFinite(secs) || secs < 0) return void 0;
200
+ return Math.min(secs * 1e3, MAX_RETRY_AFTER_MS);
201
+ }
202
+
168
203
  // src/errors.ts
169
204
  var SandboxError = class extends Error {
170
205
  sandboxId;
206
+ /**
207
+ * Machine-readable error code from the API's `code` field, when present.
208
+ *
209
+ * Branch on this, never on `message`. Messages are prose and change; codes are
210
+ * contract. It matters most where one status means several unrelated things:
211
+ * a 409 from `POST /sandboxes` is either `idempotency_in_progress` (the
212
+ * original create is still running — retry the identical request) or
213
+ * `template_not_ready` (rebuild the template; retrying cannot help).
214
+ */
215
+ code;
171
216
  constructor(message, opts) {
172
217
  super(message);
173
218
  this.name = "SandboxError";
174
219
  this.sandboxId = opts?.sandboxId;
220
+ this.code = opts?.code;
175
221
  }
176
222
  };
177
223
  var TimeoutError = class extends SandboxError {
@@ -429,6 +475,15 @@ var ApiClient = class {
429
475
  await this.delay(attempt);
430
476
  continue;
431
477
  }
478
+ if (response.status === 409 && attempt < this.maxRetries - 1) {
479
+ const parsed = await this.readErrorBody(response);
480
+ if (parsed.code === CODE_IDEMPOTENCY_IN_PROGRESS) {
481
+ const after = retryAfterMs(response);
482
+ await (after !== void 0 ? new Promise((r) => setTimeout(r, after)) : this.delay(attempt));
483
+ continue;
484
+ }
485
+ throw this.errorFrom(response, parsed);
486
+ }
432
487
  if (!response.ok) {
433
488
  throw await this.buildError(response);
434
489
  }
@@ -454,20 +509,36 @@ var ApiClient = class {
454
509
  `Request failed after ${this.maxRetries} retries: ${lastError?.message ?? "unknown error"}`
455
510
  );
456
511
  }
457
- async buildError(response) {
458
- let message;
512
+ /**
513
+ * Read an error body ONCE.
514
+ *
515
+ * `Response` bodies are single-read streams, so the 409 path cannot inspect
516
+ * the code and then hand the response to a separate error builder — the
517
+ * second read yields nothing and the error loses its message. Everything that
518
+ * needs the body goes through here, and the parsed result is passed around
519
+ * instead of the response.
520
+ */
521
+ async readErrorBody(response) {
459
522
  try {
460
- const body = await response.json();
523
+ const body = JSON.parse(await response.text());
461
524
  const bodyMsg = body.message ?? body.error ?? response.statusText;
462
- message = `HTTP ${response.status}: ${bodyMsg}`;
525
+ return {
526
+ message: `HTTP ${response.status}: ${bodyMsg}`,
527
+ code: typeof body.code === "string" ? body.code : void 0
528
+ };
463
529
  } catch {
464
- message = `HTTP ${response.status}: ${response.statusText}`;
530
+ return { message: `HTTP ${response.status}: ${response.statusText}` };
465
531
  }
532
+ }
533
+ errorFrom(response, parsed) {
466
534
  const ErrorClass = STATUS_ERROR_MAP[response.status];
467
535
  if (ErrorClass) {
468
- return new ErrorClass(message);
536
+ return new ErrorClass(parsed.message, { code: parsed.code });
469
537
  }
470
- return new SandboxError(message);
538
+ return new SandboxError(parsed.message, { code: parsed.code });
539
+ }
540
+ async buildError(response) {
541
+ return this.errorFrom(response, await this.readErrorBody(response));
471
542
  }
472
543
  async parseResponseBody(response) {
473
544
  const contentLength = response.headers.get("content-length");
@@ -641,7 +712,7 @@ function createInjectionDefenseConfig(opts) {
641
712
  enabled: opts?.enabled ?? false,
642
713
  sensitivity: opts?.sensitivity ?? "medium" /* Medium */,
643
714
  action: opts?.action ?? "log_only" /* LogOnly */,
644
- threshold: opts?.threshold ?? 0.8,
715
+ threshold: opts?.threshold ?? 0.95,
645
716
  domains: opts?.domains,
646
717
  judge: opts?.judge,
647
718
  injectionMode: opts?.injectionMode
@@ -668,7 +739,7 @@ function parseInjectionDefenseConfig(data) {
668
739
  enabled: data.enabled ?? false,
669
740
  sensitivity: data.sensitivity ?? "medium" /* Medium */,
670
741
  action: data.action ?? "log_only" /* LogOnly */,
671
- threshold: data.threshold ?? 0.8,
742
+ threshold: data.threshold ?? 0.95,
672
743
  domains: data.domains,
673
744
  judge: data.judge ? { enabled: data.judge.enabled ?? false, always: data.judge.always, policy: data.judge.policy } : void 0,
674
745
  injectionMode: data.injection_mode ?? data.injectionMode
@@ -1046,14 +1117,14 @@ function fullInjectionDefensePolicy(opts) {
1046
1117
  injectionDefense: createInjectionDefenseConfig({
1047
1118
  enabled: true,
1048
1119
  action: opts?.action ?? "block",
1049
- threshold: opts?.threshold ?? 0.8,
1120
+ threshold: opts?.threshold ?? 0.95,
1050
1121
  domains: opts?.domains,
1051
1122
  injectionMode: opts?.mode ?? "balanced",
1052
1123
  judge: { enabled: true, always: opts?.alwaysJudge ?? false, policy: opts?.agentPolicy ?? "" }
1053
1124
  }),
1054
1125
  customPolicy: createCustomPolicyConfig({
1055
1126
  enabled: true,
1056
- policyRef: "prompt-injection@v2",
1127
+ policyRef: "prompt-injection@v3",
1057
1128
  defaultDeny: false
1058
1129
  })
1059
1130
  });
@@ -1094,7 +1165,7 @@ function securityPolicyToJSON(policy) {
1094
1165
  action: injDefConfig.action,
1095
1166
  threshold: injDefConfig.threshold
1096
1167
  };
1097
- if (injDefConfig.domains !== void 0) {
1168
+ if (injDefConfig.domains !== void 0 && injDefConfig.domains.length > 0) {
1098
1169
  injDef.domains = injDefConfig.domains;
1099
1170
  }
1100
1171
  if (injDefConfig.injectionMode !== void 0) {
@@ -2206,6 +2277,311 @@ function volumeAttachmentToJSON(att) {
2206
2277
  return out;
2207
2278
  }
2208
2279
 
2280
+ // src/vault/models.ts
2281
+ function parseVaultScope(data) {
2282
+ const scope = {
2283
+ domainRegex: String(data.domain_regex ?? "")
2284
+ };
2285
+ if (data.injection_type !== void 0 && data.injection_type !== null) {
2286
+ scope.injectionType = String(data.injection_type);
2287
+ }
2288
+ if (data.header_name !== void 0 && data.header_name !== null) {
2289
+ scope.headerName = String(data.header_name);
2290
+ }
2291
+ if (data.value_prefix !== void 0 && data.value_prefix !== null) {
2292
+ scope.valuePrefix = String(data.value_prefix);
2293
+ }
2294
+ if (data.basic_username !== void 0 && data.basic_username !== null) {
2295
+ scope.basicUsername = String(data.basic_username);
2296
+ }
2297
+ if (data.extra_headers !== void 0 && data.extra_headers !== null) {
2298
+ scope.extraHeaders = data.extra_headers;
2299
+ }
2300
+ if (data.query_params !== void 0 && data.query_params !== null) {
2301
+ scope.queryParams = data.query_params;
2302
+ }
2303
+ return scope;
2304
+ }
2305
+ function parseVaultSecret(data) {
2306
+ const secret = {
2307
+ secretId: String(data.secret_id ?? ""),
2308
+ name: String(data.name ?? ""),
2309
+ createdAt: String(data.created_at ?? ""),
2310
+ updatedAt: String(data.updated_at ?? "")
2311
+ };
2312
+ const rawScopes = data.scopes;
2313
+ if (rawScopes && rawScopes.length > 0) {
2314
+ secret.scopes = rawScopes.map(parseVaultScope);
2315
+ }
2316
+ if (data.rotated_at !== void 0 && data.rotated_at !== null) {
2317
+ secret.rotatedAt = String(data.rotated_at);
2318
+ }
2319
+ if (data.rotation_interval_days !== void 0 && data.rotation_interval_days !== null) {
2320
+ secret.rotationIntervalDays = Number(data.rotation_interval_days);
2321
+ }
2322
+ if (data.rotation_due !== void 0 && data.rotation_due !== null) {
2323
+ secret.rotationDue = Boolean(data.rotation_due);
2324
+ }
2325
+ return secret;
2326
+ }
2327
+ function parseVaultPreset(data) {
2328
+ const rawScopes = data.scopes ?? [];
2329
+ const preset = {
2330
+ key: String(data.key ?? ""),
2331
+ name: String(data.name ?? ""),
2332
+ category: String(data.category ?? ""),
2333
+ keyHint: String(data.key_hint ?? ""),
2334
+ scopes: rawScopes.map(parseVaultScope)
2335
+ };
2336
+ if (data.docs_url !== void 0 && data.docs_url !== null) {
2337
+ preset.docsUrl = String(data.docs_url);
2338
+ }
2339
+ return preset;
2340
+ }
2341
+ function vaultScopeToJSON(scope) {
2342
+ const out = {
2343
+ domain_regex: scope.domainRegex
2344
+ };
2345
+ if (scope.injectionType !== void 0) {
2346
+ out.injection_type = scope.injectionType;
2347
+ }
2348
+ if (scope.headerName !== void 0) {
2349
+ out.header_name = scope.headerName;
2350
+ }
2351
+ if (scope.valuePrefix !== void 0) {
2352
+ out.value_prefix = scope.valuePrefix;
2353
+ }
2354
+ if (scope.basicUsername !== void 0) {
2355
+ out.basic_username = scope.basicUsername;
2356
+ }
2357
+ if (scope.extraHeaders !== void 0) {
2358
+ out.extra_headers = scope.extraHeaders;
2359
+ }
2360
+ if (scope.queryParams !== void 0) {
2361
+ out.query_params = scope.queryParams;
2362
+ }
2363
+ return out;
2364
+ }
2365
+
2366
+ // src/vault/vault.ts
2367
+ var DEFAULT_TEAM_NAME = "default";
2368
+ var DEFAULT_ENV_NAME = "prod";
2369
+ var defaultTeamCache = /* @__PURE__ */ new Map();
2370
+ function cacheKey(config) {
2371
+ return `${config.apiUrl}\0${config.apiKey}`;
2372
+ }
2373
+ async function resolveDefaultTeamId(config, create, timeout) {
2374
+ const key = cacheKey(config);
2375
+ const cached = defaultTeamCache.get(key);
2376
+ if (cached !== void 0) return cached;
2377
+ const client = getSharedClient(config);
2378
+ const resp = await client.get("/teams", { timeout });
2379
+ const rows = resp.teams ?? [];
2380
+ let best = null;
2381
+ for (const t of rows) {
2382
+ if (t.name === DEFAULT_TEAM_NAME) {
2383
+ if (best === null || t.created_at < best.created_at) {
2384
+ best = t;
2385
+ }
2386
+ }
2387
+ }
2388
+ if (best !== null) {
2389
+ defaultTeamCache.set(key, best.team_id);
2390
+ return best.team_id;
2391
+ }
2392
+ if (!create) return null;
2393
+ const created = await client.post("/teams", {
2394
+ json: { name: DEFAULT_TEAM_NAME },
2395
+ timeout
2396
+ });
2397
+ defaultTeamCache.set(key, created.team_id);
2398
+ return created.team_id;
2399
+ }
2400
+ async function ensureDefaultEnv(config, teamId, timeout) {
2401
+ const client = getSharedClient(config);
2402
+ const resp = await client.get(
2403
+ `/teams/${encodeURIComponent(teamId)}/environments`,
2404
+ { timeout }
2405
+ );
2406
+ const rows = resp.environments ?? [];
2407
+ if (rows.some((e) => e.name === DEFAULT_ENV_NAME)) return;
2408
+ try {
2409
+ await client.post(`/teams/${encodeURIComponent(teamId)}/environments`, {
2410
+ json: { name: DEFAULT_ENV_NAME },
2411
+ timeout
2412
+ });
2413
+ } catch {
2414
+ const resp2 = await client.get(
2415
+ `/teams/${encodeURIComponent(teamId)}/environments`,
2416
+ { timeout }
2417
+ );
2418
+ const rows2 = resp2.environments ?? [];
2419
+ if (rows2.some((e) => e.name === DEFAULT_ENV_NAME)) return;
2420
+ throw new Error(`Failed to ensure default environment "${DEFAULT_ENV_NAME}" on team ${teamId}`);
2421
+ }
2422
+ }
2423
+ async function expandVaultRefs(config, refs, timeout) {
2424
+ if (Object.keys(refs).length === 0) return refs;
2425
+ const needsTeam = Object.values(refs).some((v) => !v.startsWith("vault://"));
2426
+ if (!needsTeam) return refs;
2427
+ const teamId = await resolveDefaultTeamId(config, false, timeout);
2428
+ if (teamId === null) {
2429
+ throw new Error("vault_refs given but no vault secrets exist for this account");
2430
+ }
2431
+ const out = {};
2432
+ for (const [envVar, ref] of Object.entries(refs)) {
2433
+ out[envVar] = ref.startsWith("vault://") ? ref : `vault://${teamId}/${DEFAULT_ENV_NAME}/${ref}`;
2434
+ }
2435
+ return out;
2436
+ }
2437
+ function buildConfig(opts) {
2438
+ return new ConnectionConfig({
2439
+ apiKey: opts?.apiKey,
2440
+ domain: opts?.domain,
2441
+ apiUrl: opts?.apiUrl,
2442
+ requestTimeout: opts?.requestTimeout
2443
+ });
2444
+ }
2445
+ var Vault = class _Vault {
2446
+ // -------------------------------------------------------------------------
2447
+ // Secrets
2448
+ // -------------------------------------------------------------------------
2449
+ /**
2450
+ * Store a secret's value (server-side, in OpenBao) plus its injection
2451
+ * scopes, under the auto-provisioned default team + "prod" environment.
2452
+ * Returns metadata only — the value is never echoed.
2453
+ *
2454
+ * POST /teams/{teamId}/vault/secrets
2455
+ */
2456
+ static async createSecret(input, opts) {
2457
+ const config = buildConfig(opts);
2458
+ const teamId = await resolveDefaultTeamId(config, true, opts?.requestTimeout);
2459
+ if (teamId === null) throw new Error("Failed to resolve or create default team");
2460
+ await ensureDefaultEnv(config, teamId, opts?.requestTimeout);
2461
+ const body = {
2462
+ environment: DEFAULT_ENV_NAME,
2463
+ value: input.value
2464
+ };
2465
+ if (input.name) body.name = input.name;
2466
+ if (input.provider) body.provider = input.provider;
2467
+ if (input.scopes && input.scopes.length > 0) {
2468
+ body.scopes = input.scopes.map(vaultScopeToJSON);
2469
+ }
2470
+ if (input.rotationIntervalDays && input.rotationIntervalDays > 0) {
2471
+ body.rotation_interval_days = input.rotationIntervalDays;
2472
+ }
2473
+ const client = getSharedClient(config);
2474
+ const resp = await client.post(
2475
+ `/teams/${encodeURIComponent(teamId)}/vault/secrets`,
2476
+ { json: body, timeout: opts?.requestTimeout }
2477
+ );
2478
+ return parseVaultSecret(resp);
2479
+ }
2480
+ /**
2481
+ * List secret metadata for the default team. Returns an empty array if no
2482
+ * default team has been provisioned yet.
2483
+ *
2484
+ * GET /teams/{teamId}/vault/secrets -> {secrets}
2485
+ */
2486
+ static async listSecrets(opts) {
2487
+ const config = buildConfig(opts);
2488
+ const teamId = await resolveDefaultTeamId(config, false, opts?.requestTimeout);
2489
+ if (teamId === null) return [];
2490
+ const client = getSharedClient(config);
2491
+ const resp = await client.get(
2492
+ `/teams/${encodeURIComponent(teamId)}/vault/secrets`,
2493
+ { timeout: opts?.requestTimeout }
2494
+ );
2495
+ const rows = resp.secrets ?? [];
2496
+ return rows.map(parseVaultSecret);
2497
+ }
2498
+ /**
2499
+ * Replace a secret's value by name (server-side); scopes are unchanged.
2500
+ *
2501
+ * POST /teams/{teamId}/vault/secrets/{secretId}/rotate {value}
2502
+ */
2503
+ static async rotateSecret(name, value, opts) {
2504
+ const config = buildConfig(opts);
2505
+ const teamId = await resolveDefaultTeamId(config, false, opts?.requestTimeout);
2506
+ if (teamId === null) throw new Error(`vault secret "${name}" not found`);
2507
+ const secretId = await _Vault._resolveSecretId(config, teamId, name, opts?.requestTimeout);
2508
+ const client = getSharedClient(config);
2509
+ await client.post(
2510
+ `/teams/${encodeURIComponent(teamId)}/vault/secrets/${encodeURIComponent(secretId)}/rotate`,
2511
+ { json: { value }, timeout: opts?.requestTimeout }
2512
+ );
2513
+ }
2514
+ /**
2515
+ * Delete a secret by name — metadata and stored value.
2516
+ *
2517
+ * DELETE /teams/{teamId}/vault/secrets/{secretId}
2518
+ */
2519
+ static async deleteSecret(name, opts) {
2520
+ const config = buildConfig(opts);
2521
+ const teamId = await resolveDefaultTeamId(config, false, opts?.requestTimeout);
2522
+ if (teamId === null) throw new Error(`vault secret "${name}" not found`);
2523
+ const secretId = await _Vault._resolveSecretId(config, teamId, name, opts?.requestTimeout);
2524
+ const client = getSharedClient(config);
2525
+ await client.delete(
2526
+ `/teams/${encodeURIComponent(teamId)}/vault/secrets/${encodeURIComponent(secretId)}`,
2527
+ { timeout: opts?.requestTimeout }
2528
+ );
2529
+ }
2530
+ /**
2531
+ * Replace a secret's injection scopes by name; the value is unchanged. Use
2532
+ * this to change a secret's destination(s) or injection format in place
2533
+ * instead of delete + recreate. At least one scope is required.
2534
+ *
2535
+ * POST /teams/{teamId}/vault/secrets/{secretId}/scopes
2536
+ */
2537
+ static async updateScopes(name, scopes, opts) {
2538
+ if (!scopes || scopes.length === 0) {
2539
+ throw new Error("at least one scope is required");
2540
+ }
2541
+ const config = buildConfig(opts);
2542
+ const teamId = await resolveDefaultTeamId(config, false, opts?.requestTimeout);
2543
+ if (teamId === null) throw new Error(`vault secret "${name}" not found`);
2544
+ const secretId = await _Vault._resolveSecretId(config, teamId, name, opts?.requestTimeout);
2545
+ const client = getSharedClient(config);
2546
+ await client.post(
2547
+ `/teams/${encodeURIComponent(teamId)}/vault/secrets/${encodeURIComponent(secretId)}/scopes`,
2548
+ { json: { scopes: scopes.map(vaultScopeToJSON) }, timeout: opts?.requestTimeout }
2549
+ );
2550
+ }
2551
+ // -------------------------------------------------------------------------
2552
+ // Presets
2553
+ // -------------------------------------------------------------------------
2554
+ /**
2555
+ * List built-in provider preset catalog (templates only, no secret material).
2556
+ *
2557
+ * GET /vault/presets -> {presets}
2558
+ */
2559
+ static async listPresets(opts) {
2560
+ const client = getSharedClient(buildConfig(opts));
2561
+ const resp = await client.get("/vault/presets", {
2562
+ timeout: opts?.requestTimeout
2563
+ });
2564
+ const rows = resp.presets ?? [];
2565
+ return rows.map(parseVaultPreset);
2566
+ }
2567
+ // -------------------------------------------------------------------------
2568
+ // Internal helpers
2569
+ // -------------------------------------------------------------------------
2570
+ /** Maps a secret name to its id within the given team. */
2571
+ static async _resolveSecretId(config, teamId, name, timeout) {
2572
+ const client = getSharedClient(config);
2573
+ const resp = await client.get(
2574
+ `/teams/${encodeURIComponent(teamId)}/vault/secrets`,
2575
+ { timeout }
2576
+ );
2577
+ const rows = resp.secrets ?? [];
2578
+ for (const s of rows) {
2579
+ if (s.name === name) return String(s.secret_id ?? "");
2580
+ }
2581
+ throw new Error(`vault secret "${name}" not found`);
2582
+ }
2583
+ };
2584
+
2209
2585
  // src/sandbox/sandbox.ts
2210
2586
  var DEFAULT_TEMPLATE = "base";
2211
2587
  var DEFAULT_TIMEOUT = 300;
@@ -2351,6 +2727,9 @@ var Sandbox = class _Sandbox {
2351
2727
  if (opts?.envs) {
2352
2728
  body.envs = opts.envs;
2353
2729
  }
2730
+ if (opts?.vaultRefs) {
2731
+ body.vault_refs = await expandVaultRefs(config, opts.vaultRefs, opts.requestTimeout);
2732
+ }
2354
2733
  if (opts?.network) {
2355
2734
  body.network = networkOptsToJSON(opts.network);
2356
2735
  } else if (opts?.allowInternetAccess === false) {
@@ -2368,9 +2747,11 @@ var Sandbox = class _Sandbox {
2368
2747
  if (opts?.volumes && opts.volumes.length > 0) {
2369
2748
  body.volumes = opts.volumes.map(volumeAttachmentToJSON);
2370
2749
  }
2750
+ const idempotencyKey = newIdempotencyKey();
2371
2751
  const data = await client.post("/sandboxes", {
2372
2752
  json: body,
2373
- timeout: opts?.requestTimeout
2753
+ timeout: opts?.requestTimeout,
2754
+ ...idempotencyKey ? { headers: { "Idempotency-Key": idempotencyKey } } : {}
2374
2755
  });
2375
2756
  const sandboxId = data.sandbox_id;
2376
2757
  assertValidId(sandboxId, "sandbox ID (from server)");
@@ -3254,7 +3635,16 @@ function assertValidVolumeId3(id) {
3254
3635
  }
3255
3636
  }
3256
3637
  var Volumes = class {
3257
- /** Create a volume by streaming a tarball (gzip tar.gz) to the server. */
3638
+ /**
3639
+ * Create a volume named `name`, optionally populated with `data`.
3640
+ *
3641
+ * `POST /volumes` is the canonical create endpoint. With `data` (a gzip tar.gz)
3642
+ * the body is ingested into a new file-granular volume (or a legacy tarball
3643
+ * blob if no file-granular backend is configured). Creating an empty volume
3644
+ * (no `data`, or an empty buffer) requires a file-granular backend.
3645
+ * `empty(name)` / `ingest(name, data)` remain available for explicit,
3646
+ * backend-specific control.
3647
+ */
3258
3648
  static async create(name, data, opts) {
3259
3649
  if (!name) {
3260
3650
  throw new InvalidArgumentError("volume name is required");
@@ -3266,6 +3656,13 @@ var Volumes = class {
3266
3656
  requestTimeout: opts?.requestTimeout
3267
3657
  });
3268
3658
  const client = getSharedClient(config);
3659
+ if (data === void 0 || data.byteLength === 0) {
3660
+ const resp2 = await client.post("/volumes", {
3661
+ params: { name },
3662
+ timeout: opts?.requestTimeout
3663
+ });
3664
+ return parseVolumeInfo(resp2);
3665
+ }
3269
3666
  const body = data instanceof Uint8Array ? data : new Uint8Array(data);
3270
3667
  const resp = await client.post("/volumes", {
3271
3668
  params: { name },
@@ -3536,6 +3933,9 @@ var Governance = class {
3536
3933
  ApiClient,
3537
3934
  AuthenticationError,
3538
3935
  BuildError,
3936
+ CODE_IDEMPOTENCY_IN_PROGRESS,
3937
+ CODE_IDEMPOTENCY_KEY_REUSED,
3938
+ CODE_TEMPLATE_NOT_READY,
3539
3939
  CommandExitError,
3540
3940
  CommandHandle,
3541
3941
  Commands,
@@ -3570,6 +3970,7 @@ var Governance = class {
3570
3970
  TemplateError,
3571
3971
  TimeoutError,
3572
3972
  TransformDirection,
3973
+ Vault,
3573
3974
  VolumeFiles,
3574
3975
  VolumeLocks,
3575
3976
  Volumes,
@@ -3621,6 +4022,9 @@ var Governance = class {
3621
4022
  parseSnapshotInfo,
3622
4023
  parseTemplateBuildStatus,
3623
4024
  parseToxicityConfig,
4025
+ parseVaultPreset,
4026
+ parseVaultScope,
4027
+ parseVaultSecret,
3624
4028
  parseVolumeInfo,
3625
4029
  parseWriteInfo,
3626
4030
  requiresTlsInterception,
@@ -3628,6 +4032,7 @@ var Governance = class {
3628
4032
  securityPolicyToJSON,
3629
4033
  toxicityConfigToJSON,
3630
4034
  validateNetworkEntry,
4035
+ vaultScopeToJSON,
3631
4036
  volumeAttachmentToJSON
3632
4037
  });
3633
4038
  //# sourceMappingURL=index.cjs.map