@enclave-run/sdk 0.0.0-dev → 0.0.0-dev.5

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/dist/index.mjs CHANGED
@@ -60,7 +60,7 @@ import createClient from "openapi-fetch";
60
60
  import platform2 from "platform";
61
61
 
62
62
  // package.json
63
- var version = "0.0.0-dev";
63
+ var version = "0.0.0-dev.5";
64
64
 
65
65
  // src/utils.ts
66
66
  import platform from "platform";
@@ -244,6 +244,21 @@ var TemplateError = class extends SandboxError {
244
244
  this.name = "TemplateError";
245
245
  }
246
246
  };
247
+ var TemplateTagConflictError = class extends TemplateError {
248
+ constructor(options) {
249
+ var _a3;
250
+ super(
251
+ (_a3 = options.message) != null ? _a3 : `Template tag "${options.tag}" conflicted with another mutation. Fetch its current head with Template.getTagHead() before retrying.`
252
+ );
253
+ this.name = "TemplateTagConflictError";
254
+ this.templateId = options.templateId;
255
+ this.tag = options.tag;
256
+ this.candidateBuildId = options.candidateBuildId;
257
+ this.expectedBuildId = options.expectedBuildId;
258
+ this.expectedVersion = options.expectedVersion;
259
+ this.reason = options.reason;
260
+ }
261
+ };
247
262
  var RateLimitError = class extends SandboxError {
248
263
  constructor(message) {
249
264
  super(message);
@@ -1653,9 +1668,9 @@ var ALL_TRAFFIC = "0.0.0.0/0";
1653
1668
  function shellEscape(value) {
1654
1669
  return `'${value.replace(/'/g, `'"'"'`)}'`;
1655
1670
  }
1656
- function withCredentials(url, username, password) {
1671
+ function withCredentials(url2, username, password) {
1657
1672
  if (!username && !password) {
1658
- return url;
1673
+ return url2;
1659
1674
  }
1660
1675
  if (!username || !password) {
1661
1676
  throw new InvalidArgumentError(
@@ -1664,9 +1679,9 @@ function withCredentials(url, username, password) {
1664
1679
  }
1665
1680
  let parsed;
1666
1681
  try {
1667
- parsed = new URL(url);
1682
+ parsed = new URL(url2);
1668
1683
  } catch (e) {
1669
- throw new InvalidArgumentError(`Invalid Git URL: ${url}`);
1684
+ throw new InvalidArgumentError(`Invalid Git URL: ${url2}`);
1670
1685
  }
1671
1686
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1672
1687
  throw new InvalidArgumentError(
@@ -1677,27 +1692,27 @@ function withCredentials(url, username, password) {
1677
1692
  parsed.password = password;
1678
1693
  return parsed.toString();
1679
1694
  }
1680
- function stripCredentials(url) {
1695
+ function stripCredentials(url2) {
1681
1696
  let parsed;
1682
1697
  try {
1683
- parsed = new URL(url);
1698
+ parsed = new URL(url2);
1684
1699
  } catch (e) {
1685
- return url;
1700
+ return url2;
1686
1701
  }
1687
1702
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1688
- return url;
1703
+ return url2;
1689
1704
  }
1690
1705
  if (!parsed.username && !parsed.password) {
1691
- return url;
1706
+ return url2;
1692
1707
  }
1693
1708
  parsed.username = "";
1694
1709
  parsed.password = "";
1695
1710
  return parsed.toString();
1696
1711
  }
1697
- function deriveRepoDirFromUrl(url) {
1712
+ function deriveRepoDirFromUrl(url2) {
1698
1713
  let parsed;
1699
1714
  try {
1700
- parsed = new URL(url);
1715
+ parsed = new URL(url2);
1701
1716
  } catch (e) {
1702
1717
  return void 0;
1703
1718
  }
@@ -1984,7 +1999,7 @@ var Git = class {
1984
1999
  * @param opts Clone options.
1985
2000
  * @returns Command result from the command runner.
1986
2001
  */
1987
- async clone(url, opts) {
2002
+ async clone(url2, opts) {
1988
2003
  const _a3 = opts != null ? opts : {}, {
1989
2004
  username,
1990
2005
  password,
@@ -2006,10 +2021,10 @@ var Git = class {
2006
2021
  );
2007
2022
  }
2008
2023
  const attemptClone = async (authUsername, authPassword) => {
2009
- const urlWithCreds = authUsername && authPassword ? withCredentials(url, authUsername, authPassword) : url;
2024
+ const urlWithCreds = authUsername && authPassword ? withCredentials(url2, authUsername, authPassword) : url2;
2010
2025
  const sanitizedUrl = stripCredentials(urlWithCreds);
2011
2026
  const stripInlineCreds = !dangerouslyStoreCredentials && sanitizedUrl !== urlWithCreds;
2012
- const repoPath = stripInlineCreds ? path2 != null ? path2 : deriveRepoDirFromUrl(url) : path2;
2027
+ const repoPath = stripInlineCreds ? path2 != null ? path2 : deriveRepoDirFromUrl(url2) : path2;
2013
2028
  if (stripInlineCreds && !repoPath) {
2014
2029
  throw new InvalidArgumentError(
2015
2030
  "A destination path is required when using credentials without storing them."
@@ -2068,8 +2083,8 @@ var Git = class {
2068
2083
  * @param opts Remote add options.
2069
2084
  * @returns Command result from the command runner.
2070
2085
  */
2071
- async remoteAdd(path2, name, url, opts) {
2072
- if (!name || !url) {
2086
+ async remoteAdd(path2, name, url2, opts) {
2087
+ if (!name || !url2) {
2073
2088
  throw new InvalidArgumentError(
2074
2089
  "Both remote name and URL are required to add a git remote."
2075
2090
  );
@@ -2079,12 +2094,12 @@ var Git = class {
2079
2094
  if (fetch2) {
2080
2095
  addArgs.push("-f");
2081
2096
  }
2082
- addArgs.push(name, url);
2097
+ addArgs.push(name, url2);
2083
2098
  if (!overwrite) {
2084
2099
  return this.runGit(addArgs, path2, rest);
2085
2100
  }
2086
2101
  const addCmd = buildGitCommand(addArgs, path2);
2087
- const setUrlCmd = buildGitCommand(["remote", "set-url", name, url], path2);
2102
+ const setUrlCmd = buildGitCommand(["remote", "set-url", name, url2], path2);
2088
2103
  let cmd = `${addCmd} || ${setUrlCmd}`;
2089
2104
  if (fetch2) {
2090
2105
  const fetchCmd = buildGitCommand(["fetch", name], path2);
@@ -2529,13 +2544,13 @@ var Git = class {
2529
2544
  }
2530
2545
  async getRemoteUrl(path2, remote, opts) {
2531
2546
  const result = await this.runGit(["remote", "get-url", remote], path2, opts);
2532
- const url = result.stdout.trim();
2533
- if (!url) {
2547
+ const url2 = result.stdout.trim();
2548
+ if (!url2) {
2534
2549
  throw new InvalidArgumentError(
2535
2550
  `Remote "${remote}" URL not found in repository.`
2536
2551
  );
2537
2552
  }
2538
- return url;
2553
+ return url2;
2539
2554
  }
2540
2555
  async resolveRemoteName(path2, remote, opts) {
2541
2556
  if (remote) {
@@ -3068,6 +3083,26 @@ function getLifecycle(opts) {
3068
3083
  var _a3;
3069
3084
  return (_a3 = opts == null ? void 0 : opts.lifecycle) != null ? _a3 : { onTimeout: "kill", autoResume: false };
3070
3085
  }
3086
+ function resolveSandboxTarget(target, opts) {
3087
+ if (typeof target === "string") {
3088
+ return {
3089
+ sandboxId: target,
3090
+ config: ConnectionConfig.forSandbox(opts)
3091
+ };
3092
+ }
3093
+ const referenceRegion = ConnectionConfig.resolveRegion(target.region);
3094
+ if ((opts == null ? void 0 : opts.region) && ConnectionConfig.resolveRegion(opts.region) !== referenceRegion) {
3095
+ throw new Error(
3096
+ `Sandbox ${target.sandboxId} belongs to region "${referenceRegion}", not "${opts.region}"`
3097
+ );
3098
+ }
3099
+ return {
3100
+ sandboxId: target.sandboxId,
3101
+ config: ConnectionConfig.forSandbox(__spreadProps(__spreadValues({}, opts), {
3102
+ region: referenceRegion
3103
+ }))
3104
+ };
3105
+ }
3071
3106
  var SandboxApi = class {
3072
3107
  constructor() {
3073
3108
  }
@@ -3079,9 +3114,9 @@ var SandboxApi = class {
3079
3114
  *
3080
3115
  * @returns `true` if the sandbox was found and killed, `false` otherwise.
3081
3116
  */
3082
- static async kill(sandboxId, opts) {
3117
+ static async kill(target, opts) {
3083
3118
  var _a3;
3084
- const config = ConnectionConfig.forSandbox(opts);
3119
+ const { sandboxId, config } = resolveSandboxTarget(target, opts);
3085
3120
  const client = new ApiClient(config);
3086
3121
  const res = await client.api.DELETE("/sandboxes/{sandboxID}", {
3087
3122
  params: {
@@ -3108,8 +3143,8 @@ var SandboxApi = class {
3108
3143
  *
3109
3144
  * @returns sandbox information.
3110
3145
  */
3111
- static async getInfo(sandboxId, opts) {
3112
- const fullInfo = await this.getFullInfo(sandboxId, opts);
3146
+ static async getInfo(target, opts) {
3147
+ const fullInfo = await this.getFullInfo(target, opts);
3113
3148
  delete fullInfo.envdAccessToken;
3114
3149
  delete fullInfo.sandboxDomain;
3115
3150
  return fullInfo;
@@ -3122,9 +3157,9 @@ var SandboxApi = class {
3122
3157
  *
3123
3158
  * @returns List of sandbox metrics containing CPU, memory and disk usage information.
3124
3159
  */
3125
- static async getMetrics(sandboxId, opts) {
3160
+ static async getMetrics(target, opts) {
3126
3161
  var _a3, _b;
3127
- const config = ConnectionConfig.forSandbox(opts);
3162
+ const { sandboxId, config } = resolveSandboxTarget(target, opts);
3128
3163
  const client = new ApiClient(config);
3129
3164
  const start = (opts == null ? void 0 : opts.start) ? opts.start.getTime() : void 0;
3130
3165
  const end = (opts == null ? void 0 : opts.end) ? opts.end.getTime() : void 0;
@@ -3167,9 +3202,9 @@ var SandboxApi = class {
3167
3202
  * @param timeoutMs timeout in **milliseconds**.
3168
3203
  * @param opts connection options.
3169
3204
  */
3170
- static async setTimeout(sandboxId, timeoutMs, opts) {
3205
+ static async setTimeout(target, timeoutMs, opts) {
3171
3206
  var _a3;
3172
- const config = ConnectionConfig.forSandbox(opts);
3207
+ const { sandboxId, config } = resolveSandboxTarget(target, opts);
3173
3208
  const client = new ApiClient(config);
3174
3209
  const res = await client.api.POST("/sandboxes/{sandboxID}/timeout", {
3175
3210
  params: {
@@ -3190,9 +3225,9 @@ var SandboxApi = class {
3190
3225
  throw err;
3191
3226
  }
3192
3227
  }
3193
- static async getFullInfo(sandboxId, opts) {
3228
+ static async getFullInfo(target, opts) {
3194
3229
  var _a3, _b;
3195
- const config = ConnectionConfig.forSandbox(opts);
3230
+ const { sandboxId, config } = resolveSandboxTarget(target, opts);
3196
3231
  const client = new ApiClient(config);
3197
3232
  const res = await client.api.GET("/sandboxes/{sandboxID}", {
3198
3233
  params: {
@@ -3214,6 +3249,7 @@ var SandboxApi = class {
3214
3249
  }
3215
3250
  return __spreadProps(__spreadValues({
3216
3251
  sandboxId: res.data.sandboxID,
3252
+ buildId: res.data.buildID,
3217
3253
  templateId: res.data.templateID
3218
3254
  }, res.data.alias && { name: res.data.alias }), {
3219
3255
  metadata: (_b = res.data.metadata) != null ? _b : {},
@@ -3236,9 +3272,9 @@ var SandboxApi = class {
3236
3272
  *
3237
3273
  * @returns `true` if the sandbox got paused, `false` if the sandbox was already paused.
3238
3274
  */
3239
- static async pause(sandboxId, opts) {
3275
+ static async pause(target, opts) {
3240
3276
  var _a3, _b;
3241
- const config = ConnectionConfig.forSandbox(opts);
3277
+ const { sandboxId, config } = resolveSandboxTarget(target, opts);
3242
3278
  const client = new ApiClient(config);
3243
3279
  const res = await client.api.POST("/sandboxes/{sandboxID}/pause", {
3244
3280
  params: {
@@ -3272,9 +3308,9 @@ var SandboxApi = class {
3272
3308
  *
3273
3309
  * @returns snapshot information including the snapshot name that can be used with Sandbox.create().
3274
3310
  */
3275
- static async createSnapshot(sandboxId, opts) {
3311
+ static async createSnapshot(target, opts) {
3276
3312
  var _a3;
3277
- const config = ConnectionConfig.forSandbox(opts);
3313
+ const { sandboxId, config } = resolveSandboxTarget(target, opts);
3278
3314
  const client = new ApiClient(config);
3279
3315
  const res = await client.api.POST("/sandboxes/{sandboxID}/snapshots", {
3280
3316
  params: {
@@ -3393,14 +3429,51 @@ var SandboxApi = class {
3393
3429
  }
3394
3430
  sandbox = res.data;
3395
3431
  }
3432
+ return this.sandboxInfoFromCreate(sandbox, config, opts);
3433
+ }
3434
+ static async recoverSandboxCreate(recovery, opts) {
3435
+ if (!recovery.idempotencyKey.trim()) {
3436
+ throw new InvalidArgumentError(
3437
+ "Sandbox create recovery requires an idempotency key"
3438
+ );
3439
+ }
3440
+ const recoveryRegion = recovery.region ? ConnectionConfig.resolveRegion(recovery.region) : void 0;
3441
+ if (recoveryRegion && (opts == null ? void 0 : opts.region) && ConnectionConfig.resolveRegion(opts.region) !== recoveryRegion) {
3442
+ throw new InvalidArgumentError(
3443
+ `Sandbox create recovery belongs to region "${recoveryRegion}", not "${opts.region}"`
3444
+ );
3445
+ }
3446
+ const config = ConnectionConfig.forSandbox(__spreadProps(__spreadValues({}, opts), {
3447
+ region: recoveryRegion != null ? recoveryRegion : opts == null ? void 0 : opts.region
3448
+ }));
3449
+ const signal = config.getSignal(opts == null ? void 0 : opts.requestTimeoutMs);
3450
+ const sandbox = await this.waitForSandboxCreate(
3451
+ new ApiClient(config),
3452
+ __spreadProps(__spreadValues({}, recovery), {
3453
+ region: config.region
3454
+ }),
3455
+ signal
3456
+ );
3457
+ return this.sandboxInfoFromCreate(sandbox, config, opts);
3458
+ }
3459
+ static async sandboxInfoFromCreate(sandbox, config, opts) {
3396
3460
  if (compareVersions4(sandbox.envdVersion, "0.1.0") < 0) {
3397
- await this.kill(sandbox.sandboxID, opts);
3461
+ await this.kill(
3462
+ {
3463
+ sandboxId: sandbox.sandboxID,
3464
+ region: sandbox.region
3465
+ },
3466
+ __spreadProps(__spreadValues({}, opts), {
3467
+ region: sandbox.region
3468
+ })
3469
+ );
3398
3470
  throw new TemplateError(
3399
3471
  "You need to update the template to use the new SDK. You can do this by running `enclave template build` in the directory with the template."
3400
3472
  );
3401
3473
  }
3402
3474
  return {
3403
3475
  sandboxId: sandbox.sandboxID,
3476
+ buildId: sandbox.buildID,
3404
3477
  sandboxDomain: sandbox.domain || void 0,
3405
3478
  sandboxRegion: sandbox.region,
3406
3479
  capacityTier: sandbox.capacityTier,
@@ -3414,7 +3487,13 @@ var SandboxApi = class {
3414
3487
  };
3415
3488
  }
3416
3489
  static async waitForSandboxCreate(client, recovery, signal) {
3417
- var _a3, _b;
3490
+ var _a3, _b, _c;
3491
+ let lastRecoveryError;
3492
+ let recoveryRetryDelayMs = 25;
3493
+ const waitToRetryRecovery = async () => {
3494
+ await new Promise((resolve) => setTimeout(resolve, recoveryRetryDelayMs));
3495
+ recoveryRetryDelayMs = Math.min(recoveryRetryDelayMs * 2, 1e3);
3496
+ };
3418
3497
  while (!(signal == null ? void 0 : signal.aborted)) {
3419
3498
  let res;
3420
3499
  try {
@@ -3430,12 +3509,17 @@ var SandboxApi = class {
3430
3509
  }
3431
3510
  );
3432
3511
  } catch (cause) {
3433
- throw new OutcomeUnknownError(
3434
- "Sandbox admission committed, but its terminal result could not be recovered.",
3435
- recovery,
3436
- cause
3437
- );
3512
+ lastRecoveryError = cause;
3513
+ await waitToRetryRecovery();
3514
+ continue;
3515
+ }
3516
+ const status = res.response.status;
3517
+ if (status === 404 || status === 408 || status === 425 || status >= 500) {
3518
+ lastRecoveryError = handleApiError(res);
3519
+ await waitToRetryRecovery();
3520
+ continue;
3438
3521
  }
3522
+ recoveryRetryDelayMs = 25;
3439
3523
  const err = handleApiError(res);
3440
3524
  if (err) {
3441
3525
  throw err;
@@ -3454,13 +3538,13 @@ var SandboxApi = class {
3454
3538
  throw new OutcomeUnknownError(
3455
3539
  "Sandbox admission committed, but waiting for its terminal result timed out.",
3456
3540
  recovery,
3457
- signal == null ? void 0 : signal.reason
3541
+ (_c = signal == null ? void 0 : signal.reason) != null ? _c : lastRecoveryError
3458
3542
  );
3459
3543
  }
3460
- static async connectSandbox(sandboxId, opts) {
3544
+ static async connectSandbox(target, opts) {
3461
3545
  var _a3, _b;
3462
3546
  const timeoutMs = (_a3 = opts == null ? void 0 : opts.timeoutMs) != null ? _a3 : DEFAULT_SANDBOX_TIMEOUT_MS;
3463
- const config = ConnectionConfig.forSandbox(opts);
3547
+ const { sandboxId, config } = resolveSandboxTarget(target, opts);
3464
3548
  const client = new ApiClient(config);
3465
3549
  const res = await client.api.POST("/sandboxes/{sandboxID}/connect", {
3466
3550
  params: {
@@ -3482,6 +3566,7 @@ var SandboxApi = class {
3482
3566
  }
3483
3567
  return {
3484
3568
  sandboxId: res.data.sandboxID,
3569
+ buildId: res.data.buildID,
3485
3570
  sandboxDomain: res.data.domain || void 0,
3486
3571
  sandboxRegion: res.data.region,
3487
3572
  capacityTier: res.data.capacityTier,
@@ -3562,6 +3647,7 @@ var SandboxPaginator = class extends BasePaginator {
3562
3647
  var _a4;
3563
3648
  return __spreadProps(__spreadValues({
3564
3649
  sandboxId: sandbox.sandboxID,
3650
+ buildId: sandbox.buildID,
3565
3651
  templateId: sandbox.templateID
3566
3652
  }, sandbox.alias && { name: sandbox.alias }), {
3567
3653
  metadata: (_a4 = sandbox.metadata) != null ? _a4 : {},
@@ -3628,8 +3714,14 @@ var Sandbox = class extends SandboxApi {
3628
3714
  this.mcpPort = 50005;
3629
3715
  this.connectionConfig = ConnectionConfig.forSandbox(opts);
3630
3716
  this.sandboxId = opts.sandboxId;
3717
+ this.buildId = opts.buildId;
3631
3718
  this.sandboxDomain = (_a3 = opts.sandboxDomain) != null ? _a3 : this.connectionConfig.domain;
3632
- this.sandboxRegion = opts.sandboxRegion;
3719
+ this.sandboxRegion = this.connectionConfig.debug ? opts.sandboxRegion : ConnectionConfig.resolveRegion(opts.sandboxRegion);
3720
+ if (!this.connectionConfig.debug && this.connectionConfig.region !== this.sandboxRegion) {
3721
+ throw new Error(
3722
+ `Sandbox ${opts.sandboxId} belongs to region "${this.sandboxRegion}", not "${this.connectionConfig.region}"`
3723
+ );
3724
+ }
3633
3725
  this.capacityTier = opts.capacityTier;
3634
3726
  this.envdAccessToken = opts.envdAccessToken;
3635
3727
  this.trafficAccessToken = opts.trafficAccessToken;
@@ -3646,7 +3738,7 @@ var Sandbox = class extends SandboxApi {
3646
3738
  baseUrl: this.envdApiUrl,
3647
3739
  useBinaryFormat: false,
3648
3740
  interceptors: (opts == null ? void 0 : opts.logger) ? [createRpcLogger(opts.logger)] : void 0,
3649
- fetch: (url, options) => {
3741
+ fetch: (url2, options) => {
3650
3742
  const headers = new Headers(this.connectionConfig.headers);
3651
3743
  new Headers(options == null ? void 0 : options.headers).forEach(
3652
3744
  (value, key) => headers.append(key, value)
@@ -3661,7 +3753,7 @@ var Sandbox = class extends SandboxApi {
3661
3753
  headers,
3662
3754
  redirect: "follow"
3663
3755
  });
3664
- return fetch(url, options);
3756
+ return fetch(url2, options);
3665
3757
  }
3666
3758
  });
3667
3759
  this.envdApi = new EnvdApiClient(
@@ -3690,6 +3782,33 @@ var Sandbox = class extends SandboxApi {
3690
3782
  });
3691
3783
  this.git = new Git(this.commands);
3692
3784
  }
3785
+ /**
3786
+ * Serializable regional reference for reconnecting to this sandbox.
3787
+ */
3788
+ get ref() {
3789
+ if (this.connectionConfig.debug) {
3790
+ throw new Error("Sandbox references are unavailable in debug mode");
3791
+ }
3792
+ return {
3793
+ sandboxId: this.sandboxId,
3794
+ region: this.sandboxRegion
3795
+ };
3796
+ }
3797
+ connectionOptions(opts) {
3798
+ const ownerRegion = this.connectionConfig.debug ? this.connectionConfig.region : this.sandboxRegion;
3799
+ if (!this.connectionConfig.debug && (opts == null ? void 0 : opts.region) && ConnectionConfig.resolveRegion(opts.region) !== ownerRegion) {
3800
+ throw new Error(
3801
+ `Sandbox ${this.sandboxId} belongs to region "${ownerRegion}", not "${opts.region}"`
3802
+ );
3803
+ }
3804
+ return __spreadProps(__spreadValues(__spreadValues({}, this.connectionConfig), opts), {
3805
+ apiUrl: this.connectionConfig.apiUrl,
3806
+ sandboxUrl: this.connectionConfig.sandboxUrl,
3807
+ domain: this.connectionConfig.domain,
3808
+ debug: this.connectionConfig.debug,
3809
+ region: ownerRegion
3810
+ });
3811
+ }
3693
3812
  /**
3694
3813
  * List all sandboxes.
3695
3814
  *
@@ -3700,6 +3819,20 @@ var Sandbox = class extends SandboxApi {
3700
3819
  static list(opts) {
3701
3820
  return new SandboxPaginator(opts);
3702
3821
  }
3822
+ /**
3823
+ * Create an immutable regional scope so repeated sandbox operations do not
3824
+ * need to repeat the same region option.
3825
+ *
3826
+ * @example
3827
+ * ```ts
3828
+ * const yyz = Sandbox.inRegion("yyz")
3829
+ * const sandbox = await yyz.create()
3830
+ * const sameSandbox = await yyz.connect(sandbox.sandboxId)
3831
+ * ```
3832
+ */
3833
+ static inRegion(region) {
3834
+ return new SandboxRegionScope(this, region);
3835
+ }
3703
3836
  static async create(templateOrOpts, opts) {
3704
3837
  var _a3, _b;
3705
3838
  const { template, sandboxOpts } = typeof templateOrOpts === "string" ? {
@@ -3713,6 +3846,7 @@ var Sandbox = class extends SandboxApi {
3713
3846
  if (config.debug) {
3714
3847
  return new this(__spreadValues({
3715
3848
  sandboxId: "debug_sandbox_id",
3849
+ buildId: "debug_build_id",
3716
3850
  sandboxRegion: "debug",
3717
3851
  capacityTier: "shared",
3718
3852
  envdVersion: ENVD_DEBUG_FALLBACK
@@ -3723,7 +3857,9 @@ var Sandbox = class extends SandboxApi {
3723
3857
  (_a3 = sandboxOpts == null ? void 0 : sandboxOpts.timeoutMs) != null ? _a3 : this.defaultSandboxTimeoutMs,
3724
3858
  sandboxOpts
3725
3859
  );
3726
- const sandbox = new this(__spreadValues(__spreadValues({}, config), sandboxInfo));
3860
+ const sandbox = new this(__spreadProps(__spreadValues(__spreadValues({}, config), sandboxInfo), {
3861
+ region: sandboxInfo.sandboxRegion
3862
+ }));
3727
3863
  if (sandboxOpts == null ? void 0 : sandboxOpts.mcp) {
3728
3864
  sandbox.mcpToken = crypto.randomUUID();
3729
3865
  const res = await sandbox.commands.run(
@@ -3741,6 +3877,51 @@ var Sandbox = class extends SandboxApi {
3741
3877
  }
3742
3878
  return sandbox;
3743
3879
  }
3880
+ /**
3881
+ * Recover a create whose authoritative response was lost without resending
3882
+ * the original create payload.
3883
+ *
3884
+ * Pass `OutcomeUnknownError.recovery`. Recovery is pinned to the operation's
3885
+ * owning region and cannot create a second sandbox with different options.
3886
+ *
3887
+ * @example
3888
+ * ```ts
3889
+ * try {
3890
+ * return await Sandbox.create("base", { idempotencyKey: "job-123" })
3891
+ * } catch (error) {
3892
+ * if (error instanceof OutcomeUnknownError) {
3893
+ * return Sandbox.recoverCreate(error.recovery)
3894
+ * }
3895
+ * throw error
3896
+ * }
3897
+ * ```
3898
+ */
3899
+ static async recoverCreate(recovery, opts) {
3900
+ var _a3;
3901
+ const config = ConnectionConfig.forSandbox(__spreadProps(__spreadValues({}, opts), {
3902
+ region: (_a3 = recovery.region) != null ? _a3 : opts == null ? void 0 : opts.region
3903
+ }));
3904
+ const sandboxInfo = await SandboxApi.recoverSandboxCreate(recovery, opts);
3905
+ const sandbox = new this(__spreadProps(__spreadValues(__spreadValues({}, config), sandboxInfo), {
3906
+ region: sandboxInfo.sandboxRegion
3907
+ }));
3908
+ if (opts == null ? void 0 : opts.mcp) {
3909
+ sandbox.mcpToken = crypto.randomUUID();
3910
+ const res = await sandbox.commands.run(
3911
+ `mcp-gateway --config '${JSON.stringify(opts.mcp)}'`,
3912
+ {
3913
+ user: "root",
3914
+ envs: {
3915
+ GATEWAY_ACCESS_TOKEN: sandbox.mcpToken
3916
+ }
3917
+ }
3918
+ );
3919
+ if (res.exitCode !== 0) {
3920
+ throw new Error(`Failed to start MCP gateway: ${res.stderr}`);
3921
+ }
3922
+ }
3923
+ return sandbox;
3924
+ }
3744
3925
  static async betaCreate(templateOrOpts, opts) {
3745
3926
  var _a3, _b;
3746
3927
  const { template, sandboxOpts } = typeof templateOrOpts === "string" ? {
@@ -3754,6 +3935,7 @@ var Sandbox = class extends SandboxApi {
3754
3935
  if (config.debug) {
3755
3936
  return new this(__spreadValues({
3756
3937
  sandboxId: "debug_sandbox_id",
3938
+ buildId: "debug_build_id",
3757
3939
  sandboxRegion: "debug",
3758
3940
  capacityTier: "shared",
3759
3941
  envdVersion: ENVD_DEBUG_FALLBACK
@@ -3764,7 +3946,9 @@ var Sandbox = class extends SandboxApi {
3764
3946
  (_a3 = sandboxOpts == null ? void 0 : sandboxOpts.timeoutMs) != null ? _a3 : this.defaultSandboxTimeoutMs,
3765
3947
  sandboxOpts
3766
3948
  );
3767
- const sandbox = new this(__spreadValues(__spreadValues({}, config), sandboxInfo));
3949
+ const sandbox = new this(__spreadProps(__spreadValues(__spreadValues({}, config), sandboxInfo), {
3950
+ region: sandboxInfo.sandboxRegion
3951
+ }));
3768
3952
  if (sandboxOpts == null ? void 0 : sandboxOpts.mcp) {
3769
3953
  sandbox.mcpToken = crypto.randomUUID();
3770
3954
  const res = await sandbox.commands.run(
@@ -3786,9 +3970,11 @@ var Sandbox = class extends SandboxApi {
3786
3970
  * Connect to a sandbox. If the sandbox is paused, it will be automatically resumed.
3787
3971
  * Sandbox must be either running or be paused.
3788
3972
  *
3789
- * With sandbox ID you can connect to the same sandbox from different places or environments (serverless functions, etc).
3973
+ * Persist {@link Sandbox.ref} to reconnect through the sandbox's owning
3974
+ * regional API from another process or environment.
3790
3975
  *
3791
- * @param sandboxId sandbox ID.
3976
+ * @param target sandbox ID, or a serializable reference containing its ID
3977
+ * and owning region.
3792
3978
  * @param opts connection options.
3793
3979
  *
3794
3980
  * @returns A running sandbox instance
@@ -3802,13 +3988,15 @@ var Sandbox = class extends SandboxApi {
3802
3988
  * const sameSandbox = await Sandbox.connect(sandboxId)
3803
3989
  * ```
3804
3990
  */
3805
- static async connect(sandboxId, opts) {
3806
- const sandbox = await SandboxApi.connectSandbox(sandboxId, opts);
3807
- const config = ConnectionConfig.forSandbox(opts);
3991
+ static async connect(target, opts) {
3992
+ const sandbox = await SandboxApi.connectSandbox(target, opts);
3993
+ const config = ConnectionConfig.forSandbox(__spreadValues(__spreadValues({}, opts), typeof target === "string" ? {} : { region: target.region }));
3808
3994
  return new this(__spreadProps(__spreadValues({}, config), {
3809
- sandboxId,
3995
+ sandboxId: sandbox.sandboxId,
3996
+ buildId: sandbox.buildId,
3810
3997
  sandboxDomain: sandbox.sandboxDomain,
3811
3998
  sandboxRegion: sandbox.sandboxRegion,
3999
+ region: sandbox.sandboxRegion,
3812
4000
  capacityTier: sandbox.capacityTier,
3813
4001
  apiUrl: sandbox.apiUrl,
3814
4002
  envdAccessToken: sandbox.envdAccessToken,
@@ -3836,7 +4024,10 @@ var Sandbox = class extends SandboxApi {
3836
4024
  * ```
3837
4025
  */
3838
4026
  async connect(opts) {
3839
- await SandboxApi.connectSandbox(this.sandboxId, __spreadValues(__spreadValues({}, this.connectionConfig), opts));
4027
+ await SandboxApi.connectSandbox(
4028
+ this.sandboxId,
4029
+ this.connectionOptions(opts)
4030
+ );
3840
4031
  return this;
3841
4032
  }
3842
4033
  /**
@@ -3905,7 +4096,11 @@ var Sandbox = class extends SandboxApi {
3905
4096
  if (this.connectionConfig.debug) {
3906
4097
  return;
3907
4098
  }
3908
- await SandboxApi.setTimeout(this.sandboxId, timeoutMs, __spreadValues(__spreadValues({}, this.connectionConfig), opts));
4099
+ await SandboxApi.setTimeout(
4100
+ this.sandboxId,
4101
+ timeoutMs,
4102
+ this.connectionOptions(opts)
4103
+ );
3909
4104
  }
3910
4105
  /**
3911
4106
  * Kill the sandbox.
@@ -3916,7 +4111,7 @@ var Sandbox = class extends SandboxApi {
3916
4111
  if (this.connectionConfig.debug) {
3917
4112
  return;
3918
4113
  }
3919
- await SandboxApi.kill(this.sandboxId, __spreadValues(__spreadValues({}, this.connectionConfig), opts));
4114
+ await SandboxApi.kill(this.sandboxId, this.connectionOptions(opts));
3920
4115
  }
3921
4116
  /**
3922
4117
  * Pause a sandbox by its ID.
@@ -3932,7 +4127,7 @@ var Sandbox = class extends SandboxApi {
3932
4127
  * ```
3933
4128
  */
3934
4129
  async pause(opts) {
3935
- return await SandboxApi.pause(this.sandboxId, __spreadValues(__spreadValues({}, this.connectionConfig), opts));
4130
+ return await SandboxApi.pause(this.sandboxId, this.connectionOptions(opts));
3936
4131
  }
3937
4132
  /**
3938
4133
  * Create a snapshot of the sandbox's current state.
@@ -3960,7 +4155,10 @@ var Sandbox = class extends SandboxApi {
3960
4155
  * ```
3961
4156
  */
3962
4157
  async createSnapshot(opts) {
3963
- return await SandboxApi.createSnapshot(this.sandboxId, __spreadValues(__spreadValues({}, this.connectionConfig), opts));
4158
+ return await SandboxApi.createSnapshot(
4159
+ this.sandboxId,
4160
+ this.connectionOptions(opts)
4161
+ );
3964
4162
  }
3965
4163
  /**
3966
4164
  * List all snapshots created from this sandbox.
@@ -3970,7 +4168,7 @@ var Sandbox = class extends SandboxApi {
3970
4168
  * @returns paginator for listing snapshots from this sandbox.
3971
4169
  */
3972
4170
  listSnapshots(opts) {
3973
- return SandboxApi.listSnapshots(__spreadProps(__spreadValues(__spreadValues({}, this.connectionConfig), opts), {
4171
+ return SandboxApi.listSnapshots(__spreadProps(__spreadValues({}, this.connectionOptions(opts)), {
3974
4172
  sandboxId: this.sandboxId
3975
4173
  }));
3976
4174
  }
@@ -4022,7 +4220,7 @@ var Sandbox = class extends SandboxApi {
4022
4220
  const filePath = path2 != null ? path2 : "";
4023
4221
  const fileUrl = this.fileUrl(filePath, username);
4024
4222
  if (useSignature) {
4025
- const url = new URL(fileUrl);
4223
+ const url2 = new URL(fileUrl);
4026
4224
  const sig = await getSignature({
4027
4225
  path: filePath,
4028
4226
  operation: "write",
@@ -4030,11 +4228,11 @@ var Sandbox = class extends SandboxApi {
4030
4228
  expirationInSeconds: opts.useSignatureExpiration,
4031
4229
  envdAccessToken: this.envdAccessToken
4032
4230
  });
4033
- url.searchParams.set("signature", sig.signature);
4231
+ url2.searchParams.set("signature", sig.signature);
4034
4232
  if (sig.expiration) {
4035
- url.searchParams.set("signature_expiration", sig.expiration.toString());
4233
+ url2.searchParams.set("signature_expiration", sig.expiration.toString());
4036
4234
  }
4037
- return url.toString();
4235
+ return url2.toString();
4038
4236
  }
4039
4237
  return fileUrl;
4040
4238
  }
@@ -4061,7 +4259,7 @@ var Sandbox = class extends SandboxApi {
4061
4259
  }
4062
4260
  const fileUrl = this.fileUrl(path2, username);
4063
4261
  if (useSignature) {
4064
- const url = new URL(fileUrl);
4262
+ const url2 = new URL(fileUrl);
4065
4263
  const sig = await getSignature({
4066
4264
  path: path2,
4067
4265
  operation: "read",
@@ -4069,11 +4267,11 @@ var Sandbox = class extends SandboxApi {
4069
4267
  expirationInSeconds: opts.useSignatureExpiration,
4070
4268
  envdAccessToken: this.envdAccessToken
4071
4269
  });
4072
- url.searchParams.set("signature", sig.signature);
4270
+ url2.searchParams.set("signature", sig.signature);
4073
4271
  if (sig.expiration) {
4074
- url.searchParams.set("signature_expiration", sig.expiration.toString());
4272
+ url2.searchParams.set("signature_expiration", sig.expiration.toString());
4075
4273
  }
4076
- return url.toString();
4274
+ return url2.toString();
4077
4275
  }
4078
4276
  return fileUrl;
4079
4277
  }
@@ -4085,7 +4283,10 @@ var Sandbox = class extends SandboxApi {
4085
4283
  * @returns information about the sandbox
4086
4284
  */
4087
4285
  async getInfo(opts) {
4088
- return await SandboxApi.getInfo(this.sandboxId, __spreadValues(__spreadValues({}, this.connectionConfig), opts));
4286
+ return await SandboxApi.getInfo(
4287
+ this.sandboxId,
4288
+ this.connectionOptions(opts)
4289
+ );
4089
4290
  }
4090
4291
  /**
4091
4292
  * Get the metrics of the sandbox.
@@ -4109,7 +4310,10 @@ var Sandbox = class extends SandboxApi {
4109
4310
  );
4110
4311
  }
4111
4312
  }
4112
- return await SandboxApi.getMetrics(this.sandboxId, __spreadValues(__spreadValues({}, this.connectionConfig), opts));
4313
+ return await SandboxApi.getMetrics(
4314
+ this.sandboxId,
4315
+ this.connectionOptions(opts)
4316
+ );
4113
4317
  }
4114
4318
  fileUrl(path2, username) {
4115
4319
  const externalUrl = this.connectionConfig.getSandboxExternalUrl(
@@ -4120,19 +4324,105 @@ var Sandbox = class extends SandboxApi {
4120
4324
  port: this.envdPort
4121
4325
  }
4122
4326
  );
4123
- const url = new URL("/files", externalUrl);
4327
+ const url2 = new URL("/files", externalUrl);
4124
4328
  if (username) {
4125
- url.searchParams.set("username", username);
4329
+ url2.searchParams.set("username", username);
4126
4330
  }
4127
4331
  if (path2) {
4128
- url.searchParams.set("path", path2);
4332
+ url2.searchParams.set("path", path2);
4129
4333
  }
4130
- return url.toString();
4334
+ return url2.toString();
4131
4335
  }
4132
4336
  };
4133
4337
  Sandbox.defaultTemplate = "base";
4134
4338
  Sandbox.defaultMcpTemplate = "mcp-gateway";
4135
4339
  Sandbox.defaultSandboxTimeoutMs = DEFAULT_SANDBOX_TIMEOUT_MS;
4340
+ var SandboxRegionScope = class {
4341
+ constructor(sandboxClass, region) {
4342
+ this.sandboxClass = sandboxClass;
4343
+ this.selectedRegion = ConnectionConfig.resolveRegion(region);
4344
+ }
4345
+ get region() {
4346
+ return this.selectedRegion;
4347
+ }
4348
+ options(opts) {
4349
+ const requestedRegion = opts == null ? void 0 : opts.region;
4350
+ if (requestedRegion && ConnectionConfig.resolveRegion(requestedRegion) !== this.region) {
4351
+ throw new Error(
4352
+ `Regional sandbox scope is pinned to "${this.region}", not "${requestedRegion}"`
4353
+ );
4354
+ }
4355
+ return __spreadProps(__spreadValues({}, opts), {
4356
+ region: this.region
4357
+ });
4358
+ }
4359
+ list(opts) {
4360
+ return this.sandboxClass.list(this.options(opts));
4361
+ }
4362
+ async create(templateOrOpts, opts) {
4363
+ if (typeof templateOrOpts === "string") {
4364
+ return this.sandboxClass.create(
4365
+ templateOrOpts,
4366
+ this.options(opts)
4367
+ );
4368
+ }
4369
+ return this.sandboxClass.create(this.options(templateOrOpts));
4370
+ }
4371
+ recoverCreate(recovery, opts) {
4372
+ if (recovery.region && ConnectionConfig.resolveRegion(recovery.region) !== this.region) {
4373
+ throw new Error(
4374
+ `Sandbox create recovery belongs to region "${recovery.region}", not "${this.region}"`
4375
+ );
4376
+ }
4377
+ return this.sandboxClass.recoverCreate(
4378
+ __spreadProps(__spreadValues({}, recovery), {
4379
+ region: this.region
4380
+ }),
4381
+ this.options(opts)
4382
+ );
4383
+ }
4384
+ async betaCreate(templateOrOpts, opts) {
4385
+ if (typeof templateOrOpts === "string") {
4386
+ return this.sandboxClass.betaCreate(
4387
+ templateOrOpts,
4388
+ this.options(opts)
4389
+ );
4390
+ }
4391
+ return this.sandboxClass.betaCreate(
4392
+ this.options(templateOrOpts)
4393
+ );
4394
+ }
4395
+ connect(target, opts) {
4396
+ return this.sandboxClass.connect(target, this.options(opts));
4397
+ }
4398
+ kill(target, opts) {
4399
+ return this.sandboxClass.kill(target, this.options(opts));
4400
+ }
4401
+ getInfo(target, opts) {
4402
+ return this.sandboxClass.getInfo(target, this.options(opts));
4403
+ }
4404
+ getFullInfo(target, opts) {
4405
+ return this.sandboxClass.getFullInfo(target, this.options(opts));
4406
+ }
4407
+ getMetrics(target, opts) {
4408
+ return this.sandboxClass.getMetrics(target, this.options(opts));
4409
+ }
4410
+ setTimeout(target, timeoutMs, opts) {
4411
+ return this.sandboxClass.setTimeout(target, timeoutMs, this.options(opts));
4412
+ }
4413
+ pause(target, opts) {
4414
+ return this.sandboxClass.pause(target, this.options(opts));
4415
+ }
4416
+ createSnapshot(target, opts) {
4417
+ return this.sandboxClass.createSnapshot(target, this.options(opts));
4418
+ }
4419
+ listSnapshots(opts) {
4420
+ return this.sandboxClass.listSnapshots(this.options(opts));
4421
+ }
4422
+ deleteSnapshot(snapshotId, opts) {
4423
+ return this.sandboxClass.deleteSnapshot(snapshotId, this.options(opts));
4424
+ }
4425
+ };
4136
4426
 
4137
4427
  // src/template/logger.ts
4138
4428
  import chalk from "chalk";
@@ -4256,7 +4546,7 @@ function defaultBuildLogger(options) {
4256
4546
  import crypto2 from "crypto";
4257
4547
  import fs from "fs";
4258
4548
  import path from "path";
4259
- import { fileURLToPath } from "url";
4549
+ import url from "url";
4260
4550
 
4261
4551
  // src/template/consts.ts
4262
4552
  var FINALIZE_STEP_NAME = "finalize";
@@ -4404,7 +4694,7 @@ function getCallerDirectory(depth) {
4404
4694
  return void 0;
4405
4695
  }
4406
4696
  if (fileName.startsWith("file:")) {
4407
- fileName = fileURLToPath(fileName);
4697
+ fileName = url.fileURLToPath(fileName);
4408
4698
  }
4409
4699
  return path.dirname(fileName);
4410
4700
  }
@@ -4495,7 +4785,7 @@ async function getFileUploadLink(client, { templateID, filesHash }, stackTrace)
4495
4785
  return fileUploadLinkRes.data;
4496
4786
  }
4497
4787
  async function uploadFile(options, stackTrace) {
4498
- const { fileName, url, fileContextPath, ignorePatterns, resolveSymlinks } = options;
4788
+ const { fileName, url: url2, fileContextPath, ignorePatterns, resolveSymlinks } = options;
4499
4789
  try {
4500
4790
  const uploadStream = await tarFileStreamUpload(
4501
4791
  fileName,
@@ -4503,7 +4793,7 @@ async function uploadFile(options, stackTrace) {
4503
4793
  ignorePatterns,
4504
4794
  resolveSymlinks
4505
4795
  );
4506
- const res = await fetch(url, {
4796
+ const res = await fetch(url2, {
4507
4797
  method: "PUT",
4508
4798
  // @ts-expect-error
4509
4799
  body: uploadStream,
@@ -4723,8 +5013,8 @@ function waitForPort(port) {
4723
5013
  const cmd = `ss -tuln | grep :${port}`;
4724
5014
  return new ReadyCmd(cmd);
4725
5015
  }
4726
- function waitForURL(url, statusCode = 200) {
4727
- const cmd = `curl -s -o /dev/null -w "%{http_code}" ${url} | grep -q "${statusCode}"`;
5016
+ function waitForURL(url2, statusCode = 200) {
5017
+ const cmd = `curl -s -o /dev/null -w "%{http_code}" ${url2} | grep -q "${statusCode}"`;
4728
5018
  return new ReadyCmd(cmd);
4729
5019
  }
4730
5020
  function waitForProcess(processName) {
@@ -4929,6 +5219,7 @@ function handleCmdEntrypointInstruction(instruction, templateBuilder) {
4929
5219
  // src/template/activationApi.ts
4930
5220
  async function activateTemplate(build, options = {}) {
4931
5221
  assertBuildIdentity(build);
5222
+ assertExpectedVersion(options.expectedVersion);
4932
5223
  const region = resolveRegion(options.region);
4933
5224
  const config = ConnectionConfig.forGlobal(options);
4934
5225
  const client = new ApiClient(config);
@@ -4945,7 +5236,8 @@ async function activateTemplate(build, options = {}) {
4945
5236
  buildID: build.buildId,
4946
5237
  active: true,
4947
5238
  requiredReadyNodes: options.requiredReadyNodes,
4948
- allowRemoteFallback: options.allowRemoteFallback
5239
+ allowRemoteFallback: options.allowRemoteFallback,
5240
+ expectedVersion: options.expectedVersion
4949
5241
  },
4950
5242
  signal: config.getSignal(options.requestTimeoutMs)
4951
5243
  }
@@ -4954,6 +5246,7 @@ async function activateTemplate(build, options = {}) {
4954
5246
  }
4955
5247
  async function deactivateTemplate(build, options = {}) {
4956
5248
  assertBuildIdentity(build);
5249
+ assertExpectedVersion(options.expectedVersion);
4957
5250
  const region = resolveRegion(options.region);
4958
5251
  const config = ConnectionConfig.forGlobal(options);
4959
5252
  const client = new ApiClient(config);
@@ -4968,7 +5261,8 @@ async function deactivateTemplate(build, options = {}) {
4968
5261
  },
4969
5262
  body: {
4970
5263
  buildID: build.buildId,
4971
- active: false
5264
+ active: false,
5265
+ expectedVersion: options.expectedVersion
4972
5266
  },
4973
5267
  signal: config.getSignal(options.requestTimeoutMs)
4974
5268
  }
@@ -4979,6 +5273,9 @@ async function getTemplateActivation(templateId, options = {}) {
4979
5273
  if (!templateId.trim()) {
4980
5274
  throw new InvalidArgumentError("templateId is required");
4981
5275
  }
5276
+ if (options.buildId !== void 0 && !options.buildId.trim()) {
5277
+ throw new InvalidArgumentError("buildId must not be empty");
5278
+ }
4982
5279
  const region = resolveRegion(options.region);
4983
5280
  const config = ConnectionConfig.forSandbox(__spreadProps(__spreadValues({}, options), { region }));
4984
5281
  const client = new ApiClient(config);
@@ -4989,6 +5286,9 @@ async function getTemplateActivation(templateId, options = {}) {
4989
5286
  path: {
4990
5287
  templateID: templateId,
4991
5288
  region
5289
+ },
5290
+ query: {
5291
+ buildID: options.buildId
4992
5292
  }
4993
5293
  },
4994
5294
  signal: config.getSignal(options.requestTimeoutMs)
@@ -5004,6 +5304,100 @@ function assertBuildIdentity(build) {
5004
5304
  function resolveRegion(region) {
5005
5305
  return ConnectionConfig.resolveRegion(region);
5006
5306
  }
5307
+ function assertExpectedVersion(expectedVersion) {
5308
+ if (expectedVersion !== void 0 && (!Number.isSafeInteger(expectedVersion) || expectedVersion < 0)) {
5309
+ throw new InvalidArgumentError(
5310
+ "expectedVersion must be a non-negative safe integer"
5311
+ );
5312
+ }
5313
+ }
5314
+
5315
+ // src/template/tagHeadApi.ts
5316
+ async function getTemplateTagHead(client, templateId, tag, signal) {
5317
+ assertTemplateAndTag(templateId, tag);
5318
+ const response = await client.api.GET("/templates/{templateID}/tags/{tag}", {
5319
+ params: {
5320
+ path: {
5321
+ templateID: templateId,
5322
+ tag
5323
+ }
5324
+ },
5325
+ signal
5326
+ });
5327
+ const error = handleApiError(response, TemplateError);
5328
+ if (error) {
5329
+ throw error;
5330
+ }
5331
+ if (!response.data) {
5332
+ throw new TemplateError("Enclave API returned no template tag head");
5333
+ }
5334
+ return mapTagHead(response.data);
5335
+ }
5336
+ async function promoteTemplateTag(client, candidate, tag, expected, signal) {
5337
+ var _a3, _b, _c, _d, _e, _f;
5338
+ assertBuildIdentity2(candidate);
5339
+ assertTemplateAndTag(candidate.templateId, tag);
5340
+ assertExpectation(expected);
5341
+ const response = await client.api.PUT("/templates/{templateID}/tags/{tag}", {
5342
+ params: {
5343
+ path: {
5344
+ templateID: candidate.templateId,
5345
+ tag
5346
+ }
5347
+ },
5348
+ body: {
5349
+ buildID: candidate.buildId,
5350
+ expectedBuildID: expected == null ? void 0 : expected.buildId,
5351
+ expectedVersion: (_a3 = expected == null ? void 0 : expected.version) != null ? _a3 : 0
5352
+ },
5353
+ signal
5354
+ });
5355
+ if (response.response.status === 409 && (((_b = response.error) == null ? void 0 : _b.code) === "tag_changed" || ((_c = response.error) == null ? void 0 : _c.code) === "concurrent_mutation")) {
5356
+ const message = (_e = (_d = response.error) == null ? void 0 : _d.message) != null ? _e : "The template tag changed after it was read";
5357
+ throw new TemplateTagConflictError({
5358
+ templateId: candidate.templateId,
5359
+ tag,
5360
+ candidateBuildId: candidate.buildId,
5361
+ expectedBuildId: expected == null ? void 0 : expected.buildId,
5362
+ expectedVersion: (_f = expected == null ? void 0 : expected.version) != null ? _f : 0,
5363
+ reason: response.error.code,
5364
+ message: `409: ${message}. Fetch its current head with Template.getTagHead() before retrying.`
5365
+ });
5366
+ }
5367
+ const error = handleApiError(response, TemplateError);
5368
+ if (error) {
5369
+ throw error;
5370
+ }
5371
+ if (!response.data) {
5372
+ throw new TemplateError("Enclave API returned no promoted template tag");
5373
+ }
5374
+ return mapTagHead(response.data);
5375
+ }
5376
+ function mapTagHead(head) {
5377
+ return {
5378
+ tag: head.tag,
5379
+ buildId: head.buildID,
5380
+ version: head.version,
5381
+ createdAt: new Date(head.createdAt)
5382
+ };
5383
+ }
5384
+ function assertTemplateAndTag(templateId, tag) {
5385
+ if (!templateId.trim() || !tag.trim()) {
5386
+ throw new InvalidArgumentError("templateId and tag are required");
5387
+ }
5388
+ }
5389
+ function assertBuildIdentity2(candidate) {
5390
+ if (!candidate.templateId.trim() || !candidate.buildId.trim()) {
5391
+ throw new InvalidArgumentError("templateId and buildId are required");
5392
+ }
5393
+ }
5394
+ function assertExpectation(expected) {
5395
+ if (expected !== null && (!expected.buildId.trim() || !Number.isSafeInteger(expected.version) || expected.version < 1)) {
5396
+ throw new InvalidArgumentError(
5397
+ "expected tag head must include a buildId and a positive safe-integer version"
5398
+ );
5399
+ }
5400
+ }
5007
5401
 
5008
5402
  // src/template/index.ts
5009
5403
  var _a2;
@@ -5255,6 +5649,42 @@ var TemplateBase = class {
5255
5649
  const client = new ApiClient(config);
5256
5650
  return getTemplateTags(client, { templateID: templateId });
5257
5651
  }
5652
+ /**
5653
+ * Get the authoritative traffic-selection token for one template tag.
5654
+ *
5655
+ * Pass the returned head directly to {@link promoteTag}; its monotonic
5656
+ * version prevents concurrent releasers from silently overwriting each
5657
+ * other.
5658
+ */
5659
+ static async getTagHead(templateId, tag, options) {
5660
+ const config = ConnectionConfig.forGlobal(options);
5661
+ const client = new ApiClient(config);
5662
+ return getTemplateTagHead(
5663
+ client,
5664
+ templateId,
5665
+ tag,
5666
+ config.getSignal(options == null ? void 0 : options.requestTimeoutMs)
5667
+ );
5668
+ }
5669
+ /**
5670
+ * Atomically move one template tag to an exact ready, durable build.
5671
+ *
5672
+ * Use a head returned by {@link getTagHead} as `expected`. Pass `null` only
5673
+ * when creating a tag that must not already exist. A concurrent change throws
5674
+ * `TemplateTagConflictError`; fetch the head again before deciding whether to
5675
+ * retry.
5676
+ */
5677
+ static async promoteTag(candidate, tag, expected, options) {
5678
+ const config = ConnectionConfig.forGlobal(options);
5679
+ const client = new ApiClient(config);
5680
+ return promoteTemplateTag(
5681
+ client,
5682
+ candidate,
5683
+ tag,
5684
+ expected,
5685
+ config.getSignal(options == null ? void 0 : options.requestTimeoutMs)
5686
+ );
5687
+ }
5258
5688
  fromDebianImage(variant = "stable") {
5259
5689
  return this.fromImage(`debian:${variant}`);
5260
5690
  }
@@ -5552,8 +5982,8 @@ var TemplateBase = class {
5552
5982
  })
5553
5983
  );
5554
5984
  }
5555
- gitClone(url, path2, options) {
5556
- const args = ["git", "clone", url];
5985
+ gitClone(url2, path2, options) {
5986
+ const args = ["git", "clone", url2];
5557
5987
  if (options == null ? void 0 : options.branch) {
5558
5988
  args.push(`--branch ${options.branch}`);
5559
5989
  args.push("--single-branch");
@@ -5805,7 +6235,7 @@ var TemplateBase = class {
5805
6235
  if (index + 1 >= 0 && index + 1 < this.stackTraces.length) {
5806
6236
  stackTrace = this.stackTraces[index + 1];
5807
6237
  }
5808
- const { present, url } = await getFileUploadLink(
6238
+ const { present, url: url2 } = await getFileUploadLink(
5809
6239
  client,
5810
6240
  {
5811
6241
  templateID,
@@ -5813,12 +6243,12 @@ var TemplateBase = class {
5813
6243
  },
5814
6244
  stackTrace
5815
6245
  );
5816
- if (forceUpload && url != null || present === false && url != null) {
6246
+ if (forceUpload && url2 != null || present === false && url2 != null) {
5817
6247
  await uploadFile(
5818
6248
  {
5819
6249
  fileName: src,
5820
6250
  fileContextPath: this.fileContextPath.toString(),
5821
- url,
6251
+ url: url2,
5822
6252
  ignorePatterns: [
5823
6253
  ...this.fileIgnorePatterns,
5824
6254
  ...readDockerignore(this.fileContextPath.toString())
@@ -5939,6 +6369,8 @@ Template.exists = TemplateBase.exists;
5939
6369
  Template.assignTags = TemplateBase.assignTags;
5940
6370
  Template.removeTags = TemplateBase.removeTags;
5941
6371
  Template.getTags = TemplateBase.getTags;
6372
+ Template.getTagHead = TemplateBase.getTagHead;
6373
+ Template.promoteTag = TemplateBase.promoteTag;
5942
6374
  Template.toJSON = TemplateBase.toJSON;
5943
6375
  Template.toDockerfile = TemplateBase.toDockerfile;
5944
6376
 
@@ -5972,9 +6404,11 @@ export {
5972
6404
  Sandbox,
5973
6405
  SandboxCreateRejectedError,
5974
6406
  SandboxError,
6407
+ SandboxRegionScope,
5975
6408
  Template,
5976
6409
  TemplateBase,
5977
6410
  TemplateError,
6411
+ TemplateTagConflictError,
5978
6412
  TimeoutError,
5979
6413
  src_default as default,
5980
6414
  defaultBuildLogger,