@enclave-run/sdk 0.0.0-dev.4 → 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.js CHANGED
@@ -103,9 +103,11 @@ __export(src_exports, {
103
103
  Sandbox: () => Sandbox,
104
104
  SandboxCreateRejectedError: () => SandboxCreateRejectedError,
105
105
  SandboxError: () => SandboxError,
106
+ SandboxRegionScope: () => SandboxRegionScope,
106
107
  Template: () => Template,
107
108
  TemplateBase: () => TemplateBase,
108
109
  TemplateError: () => TemplateError,
110
+ TemplateTagConflictError: () => TemplateTagConflictError,
109
111
  TimeoutError: () => TimeoutError,
110
112
  default: () => src_default,
111
113
  defaultBuildLogger: () => defaultBuildLogger,
@@ -125,7 +127,7 @@ var import_openapi_fetch = __toESM(require("openapi-fetch"));
125
127
  var import_platform2 = __toESM(require("platform"));
126
128
 
127
129
  // package.json
128
- var version = "0.0.0-dev.4";
130
+ var version = "0.0.0-dev.5";
129
131
 
130
132
  // src/utils.ts
131
133
  var import_platform = __toESM(require("platform"));
@@ -309,6 +311,21 @@ var TemplateError = class extends SandboxError {
309
311
  this.name = "TemplateError";
310
312
  }
311
313
  };
314
+ var TemplateTagConflictError = class extends TemplateError {
315
+ constructor(options) {
316
+ var _a3;
317
+ super(
318
+ (_a3 = options.message) != null ? _a3 : `Template tag "${options.tag}" conflicted with another mutation. Fetch its current head with Template.getTagHead() before retrying.`
319
+ );
320
+ this.name = "TemplateTagConflictError";
321
+ this.templateId = options.templateId;
322
+ this.tag = options.tag;
323
+ this.candidateBuildId = options.candidateBuildId;
324
+ this.expectedBuildId = options.expectedBuildId;
325
+ this.expectedVersion = options.expectedVersion;
326
+ this.reason = options.reason;
327
+ }
328
+ };
312
329
  var RateLimitError = class extends SandboxError {
313
330
  constructor(message) {
314
331
  super(message);
@@ -1714,9 +1731,9 @@ var ALL_TRAFFIC = "0.0.0.0/0";
1714
1731
  function shellEscape(value) {
1715
1732
  return `'${value.replace(/'/g, `'"'"'`)}'`;
1716
1733
  }
1717
- function withCredentials(url, username, password) {
1734
+ function withCredentials(url2, username, password) {
1718
1735
  if (!username && !password) {
1719
- return url;
1736
+ return url2;
1720
1737
  }
1721
1738
  if (!username || !password) {
1722
1739
  throw new InvalidArgumentError(
@@ -1725,9 +1742,9 @@ function withCredentials(url, username, password) {
1725
1742
  }
1726
1743
  let parsed;
1727
1744
  try {
1728
- parsed = new URL(url);
1745
+ parsed = new URL(url2);
1729
1746
  } catch (e) {
1730
- throw new InvalidArgumentError(`Invalid Git URL: ${url}`);
1747
+ throw new InvalidArgumentError(`Invalid Git URL: ${url2}`);
1731
1748
  }
1732
1749
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1733
1750
  throw new InvalidArgumentError(
@@ -1738,27 +1755,27 @@ function withCredentials(url, username, password) {
1738
1755
  parsed.password = password;
1739
1756
  return parsed.toString();
1740
1757
  }
1741
- function stripCredentials(url) {
1758
+ function stripCredentials(url2) {
1742
1759
  let parsed;
1743
1760
  try {
1744
- parsed = new URL(url);
1761
+ parsed = new URL(url2);
1745
1762
  } catch (e) {
1746
- return url;
1763
+ return url2;
1747
1764
  }
1748
1765
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1749
- return url;
1766
+ return url2;
1750
1767
  }
1751
1768
  if (!parsed.username && !parsed.password) {
1752
- return url;
1769
+ return url2;
1753
1770
  }
1754
1771
  parsed.username = "";
1755
1772
  parsed.password = "";
1756
1773
  return parsed.toString();
1757
1774
  }
1758
- function deriveRepoDirFromUrl(url) {
1775
+ function deriveRepoDirFromUrl(url2) {
1759
1776
  let parsed;
1760
1777
  try {
1761
- parsed = new URL(url);
1778
+ parsed = new URL(url2);
1762
1779
  } catch (e) {
1763
1780
  return void 0;
1764
1781
  }
@@ -2045,7 +2062,7 @@ var Git = class {
2045
2062
  * @param opts Clone options.
2046
2063
  * @returns Command result from the command runner.
2047
2064
  */
2048
- async clone(url, opts) {
2065
+ async clone(url2, opts) {
2049
2066
  const _a3 = opts != null ? opts : {}, {
2050
2067
  username,
2051
2068
  password,
@@ -2067,10 +2084,10 @@ var Git = class {
2067
2084
  );
2068
2085
  }
2069
2086
  const attemptClone = async (authUsername, authPassword) => {
2070
- const urlWithCreds = authUsername && authPassword ? withCredentials(url, authUsername, authPassword) : url;
2087
+ const urlWithCreds = authUsername && authPassword ? withCredentials(url2, authUsername, authPassword) : url2;
2071
2088
  const sanitizedUrl = stripCredentials(urlWithCreds);
2072
2089
  const stripInlineCreds = !dangerouslyStoreCredentials && sanitizedUrl !== urlWithCreds;
2073
- const repoPath = stripInlineCreds ? path2 != null ? path2 : deriveRepoDirFromUrl(url) : path2;
2090
+ const repoPath = stripInlineCreds ? path2 != null ? path2 : deriveRepoDirFromUrl(url2) : path2;
2074
2091
  if (stripInlineCreds && !repoPath) {
2075
2092
  throw new InvalidArgumentError(
2076
2093
  "A destination path is required when using credentials without storing them."
@@ -2129,8 +2146,8 @@ var Git = class {
2129
2146
  * @param opts Remote add options.
2130
2147
  * @returns Command result from the command runner.
2131
2148
  */
2132
- async remoteAdd(path2, name, url, opts) {
2133
- if (!name || !url) {
2149
+ async remoteAdd(path2, name, url2, opts) {
2150
+ if (!name || !url2) {
2134
2151
  throw new InvalidArgumentError(
2135
2152
  "Both remote name and URL are required to add a git remote."
2136
2153
  );
@@ -2140,12 +2157,12 @@ var Git = class {
2140
2157
  if (fetch2) {
2141
2158
  addArgs.push("-f");
2142
2159
  }
2143
- addArgs.push(name, url);
2160
+ addArgs.push(name, url2);
2144
2161
  if (!overwrite) {
2145
2162
  return this.runGit(addArgs, path2, rest);
2146
2163
  }
2147
2164
  const addCmd = buildGitCommand(addArgs, path2);
2148
- const setUrlCmd = buildGitCommand(["remote", "set-url", name, url], path2);
2165
+ const setUrlCmd = buildGitCommand(["remote", "set-url", name, url2], path2);
2149
2166
  let cmd = `${addCmd} || ${setUrlCmd}`;
2150
2167
  if (fetch2) {
2151
2168
  const fetchCmd = buildGitCommand(["fetch", name], path2);
@@ -2590,13 +2607,13 @@ var Git = class {
2590
2607
  }
2591
2608
  async getRemoteUrl(path2, remote, opts) {
2592
2609
  const result = await this.runGit(["remote", "get-url", remote], path2, opts);
2593
- const url = result.stdout.trim();
2594
- if (!url) {
2610
+ const url2 = result.stdout.trim();
2611
+ if (!url2) {
2595
2612
  throw new InvalidArgumentError(
2596
2613
  `Remote "${remote}" URL not found in repository.`
2597
2614
  );
2598
2615
  }
2599
- return url;
2616
+ return url2;
2600
2617
  }
2601
2618
  async resolveRemoteName(path2, remote, opts) {
2602
2619
  if (remote) {
@@ -3121,6 +3138,26 @@ function getLifecycle(opts) {
3121
3138
  var _a3;
3122
3139
  return (_a3 = opts == null ? void 0 : opts.lifecycle) != null ? _a3 : { onTimeout: "kill", autoResume: false };
3123
3140
  }
3141
+ function resolveSandboxTarget(target, opts) {
3142
+ if (typeof target === "string") {
3143
+ return {
3144
+ sandboxId: target,
3145
+ config: ConnectionConfig.forSandbox(opts)
3146
+ };
3147
+ }
3148
+ const referenceRegion = ConnectionConfig.resolveRegion(target.region);
3149
+ if ((opts == null ? void 0 : opts.region) && ConnectionConfig.resolveRegion(opts.region) !== referenceRegion) {
3150
+ throw new Error(
3151
+ `Sandbox ${target.sandboxId} belongs to region "${referenceRegion}", not "${opts.region}"`
3152
+ );
3153
+ }
3154
+ return {
3155
+ sandboxId: target.sandboxId,
3156
+ config: ConnectionConfig.forSandbox(__spreadProps(__spreadValues({}, opts), {
3157
+ region: referenceRegion
3158
+ }))
3159
+ };
3160
+ }
3124
3161
  var SandboxApi = class {
3125
3162
  constructor() {
3126
3163
  }
@@ -3132,9 +3169,9 @@ var SandboxApi = class {
3132
3169
  *
3133
3170
  * @returns `true` if the sandbox was found and killed, `false` otherwise.
3134
3171
  */
3135
- static async kill(sandboxId, opts) {
3172
+ static async kill(target, opts) {
3136
3173
  var _a3;
3137
- const config = ConnectionConfig.forSandbox(opts);
3174
+ const { sandboxId, config } = resolveSandboxTarget(target, opts);
3138
3175
  const client = new ApiClient(config);
3139
3176
  const res = await client.api.DELETE("/sandboxes/{sandboxID}", {
3140
3177
  params: {
@@ -3161,8 +3198,8 @@ var SandboxApi = class {
3161
3198
  *
3162
3199
  * @returns sandbox information.
3163
3200
  */
3164
- static async getInfo(sandboxId, opts) {
3165
- const fullInfo = await this.getFullInfo(sandboxId, opts);
3201
+ static async getInfo(target, opts) {
3202
+ const fullInfo = await this.getFullInfo(target, opts);
3166
3203
  delete fullInfo.envdAccessToken;
3167
3204
  delete fullInfo.sandboxDomain;
3168
3205
  return fullInfo;
@@ -3175,9 +3212,9 @@ var SandboxApi = class {
3175
3212
  *
3176
3213
  * @returns List of sandbox metrics containing CPU, memory and disk usage information.
3177
3214
  */
3178
- static async getMetrics(sandboxId, opts) {
3215
+ static async getMetrics(target, opts) {
3179
3216
  var _a3, _b;
3180
- const config = ConnectionConfig.forSandbox(opts);
3217
+ const { sandboxId, config } = resolveSandboxTarget(target, opts);
3181
3218
  const client = new ApiClient(config);
3182
3219
  const start = (opts == null ? void 0 : opts.start) ? opts.start.getTime() : void 0;
3183
3220
  const end = (opts == null ? void 0 : opts.end) ? opts.end.getTime() : void 0;
@@ -3220,9 +3257,9 @@ var SandboxApi = class {
3220
3257
  * @param timeoutMs timeout in **milliseconds**.
3221
3258
  * @param opts connection options.
3222
3259
  */
3223
- static async setTimeout(sandboxId, timeoutMs, opts) {
3260
+ static async setTimeout(target, timeoutMs, opts) {
3224
3261
  var _a3;
3225
- const config = ConnectionConfig.forSandbox(opts);
3262
+ const { sandboxId, config } = resolveSandboxTarget(target, opts);
3226
3263
  const client = new ApiClient(config);
3227
3264
  const res = await client.api.POST("/sandboxes/{sandboxID}/timeout", {
3228
3265
  params: {
@@ -3243,9 +3280,9 @@ var SandboxApi = class {
3243
3280
  throw err;
3244
3281
  }
3245
3282
  }
3246
- static async getFullInfo(sandboxId, opts) {
3283
+ static async getFullInfo(target, opts) {
3247
3284
  var _a3, _b;
3248
- const config = ConnectionConfig.forSandbox(opts);
3285
+ const { sandboxId, config } = resolveSandboxTarget(target, opts);
3249
3286
  const client = new ApiClient(config);
3250
3287
  const res = await client.api.GET("/sandboxes/{sandboxID}", {
3251
3288
  params: {
@@ -3267,6 +3304,7 @@ var SandboxApi = class {
3267
3304
  }
3268
3305
  return __spreadProps(__spreadValues({
3269
3306
  sandboxId: res.data.sandboxID,
3307
+ buildId: res.data.buildID,
3270
3308
  templateId: res.data.templateID
3271
3309
  }, res.data.alias && { name: res.data.alias }), {
3272
3310
  metadata: (_b = res.data.metadata) != null ? _b : {},
@@ -3289,9 +3327,9 @@ var SandboxApi = class {
3289
3327
  *
3290
3328
  * @returns `true` if the sandbox got paused, `false` if the sandbox was already paused.
3291
3329
  */
3292
- static async pause(sandboxId, opts) {
3330
+ static async pause(target, opts) {
3293
3331
  var _a3, _b;
3294
- const config = ConnectionConfig.forSandbox(opts);
3332
+ const { sandboxId, config } = resolveSandboxTarget(target, opts);
3295
3333
  const client = new ApiClient(config);
3296
3334
  const res = await client.api.POST("/sandboxes/{sandboxID}/pause", {
3297
3335
  params: {
@@ -3325,9 +3363,9 @@ var SandboxApi = class {
3325
3363
  *
3326
3364
  * @returns snapshot information including the snapshot name that can be used with Sandbox.create().
3327
3365
  */
3328
- static async createSnapshot(sandboxId, opts) {
3366
+ static async createSnapshot(target, opts) {
3329
3367
  var _a3;
3330
- const config = ConnectionConfig.forSandbox(opts);
3368
+ const { sandboxId, config } = resolveSandboxTarget(target, opts);
3331
3369
  const client = new ApiClient(config);
3332
3370
  const res = await client.api.POST("/sandboxes/{sandboxID}/snapshots", {
3333
3371
  params: {
@@ -3446,14 +3484,51 @@ var SandboxApi = class {
3446
3484
  }
3447
3485
  sandbox = res.data;
3448
3486
  }
3487
+ return this.sandboxInfoFromCreate(sandbox, config, opts);
3488
+ }
3489
+ static async recoverSandboxCreate(recovery, opts) {
3490
+ if (!recovery.idempotencyKey.trim()) {
3491
+ throw new InvalidArgumentError(
3492
+ "Sandbox create recovery requires an idempotency key"
3493
+ );
3494
+ }
3495
+ const recoveryRegion = recovery.region ? ConnectionConfig.resolveRegion(recovery.region) : void 0;
3496
+ if (recoveryRegion && (opts == null ? void 0 : opts.region) && ConnectionConfig.resolveRegion(opts.region) !== recoveryRegion) {
3497
+ throw new InvalidArgumentError(
3498
+ `Sandbox create recovery belongs to region "${recoveryRegion}", not "${opts.region}"`
3499
+ );
3500
+ }
3501
+ const config = ConnectionConfig.forSandbox(__spreadProps(__spreadValues({}, opts), {
3502
+ region: recoveryRegion != null ? recoveryRegion : opts == null ? void 0 : opts.region
3503
+ }));
3504
+ const signal = config.getSignal(opts == null ? void 0 : opts.requestTimeoutMs);
3505
+ const sandbox = await this.waitForSandboxCreate(
3506
+ new ApiClient(config),
3507
+ __spreadProps(__spreadValues({}, recovery), {
3508
+ region: config.region
3509
+ }),
3510
+ signal
3511
+ );
3512
+ return this.sandboxInfoFromCreate(sandbox, config, opts);
3513
+ }
3514
+ static async sandboxInfoFromCreate(sandbox, config, opts) {
3449
3515
  if ((0, import_compare_versions4.compareVersions)(sandbox.envdVersion, "0.1.0") < 0) {
3450
- await this.kill(sandbox.sandboxID, opts);
3516
+ await this.kill(
3517
+ {
3518
+ sandboxId: sandbox.sandboxID,
3519
+ region: sandbox.region
3520
+ },
3521
+ __spreadProps(__spreadValues({}, opts), {
3522
+ region: sandbox.region
3523
+ })
3524
+ );
3451
3525
  throw new TemplateError(
3452
3526
  "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."
3453
3527
  );
3454
3528
  }
3455
3529
  return {
3456
3530
  sandboxId: sandbox.sandboxID,
3531
+ buildId: sandbox.buildID,
3457
3532
  sandboxDomain: sandbox.domain || void 0,
3458
3533
  sandboxRegion: sandbox.region,
3459
3534
  capacityTier: sandbox.capacityTier,
@@ -3467,7 +3542,13 @@ var SandboxApi = class {
3467
3542
  };
3468
3543
  }
3469
3544
  static async waitForSandboxCreate(client, recovery, signal) {
3470
- var _a3, _b;
3545
+ var _a3, _b, _c;
3546
+ let lastRecoveryError;
3547
+ let recoveryRetryDelayMs = 25;
3548
+ const waitToRetryRecovery = async () => {
3549
+ await new Promise((resolve) => setTimeout(resolve, recoveryRetryDelayMs));
3550
+ recoveryRetryDelayMs = Math.min(recoveryRetryDelayMs * 2, 1e3);
3551
+ };
3471
3552
  while (!(signal == null ? void 0 : signal.aborted)) {
3472
3553
  let res;
3473
3554
  try {
@@ -3483,12 +3564,17 @@ var SandboxApi = class {
3483
3564
  }
3484
3565
  );
3485
3566
  } catch (cause) {
3486
- throw new OutcomeUnknownError(
3487
- "Sandbox admission committed, but its terminal result could not be recovered.",
3488
- recovery,
3489
- cause
3490
- );
3567
+ lastRecoveryError = cause;
3568
+ await waitToRetryRecovery();
3569
+ continue;
3570
+ }
3571
+ const status = res.response.status;
3572
+ if (status === 404 || status === 408 || status === 425 || status >= 500) {
3573
+ lastRecoveryError = handleApiError(res);
3574
+ await waitToRetryRecovery();
3575
+ continue;
3491
3576
  }
3577
+ recoveryRetryDelayMs = 25;
3492
3578
  const err = handleApiError(res);
3493
3579
  if (err) {
3494
3580
  throw err;
@@ -3507,13 +3593,13 @@ var SandboxApi = class {
3507
3593
  throw new OutcomeUnknownError(
3508
3594
  "Sandbox admission committed, but waiting for its terminal result timed out.",
3509
3595
  recovery,
3510
- signal == null ? void 0 : signal.reason
3596
+ (_c = signal == null ? void 0 : signal.reason) != null ? _c : lastRecoveryError
3511
3597
  );
3512
3598
  }
3513
- static async connectSandbox(sandboxId, opts) {
3599
+ static async connectSandbox(target, opts) {
3514
3600
  var _a3, _b;
3515
3601
  const timeoutMs = (_a3 = opts == null ? void 0 : opts.timeoutMs) != null ? _a3 : DEFAULT_SANDBOX_TIMEOUT_MS;
3516
- const config = ConnectionConfig.forSandbox(opts);
3602
+ const { sandboxId, config } = resolveSandboxTarget(target, opts);
3517
3603
  const client = new ApiClient(config);
3518
3604
  const res = await client.api.POST("/sandboxes/{sandboxID}/connect", {
3519
3605
  params: {
@@ -3535,6 +3621,7 @@ var SandboxApi = class {
3535
3621
  }
3536
3622
  return {
3537
3623
  sandboxId: res.data.sandboxID,
3624
+ buildId: res.data.buildID,
3538
3625
  sandboxDomain: res.data.domain || void 0,
3539
3626
  sandboxRegion: res.data.region,
3540
3627
  capacityTier: res.data.capacityTier,
@@ -3615,6 +3702,7 @@ var SandboxPaginator = class extends BasePaginator {
3615
3702
  var _a4;
3616
3703
  return __spreadProps(__spreadValues({
3617
3704
  sandboxId: sandbox.sandboxID,
3705
+ buildId: sandbox.buildID,
3618
3706
  templateId: sandbox.templateID
3619
3707
  }, sandbox.alias && { name: sandbox.alias }), {
3620
3708
  metadata: (_a4 = sandbox.metadata) != null ? _a4 : {},
@@ -3681,8 +3769,14 @@ var Sandbox = class extends SandboxApi {
3681
3769
  this.mcpPort = 50005;
3682
3770
  this.connectionConfig = ConnectionConfig.forSandbox(opts);
3683
3771
  this.sandboxId = opts.sandboxId;
3772
+ this.buildId = opts.buildId;
3684
3773
  this.sandboxDomain = (_a3 = opts.sandboxDomain) != null ? _a3 : this.connectionConfig.domain;
3685
- this.sandboxRegion = opts.sandboxRegion;
3774
+ this.sandboxRegion = this.connectionConfig.debug ? opts.sandboxRegion : ConnectionConfig.resolveRegion(opts.sandboxRegion);
3775
+ if (!this.connectionConfig.debug && this.connectionConfig.region !== this.sandboxRegion) {
3776
+ throw new Error(
3777
+ `Sandbox ${opts.sandboxId} belongs to region "${this.sandboxRegion}", not "${this.connectionConfig.region}"`
3778
+ );
3779
+ }
3686
3780
  this.capacityTier = opts.capacityTier;
3687
3781
  this.envdAccessToken = opts.envdAccessToken;
3688
3782
  this.trafficAccessToken = opts.trafficAccessToken;
@@ -3699,7 +3793,7 @@ var Sandbox = class extends SandboxApi {
3699
3793
  baseUrl: this.envdApiUrl,
3700
3794
  useBinaryFormat: false,
3701
3795
  interceptors: (opts == null ? void 0 : opts.logger) ? [createRpcLogger(opts.logger)] : void 0,
3702
- fetch: (url, options) => {
3796
+ fetch: (url2, options) => {
3703
3797
  const headers = new Headers(this.connectionConfig.headers);
3704
3798
  new Headers(options == null ? void 0 : options.headers).forEach(
3705
3799
  (value, key) => headers.append(key, value)
@@ -3714,7 +3808,7 @@ var Sandbox = class extends SandboxApi {
3714
3808
  headers,
3715
3809
  redirect: "follow"
3716
3810
  });
3717
- return fetch(url, options);
3811
+ return fetch(url2, options);
3718
3812
  }
3719
3813
  });
3720
3814
  this.envdApi = new EnvdApiClient(
@@ -3743,6 +3837,33 @@ var Sandbox = class extends SandboxApi {
3743
3837
  });
3744
3838
  this.git = new Git(this.commands);
3745
3839
  }
3840
+ /**
3841
+ * Serializable regional reference for reconnecting to this sandbox.
3842
+ */
3843
+ get ref() {
3844
+ if (this.connectionConfig.debug) {
3845
+ throw new Error("Sandbox references are unavailable in debug mode");
3846
+ }
3847
+ return {
3848
+ sandboxId: this.sandboxId,
3849
+ region: this.sandboxRegion
3850
+ };
3851
+ }
3852
+ connectionOptions(opts) {
3853
+ const ownerRegion = this.connectionConfig.debug ? this.connectionConfig.region : this.sandboxRegion;
3854
+ if (!this.connectionConfig.debug && (opts == null ? void 0 : opts.region) && ConnectionConfig.resolveRegion(opts.region) !== ownerRegion) {
3855
+ throw new Error(
3856
+ `Sandbox ${this.sandboxId} belongs to region "${ownerRegion}", not "${opts.region}"`
3857
+ );
3858
+ }
3859
+ return __spreadProps(__spreadValues(__spreadValues({}, this.connectionConfig), opts), {
3860
+ apiUrl: this.connectionConfig.apiUrl,
3861
+ sandboxUrl: this.connectionConfig.sandboxUrl,
3862
+ domain: this.connectionConfig.domain,
3863
+ debug: this.connectionConfig.debug,
3864
+ region: ownerRegion
3865
+ });
3866
+ }
3746
3867
  /**
3747
3868
  * List all sandboxes.
3748
3869
  *
@@ -3753,6 +3874,20 @@ var Sandbox = class extends SandboxApi {
3753
3874
  static list(opts) {
3754
3875
  return new SandboxPaginator(opts);
3755
3876
  }
3877
+ /**
3878
+ * Create an immutable regional scope so repeated sandbox operations do not
3879
+ * need to repeat the same region option.
3880
+ *
3881
+ * @example
3882
+ * ```ts
3883
+ * const yyz = Sandbox.inRegion("yyz")
3884
+ * const sandbox = await yyz.create()
3885
+ * const sameSandbox = await yyz.connect(sandbox.sandboxId)
3886
+ * ```
3887
+ */
3888
+ static inRegion(region) {
3889
+ return new SandboxRegionScope(this, region);
3890
+ }
3756
3891
  static async create(templateOrOpts, opts) {
3757
3892
  var _a3, _b;
3758
3893
  const { template, sandboxOpts } = typeof templateOrOpts === "string" ? {
@@ -3766,6 +3901,7 @@ var Sandbox = class extends SandboxApi {
3766
3901
  if (config.debug) {
3767
3902
  return new this(__spreadValues({
3768
3903
  sandboxId: "debug_sandbox_id",
3904
+ buildId: "debug_build_id",
3769
3905
  sandboxRegion: "debug",
3770
3906
  capacityTier: "shared",
3771
3907
  envdVersion: ENVD_DEBUG_FALLBACK
@@ -3776,7 +3912,9 @@ var Sandbox = class extends SandboxApi {
3776
3912
  (_a3 = sandboxOpts == null ? void 0 : sandboxOpts.timeoutMs) != null ? _a3 : this.defaultSandboxTimeoutMs,
3777
3913
  sandboxOpts
3778
3914
  );
3779
- const sandbox = new this(__spreadValues(__spreadValues({}, config), sandboxInfo));
3915
+ const sandbox = new this(__spreadProps(__spreadValues(__spreadValues({}, config), sandboxInfo), {
3916
+ region: sandboxInfo.sandboxRegion
3917
+ }));
3780
3918
  if (sandboxOpts == null ? void 0 : sandboxOpts.mcp) {
3781
3919
  sandbox.mcpToken = crypto.randomUUID();
3782
3920
  const res = await sandbox.commands.run(
@@ -3794,6 +3932,51 @@ var Sandbox = class extends SandboxApi {
3794
3932
  }
3795
3933
  return sandbox;
3796
3934
  }
3935
+ /**
3936
+ * Recover a create whose authoritative response was lost without resending
3937
+ * the original create payload.
3938
+ *
3939
+ * Pass `OutcomeUnknownError.recovery`. Recovery is pinned to the operation's
3940
+ * owning region and cannot create a second sandbox with different options.
3941
+ *
3942
+ * @example
3943
+ * ```ts
3944
+ * try {
3945
+ * return await Sandbox.create("base", { idempotencyKey: "job-123" })
3946
+ * } catch (error) {
3947
+ * if (error instanceof OutcomeUnknownError) {
3948
+ * return Sandbox.recoverCreate(error.recovery)
3949
+ * }
3950
+ * throw error
3951
+ * }
3952
+ * ```
3953
+ */
3954
+ static async recoverCreate(recovery, opts) {
3955
+ var _a3;
3956
+ const config = ConnectionConfig.forSandbox(__spreadProps(__spreadValues({}, opts), {
3957
+ region: (_a3 = recovery.region) != null ? _a3 : opts == null ? void 0 : opts.region
3958
+ }));
3959
+ const sandboxInfo = await SandboxApi.recoverSandboxCreate(recovery, opts);
3960
+ const sandbox = new this(__spreadProps(__spreadValues(__spreadValues({}, config), sandboxInfo), {
3961
+ region: sandboxInfo.sandboxRegion
3962
+ }));
3963
+ if (opts == null ? void 0 : opts.mcp) {
3964
+ sandbox.mcpToken = crypto.randomUUID();
3965
+ const res = await sandbox.commands.run(
3966
+ `mcp-gateway --config '${JSON.stringify(opts.mcp)}'`,
3967
+ {
3968
+ user: "root",
3969
+ envs: {
3970
+ GATEWAY_ACCESS_TOKEN: sandbox.mcpToken
3971
+ }
3972
+ }
3973
+ );
3974
+ if (res.exitCode !== 0) {
3975
+ throw new Error(`Failed to start MCP gateway: ${res.stderr}`);
3976
+ }
3977
+ }
3978
+ return sandbox;
3979
+ }
3797
3980
  static async betaCreate(templateOrOpts, opts) {
3798
3981
  var _a3, _b;
3799
3982
  const { template, sandboxOpts } = typeof templateOrOpts === "string" ? {
@@ -3807,6 +3990,7 @@ var Sandbox = class extends SandboxApi {
3807
3990
  if (config.debug) {
3808
3991
  return new this(__spreadValues({
3809
3992
  sandboxId: "debug_sandbox_id",
3993
+ buildId: "debug_build_id",
3810
3994
  sandboxRegion: "debug",
3811
3995
  capacityTier: "shared",
3812
3996
  envdVersion: ENVD_DEBUG_FALLBACK
@@ -3817,7 +4001,9 @@ var Sandbox = class extends SandboxApi {
3817
4001
  (_a3 = sandboxOpts == null ? void 0 : sandboxOpts.timeoutMs) != null ? _a3 : this.defaultSandboxTimeoutMs,
3818
4002
  sandboxOpts
3819
4003
  );
3820
- const sandbox = new this(__spreadValues(__spreadValues({}, config), sandboxInfo));
4004
+ const sandbox = new this(__spreadProps(__spreadValues(__spreadValues({}, config), sandboxInfo), {
4005
+ region: sandboxInfo.sandboxRegion
4006
+ }));
3821
4007
  if (sandboxOpts == null ? void 0 : sandboxOpts.mcp) {
3822
4008
  sandbox.mcpToken = crypto.randomUUID();
3823
4009
  const res = await sandbox.commands.run(
@@ -3839,9 +4025,11 @@ var Sandbox = class extends SandboxApi {
3839
4025
  * Connect to a sandbox. If the sandbox is paused, it will be automatically resumed.
3840
4026
  * Sandbox must be either running or be paused.
3841
4027
  *
3842
- * With sandbox ID you can connect to the same sandbox from different places or environments (serverless functions, etc).
4028
+ * Persist {@link Sandbox.ref} to reconnect through the sandbox's owning
4029
+ * regional API from another process or environment.
3843
4030
  *
3844
- * @param sandboxId sandbox ID.
4031
+ * @param target sandbox ID, or a serializable reference containing its ID
4032
+ * and owning region.
3845
4033
  * @param opts connection options.
3846
4034
  *
3847
4035
  * @returns A running sandbox instance
@@ -3855,13 +4043,15 @@ var Sandbox = class extends SandboxApi {
3855
4043
  * const sameSandbox = await Sandbox.connect(sandboxId)
3856
4044
  * ```
3857
4045
  */
3858
- static async connect(sandboxId, opts) {
3859
- const sandbox = await SandboxApi.connectSandbox(sandboxId, opts);
3860
- const config = ConnectionConfig.forSandbox(opts);
4046
+ static async connect(target, opts) {
4047
+ const sandbox = await SandboxApi.connectSandbox(target, opts);
4048
+ const config = ConnectionConfig.forSandbox(__spreadValues(__spreadValues({}, opts), typeof target === "string" ? {} : { region: target.region }));
3861
4049
  return new this(__spreadProps(__spreadValues({}, config), {
3862
- sandboxId,
4050
+ sandboxId: sandbox.sandboxId,
4051
+ buildId: sandbox.buildId,
3863
4052
  sandboxDomain: sandbox.sandboxDomain,
3864
4053
  sandboxRegion: sandbox.sandboxRegion,
4054
+ region: sandbox.sandboxRegion,
3865
4055
  capacityTier: sandbox.capacityTier,
3866
4056
  apiUrl: sandbox.apiUrl,
3867
4057
  envdAccessToken: sandbox.envdAccessToken,
@@ -3889,7 +4079,10 @@ var Sandbox = class extends SandboxApi {
3889
4079
  * ```
3890
4080
  */
3891
4081
  async connect(opts) {
3892
- await SandboxApi.connectSandbox(this.sandboxId, __spreadValues(__spreadValues({}, this.connectionConfig), opts));
4082
+ await SandboxApi.connectSandbox(
4083
+ this.sandboxId,
4084
+ this.connectionOptions(opts)
4085
+ );
3893
4086
  return this;
3894
4087
  }
3895
4088
  /**
@@ -3958,7 +4151,11 @@ var Sandbox = class extends SandboxApi {
3958
4151
  if (this.connectionConfig.debug) {
3959
4152
  return;
3960
4153
  }
3961
- await SandboxApi.setTimeout(this.sandboxId, timeoutMs, __spreadValues(__spreadValues({}, this.connectionConfig), opts));
4154
+ await SandboxApi.setTimeout(
4155
+ this.sandboxId,
4156
+ timeoutMs,
4157
+ this.connectionOptions(opts)
4158
+ );
3962
4159
  }
3963
4160
  /**
3964
4161
  * Kill the sandbox.
@@ -3969,7 +4166,7 @@ var Sandbox = class extends SandboxApi {
3969
4166
  if (this.connectionConfig.debug) {
3970
4167
  return;
3971
4168
  }
3972
- await SandboxApi.kill(this.sandboxId, __spreadValues(__spreadValues({}, this.connectionConfig), opts));
4169
+ await SandboxApi.kill(this.sandboxId, this.connectionOptions(opts));
3973
4170
  }
3974
4171
  /**
3975
4172
  * Pause a sandbox by its ID.
@@ -3985,7 +4182,7 @@ var Sandbox = class extends SandboxApi {
3985
4182
  * ```
3986
4183
  */
3987
4184
  async pause(opts) {
3988
- return await SandboxApi.pause(this.sandboxId, __spreadValues(__spreadValues({}, this.connectionConfig), opts));
4185
+ return await SandboxApi.pause(this.sandboxId, this.connectionOptions(opts));
3989
4186
  }
3990
4187
  /**
3991
4188
  * Create a snapshot of the sandbox's current state.
@@ -4013,7 +4210,10 @@ var Sandbox = class extends SandboxApi {
4013
4210
  * ```
4014
4211
  */
4015
4212
  async createSnapshot(opts) {
4016
- return await SandboxApi.createSnapshot(this.sandboxId, __spreadValues(__spreadValues({}, this.connectionConfig), opts));
4213
+ return await SandboxApi.createSnapshot(
4214
+ this.sandboxId,
4215
+ this.connectionOptions(opts)
4216
+ );
4017
4217
  }
4018
4218
  /**
4019
4219
  * List all snapshots created from this sandbox.
@@ -4023,7 +4223,7 @@ var Sandbox = class extends SandboxApi {
4023
4223
  * @returns paginator for listing snapshots from this sandbox.
4024
4224
  */
4025
4225
  listSnapshots(opts) {
4026
- return SandboxApi.listSnapshots(__spreadProps(__spreadValues(__spreadValues({}, this.connectionConfig), opts), {
4226
+ return SandboxApi.listSnapshots(__spreadProps(__spreadValues({}, this.connectionOptions(opts)), {
4027
4227
  sandboxId: this.sandboxId
4028
4228
  }));
4029
4229
  }
@@ -4075,7 +4275,7 @@ var Sandbox = class extends SandboxApi {
4075
4275
  const filePath = path2 != null ? path2 : "";
4076
4276
  const fileUrl = this.fileUrl(filePath, username);
4077
4277
  if (useSignature) {
4078
- const url = new URL(fileUrl);
4278
+ const url2 = new URL(fileUrl);
4079
4279
  const sig = await getSignature({
4080
4280
  path: filePath,
4081
4281
  operation: "write",
@@ -4083,11 +4283,11 @@ var Sandbox = class extends SandboxApi {
4083
4283
  expirationInSeconds: opts.useSignatureExpiration,
4084
4284
  envdAccessToken: this.envdAccessToken
4085
4285
  });
4086
- url.searchParams.set("signature", sig.signature);
4286
+ url2.searchParams.set("signature", sig.signature);
4087
4287
  if (sig.expiration) {
4088
- url.searchParams.set("signature_expiration", sig.expiration.toString());
4288
+ url2.searchParams.set("signature_expiration", sig.expiration.toString());
4089
4289
  }
4090
- return url.toString();
4290
+ return url2.toString();
4091
4291
  }
4092
4292
  return fileUrl;
4093
4293
  }
@@ -4114,7 +4314,7 @@ var Sandbox = class extends SandboxApi {
4114
4314
  }
4115
4315
  const fileUrl = this.fileUrl(path2, username);
4116
4316
  if (useSignature) {
4117
- const url = new URL(fileUrl);
4317
+ const url2 = new URL(fileUrl);
4118
4318
  const sig = await getSignature({
4119
4319
  path: path2,
4120
4320
  operation: "read",
@@ -4122,11 +4322,11 @@ var Sandbox = class extends SandboxApi {
4122
4322
  expirationInSeconds: opts.useSignatureExpiration,
4123
4323
  envdAccessToken: this.envdAccessToken
4124
4324
  });
4125
- url.searchParams.set("signature", sig.signature);
4325
+ url2.searchParams.set("signature", sig.signature);
4126
4326
  if (sig.expiration) {
4127
- url.searchParams.set("signature_expiration", sig.expiration.toString());
4327
+ url2.searchParams.set("signature_expiration", sig.expiration.toString());
4128
4328
  }
4129
- return url.toString();
4329
+ return url2.toString();
4130
4330
  }
4131
4331
  return fileUrl;
4132
4332
  }
@@ -4138,7 +4338,10 @@ var Sandbox = class extends SandboxApi {
4138
4338
  * @returns information about the sandbox
4139
4339
  */
4140
4340
  async getInfo(opts) {
4141
- return await SandboxApi.getInfo(this.sandboxId, __spreadValues(__spreadValues({}, this.connectionConfig), opts));
4341
+ return await SandboxApi.getInfo(
4342
+ this.sandboxId,
4343
+ this.connectionOptions(opts)
4344
+ );
4142
4345
  }
4143
4346
  /**
4144
4347
  * Get the metrics of the sandbox.
@@ -4162,7 +4365,10 @@ var Sandbox = class extends SandboxApi {
4162
4365
  );
4163
4366
  }
4164
4367
  }
4165
- return await SandboxApi.getMetrics(this.sandboxId, __spreadValues(__spreadValues({}, this.connectionConfig), opts));
4368
+ return await SandboxApi.getMetrics(
4369
+ this.sandboxId,
4370
+ this.connectionOptions(opts)
4371
+ );
4166
4372
  }
4167
4373
  fileUrl(path2, username) {
4168
4374
  const externalUrl = this.connectionConfig.getSandboxExternalUrl(
@@ -4173,19 +4379,105 @@ var Sandbox = class extends SandboxApi {
4173
4379
  port: this.envdPort
4174
4380
  }
4175
4381
  );
4176
- const url = new URL("/files", externalUrl);
4382
+ const url2 = new URL("/files", externalUrl);
4177
4383
  if (username) {
4178
- url.searchParams.set("username", username);
4384
+ url2.searchParams.set("username", username);
4179
4385
  }
4180
4386
  if (path2) {
4181
- url.searchParams.set("path", path2);
4387
+ url2.searchParams.set("path", path2);
4182
4388
  }
4183
- return url.toString();
4389
+ return url2.toString();
4184
4390
  }
4185
4391
  };
4186
4392
  Sandbox.defaultTemplate = "base";
4187
4393
  Sandbox.defaultMcpTemplate = "mcp-gateway";
4188
4394
  Sandbox.defaultSandboxTimeoutMs = DEFAULT_SANDBOX_TIMEOUT_MS;
4395
+ var SandboxRegionScope = class {
4396
+ constructor(sandboxClass, region) {
4397
+ this.sandboxClass = sandboxClass;
4398
+ this.selectedRegion = ConnectionConfig.resolveRegion(region);
4399
+ }
4400
+ get region() {
4401
+ return this.selectedRegion;
4402
+ }
4403
+ options(opts) {
4404
+ const requestedRegion = opts == null ? void 0 : opts.region;
4405
+ if (requestedRegion && ConnectionConfig.resolveRegion(requestedRegion) !== this.region) {
4406
+ throw new Error(
4407
+ `Regional sandbox scope is pinned to "${this.region}", not "${requestedRegion}"`
4408
+ );
4409
+ }
4410
+ return __spreadProps(__spreadValues({}, opts), {
4411
+ region: this.region
4412
+ });
4413
+ }
4414
+ list(opts) {
4415
+ return this.sandboxClass.list(this.options(opts));
4416
+ }
4417
+ async create(templateOrOpts, opts) {
4418
+ if (typeof templateOrOpts === "string") {
4419
+ return this.sandboxClass.create(
4420
+ templateOrOpts,
4421
+ this.options(opts)
4422
+ );
4423
+ }
4424
+ return this.sandboxClass.create(this.options(templateOrOpts));
4425
+ }
4426
+ recoverCreate(recovery, opts) {
4427
+ if (recovery.region && ConnectionConfig.resolveRegion(recovery.region) !== this.region) {
4428
+ throw new Error(
4429
+ `Sandbox create recovery belongs to region "${recovery.region}", not "${this.region}"`
4430
+ );
4431
+ }
4432
+ return this.sandboxClass.recoverCreate(
4433
+ __spreadProps(__spreadValues({}, recovery), {
4434
+ region: this.region
4435
+ }),
4436
+ this.options(opts)
4437
+ );
4438
+ }
4439
+ async betaCreate(templateOrOpts, opts) {
4440
+ if (typeof templateOrOpts === "string") {
4441
+ return this.sandboxClass.betaCreate(
4442
+ templateOrOpts,
4443
+ this.options(opts)
4444
+ );
4445
+ }
4446
+ return this.sandboxClass.betaCreate(
4447
+ this.options(templateOrOpts)
4448
+ );
4449
+ }
4450
+ connect(target, opts) {
4451
+ return this.sandboxClass.connect(target, this.options(opts));
4452
+ }
4453
+ kill(target, opts) {
4454
+ return this.sandboxClass.kill(target, this.options(opts));
4455
+ }
4456
+ getInfo(target, opts) {
4457
+ return this.sandboxClass.getInfo(target, this.options(opts));
4458
+ }
4459
+ getFullInfo(target, opts) {
4460
+ return this.sandboxClass.getFullInfo(target, this.options(opts));
4461
+ }
4462
+ getMetrics(target, opts) {
4463
+ return this.sandboxClass.getMetrics(target, this.options(opts));
4464
+ }
4465
+ setTimeout(target, timeoutMs, opts) {
4466
+ return this.sandboxClass.setTimeout(target, timeoutMs, this.options(opts));
4467
+ }
4468
+ pause(target, opts) {
4469
+ return this.sandboxClass.pause(target, this.options(opts));
4470
+ }
4471
+ createSnapshot(target, opts) {
4472
+ return this.sandboxClass.createSnapshot(target, this.options(opts));
4473
+ }
4474
+ listSnapshots(opts) {
4475
+ return this.sandboxClass.listSnapshots(this.options(opts));
4476
+ }
4477
+ deleteSnapshot(snapshotId, opts) {
4478
+ return this.sandboxClass.deleteSnapshot(snapshotId, this.options(opts));
4479
+ }
4480
+ };
4189
4481
 
4190
4482
  // src/template/logger.ts
4191
4483
  var import_chalk = __toESM(require("chalk"));
@@ -4309,7 +4601,7 @@ function defaultBuildLogger(options) {
4309
4601
  var import_node_crypto = __toESM(require("crypto"));
4310
4602
  var import_node_fs = __toESM(require("fs"));
4311
4603
  var import_node_path = __toESM(require("path"));
4312
- var import_node_url = require("url");
4604
+ var import_node_url = __toESM(require("url"));
4313
4605
 
4314
4606
  // src/template/consts.ts
4315
4607
  var FINALIZE_STEP_NAME = "finalize";
@@ -4457,7 +4749,7 @@ function getCallerDirectory(depth) {
4457
4749
  return void 0;
4458
4750
  }
4459
4751
  if (fileName.startsWith("file:")) {
4460
- fileName = (0, import_node_url.fileURLToPath)(fileName);
4752
+ fileName = import_node_url.default.fileURLToPath(fileName);
4461
4753
  }
4462
4754
  return import_node_path.default.dirname(fileName);
4463
4755
  }
@@ -4548,7 +4840,7 @@ async function getFileUploadLink(client, { templateID, filesHash }, stackTrace)
4548
4840
  return fileUploadLinkRes.data;
4549
4841
  }
4550
4842
  async function uploadFile(options, stackTrace) {
4551
- const { fileName, url, fileContextPath, ignorePatterns, resolveSymlinks } = options;
4843
+ const { fileName, url: url2, fileContextPath, ignorePatterns, resolveSymlinks } = options;
4552
4844
  try {
4553
4845
  const uploadStream = await tarFileStreamUpload(
4554
4846
  fileName,
@@ -4556,7 +4848,7 @@ async function uploadFile(options, stackTrace) {
4556
4848
  ignorePatterns,
4557
4849
  resolveSymlinks
4558
4850
  );
4559
- const res = await fetch(url, {
4851
+ const res = await fetch(url2, {
4560
4852
  method: "PUT",
4561
4853
  // @ts-expect-error
4562
4854
  body: uploadStream,
@@ -4774,8 +5066,8 @@ function waitForPort(port) {
4774
5066
  const cmd = `ss -tuln | grep :${port}`;
4775
5067
  return new ReadyCmd(cmd);
4776
5068
  }
4777
- function waitForURL(url, statusCode = 200) {
4778
- const cmd = `curl -s -o /dev/null -w "%{http_code}" ${url} | grep -q "${statusCode}"`;
5069
+ function waitForURL(url2, statusCode = 200) {
5070
+ const cmd = `curl -s -o /dev/null -w "%{http_code}" ${url2} | grep -q "${statusCode}"`;
4779
5071
  return new ReadyCmd(cmd);
4780
5072
  }
4781
5073
  function waitForProcess(processName) {
@@ -4980,6 +5272,7 @@ function handleCmdEntrypointInstruction(instruction, templateBuilder) {
4980
5272
  // src/template/activationApi.ts
4981
5273
  async function activateTemplate(build, options = {}) {
4982
5274
  assertBuildIdentity(build);
5275
+ assertExpectedVersion(options.expectedVersion);
4983
5276
  const region = resolveRegion(options.region);
4984
5277
  const config = ConnectionConfig.forGlobal(options);
4985
5278
  const client = new ApiClient(config);
@@ -4996,7 +5289,8 @@ async function activateTemplate(build, options = {}) {
4996
5289
  buildID: build.buildId,
4997
5290
  active: true,
4998
5291
  requiredReadyNodes: options.requiredReadyNodes,
4999
- allowRemoteFallback: options.allowRemoteFallback
5292
+ allowRemoteFallback: options.allowRemoteFallback,
5293
+ expectedVersion: options.expectedVersion
5000
5294
  },
5001
5295
  signal: config.getSignal(options.requestTimeoutMs)
5002
5296
  }
@@ -5005,6 +5299,7 @@ async function activateTemplate(build, options = {}) {
5005
5299
  }
5006
5300
  async function deactivateTemplate(build, options = {}) {
5007
5301
  assertBuildIdentity(build);
5302
+ assertExpectedVersion(options.expectedVersion);
5008
5303
  const region = resolveRegion(options.region);
5009
5304
  const config = ConnectionConfig.forGlobal(options);
5010
5305
  const client = new ApiClient(config);
@@ -5019,7 +5314,8 @@ async function deactivateTemplate(build, options = {}) {
5019
5314
  },
5020
5315
  body: {
5021
5316
  buildID: build.buildId,
5022
- active: false
5317
+ active: false,
5318
+ expectedVersion: options.expectedVersion
5023
5319
  },
5024
5320
  signal: config.getSignal(options.requestTimeoutMs)
5025
5321
  }
@@ -5030,6 +5326,9 @@ async function getTemplateActivation(templateId, options = {}) {
5030
5326
  if (!templateId.trim()) {
5031
5327
  throw new InvalidArgumentError("templateId is required");
5032
5328
  }
5329
+ if (options.buildId !== void 0 && !options.buildId.trim()) {
5330
+ throw new InvalidArgumentError("buildId must not be empty");
5331
+ }
5033
5332
  const region = resolveRegion(options.region);
5034
5333
  const config = ConnectionConfig.forSandbox(__spreadProps(__spreadValues({}, options), { region }));
5035
5334
  const client = new ApiClient(config);
@@ -5040,6 +5339,9 @@ async function getTemplateActivation(templateId, options = {}) {
5040
5339
  path: {
5041
5340
  templateID: templateId,
5042
5341
  region
5342
+ },
5343
+ query: {
5344
+ buildID: options.buildId
5043
5345
  }
5044
5346
  },
5045
5347
  signal: config.getSignal(options.requestTimeoutMs)
@@ -5055,6 +5357,100 @@ function assertBuildIdentity(build) {
5055
5357
  function resolveRegion(region) {
5056
5358
  return ConnectionConfig.resolveRegion(region);
5057
5359
  }
5360
+ function assertExpectedVersion(expectedVersion) {
5361
+ if (expectedVersion !== void 0 && (!Number.isSafeInteger(expectedVersion) || expectedVersion < 0)) {
5362
+ throw new InvalidArgumentError(
5363
+ "expectedVersion must be a non-negative safe integer"
5364
+ );
5365
+ }
5366
+ }
5367
+
5368
+ // src/template/tagHeadApi.ts
5369
+ async function getTemplateTagHead(client, templateId, tag, signal) {
5370
+ assertTemplateAndTag(templateId, tag);
5371
+ const response = await client.api.GET("/templates/{templateID}/tags/{tag}", {
5372
+ params: {
5373
+ path: {
5374
+ templateID: templateId,
5375
+ tag
5376
+ }
5377
+ },
5378
+ signal
5379
+ });
5380
+ const error = handleApiError(response, TemplateError);
5381
+ if (error) {
5382
+ throw error;
5383
+ }
5384
+ if (!response.data) {
5385
+ throw new TemplateError("Enclave API returned no template tag head");
5386
+ }
5387
+ return mapTagHead(response.data);
5388
+ }
5389
+ async function promoteTemplateTag(client, candidate, tag, expected, signal) {
5390
+ var _a3, _b, _c, _d, _e, _f;
5391
+ assertBuildIdentity2(candidate);
5392
+ assertTemplateAndTag(candidate.templateId, tag);
5393
+ assertExpectation(expected);
5394
+ const response = await client.api.PUT("/templates/{templateID}/tags/{tag}", {
5395
+ params: {
5396
+ path: {
5397
+ templateID: candidate.templateId,
5398
+ tag
5399
+ }
5400
+ },
5401
+ body: {
5402
+ buildID: candidate.buildId,
5403
+ expectedBuildID: expected == null ? void 0 : expected.buildId,
5404
+ expectedVersion: (_a3 = expected == null ? void 0 : expected.version) != null ? _a3 : 0
5405
+ },
5406
+ signal
5407
+ });
5408
+ 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")) {
5409
+ const message = (_e = (_d = response.error) == null ? void 0 : _d.message) != null ? _e : "The template tag changed after it was read";
5410
+ throw new TemplateTagConflictError({
5411
+ templateId: candidate.templateId,
5412
+ tag,
5413
+ candidateBuildId: candidate.buildId,
5414
+ expectedBuildId: expected == null ? void 0 : expected.buildId,
5415
+ expectedVersion: (_f = expected == null ? void 0 : expected.version) != null ? _f : 0,
5416
+ reason: response.error.code,
5417
+ message: `409: ${message}. Fetch its current head with Template.getTagHead() before retrying.`
5418
+ });
5419
+ }
5420
+ const error = handleApiError(response, TemplateError);
5421
+ if (error) {
5422
+ throw error;
5423
+ }
5424
+ if (!response.data) {
5425
+ throw new TemplateError("Enclave API returned no promoted template tag");
5426
+ }
5427
+ return mapTagHead(response.data);
5428
+ }
5429
+ function mapTagHead(head) {
5430
+ return {
5431
+ tag: head.tag,
5432
+ buildId: head.buildID,
5433
+ version: head.version,
5434
+ createdAt: new Date(head.createdAt)
5435
+ };
5436
+ }
5437
+ function assertTemplateAndTag(templateId, tag) {
5438
+ if (!templateId.trim() || !tag.trim()) {
5439
+ throw new InvalidArgumentError("templateId and tag are required");
5440
+ }
5441
+ }
5442
+ function assertBuildIdentity2(candidate) {
5443
+ if (!candidate.templateId.trim() || !candidate.buildId.trim()) {
5444
+ throw new InvalidArgumentError("templateId and buildId are required");
5445
+ }
5446
+ }
5447
+ function assertExpectation(expected) {
5448
+ if (expected !== null && (!expected.buildId.trim() || !Number.isSafeInteger(expected.version) || expected.version < 1)) {
5449
+ throw new InvalidArgumentError(
5450
+ "expected tag head must include a buildId and a positive safe-integer version"
5451
+ );
5452
+ }
5453
+ }
5058
5454
 
5059
5455
  // src/template/index.ts
5060
5456
  var _a2;
@@ -5306,6 +5702,42 @@ var TemplateBase = class {
5306
5702
  const client = new ApiClient(config);
5307
5703
  return getTemplateTags(client, { templateID: templateId });
5308
5704
  }
5705
+ /**
5706
+ * Get the authoritative traffic-selection token for one template tag.
5707
+ *
5708
+ * Pass the returned head directly to {@link promoteTag}; its monotonic
5709
+ * version prevents concurrent releasers from silently overwriting each
5710
+ * other.
5711
+ */
5712
+ static async getTagHead(templateId, tag, options) {
5713
+ const config = ConnectionConfig.forGlobal(options);
5714
+ const client = new ApiClient(config);
5715
+ return getTemplateTagHead(
5716
+ client,
5717
+ templateId,
5718
+ tag,
5719
+ config.getSignal(options == null ? void 0 : options.requestTimeoutMs)
5720
+ );
5721
+ }
5722
+ /**
5723
+ * Atomically move one template tag to an exact ready, durable build.
5724
+ *
5725
+ * Use a head returned by {@link getTagHead} as `expected`. Pass `null` only
5726
+ * when creating a tag that must not already exist. A concurrent change throws
5727
+ * `TemplateTagConflictError`; fetch the head again before deciding whether to
5728
+ * retry.
5729
+ */
5730
+ static async promoteTag(candidate, tag, expected, options) {
5731
+ const config = ConnectionConfig.forGlobal(options);
5732
+ const client = new ApiClient(config);
5733
+ return promoteTemplateTag(
5734
+ client,
5735
+ candidate,
5736
+ tag,
5737
+ expected,
5738
+ config.getSignal(options == null ? void 0 : options.requestTimeoutMs)
5739
+ );
5740
+ }
5309
5741
  fromDebianImage(variant = "stable") {
5310
5742
  return this.fromImage(`debian:${variant}`);
5311
5743
  }
@@ -5603,8 +6035,8 @@ var TemplateBase = class {
5603
6035
  })
5604
6036
  );
5605
6037
  }
5606
- gitClone(url, path2, options) {
5607
- const args = ["git", "clone", url];
6038
+ gitClone(url2, path2, options) {
6039
+ const args = ["git", "clone", url2];
5608
6040
  if (options == null ? void 0 : options.branch) {
5609
6041
  args.push(`--branch ${options.branch}`);
5610
6042
  args.push("--single-branch");
@@ -5856,7 +6288,7 @@ var TemplateBase = class {
5856
6288
  if (index + 1 >= 0 && index + 1 < this.stackTraces.length) {
5857
6289
  stackTrace = this.stackTraces[index + 1];
5858
6290
  }
5859
- const { present, url } = await getFileUploadLink(
6291
+ const { present, url: url2 } = await getFileUploadLink(
5860
6292
  client,
5861
6293
  {
5862
6294
  templateID,
@@ -5864,12 +6296,12 @@ var TemplateBase = class {
5864
6296
  },
5865
6297
  stackTrace
5866
6298
  );
5867
- if (forceUpload && url != null || present === false && url != null) {
6299
+ if (forceUpload && url2 != null || present === false && url2 != null) {
5868
6300
  await uploadFile(
5869
6301
  {
5870
6302
  fileName: src,
5871
6303
  fileContextPath: this.fileContextPath.toString(),
5872
- url,
6304
+ url: url2,
5873
6305
  ignorePatterns: [
5874
6306
  ...this.fileIgnorePatterns,
5875
6307
  ...readDockerignore(this.fileContextPath.toString())
@@ -5990,6 +6422,8 @@ Template.exists = TemplateBase.exists;
5990
6422
  Template.assignTags = TemplateBase.assignTags;
5991
6423
  Template.removeTags = TemplateBase.removeTags;
5992
6424
  Template.getTags = TemplateBase.getTags;
6425
+ Template.getTagHead = TemplateBase.getTagHead;
6426
+ Template.promoteTag = TemplateBase.promoteTag;
5993
6427
  Template.toJSON = TemplateBase.toJSON;
5994
6428
  Template.toDockerfile = TemplateBase.toDockerfile;
5995
6429
 
@@ -6024,9 +6458,11 @@ var src_default = Sandbox;
6024
6458
  Sandbox,
6025
6459
  SandboxCreateRejectedError,
6026
6460
  SandboxError,
6461
+ SandboxRegionScope,
6027
6462
  Template,
6028
6463
  TemplateBase,
6029
6464
  TemplateError,
6465
+ TemplateTagConflictError,
6030
6466
  TimeoutError,
6031
6467
  defaultBuildLogger,
6032
6468
  getSignature,