@decocms/sandbox-controller 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ import "./chunk-QGM4M3NI.js";
2
+
1
3
  // index.ts
2
4
  import { KubeConfig } from "@kubernetes/client-node";
3
5
 
@@ -24,16 +26,16 @@ function delay(ms, options = {}) {
24
26
  });
25
27
  }
26
28
  var I32_MAX = 2 ** 31 - 1;
27
- function setArbitraryLengthTimeout(callback, delay2) {
28
- delay2 = Math.trunc(Math.max(delay2, 0) || 0);
29
- if (delay2 <= I32_MAX) {
30
- const id2 = Number(setTimeout(callback, delay2));
29
+ function setArbitraryLengthTimeout(callback, delay3) {
30
+ delay3 = Math.trunc(Math.max(delay3, 0) || 0);
31
+ if (delay3 <= I32_MAX) {
32
+ const id2 = Number(setTimeout(callback, delay3));
31
33
  return { valueOf: () => id2 };
32
34
  }
33
35
  const start = Date.now();
34
36
  let timeoutId;
35
37
  const queueTimeout = () => {
36
- const currentDelay = delay2 - (Date.now() - start);
38
+ const currentDelay = delay3 - (Date.now() - start);
37
39
  timeoutId = currentDelay > I32_MAX ? Number(setTimeout(queueTimeout, I32_MAX)) : Number(setTimeout(callback, currentDelay));
38
40
  };
39
41
  queueTimeout();
@@ -840,6 +842,16 @@ function refSlugSource(projectRef) {
840
842
  }
841
843
 
842
844
  // ../sandbox/server/provider/types.ts
845
+ var SANDBOX_PLACEMENT_REASONS = [
846
+ "existing",
847
+ "requested",
848
+ "split",
849
+ "capacity",
850
+ "fallback",
851
+ "image",
852
+ "warm-pool",
853
+ "freestyle-unavailable"
854
+ ];
843
855
  function sandboxIdKey(id2) {
844
856
  return `${id2.userId}:${id2.projectRef}`;
845
857
  }
@@ -3650,7 +3662,8 @@ var AgentSandboxProvider = class {
3650
3662
  handle: rec.handle,
3651
3663
  workdir: rec.workdir,
3652
3664
  previewUrl: this.composePreviewUrl(rec),
3653
- warmPoolAdopted: rec.tenantPoolPodBound === true
3665
+ warmPoolAdopted: rec.tenantPoolPodBound === true,
3666
+ provider: "kubernetes"
3654
3667
  };
3655
3668
  }
3656
3669
  // ---- Persistence ----------------------------------------------------------
@@ -4461,239 +4474,3296 @@ function tenantPoolReader(store, cluster, { refreshMs = DEFAULT_TENANT_POOL_REFR
4461
4474
  };
4462
4475
  }
4463
4476
 
4464
- // ../sandbox/server/provider/sandbox-api.ts
4465
- import { z as z4 } from "zod";
4466
- var SANDBOX_TOOLS = {
4467
- ensure: "SANDBOX_ENSURE",
4468
- status: "SANDBOX_STATUS",
4469
- delete: "SANDBOX_DELETE",
4470
- lifetime: "SANDBOX_LIFETIME",
4471
- capacity: "SANDBOX_CAPACITY",
4472
- tenantPoolsPush: "SANDBOX_TENANT_POOLS_PUSH",
4473
- credentialsPush: "SANDBOX_CREDENTIALS_PUSH",
4474
- list: "SANDBOX_LIST"
4477
+ // ../sandbox/server/provider/freestyle.ts
4478
+ import { createHash as createHash3, randomBytes as randomBytes2 } from "crypto";
4479
+
4480
+ // ../../node_modules/.bun/freestyle@0.2.16/node_modules/freestyle/dist/errors.js
4481
+ var FreestyleApiError = class _FreestyleApiError extends Error {
4482
+ /** A stable identifier for the failure, e.g. `"NOT_FOUND"`. */
4483
+ code;
4484
+ /** The HTTP status code of the response. */
4485
+ status;
4486
+ /** The request path that failed, for debugging. */
4487
+ path;
4488
+ constructor(status, body, path) {
4489
+ super(body.message || `Freestyle API error: ${body.code}`);
4490
+ this.name = "FreestyleApiError";
4491
+ this.code = body.code;
4492
+ this.status = status;
4493
+ this.path = path;
4494
+ Object.setPrototypeOf(this, _FreestyleApiError.prototype);
4495
+ }
4475
4496
  };
4476
- var SANDBOX_WATCH_PATH = "/api/sandbox/watch";
4477
- var SANDBOX_WATCH_KEEPALIVE_MS = 15e3;
4478
- var id = z4.string().min(1).max(512);
4479
- var CLONE_URL_MAX = 4096;
4480
- var ORG_FS_CONFIG_MAX = 65536;
4481
- var expiresAt = z4.number().int().positive();
4482
- var sandboxIdSchema = z4.object({
4483
- userId: id,
4484
- projectRef: id
4485
- });
4486
- var tenantSchema = z4.object({
4487
- orgId: id,
4488
- userId: id,
4489
- orgSlug: z4.string().optional(),
4490
- orgName: z4.string().optional(),
4491
- userEmail: z4.string().optional(),
4492
- userName: z4.string().optional()
4493
- });
4494
- var repoSchema = z4.object({
4495
- cloneUrl: z4.string().min(1).max(CLONE_URL_MAX),
4496
- connectionId: z4.string().optional(),
4497
- repositoryId: z4.string().optional(),
4498
- userName: z4.string(),
4499
- userEmail: z4.string(),
4500
- branch: z4.string().optional(),
4501
- displayName: z4.string().optional(),
4502
- submoduleCredentials: z4.array(z4.object({ host: z4.string(), token: z4.string() })).optional(),
4503
- directoryName: z4.string().optional(),
4504
- credentialExpiresAt: expiresAt.optional()
4505
- });
4506
- var ensureOptionsSchema = z4.object({
4507
- purpose: z4.enum(["interactive", "harness-run"]).optional(),
4508
- sandboxImage: SandboxImageSchema.optional(),
4509
- branch: z4.string().optional(),
4510
- repo: repoSchema.optional(),
4511
- extraRepos: z4.array(repoSchema).optional(),
4512
- workload: z4.object({
4513
- runtime: z4.enum(["node", "bun", "deno"]),
4514
- packageManager: z4.enum(["npm", "pnpm", "yarn", "bun", "deno"]),
4515
- devPort: z4.number().int().optional(),
4516
- packageManagerPath: z4.string().optional()
4517
- }).optional(),
4518
- cloneOnly: z4.boolean().optional(),
4519
- env: z4.record(z4.string(), z4.string()).optional(),
4520
- tenant: tenantSchema.optional(),
4521
- orgFsConfigJson: z4.string().max(ORG_FS_CONFIG_MAX).optional()
4522
- });
4523
- var daemonSchema = z4.object({
4524
- url: z4.url({ protocol: /^https?$/ }),
4525
- token: z4.string().min(1)
4526
- });
4527
- var ensureInputSchema = z4.object({
4528
- id: sandboxIdSchema,
4529
- opts: ensureOptionsSchema,
4530
- /**
4531
- * Until when `opts.orgFsConfigJson` stays valid, at least; unset keeps it
4532
- * out of the host's store. Clone credentials carry their own
4533
- * `credentialExpiresAt`.
4534
- */
4535
- credentialsValidUntil: z4.object({ orgFsConfig: expiresAt.optional() }).optional()
4536
- });
4537
- var ensureOutputSchema = z4.object({
4538
- handle: z4.string().min(1),
4539
- workdir: z4.string().min(1),
4540
- previewUrl: z4.string().nullable(),
4541
- warmPoolAdopted: z4.boolean(),
4542
- daemon: daemonSchema
4543
- });
4544
- var handleInputSchema = z4.object({ handle: z4.string().min(1) });
4545
- var statusInputSchema = z4.object({
4546
- handle: z4.string().min(1),
4547
- /** Re-provision an evicted claim before answering. */
4548
- resurrect: z4.boolean().optional()
4549
- });
4550
- var podTerminationSchema = z4.object({
4551
- reason: z4.string(),
4552
- oomKilled: z4.boolean(),
4553
- exitCode: z4.number().int().optional(),
4554
- memoryLimit: z4.string().optional(),
4555
- evictionMessage: z4.string().optional()
4556
- });
4557
- var statusOutputSchema = z4.object({
4558
- alive: z4.boolean(),
4559
- previewUrl: z4.string().nullable(),
4560
- daemon: daemonSchema.nullable(),
4561
- lastTermination: podTerminationSchema.nullable()
4562
- });
4563
- var lifetimeInputSchema = z4.object({
4564
- handle: z4.string().min(1),
4565
- graceMs: z4.number().int().min(0).optional()
4566
- });
4567
- var emptySchema = z4.object({});
4568
- var capacityOutputSchema = z4.object({ schedulable: z4.boolean() });
4569
- var tenantPoolsPushInputSchema = z4.object({
4570
- repoUrl: z4.string().min(1).max(CLONE_URL_MAX),
4571
- ref: z4.string().min(1)
4572
- });
4573
- var tenantPoolsPushOutputSchema = z4.object({
4574
- pools: z4.array(z4.string())
4575
- });
4576
- var toolErrorSchema = z4.object({
4577
- code: z4.enum(["bootstrap-rejected", "internal"]),
4578
- error: z4.string(),
4579
- status: z4.number().int().optional()
4580
- });
4581
- var since = z4.number();
4582
- var claimPhaseSchema = z4.discriminatedUnion(
4583
- "kind",
4584
- [
4585
- z4.object({ kind: z4.literal("claiming"), since }),
4586
- z4.object({
4587
- kind: z4.literal("waiting-for-capacity"),
4588
- since,
4589
- message: z4.string().optional(),
4590
- nodeClaim: z4.string().optional()
4591
- }),
4592
- z4.object({ kind: z4.literal("pulling-image"), since }),
4593
- z4.object({ kind: z4.literal("starting-container"), since }),
4594
- z4.object({ kind: z4.literal("warming-daemon"), since }),
4595
- z4.object({ kind: z4.literal("ready") }),
4596
- z4.object({
4597
- kind: z4.literal("failed"),
4598
- reason: z4.enum([
4599
- "image-pull-backoff",
4600
- "crash-loop-backoff",
4601
- "scheduling-timeout",
4602
- "claim-never-created",
4603
- "reconciler-error",
4604
- "unknown"
4605
- ]),
4606
- message: z4.string()
4607
- })
4608
- ]
4609
- );
4610
- var tenantIdsSchema = z4.object({ orgId: id, userId: id });
4611
- var repoIdentitySchema = z4.union([
4612
- z4.object({ repositoryId: id }).strict(),
4613
- z4.object({ connectionId: id, repo: id }).strict()
4614
- ]);
4615
- var CREDENTIALS_PUSH_MAX = 2e3;
4616
- var pushedCloneUrl = z4.url({ protocol: /^https?$/ }).max(CLONE_URL_MAX);
4617
- var credentialsPushInputSchema = z4.object({
4618
- cloneUrls: z4.array(
4619
- z4.object({
4620
- tenant: tenantIdsSchema,
4621
- repo: repoIdentitySchema,
4622
- cloneUrl: pushedCloneUrl,
4623
- expiresAt
4624
- })
4625
- ).max(CREDENTIALS_PUSH_MAX),
4626
- /** Clone URLs for tenant pool allocations, keyed by the pool's tenant and repo. */
4627
- poolCloneUrls: z4.array(
4628
- z4.object({
4629
- tenant: id,
4630
- repoUrl: z4.string().min(1).max(CLONE_URL_MAX),
4631
- cloneUrl: pushedCloneUrl,
4632
- expiresAt
4633
- })
4634
- ).max(CREDENTIALS_PUSH_MAX).default([]),
4635
- orgFsConfigs: z4.array(
4636
- z4.object({
4637
- tenant: tenantIdsSchema,
4638
- orgFsConfigJson: z4.string().min(1).max(ORG_FS_CONFIG_MAX),
4639
- expiresAt
4640
- })
4641
- ).max(CREDENTIALS_PUSH_MAX)
4642
- });
4643
- var credentialsPushOutputSchema = z4.object({
4644
- stored: z4.number().int().nonnegative(),
4645
- kept: z4.number().int().nonnegative()
4646
- });
4647
- var SANDBOX_LIST_MAX = 5e3;
4648
- var SANDBOX_LIST_POOLS_MAX = 500;
4649
- var SANDBOX_LIST_POOL_REPOS_MAX = 100;
4650
- var listOutputSchema = z4.object({
4651
- sandboxes: z4.array(
4652
- z4.object({
4653
- handle: id,
4654
- tenant: tenantIdsSchema.nullable(),
4655
- repos: z4.array(repoIdentitySchema).max(16),
4656
- /** The sandbox mounts org-fs. */
4657
- orgFs: z4.boolean(),
4658
- /** When the org-fs config the host holds for its tenant expires. */
4659
- orgFsConfigExpiresAt: expiresAt.nullable()
4660
- })
4661
- ).max(SANDBOX_LIST_MAX),
4662
- /** Defaulted so a host that predates pool listing still lists its sandboxes. */
4663
- pools: z4.array(
4664
- z4.object({
4665
- name: id,
4666
- tenant: id,
4667
- image: SandboxImageSchema,
4668
- repos: z4.array(
4669
- z4.object({
4670
- repoUrl: z4.string().min(1).max(CLONE_URL_MAX),
4671
- branch: z4.string().min(1)
4672
- })
4673
- ).max(SANDBOX_LIST_POOL_REPOS_MAX)
4674
- })
4675
- ).max(SANDBOX_LIST_POOLS_MAX).default([])
4676
- });
4677
4497
 
4678
- // ../sandbox/server/provider/sandbox-server.ts
4679
- function messageChain(err) {
4680
- const parts = [];
4681
- for (let e = err, depth = 0; e && depth < 5; depth++) {
4682
- parts.push(e instanceof Error ? e.message : String(e));
4683
- e = e instanceof Error ? e.cause : void 0;
4498
+ // ../../node_modules/.bun/freestyle@0.2.16/node_modules/freestyle/dist/runtime.js
4499
+ function isNodeRuntime() {
4500
+ return typeof process !== "undefined" && Boolean(process.versions?.node);
4501
+ }
4502
+ var globalFetch = (input, init) => globalThis.fetch(input, init);
4503
+
4504
+ // ../../node_modules/.bun/freestyle@0.2.16/node_modules/freestyle/dist/transport.js
4505
+ async function loadUndici() {
4506
+ if (!isNodeRuntime())
4507
+ return null;
4508
+ try {
4509
+ const specifier = "undici";
4510
+ return await import(
4511
+ /* @vite-ignore */
4512
+ specifier
4513
+ );
4514
+ } catch {
4515
+ return null;
4684
4516
  }
4685
- return parts.join(": ");
4686
4517
  }
4687
- function tool(def) {
4688
- return {
4689
- ...def,
4690
- // A thrown message is all an MCP error carries, so it carries JSON.
4691
- execute: (input) => def.execute(input).catch((err) => {
4692
- const body = err instanceof ConfigRequestError ? {
4693
- code: "bootstrap-rejected",
4694
- error: err.message,
4695
- status: err.status
4696
- } : { code: "internal", error: messageChain(err) };
4518
+ var MAX_CONNECTIONS = 4;
4519
+ async function createHttp2Fetch() {
4520
+ const undici = await loadUndici();
4521
+ if (!undici)
4522
+ return null;
4523
+ const dispatcher = new undici.Agent({ allowH2: true, connections: MAX_CONNECTIONS });
4524
+ return ((input, init) => undici.fetch(input, { ...init, dispatcher }));
4525
+ }
4526
+ function isTls(input) {
4527
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
4528
+ return url.startsWith("https:");
4529
+ }
4530
+ function defaultFetch() {
4531
+ let pending;
4532
+ const resolve = () => {
4533
+ pending ??= createHttp2Fetch().then((h2) => h2 ?? globalFetch).catch(() => globalFetch);
4534
+ return pending;
4535
+ };
4536
+ return (input, init) => isTls(input) ? resolve().then((impl) => impl(input, init)) : globalFetch(input, init);
4537
+ }
4538
+
4539
+ // ../../node_modules/.bun/freestyle@0.2.16/node_modules/freestyle/dist/client.js
4540
+ var IDENTITY_TOKEN_HEADER = "x-freestyle-identity-access-token";
4541
+ var BACKGROUND_AFTER_SECS_HEADER = "x-freestyle-background-after-secs";
4542
+ var BACKGROUND_REQUEST_ID_HEADER = "x-freestyle-background-request-id";
4543
+ var BACKGROUND_AFTER_SECS = 5;
4544
+ var BACKGROUND_POLL_INTERVAL_MS = 2e3;
4545
+ var DEFAULT_BASE_URL = "https://api.freestyle.sh";
4546
+ function delay2(ms) {
4547
+ return new Promise((resolve) => setTimeout(resolve, ms));
4548
+ }
4549
+ function buildQuery(query) {
4550
+ if (!query)
4551
+ return "";
4552
+ const params = new URLSearchParams();
4553
+ for (const [key, value] of Object.entries(query)) {
4554
+ if (value === void 0 || value === null)
4555
+ continue;
4556
+ params.set(key, String(value));
4557
+ }
4558
+ const qs = params.toString();
4559
+ return qs ? `?${qs}` : "";
4560
+ }
4561
+ function segment(value) {
4562
+ return encodeURIComponent(value);
4563
+ }
4564
+ var FreestyleClient = class {
4565
+ baseUrl;
4566
+ authenticationHeaders;
4567
+ fetchImpl;
4568
+ /** Identity-token callers cannot poll `/v5/background-requests`, so their requests are never backgrounded. */
4569
+ backgroundEligible;
4570
+ constructor(options = {}) {
4571
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
4572
+ if (!options.fetch && !globalThis.fetch) {
4573
+ throw new Error("No fetch implementation available. Pass `fetch` in FreestyleOptions when running on a runtime without a global fetch.");
4574
+ }
4575
+ this.fetchImpl = options.fetch ?? defaultFetch();
4576
+ if ("identityAccessToken" in options && options.identityAccessToken) {
4577
+ this.authenticationHeaders = {
4578
+ [IDENTITY_TOKEN_HEADER]: options.identityAccessToken
4579
+ };
4580
+ this.backgroundEligible = false;
4581
+ return;
4582
+ }
4583
+ this.backgroundEligible = true;
4584
+ if ("stackAccessToken" in options && options.stackAccessToken) {
4585
+ this.authenticationHeaders = {
4586
+ Authorization: `Bearer ${options.stackAccessToken}`,
4587
+ "X-Freestyle-Team-Id": options.teamId
4588
+ };
4589
+ return;
4590
+ }
4591
+ const apiKey = options.apiKey ?? globalThis.process?.env?.FREESTYLE_API_KEY;
4592
+ if (!apiKey) {
4593
+ throw new Error("No Freestyle API key provided. Pass `apiKey` in FreestyleOptions or set FREESTYLE_API_KEY.");
4594
+ }
4595
+ this.authenticationHeaders = { Authorization: `Bearer ${apiKey}` };
4596
+ }
4597
+ /** Headers this client authenticates every request with, e.g. for a raw WebSocket handshake. */
4598
+ authHeaders() {
4599
+ return { ...this.authenticationHeaders };
4600
+ }
4601
+ /** Absolute URL for an API path, e.g. `/v5/vms/{id}` → `https://.../v5/vms/{id}`. */
4602
+ url(path, query) {
4603
+ return `${this.baseUrl}${path}${buildQuery(query)}`;
4604
+ }
4605
+ /**
4606
+ * Low-level escape hatch: an authenticated `fetch` against the API, relative
4607
+ * to `baseUrl`. Pass the whole path, `/v5` included — the same paths the
4608
+ * methods above use, and the same ones the API declares. It is also how the
4609
+ * client follows a server-supplied URL (a 202's `resultUrl`), and the only
4610
+ * way to reach the unversioned `/status` and `/openapi.json`.
4611
+ */
4612
+ async fetch(path, init = {}) {
4613
+ const headers = new Headers(init.headers);
4614
+ for (const [name, value] of Object.entries(this.authHeaders())) {
4615
+ if (!headers.has(name))
4616
+ headers.set(name, value);
4617
+ }
4618
+ return this.fetchImpl(this.absoluteUrl(path), { ...init, headers });
4619
+ }
4620
+ /** A path or URL as this client's `fetch` resolves it: absolute URLs are
4621
+ * followed as given, everything else hangs off `baseUrl` with no version
4622
+ * segment added — the caller already supplied whatever version it wants. */
4623
+ absoluteUrl(pathOrUrl) {
4624
+ return /^https?:\/\//i.test(pathOrUrl) ? pathOrUrl : `${this.baseUrl}${pathOrUrl}`;
4625
+ }
4626
+ async request(method, path, { query, body, rawBody, signal } = {}) {
4627
+ const headers = { ...this.authHeaders() };
4628
+ if (this.backgroundEligible) {
4629
+ headers[BACKGROUND_AFTER_SECS_HEADER] = String(BACKGROUND_AFTER_SECS);
4630
+ }
4631
+ let payload;
4632
+ if (rawBody !== void 0) {
4633
+ headers["Content-Type"] = "application/octet-stream";
4634
+ payload = rawBody;
4635
+ } else if (body !== void 0) {
4636
+ headers["Content-Type"] = "application/json";
4637
+ payload = JSON.stringify(body);
4638
+ }
4639
+ const url = this.url(path, query);
4640
+ const response = await this.fetchImpl(url, {
4641
+ method,
4642
+ headers,
4643
+ body: payload,
4644
+ signal
4645
+ });
4646
+ if (this.backgroundEligible && response.status === 202) {
4647
+ try {
4648
+ return await this.pollBackgroundRequest(response, path);
4649
+ } catch (error) {
4650
+ if (method === "GET" && error instanceof FreestyleApiError && error.code === "BACKGROUND_RESULT_UNSTORABLE") {
4651
+ delete headers[BACKGROUND_AFTER_SECS_HEADER];
4652
+ const retry2 = await this.fetchImpl(url, { method, headers, body: payload });
4653
+ return this.finalize(retry2, path);
4654
+ }
4655
+ throw error;
4656
+ }
4657
+ }
4658
+ return this.finalize(response, path);
4659
+ }
4660
+ /**
4661
+ * Follow a 202 from a backgrounded request: poll its result URL until the
4662
+ * stored response — success or error, identical to what the inline call
4663
+ * would have produced — is ready.
4664
+ */
4665
+ async pollBackgroundRequest(accepted, path) {
4666
+ const acceptedBody = await accepted.json().catch(() => ({}));
4667
+ const requestId = accepted.headers.get(BACKGROUND_REQUEST_ID_HEADER) ?? acceptedBody.requestId;
4668
+ const resultPath = acceptedBody.resultUrl ?? (requestId ? `/v5/background-requests/${segment(requestId)}` : void 0);
4669
+ if (!resultPath) {
4670
+ throw new FreestyleApiError(accepted.status, { code: "BACKGROUND_PROTOCOL_ERROR", message: "202 Accepted carried no background request id" }, path);
4671
+ }
4672
+ let consecutiveFailures = 0;
4673
+ for (; ; ) {
4674
+ await delay2(BACKGROUND_POLL_INTERVAL_MS);
4675
+ let response;
4676
+ try {
4677
+ response = await this.fetch(resultPath, { method: "GET" });
4678
+ consecutiveFailures = 0;
4679
+ } catch (error) {
4680
+ if (++consecutiveFailures >= 5)
4681
+ throw error;
4682
+ continue;
4683
+ }
4684
+ if (response.status === 202)
4685
+ continue;
4686
+ return this.finalize(response, path);
4687
+ }
4688
+ }
4689
+ /** Turn a terminal API response into the call's value, or throw its error. */
4690
+ async finalize(response, path) {
4691
+ if (!response.ok) {
4692
+ let errorBody;
4693
+ try {
4694
+ errorBody = await response.json();
4695
+ } catch {
4696
+ errorBody = { code: "UNKNOWN_ERROR", message: await response.text().catch(() => response.statusText) };
4697
+ }
4698
+ throw new FreestyleApiError(response.status, errorBody, path);
4699
+ }
4700
+ if (response.status === 204)
4701
+ return void 0;
4702
+ const text = await response.text();
4703
+ if (!text)
4704
+ return void 0;
4705
+ return JSON.parse(text);
4706
+ }
4707
+ get(path, query) {
4708
+ return this.request("GET", path, { query });
4709
+ }
4710
+ post(path, body) {
4711
+ return this.request("POST", path, { body: body ?? {} });
4712
+ }
4713
+ patch(path, body) {
4714
+ return this.request("PATCH", path, { body: body ?? {} });
4715
+ }
4716
+ put(path, body) {
4717
+ return this.request("PUT", path, { body: body ?? {} });
4718
+ }
4719
+ putEmpty(path) {
4720
+ return this.request("PUT", path, {});
4721
+ }
4722
+ /** PUT raw bytes as `application/octet-stream`: a whole file, or one chunk of one. */
4723
+ putBytes(path, rawBody, query, signal) {
4724
+ return this.request("PUT", path, { query, rawBody, signal });
4725
+ }
4726
+ /**
4727
+ * GET a response whose body is bytes rather than JSON, returned unread so the
4728
+ * caller can stream it. Errors still surface as {@link FreestyleApiError}.
4729
+ */
4730
+ async getBytes(path, query, init = {}) {
4731
+ const headers = { ...this.authHeaders() };
4732
+ if (init.range)
4733
+ headers["Range"] = init.range;
4734
+ const response = await this.fetchImpl(this.url(path, query), {
4735
+ method: "GET",
4736
+ headers,
4737
+ signal: init.signal
4738
+ });
4739
+ if (!response.ok) {
4740
+ let errorBody;
4741
+ try {
4742
+ errorBody = await response.json();
4743
+ } catch {
4744
+ errorBody = {
4745
+ code: "UNKNOWN_ERROR",
4746
+ message: await response.text().catch(() => response.statusText)
4747
+ };
4748
+ }
4749
+ throw new FreestyleApiError(response.status, errorBody, path);
4750
+ }
4751
+ return response;
4752
+ }
4753
+ delete(path, query) {
4754
+ return this.request("DELETE", path, { query });
4755
+ }
4756
+ };
4757
+
4758
+ // ../../node_modules/.bun/freestyle@0.2.16/node_modules/freestyle/dist/domains.js
4759
+ var DomainsNamespace = class {
4760
+ client;
4761
+ verifications;
4762
+ certificates;
4763
+ /** The branding on the 404 page for hostnames under your domains that nothing serves. */
4764
+ placeholder;
4765
+ constructor(client) {
4766
+ this.client = client;
4767
+ this.verifications = new DomainVerificationsNamespace(client);
4768
+ this.certificates = new DomainCertificatesNamespace(client);
4769
+ this.placeholder = new DomainPlaceholderNamespace(client);
4770
+ }
4771
+ /**
4772
+ * List the domains you hold: those you verified, and any Freestyle
4773
+ * subdomain you have taken by publishing it.
4774
+ */
4775
+ async list() {
4776
+ const { domains } = await this.client.get("/v5/domains");
4777
+ return domains;
4778
+ }
4779
+ };
4780
+ var DomainVerificationsNamespace = class {
4781
+ client;
4782
+ constructor(client) {
4783
+ this.client = client;
4784
+ }
4785
+ /**
4786
+ * Issue a challenge. Publish the returned `verificationCode` as a TXT
4787
+ * record at `recordName`, then `complete()` it.
4788
+ */
4789
+ create(domain) {
4790
+ return this.client.post("/v5/verifications", { domain });
4791
+ }
4792
+ async list(options = {}) {
4793
+ const { verifications } = await this.client.get("/v5/verifications", options);
4794
+ return verifications;
4795
+ }
4796
+ /** Fetch a verification challenge by its id or its domain. */
4797
+ get(domainOrId) {
4798
+ return this.client.get(`/v5/verifications/${segment(domainOrId)}`);
4799
+ }
4800
+ /**
4801
+ * Check DNS and record ownership. Addressed by id, only that challenge's
4802
+ * own code counts; use this when several people are verifying the same
4803
+ * domain.
4804
+ */
4805
+ complete(domainOrId) {
4806
+ return this.client.putEmpty(`/v5/verifications/${segment(domainOrId)}`);
4807
+ }
4808
+ /**
4809
+ * Withdraw outstanding challenges; by domain this drops all of them. A
4810
+ * challenge that already verified is a permanent record and cannot be
4811
+ * deleted.
4812
+ */
4813
+ delete(domainOrId) {
4814
+ return this.client.delete(`/v5/verifications/${segment(domainOrId)}`);
4815
+ }
4816
+ };
4817
+ var DomainCertificatesNamespace = class {
4818
+ client;
4819
+ constructor(client) {
4820
+ this.client = client;
4821
+ }
4822
+ async list() {
4823
+ const { certificates } = await this.client.get("/v5/certificates");
4824
+ return certificates;
4825
+ }
4826
+ /**
4827
+ * Request an ongoing `*.domain` certificate. Requires
4828
+ * `_acme-challenge.<domain>` to be NS-delegated to Freestyle's
4829
+ * nameservers, since wildcards can only be proven over DNS.
4830
+ */
4831
+ createWildcard(domain) {
4832
+ return this.client.post("/v5/certificates/wildcard", { domain });
4833
+ }
4834
+ };
4835
+ var DomainPlaceholderNamespace = class {
4836
+ client;
4837
+ constructor(client) {
4838
+ this.client = client;
4839
+ }
4840
+ /** Your placeholder branding, or `null` when pages carry the Freestyle default. */
4841
+ async get() {
4842
+ try {
4843
+ return await this.client.get("/v5/domains/placeholder");
4844
+ } catch (error) {
4845
+ if (error instanceof FreestyleApiError && error.status === 404)
4846
+ return null;
4847
+ throw error;
4848
+ }
4849
+ }
4850
+ /**
4851
+ * Set the branding for every placeholder page under your verified domains,
4852
+ * including domains you verify later. Replaces any earlier setting.
4853
+ */
4854
+ set(options) {
4855
+ return this.client.put("/v5/domains/placeholder", options);
4856
+ }
4857
+ /** Remove your branding; pages go back to the Freestyle default. */
4858
+ delete() {
4859
+ return this.client.delete("/v5/domains/placeholder");
4860
+ }
4861
+ };
4862
+
4863
+ // ../../node_modules/.bun/freestyle@0.2.16/node_modules/freestyle/dist/firewall.js
4864
+ var FirewallRuleValidationError = class _FirewallRuleValidationError extends Error {
4865
+ /** The field the complaint is about, in dotted form. */
4866
+ path;
4867
+ constructor(path, message) {
4868
+ super(message);
4869
+ this.name = "FirewallRuleValidationError";
4870
+ this.path = path;
4871
+ Object.setPrototypeOf(this, _FirewallRuleValidationError.prototype);
4872
+ }
4873
+ };
4874
+ var ENDPOINT_FIELDS = [
4875
+ "vmId",
4876
+ "vpcId",
4877
+ "tunnelId",
4878
+ "cidr",
4879
+ "public",
4880
+ "port",
4881
+ "protocol"
4882
+ ];
4883
+ var IDENTITY_FIELDS = ["vmId", "vpcId", "tunnelId", "cidr", "public"];
4884
+ var PROTOCOLS = ["tcp", "udp", "icmp"];
4885
+ var PORTED_PROTOCOLS = ["tcp", "udp"];
4886
+ var MAX_DESCRIPTION_LENGTH = 1024;
4887
+ function fail(path, message) {
4888
+ throw new FirewallRuleValidationError(path, message);
4889
+ }
4890
+ function hasIdentity(endpoint) {
4891
+ return IDENTITY_FIELDS.some((field) => endpoint[field] !== void 0);
4892
+ }
4893
+ function isPlainObject(value) {
4894
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4895
+ }
4896
+ function validateCidr(path, cidr) {
4897
+ const slash = cidr.lastIndexOf("/");
4898
+ if (slash < 0) {
4899
+ fail(path, `${path} \`${cidr}\` is not a CIDR: expected address/prefix, e.g. 203.0.113.0/24 or 2001:db8::/32`);
4900
+ }
4901
+ const address = cidr.slice(0, slash);
4902
+ const prefixText = cidr.slice(slash + 1);
4903
+ if (!/^\d+$/.test(prefixText)) {
4904
+ fail(path, `${path} \`${cidr}\`: prefix length must be a number`);
4905
+ }
4906
+ const prefix = Number(prefixText);
4907
+ const isIpv6 = address.includes(":");
4908
+ const max = isIpv6 ? 128 : 32;
4909
+ if (prefix > max) {
4910
+ fail(path, `${path} \`${cidr}\`: prefix length must be 0\u2013${max}`);
4911
+ }
4912
+ if (!isIpv6 && !/^\d{1,3}(\.\d{1,3}){3}$/.test(address)) {
4913
+ fail(path, `${path} \`${cidr}\`: \`${address}\` is not an IP address`);
4914
+ }
4915
+ }
4916
+ function validateFirewallEndpoint(endpoint, path) {
4917
+ if (!isPlainObject(endpoint)) {
4918
+ fail(path, `${path} must be an object`);
4919
+ }
4920
+ for (const field of Object.keys(endpoint)) {
4921
+ if (!ENDPOINT_FIELDS.includes(field)) {
4922
+ fail(`${path}.${field}`, `${path} has unknown field \`${field}\`. Expected one of: ${ENDPOINT_FIELDS.join(", ")}`);
4923
+ }
4924
+ }
4925
+ const matcher = endpoint;
4926
+ for (const field of ["vmId", "vpcId", "tunnelId", "cidr"]) {
4927
+ const value = matcher[field];
4928
+ if (value !== void 0 && (typeof value !== "string" || value.length === 0)) {
4929
+ fail(`${path}.${field}`, `${path}.${field} must be a non-empty string`);
4930
+ }
4931
+ }
4932
+ if (matcher.public !== void 0 && matcher.public !== true) {
4933
+ fail(`${path}.public`, `${path}.public must be true when present: it declares the public Internet. Omit the field to match something else`);
4934
+ }
4935
+ if (matcher.public === true) {
4936
+ if (matcher.vmId !== void 0) {
4937
+ fail(`${path}.public`, `${path} cannot combine \`public: true\` with \`vmId\`: that would mean the VM only when reached over its public address, a distinction this API does not make yet. Use \`vmId\` alone to match the VM on any path, or \`public: true\` alone to match public addresses`);
4938
+ }
4939
+ if (matcher.vpcId !== void 0) {
4940
+ fail(`${path}.public`, `${path} cannot combine \`public: true\` with \`vpcId\`: that would mean the network's members only when reached over their public addresses, a distinction this API does not make yet. Use \`vpcId\` alone or \`public: true\` alone`);
4941
+ }
4942
+ if (matcher.tunnelId !== void 0) {
4943
+ fail(`${path}.public`, `${path} cannot combine \`public: true\` with \`tunnelId\`: traffic through a tunnel arrives over the tunnel, not over the public Internet. Use \`tunnelId\` alone`);
4944
+ }
4945
+ }
4946
+ if (matcher.cidr !== void 0) {
4947
+ validateCidr(`${path}.cidr`, matcher.cidr);
4948
+ }
4949
+ if (matcher.protocol !== void 0 && !PROTOCOLS.includes(matcher.protocol)) {
4950
+ fail(`${path}.protocol`, `${path}.protocol must be one of: ${PROTOCOLS.join(", ")} (got \`${String(matcher.protocol)}\`)`);
4951
+ }
4952
+ if (matcher.port !== void 0) {
4953
+ if (!Number.isInteger(matcher.port) || matcher.port < 1 || matcher.port > 65535) {
4954
+ fail(`${path}.port`, `${path}.port must be an integer between 1 and 65535`);
4955
+ }
4956
+ if (matcher.protocol === void 0) {
4957
+ fail(`${path}.protocol`, `${path}.port ${matcher.port} requires ${path}.protocol: set it to "tcp" or "udp" so the rule opens one transport rather than both`);
4958
+ }
4959
+ if (!PORTED_PROTOCOLS.includes(matcher.protocol)) {
4960
+ fail(`${path}.port`, `${path}.protocol "${matcher.protocol}" has no ports; remove ${path}.port ${matcher.port}`);
4961
+ }
4962
+ }
4963
+ }
4964
+ var RULE_FIELDS = ["action", "source", "destination", "description"];
4965
+ function validateRuleShape(rule, prefix) {
4966
+ if (!isPlainObject(rule)) {
4967
+ fail(prefix || "rule", `${prefix || "rule"} must be an object`);
4968
+ }
4969
+ for (const field of Object.keys(rule)) {
4970
+ if (!RULE_FIELDS.includes(field)) {
4971
+ fail(`${prefix}${field}`, `Unknown field \`${field}\`. Expected one of: ${RULE_FIELDS.join(", ")}`);
4972
+ }
4973
+ }
4974
+ const typed = rule;
4975
+ if (typed.action === void 0) {
4976
+ fail(`${prefix}action`, `${prefix}action is required. Write \`action: "allow"\` \u2014 every rule states what it does rather than defaulting to it`);
4977
+ }
4978
+ if (typed.action !== "allow") {
4979
+ fail(`${prefix}action`, `${prefix}action must be "allow" (got \`${String(typed.action)}\`). Only allow rules exist today`);
4980
+ }
4981
+ if (typed.description !== void 0) {
4982
+ if (typeof typed.description !== "string") {
4983
+ fail(`${prefix}description`, "description must be a string");
4984
+ }
4985
+ if (typed.description.length > MAX_DESCRIPTION_LENGTH) {
4986
+ fail(`${prefix}description`, `description is too long (${typed.description.length} characters, maximum ${MAX_DESCRIPTION_LENGTH})`);
4987
+ }
4988
+ }
4989
+ if (typed.source === void 0)
4990
+ fail(`${prefix}source`, `${prefix}source is required`);
4991
+ if (typed.destination === void 0) {
4992
+ fail(`${prefix}destination`, `${prefix}destination is required`);
4993
+ }
4994
+ validateFirewallEndpoint(typed.source, `${prefix}source`);
4995
+ validateFirewallEndpoint(typed.destination, `${prefix}destination`);
4996
+ return typed;
4997
+ }
4998
+ function validateFirewallRule(rule) {
4999
+ const typed = validateRuleShape(rule, "");
5000
+ for (const side of ["source", "destination"]) {
5001
+ const endpoint = typed[side];
5002
+ if (hasIdentity(endpoint))
5003
+ continue;
5004
+ const narrowing = [
5005
+ endpoint.port !== void 0 ? `port ${endpoint.port}` : void 0,
5006
+ endpoint.protocol !== void 0 ? `protocol ${endpoint.protocol}` : void 0
5007
+ ].filter(Boolean).join(", ");
5008
+ fail(side, narrowing ? `${side} only narrows traffic (${narrowing}) without saying what it is. Add \`vmId\`, \`vpcId\`, \`tunnelId\`, \`cidr\`, or \`public: true\`. Leaving it out to mean "this VM" only works inside vms.create` : `${side} is empty. A matcher must say what the traffic is: \`vmId\`, \`vpcId\`, \`tunnelId\`, \`cidr\`, or \`public: true\` for the public Internet`);
5009
+ }
5010
+ return typed;
5011
+ }
5012
+ function validateFirewallSpec(spec, subject = "VM") {
5013
+ if (spec === void 0 || spec === null) {
5014
+ fail("firewall", `firewall is required on a ${subject} create. A ${subject} gets nothing implicitly, so state what it may reach \u2014 \`{ rules: [] }\` if the answer is nothing`);
5015
+ }
5016
+ if (!isPlainObject(spec)) {
5017
+ fail("firewall", "firewall must be an object with a `rules` array");
5018
+ }
5019
+ for (const field of Object.keys(spec)) {
5020
+ if (field !== "rules") {
5021
+ fail(`firewall.${field}`, `firewall has unknown field \`${field}\`. Expected: rules`);
5022
+ }
5023
+ }
5024
+ const rules = spec.rules;
5025
+ if (!Array.isArray(rules)) {
5026
+ fail("firewall.rules", "firewall.rules must be an array");
5027
+ }
5028
+ rules.forEach((rule, index) => {
5029
+ const prefix = `firewall.rules[${index}].`;
5030
+ const typed = validateRuleShape(rule, prefix);
5031
+ const sourceNames = hasIdentity(typed.source);
5032
+ const destinationNames = hasIdentity(typed.destination);
5033
+ if (!sourceNames && !destinationNames && subject !== "network") {
5034
+ fail(`firewall.rules[${index}]`, `firewall.rules[${index}]: both source and destination refer to the ${subject} being created, which allows nothing. Give one of them a \`vmId\`, \`vpcId\`, \`tunnelId\`, \`cidr\`, or \`public: true\``);
5035
+ }
5036
+ if (sourceNames && destinationNames) {
5037
+ fail(`firewall.rules[${index}]`, `firewall.rules[${index}]: neither end refers to the ${subject} being created. Leave one endpoint's identity unset so it means this ${subject}, or create the rule with firewall.rules.create instead`);
5038
+ }
5039
+ });
5040
+ return spec;
5041
+ }
5042
+ function rulePath(ruleId) {
5043
+ return `/v5/firewall/rules/${segment(ruleId)}`;
5044
+ }
5045
+ var FirewallRulesNamespace = class {
5046
+ client;
5047
+ constructor(client) {
5048
+ this.client = client;
5049
+ }
5050
+ /**
5051
+ * Allow traffic from one place to another.
5052
+ *
5053
+ * @example
5054
+ * // Public Internet -> a VM, on HTTPS.
5055
+ * await freestyle.firewall.rules.create({
5056
+ * action: "allow",
5057
+ * source: { public: true },
5058
+ * destination: { vmId: "vm-123456", port: 443, protocol: "tcp" },
5059
+ * });
5060
+ *
5061
+ * @example
5062
+ * // One private network -> another, on Postgres.
5063
+ * await freestyle.firewall.rules.create({
5064
+ * action: "allow",
5065
+ * source: { vpcId: "vpc-frontend" },
5066
+ * destination: { vpcId: "vpc-backend", port: 5432, protocol: "tcp" },
5067
+ * });
5068
+ *
5069
+ * @throws {FirewallRuleValidationError} before any request, when the rule
5070
+ * cannot be well formed.
5071
+ */
5072
+ // `async` so a validation failure arrives as a rejected promise like every
5073
+ // other error from this client, instead of a synchronous throw that a
5074
+ // `.catch()` chain would sail straight past.
5075
+ async create(options) {
5076
+ const validated = validateFirewallRule(options);
5077
+ return this.client.post("/v5/firewall/rules", validated);
5078
+ }
5079
+ /**
5080
+ * List your rules, newest first. Pass `vmId` for the rules that apply to one
5081
+ * VM — those naming it, plus those naming a private network it is on.
5082
+ */
5083
+ list(options = {}) {
5084
+ return this.client.get("/v5/firewall/rules", options);
5085
+ }
5086
+ get(ruleId) {
5087
+ return this.client.get(rulePath(ruleId));
5088
+ }
5089
+ /** Delete a rule. The resources it named are untouched. */
5090
+ delete(ruleId) {
5091
+ return this.client.delete(rulePath(ruleId));
5092
+ }
5093
+ };
5094
+ var FirewallNamespace = class {
5095
+ client;
5096
+ rules;
5097
+ constructor(client) {
5098
+ this.client = client;
5099
+ this.rules = new FirewallRulesNamespace(client);
5100
+ }
5101
+ /**
5102
+ * Ask whether a connection would be allowed, without changing anything.
5103
+ *
5104
+ * Useful for checking a rule set before you rely on it — and for confirming
5105
+ * what you already believe, e.g. that a mapped domain reaches a VM with no
5106
+ * rules at all.
5107
+ *
5108
+ * @example
5109
+ * // A mapped domain: allowed whatever your rules say.
5110
+ * await freestyle.firewall.evaluate({
5111
+ * source: { platform: true },
5112
+ * destination: { vmId: "vm-123456", port: 3000 },
5113
+ * protocol: "tcp",
5114
+ * });
5115
+ * // => { outcome: "allowedByPlatform", side: "source" }
5116
+ */
5117
+ evaluate(traffic) {
5118
+ return this.client.post("/v5/firewall/evaluate", traffic);
5119
+ }
5120
+ };
5121
+
5122
+ // ../../node_modules/.bun/freestyle@0.2.16/node_modules/freestyle/dist/identities.js
5123
+ function identityPath(id2) {
5124
+ return `/v5/identities/${segment(id2)}`;
5125
+ }
5126
+ var IdentityTokensNamespace = class {
5127
+ client;
5128
+ basePath;
5129
+ constructor(client, basePath) {
5130
+ this.client = client;
5131
+ this.basePath = basePath;
5132
+ }
5133
+ /** Mint an access token for the identity. */
5134
+ create() {
5135
+ return this.client.post(`${this.basePath}/tokens`);
5136
+ }
5137
+ list() {
5138
+ return this.client.get(`${this.basePath}/tokens`);
5139
+ }
5140
+ revoke(tokenId) {
5141
+ return this.client.delete(`${this.basePath}/tokens/${segment(tokenId)}`);
5142
+ }
5143
+ };
5144
+ var IdentityVmPermissionsNamespace = class {
5145
+ client;
5146
+ basePath;
5147
+ constructor(client, basePath) {
5148
+ this.client = client;
5149
+ this.basePath = basePath;
5150
+ }
5151
+ /** List the VMs this identity has been granted access to. */
5152
+ list() {
5153
+ return this.client.get(`${this.basePath}/permissions/vm`);
5154
+ }
5155
+ /** Grant this identity access to a VM. */
5156
+ grant(options) {
5157
+ return this.client.post(`${this.basePath}/permissions/vm`, options);
5158
+ }
5159
+ /** Fetch this identity's grant on a VM. */
5160
+ get(vmId) {
5161
+ return this.client.get(`${this.basePath}/permissions/vm/${segment(vmId)}`);
5162
+ }
5163
+ /** Change which Linux users this identity may act as on a VM. */
5164
+ update(vmId, allowedLinuxUsers) {
5165
+ return this.client.patch(`${this.basePath}/permissions/vm/${segment(vmId)}`, {
5166
+ allowedLinuxUsers
5167
+ });
5168
+ }
5169
+ /** Revoke this identity's access to a VM. */
5170
+ revoke(vmId) {
5171
+ return this.client.delete(`${this.basePath}/permissions/vm/${segment(vmId)}`);
5172
+ }
5173
+ };
5174
+ var Identity = class {
5175
+ id;
5176
+ tokens;
5177
+ permissions;
5178
+ client;
5179
+ basePath;
5180
+ constructor(client, id2) {
5181
+ this.client = client;
5182
+ this.id = id2;
5183
+ this.basePath = identityPath(id2);
5184
+ this.tokens = new IdentityTokensNamespace(client, this.basePath);
5185
+ this.permissions = { vm: new IdentityVmPermissionsNamespace(client, this.basePath) };
5186
+ }
5187
+ data() {
5188
+ return this.client.get(this.basePath);
5189
+ }
5190
+ /** Delete the identity, along with its tokens and grants. Managed identities cannot be deleted. */
5191
+ delete() {
5192
+ return this.client.delete(this.basePath);
5193
+ }
5194
+ };
5195
+ var IdentitiesNamespace = class {
5196
+ client;
5197
+ constructor(client) {
5198
+ this.client = client;
5199
+ }
5200
+ /** Create an identity. */
5201
+ async create(options = {}) {
5202
+ const data = await this.client.post("/v5/identities", options);
5203
+ return { identity: new Identity(this.client, data.id), identityId: data.id, data };
5204
+ }
5205
+ list(options = {}) {
5206
+ return this.client.get("/v5/identities", options);
5207
+ }
5208
+ get(identityId) {
5209
+ return this.client.get(identityPath(identityId));
5210
+ }
5211
+ /** A handle to an identity, without a network call. */
5212
+ ref(identityId) {
5213
+ return new Identity(this.client, identityId);
5214
+ }
5215
+ delete(identityId) {
5216
+ return this.client.delete(identityPath(identityId));
5217
+ }
5218
+ };
5219
+
5220
+ // ../../node_modules/.bun/freestyle@0.2.16/node_modules/freestyle/dist/tls.js
5221
+ var TlsRuleValidationError = class _TlsRuleValidationError extends Error {
5222
+ /** The field the complaint is about, in dotted form. */
5223
+ path;
5224
+ constructor(path, message) {
5225
+ super(message);
5226
+ this.name = "TlsRuleValidationError";
5227
+ this.path = path;
5228
+ Object.setPrototypeOf(this, _TlsRuleValidationError.prototype);
5229
+ }
5230
+ };
5231
+ var TlsForwardAuthValidationError = class _TlsForwardAuthValidationError extends Error {
5232
+ /** The field the complaint is about, in dotted form. */
5233
+ path;
5234
+ constructor(path, message) {
5235
+ super(message);
5236
+ this.name = "TlsForwardAuthValidationError";
5237
+ this.path = path;
5238
+ Object.setPrototypeOf(this, _TlsForwardAuthValidationError.prototype);
5239
+ }
5240
+ };
5241
+ var ENDPOINT_FIELDS2 = ["vmId", "vpcId", "public", "host", "port"];
5242
+ var IDENTITY_FIELDS2 = ["vmId", "vpcId", "public", "host"];
5243
+ var PROTOCOLS2 = [
5244
+ "tcp",
5245
+ "http",
5246
+ "postgres",
5247
+ "minecraft",
5248
+ "smtp",
5249
+ "imap",
5250
+ "imaps",
5251
+ "socks5"
5252
+ ];
5253
+ var MAX_DOMAIN_LENGTH = 253;
5254
+ var MAX_LABEL_LENGTH = 63;
5255
+ var FORWARD_AUTH_FIELDS = [
5256
+ "url",
5257
+ "headers",
5258
+ "timeoutMs",
5259
+ "protectedCookies",
5260
+ "authResponseHeaders"
5261
+ ];
5262
+ var FORWARD_AUTH_REFERENCE_FIELDS = ["id"];
5263
+ var HTTP_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
5264
+ var FORWARD_AUTH_HEADER_NAME = /^[0-9A-Za-z-]+$/;
5265
+ var MAX_FORWARD_AUTH_URL_LENGTH = 2048;
5266
+ var MAX_FORWARD_AUTH_HEADERS = 32;
5267
+ var MAX_FORWARD_AUTH_HEADER_BYTES = 16 * 1024;
5268
+ var MAX_FORWARD_AUTH_RESPONSE_HEADERS = 32;
5269
+ var MAX_FORWARD_AUTH_COOKIES = 32;
5270
+ var MAX_FORWARD_AUTH_COOKIE_NAME_LENGTH = 128;
5271
+ var RESERVED_FORWARD_AUTH_HEADERS = /* @__PURE__ */ new Set([
5272
+ "host",
5273
+ "content-length",
5274
+ "connection",
5275
+ "keep-alive",
5276
+ "proxy-authenticate",
5277
+ "proxy-authorization",
5278
+ "proxy-connection",
5279
+ "te",
5280
+ "trailer",
5281
+ "transfer-encoding",
5282
+ "upgrade"
5283
+ ]);
5284
+ function fail2(path, message) {
5285
+ throw new TlsRuleValidationError(path, message);
5286
+ }
5287
+ function failForwardAuth(path, message) {
5288
+ throw new TlsForwardAuthValidationError(path, message);
5289
+ }
5290
+ function hasTlsIdentity(endpoint) {
5291
+ return IDENTITY_FIELDS2.some((field) => endpoint[field] !== void 0);
5292
+ }
5293
+ function isEmptyEndpoint(endpoint) {
5294
+ return !hasTlsIdentity(endpoint) && endpoint.port === void 0;
5295
+ }
5296
+ function isPlainObject2(value) {
5297
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5298
+ }
5299
+ function validateForwardAuthHeaderName(value, path, responseHeader) {
5300
+ if (typeof value !== "string" || !FORWARD_AUTH_HEADER_NAME.test(value)) {
5301
+ failForwardAuth(path, `${path} must be a valid HTTP header name`);
5302
+ }
5303
+ const lower = value.toLowerCase();
5304
+ if (RESERVED_FORWARD_AUTH_HEADERS.has(lower) || lower.startsWith("x-forwarded-") || lower.startsWith("x-freestyle-") || responseHeader && (lower === "cookie" || lower === "set-cookie")) {
5305
+ failForwardAuth(path, `${path} may not use reserved header \`${value}\``);
5306
+ }
5307
+ }
5308
+ function validateHeaderNameList(value, path) {
5309
+ if (!Array.isArray(value)) {
5310
+ failForwardAuth(path, `${path} must be an array of header names`);
5311
+ }
5312
+ if (value.length > MAX_FORWARD_AUTH_RESPONSE_HEADERS) {
5313
+ failForwardAuth(path, `${path} may contain at most ${MAX_FORWARD_AUTH_RESPONSE_HEADERS} names`);
5314
+ }
5315
+ const seen = /* @__PURE__ */ new Set();
5316
+ value.forEach((name, index) => {
5317
+ validateForwardAuthHeaderName(name, `${path}[${index}]`, true);
5318
+ const lower = name.toLowerCase();
5319
+ if (seen.has(lower)) {
5320
+ failForwardAuth(`${path}[${index}]`, `${path} contains duplicate header \`${name}\``);
5321
+ }
5322
+ seen.add(lower);
5323
+ });
5324
+ }
5325
+ function validateTlsForwardAuth(options) {
5326
+ if (!isPlainObject2(options)) {
5327
+ failForwardAuth("forwardAuth", "forwardAuth must be an object");
5328
+ }
5329
+ for (const field of Object.keys(options)) {
5330
+ if (!FORWARD_AUTH_FIELDS.includes(field)) {
5331
+ failForwardAuth(field, `Unknown field \`${field}\`. Expected one of: ${FORWARD_AUTH_FIELDS.join(", ")}`);
5332
+ }
5333
+ }
5334
+ const typed = options;
5335
+ if (typeof typed.url !== "string" || typed.url.length === 0 || typed.url.length > MAX_FORWARD_AUTH_URL_LENGTH) {
5336
+ failForwardAuth("url", "url is required and must be an absolute HTTPS URL");
5337
+ }
5338
+ let url;
5339
+ try {
5340
+ url = new URL(typed.url);
5341
+ } catch {
5342
+ failForwardAuth("url", "url must be an absolute HTTPS URL");
5343
+ }
5344
+ if (url.protocol !== "https:" || url.hostname.length === 0 || url.username.length > 0 || url.password.length > 0 || url.hash.length > 0 || url.port === "0") {
5345
+ failForwardAuth("url", "url must be an absolute HTTPS URL");
5346
+ }
5347
+ if (typed.headers !== void 0) {
5348
+ if (!isPlainObject2(typed.headers)) {
5349
+ failForwardAuth("headers", "headers must be an object of header name to value");
5350
+ }
5351
+ const entries = Object.entries(typed.headers);
5352
+ if (entries.length > MAX_FORWARD_AUTH_HEADERS) {
5353
+ failForwardAuth("headers", `headers may contain at most ${MAX_FORWARD_AUTH_HEADERS} entries`);
5354
+ }
5355
+ let totalBytes = 0;
5356
+ const seenNames = /* @__PURE__ */ new Set();
5357
+ for (const [name, value] of entries) {
5358
+ validateForwardAuthHeaderName(name, `headers.${name}`, false);
5359
+ const lower = name.toLowerCase();
5360
+ if (seenNames.has(lower)) {
5361
+ failForwardAuth(`headers.${name}`, `headers contains duplicate header \`${name}\``);
5362
+ }
5363
+ seenNames.add(lower);
5364
+ if (typeof value !== "string" || /[\0\r\n]/.test(value)) {
5365
+ failForwardAuth(`headers.${name}`, `headers.${name} must be a string`);
5366
+ }
5367
+ totalBytes += name.length + value.length;
5368
+ }
5369
+ if (totalBytes > MAX_FORWARD_AUTH_HEADER_BYTES) {
5370
+ failForwardAuth("headers", `headers may contain at most ${MAX_FORWARD_AUTH_HEADER_BYTES} bytes`);
5371
+ }
5372
+ }
5373
+ if (typed.timeoutMs !== void 0 && (!Number.isInteger(typed.timeoutMs) || typed.timeoutMs < 100 || typed.timeoutMs > 5e3)) {
5374
+ failForwardAuth("timeoutMs", "timeoutMs must be an integer between 100 and 5000");
5375
+ }
5376
+ if (typed.protectedCookies !== void 0) {
5377
+ if (!Array.isArray(typed.protectedCookies)) {
5378
+ failForwardAuth("protectedCookies", "protectedCookies must be an array of cookie names");
5379
+ }
5380
+ if (typed.protectedCookies.length > MAX_FORWARD_AUTH_COOKIES) {
5381
+ failForwardAuth("protectedCookies", `protectedCookies may contain at most ${MAX_FORWARD_AUTH_COOKIES} names`);
5382
+ }
5383
+ const seen = /* @__PURE__ */ new Set();
5384
+ typed.protectedCookies.forEach((name, index) => {
5385
+ if (typeof name !== "string" || name.length > MAX_FORWARD_AUTH_COOKIE_NAME_LENGTH || !HTTP_TOKEN.test(name)) {
5386
+ failForwardAuth(`protectedCookies[${index}]`, `protectedCookies[${index}] must be a valid HTTP cookie name`);
5387
+ }
5388
+ if (seen.has(name)) {
5389
+ failForwardAuth(`protectedCookies[${index}]`, `protectedCookies contains duplicate cookie \`${name}\``);
5390
+ }
5391
+ seen.add(name);
5392
+ });
5393
+ }
5394
+ if (typed.authResponseHeaders !== void 0) {
5395
+ validateHeaderNameList(typed.authResponseHeaders, "authResponseHeaders");
5396
+ }
5397
+ return typed;
5398
+ }
5399
+ function validateTlsForwardAuthReference(value, path) {
5400
+ if (!isPlainObject2(value)) {
5401
+ fail2(path, `${path} must be an object with an \`id\``);
5402
+ }
5403
+ for (const field of Object.keys(value)) {
5404
+ if (!FORWARD_AUTH_REFERENCE_FIELDS.includes(field)) {
5405
+ fail2(`${path}.${field}`, `${path} has unknown field \`${field}\`. Expected: ${FORWARD_AUTH_REFERENCE_FIELDS.join(", ")}`);
5406
+ }
5407
+ }
5408
+ const id2 = value.id;
5409
+ if (typeof id2 !== "string" || id2.length === 0) {
5410
+ fail2(`${path}.id`, `${path}.id is required and must be a non-empty string`);
5411
+ }
5412
+ }
5413
+ function validateTlsDomain(domain) {
5414
+ if (typeof domain !== "string") {
5415
+ fail2("domain", "domain is required and must be a string");
5416
+ }
5417
+ if (domain === "*")
5418
+ return;
5419
+ const bare = domain.startsWith("*.") ? domain.slice(2) : domain;
5420
+ if (bare.length === 0 || bare.length > MAX_DOMAIN_LENGTH) {
5421
+ fail2("domain", `domain \`${domain}\` is not a hostname`);
5422
+ }
5423
+ const labels = bare.split(".");
5424
+ if (labels.length < 2) {
5425
+ fail2("domain", `domain \`${domain}\` must have at least two labels, e.g. acme.com`);
5426
+ }
5427
+ for (const label of labels) {
5428
+ if (label.length === 0 || label.length > MAX_LABEL_LENGTH) {
5429
+ fail2("domain", `domain \`${domain}\` has an empty or over-long label`);
5430
+ }
5431
+ if (!/^[a-z0-9-]+$/.test(label)) {
5432
+ fail2("domain", `domain \`${domain}\`: labels are lowercase letters, digits, and hyphens (a \`*.\` prefix is the only wildcard)`);
5433
+ }
5434
+ if (label.startsWith("-") || label.endsWith("-")) {
5435
+ fail2("domain", `domain \`${domain}\`: a label cannot start or end with a hyphen`);
5436
+ }
5437
+ }
5438
+ }
5439
+ var ATEXT_PUNCT = "!#$%&'*+-/=?^_`{|}~.";
5440
+ function validateTlsMailRoute(domain) {
5441
+ if (typeof domain !== "string") {
5442
+ fail2("domain", "domain is required and must be a string");
5443
+ }
5444
+ const at = domain.lastIndexOf("@");
5445
+ if (at === -1) {
5446
+ validateTlsDomain(domain);
5447
+ return;
5448
+ }
5449
+ const local = domain.slice(0, at);
5450
+ const bare = domain.slice(at + 1);
5451
+ if (local.length === 0 || local.length > 64) {
5452
+ fail2("domain", `mail route \`${domain}\`: the part before \`@\` must be 1-64 characters`);
5453
+ }
5454
+ if (local.startsWith(".") || local.endsWith(".") || local.includes("..")) {
5455
+ fail2("domain", `mail route \`${domain}\`: the part before \`@\` cannot start or end with a dot, or contain \`..\``);
5456
+ }
5457
+ for (const character of local) {
5458
+ if (!/[a-zA-Z0-9]/.test(character) && !ATEXT_PUNCT.includes(character)) {
5459
+ fail2("domain", `mail route \`${domain}\`: the part before \`@\` may use letters, digits, dots, and \`${ATEXT_PUNCT.slice(0, -1)}\` (quoted mailboxes are not supported)`);
5460
+ }
5461
+ }
5462
+ if (bare === "*") {
5463
+ fail2("domain", `mail route \`${domain}\`: \`*\` is not a mail domain \u2014 name the domain the mailbox lives at`);
5464
+ }
5465
+ validateTlsDomain(bare);
5466
+ }
5467
+ function validateTlsEndpoint(endpoint, side) {
5468
+ if (!isPlainObject2(endpoint)) {
5469
+ fail2(side, `${side} must be an object`);
5470
+ }
5471
+ for (const field of Object.keys(endpoint)) {
5472
+ if (!ENDPOINT_FIELDS2.includes(field)) {
5473
+ fail2(`${side}.${field}`, `${side} has unknown field \`${field}\`. Expected one of: ${ENDPOINT_FIELDS2.join(", ")}`);
5474
+ }
5475
+ }
5476
+ const matcher = endpoint;
5477
+ for (const field of ["vmId", "vpcId", "host"]) {
5478
+ const value = matcher[field];
5479
+ if (value !== void 0 && (typeof value !== "string" || value.length === 0)) {
5480
+ fail2(`${side}.${field}`, `${side}.${field} must be a non-empty string`);
5481
+ }
5482
+ }
5483
+ if (matcher.public !== void 0 && matcher.public !== true) {
5484
+ fail2(`${side}.public`, `${side}.public must be true when present. Omit the field to match something else`);
5485
+ }
5486
+ if (side === "source") {
5487
+ if (matcher.host !== void 0) {
5488
+ fail2("source.host", "source.host is not allowed: a source is who opens the session, not where it lands. Put `host` on the destination");
5489
+ }
5490
+ if (matcher.port !== void 0) {
5491
+ fail2("source.port", "source.port is not allowed: a source is who opens the session, not where it lands. Put `port` on the destination");
5492
+ }
5493
+ if (matcher.public === true && (matcher.vmId !== void 0 || matcher.vpcId !== void 0)) {
5494
+ fail2("source.public", "source cannot combine `public: true` with `vmId` or `vpcId`: use one alone. `public` is every client on the Internet; `vmId`/`vpcId` is a specific dialer");
5495
+ }
5496
+ } else {
5497
+ if (matcher.vpcId !== void 0) {
5498
+ fail2("destination.vpcId", "destination.vpcId is not allowed: a session lands in one place, not on a whole network. Name a `vmId`, a `host`, or `public: true` for the domain's own origin. A whole network may be a source");
5499
+ }
5500
+ const landings = [
5501
+ matcher.vmId !== void 0,
5502
+ matcher.host !== void 0,
5503
+ matcher.public === true
5504
+ ].filter(Boolean).length;
5505
+ if (landings > 1) {
5506
+ fail2("destination", "destination names more than one landing: give exactly one of `vmId`, `host`, or `public: true`");
5507
+ }
5508
+ if ((matcher.vmId !== void 0 || matcher.host !== void 0) && matcher.port === void 0) {
5509
+ fail2("destination.port", `destination.port is required with ${matcher.vmId !== void 0 ? "`vmId`" : "`host`"}: say which port the session lands on`);
5510
+ }
5511
+ }
5512
+ if (matcher.port !== void 0) {
5513
+ if (!Number.isInteger(matcher.port) || matcher.port < 1 || matcher.port > 65535) {
5514
+ fail2(`${side}.port`, `${side}.port must be an integer between 1 and 65535`);
5515
+ }
5516
+ }
5517
+ }
5518
+ function transformProtocol(transform) {
5519
+ if ("postgres" in transform)
5520
+ return "postgres";
5521
+ if ("smtp" in transform)
5522
+ return "smtp";
5523
+ if ("socks5" in transform)
5524
+ return "socks5";
5525
+ return "http";
5526
+ }
5527
+ function validateS3Transform(value, path) {
5528
+ function object(value2, fields, path2) {
5529
+ if (!isPlainObject2(value2) || Object.keys(value2).some((key) => !fields.includes(key))) {
5530
+ fail2(path2, `${path2} must contain only ${fields.join(", ")}`);
5531
+ }
5532
+ }
5533
+ object(value, ["region", "credentials", "scope"], path);
5534
+ if (typeof value.region !== "string" || !/^[a-z0-9-]{1,63}$/.test(value.region)) {
5535
+ fail2(`${path}.region`, "invalid s3.region");
5536
+ }
5537
+ object(value.credentials, ["accessKeyId", "secretAccessKey", "sessionToken"], `${path}.credentials`);
5538
+ for (const field of ["accessKeyId", "secretAccessKey", "sessionToken"]) {
5539
+ const credential = value.credentials[field];
5540
+ if (field === "sessionToken" && credential === void 0)
5541
+ continue;
5542
+ if (typeof credential !== "string" || !/^[\x21-\x7e]{1,16384}$/.test(credential)) {
5543
+ fail2(`${path}.credentials.${field}`, "invalid s3 credentials");
5544
+ }
5545
+ }
5546
+ if (!/^[a-zA-Z0-9]+$/.test(value.credentials.accessKeyId)) {
5547
+ fail2(`${path}.credentials.accessKeyId`, "invalid s3 access key id");
5548
+ }
5549
+ object(value.scope, ["bucket", "prefix", "operations"], `${path}.scope`);
5550
+ const { bucket, prefix, operations } = value.scope;
5551
+ if (typeof bucket !== "string" || !/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/.test(bucket) || bucket.includes("..")) {
5552
+ fail2(`${path}.scope.bucket`, "invalid s3.scope.bucket");
5553
+ }
5554
+ if (typeof prefix !== "string" || Buffer.byteLength(prefix, "utf8") > 1024 || /[\x00-\x1f\x7f-\x9f]/u.test(prefix)) {
5555
+ fail2(`${path}.scope.prefix`, "invalid s3.scope.prefix");
5556
+ }
5557
+ const allowed = ["GetObject", "HeadObject", "PutObject", "DeleteObject", "ListObjectsV2", "CreateMultipartUpload", "UploadPart", "ListParts", "CompleteMultipartUpload", "AbortMultipartUpload", "ListMultipartUploads"];
5558
+ if (!Array.isArray(operations) || operations.length === 0 || operations.length > allowed.length || operations.some((op) => !allowed.includes(op)) || new Set(operations).size !== operations.length) {
5559
+ fail2(`${path}.scope.operations`, "s3.scope.operations must be a nonempty list of distinct supported operations");
5560
+ }
5561
+ }
5562
+ function validateTlsTransform(transform, path) {
5563
+ if (!isPlainObject2(transform)) {
5564
+ fail2(path, `${path} must be an object: \`{ headers: { \u2026 } }\`, \`{ postgres: { \u2026 } }\`, \`{ smtp: { \u2026 } }\` or \`{ socks5: { \u2026 } }\``);
5565
+ }
5566
+ const keys = Object.keys(transform);
5567
+ if (keys.length !== 1 || !["headers", "jsonPatch", "postgres", "smtp", "socks5", "s3"].includes(keys[0] ?? "")) {
5568
+ fail2(path, `${path} must have exactly one of \`headers\`, \`jsonPatch\`, \`postgres\`, \`smtp\`, \`socks5\` or \`s3\` (got: ${keys.length === 0 ? "nothing" : keys.join(", ")})`);
5569
+ }
5570
+ if (keys[0] === "s3") {
5571
+ validateS3Transform(transform.s3, `${path}.s3`);
5572
+ return;
5573
+ }
5574
+ if (keys[0] === "jsonPatch") {
5575
+ const patch = transform.jsonPatch;
5576
+ if (!Array.isArray(patch) || patch.length > 128) {
5577
+ fail2(path, `${path}.jsonPatch must be an array of at most 128 operations`);
5578
+ }
5579
+ const pointer = (value) => typeof value === "string" && (value === "" || value.startsWith("/")) && !/~(?:[^01]|$)/u.test(value);
5580
+ const jsonValue = (value, seen = /* @__PURE__ */ new Set(), depth = 0) => {
5581
+ if (depth > 64)
5582
+ return false;
5583
+ if (value === null || typeof value === "string" || typeof value === "boolean")
5584
+ return true;
5585
+ if (typeof value === "number")
5586
+ return Number.isFinite(value);
5587
+ if (typeof value !== "object" || seen.has(value))
5588
+ return false;
5589
+ seen.add(value);
5590
+ const valid = (Array.isArray(value) ? Array.from(value) : isPlainObject2(value) ? Object.values(value) : [void 0]).every((entry) => jsonValue(entry, seen, depth + 1));
5591
+ seen.delete(value);
5592
+ return valid;
5593
+ };
5594
+ for (const [index, operation] of patch.entries()) {
5595
+ if (!isPlainObject2(operation) || !["add", "remove", "replace", "move", "copy", "test"].includes(String(operation.op)) || !pointer(operation.path)) {
5596
+ fail2(path, `${path}.jsonPatch[${index}] requires a standard op and valid JSON Pointer path`);
5597
+ }
5598
+ if (["move", "copy"].includes(operation.op) && !pointer(operation.from)) {
5599
+ fail2(path, `${path}.jsonPatch[${index}].from must be a JSON Pointer`);
5600
+ }
5601
+ if (["add", "replace", "test"].includes(operation.op) && (!Object.hasOwn(operation, "value") || !jsonValue(operation.value))) {
5602
+ fail2(path, `${path}.jsonPatch[${index}].value must be a JSON value (null is allowed)`);
5603
+ }
5604
+ }
5605
+ let encoded;
5606
+ try {
5607
+ encoded = JSON.stringify(patch);
5608
+ } catch {
5609
+ fail2(path, "jsonPatch must be JSON serializable");
5610
+ }
5611
+ if (Buffer.byteLength(encoded, "utf8") > 64 * 1024)
5612
+ fail2(path, "jsonPatch exceeds 64 KiB");
5613
+ return;
5614
+ }
5615
+ if (keys[0] === "socks5") {
5616
+ const socks5 = transform.socks5;
5617
+ if (!isPlainObject2(socks5)) {
5618
+ fail2(`${path}.socks5`, `${path}.socks5 must be an object with \`username\` and \`password\``);
5619
+ }
5620
+ for (const field of Object.keys(socks5)) {
5621
+ if (!["username", "password"].includes(field)) {
5622
+ fail2(`${path}.socks5.${field}`, `${path}.socks5 has unknown field \`${field}\`. Expected: username, password`);
5623
+ }
5624
+ }
5625
+ for (const field of ["username", "password"]) {
5626
+ if (typeof socks5[field] !== "string" || socks5[field].length === 0) {
5627
+ fail2(`${path}.socks5.${field}`, `${path}.socks5.${field} is required and must be a non-empty string`);
5628
+ }
5629
+ }
5630
+ return;
5631
+ }
5632
+ if (keys[0] === "smtp") {
5633
+ const smtp = transform.smtp;
5634
+ if (!isPlainObject2(smtp)) {
5635
+ fail2(`${path}.smtp`, `${path}.smtp must be an object with \`username\` and \`password\``);
5636
+ }
5637
+ for (const field of Object.keys(smtp)) {
5638
+ if (!["username", "password"].includes(field)) {
5639
+ fail2(`${path}.smtp.${field}`, `${path}.smtp has unknown field \`${field}\`. Expected: username, password`);
5640
+ }
5641
+ }
5642
+ for (const field of ["username", "password"]) {
5643
+ if (typeof smtp[field] !== "string" || smtp[field].length === 0) {
5644
+ fail2(`${path}.smtp.${field}`, `${path}.smtp.${field} is required and must be a non-empty string`);
5645
+ }
5646
+ }
5647
+ return;
5648
+ }
5649
+ if (keys[0] === "headers") {
5650
+ const headers = transform.headers;
5651
+ if (!isPlainObject2(headers)) {
5652
+ fail2(`${path}.headers`, `${path}.headers must be an object of header name to value`);
5653
+ }
5654
+ for (const [name, value] of Object.entries(headers)) {
5655
+ if (typeof value !== "string") {
5656
+ fail2(`${path}.headers.${name}`, `${path}.headers.${name} must be a string`);
5657
+ }
5658
+ }
5659
+ } else {
5660
+ const postgres2 = transform.postgres;
5661
+ if (!isPlainObject2(postgres2)) {
5662
+ fail2(`${path}.postgres`, `${path}.postgres must be an object with \`username\` and \`password\``);
5663
+ }
5664
+ for (const field of Object.keys(postgres2)) {
5665
+ if (!["username", "password", "database"].includes(field)) {
5666
+ fail2(`${path}.postgres.${field}`, `${path}.postgres has unknown field \`${field}\`. Expected: username, password, database`);
5667
+ }
5668
+ }
5669
+ const pg = postgres2;
5670
+ for (const field of ["username", "password"]) {
5671
+ if (typeof pg[field] !== "string" || pg[field].length === 0) {
5672
+ fail2(`${path}.postgres.${field}`, `${path}.postgres.${field} is required and must be a non-empty string`);
5673
+ }
5674
+ }
5675
+ if (pg.database !== void 0 && typeof pg.database !== "string") {
5676
+ fail2(`${path}.postgres.database`, `${path}.postgres.database must be a string`);
5677
+ }
5678
+ }
5679
+ }
5680
+ function resolveTlsProtocol(protocol, transforms) {
5681
+ let resolved = protocol;
5682
+ transforms.forEach((transform, index) => {
5683
+ const needed = transformProtocol(transform);
5684
+ if (resolved === void 0) {
5685
+ resolved = needed;
5686
+ } else if (resolved !== needed) {
5687
+ fail2(`transform[${index}]`, `transform requires protocol \`${needed}\` but the rule is \`${resolved}\`; a rule terminates one protocol`);
5688
+ }
5689
+ });
5690
+ return resolved ?? "http";
5691
+ }
5692
+ function validateHttpRequestMatch(value, path) {
5693
+ if (!isPlainObject2(value) || Object.keys(value).some((key) => key !== "method" && key !== "path")) {
5694
+ fail2(path, `${path} must be an object containing only method and path`);
5695
+ }
5696
+ if (value.method !== void 0 && (!Array.isArray(value.method) || value.method.some((method) => typeof method !== "string" || method.length === 0 || /[^!#$%&'*+.^_`|~0-9A-Za-z-]/.test(method)))) {
5697
+ fail2(`${path}.method`, `${path}.method must be an array of HTTP method tokens`);
5698
+ }
5699
+ if (value.path !== void 0) {
5700
+ const matcher = value.path;
5701
+ if (!isPlainObject2(matcher) || Object.keys(matcher).length !== 1 || typeof matcher.exact !== "string" || !matcher.exact.startsWith("/") || /[\s?#]/.test(matcher.exact)) {
5702
+ fail2(`${path}.path`, `${path}.path must contain exact: an absolute path without a query or fragment`);
5703
+ }
5704
+ }
5705
+ }
5706
+ var RULE_FIELDS2 = [
5707
+ "action",
5708
+ "domain",
5709
+ "protocol",
5710
+ "source",
5711
+ "destination",
5712
+ "transform",
5713
+ "match",
5714
+ "forwardAuth"
5715
+ ];
5716
+ function validateRuleShape2(rule, prefix) {
5717
+ if (!isPlainObject2(rule)) {
5718
+ fail2(prefix || "rule", `${prefix || "rule"} must be an object`);
5719
+ }
5720
+ for (const field of Object.keys(rule)) {
5721
+ if (!RULE_FIELDS2.includes(field)) {
5722
+ fail2(`${prefix}${field}`, `Unknown field \`${field}\`. Expected one of: ${RULE_FIELDS2.join(", ")}`);
5723
+ }
5724
+ }
5725
+ const typed = rule;
5726
+ if (typed.action === void 0) {
5727
+ fail2(`${prefix}action`, `${prefix}action is required. Write \`action: "allow"\` \u2014 every rule states what it does rather than defaulting to it`);
5728
+ }
5729
+ if (typed.action !== "allow") {
5730
+ fail2(`${prefix}action`, `${prefix}action must be "allow" (got \`${String(typed.action)}\`). Only allow rules exist today`);
5731
+ }
5732
+ if (typed.protocol !== void 0 && !PROTOCOLS2.includes(typed.protocol)) {
5733
+ fail2(`${prefix}protocol`, `${prefix}protocol must be one of: ${PROTOCOLS2.join(", ")} (got \`${String(typed.protocol)}\`)`);
5734
+ }
5735
+ const source = typed.source ?? {};
5736
+ const destination = typed.destination ?? {};
5737
+ validateTlsEndpoint(source, "source");
5738
+ validateTlsEndpoint(destination, "destination");
5739
+ const transforms = typed.transform ?? [];
5740
+ if (!Array.isArray(transforms)) {
5741
+ fail2(`${prefix}transform`, `${prefix}transform must be an array`);
5742
+ }
5743
+ transforms.forEach((transform, index) => validateTlsTransform(transform, `${prefix}transform[${index}]`));
5744
+ const protocol = resolveTlsProtocol(typed.protocol, transforms);
5745
+ if (typed.match !== void 0) {
5746
+ validateHttpRequestMatch(typed.match, `${prefix}match`);
5747
+ if (protocol !== "http" || source.public === true || !(destination.public === true || destination.host !== void 0)) {
5748
+ fail2(`${prefix}match`, "match requires HTTP egress from a VM or VPC to a public origin or host");
5749
+ }
5750
+ }
5751
+ if (transforms.some((entry) => "s3" in entry)) {
5752
+ if (transforms.length !== 1 || typed.match !== void 0 || source.public === true || !(destination.public === true || destination.host !== void 0) || typed.domain.includes("*")) {
5753
+ fail2(`${prefix}transform`, "s3 requires a sole transform on HTTP egress to a concrete origin, without match");
5754
+ }
5755
+ }
5756
+ const patches = transforms.filter((entry) => "jsonPatch" in entry);
5757
+ if (patches.length > 1) {
5758
+ fail2(`${prefix}transform`, "a rule may carry only one jsonPatch transform");
5759
+ }
5760
+ if (patches.length && (source.public === true || !(destination.public === true || destination.host !== void 0))) {
5761
+ fail2(`${prefix}transform`, "jsonPatch requires HTTP egress from a VM or VPC to a public origin or host");
5762
+ }
5763
+ if (typed.forwardAuth !== void 0) {
5764
+ validateTlsForwardAuthReference(typed.forwardAuth, `${prefix}forwardAuth`);
5765
+ const destinationIsVm = destination.vmId !== void 0 || !hasTlsIdentity(destination) && destination.port !== void 0;
5766
+ if (protocol !== "http" || source.public !== true || !destinationIsVm || typed.domain === "*") {
5767
+ fail2(`${prefix}forwardAuth`, `${prefix}forwardAuth is only supported on HTTP public ingress: \`{ public: true } -> { vmId, port }\``);
5768
+ }
5769
+ }
5770
+ if (protocol === "smtp") {
5771
+ validateTlsMailRoute(typed.domain);
5772
+ } else {
5773
+ validateTlsDomain(typed.domain);
5774
+ }
5775
+ const internalService = (source.vmId !== void 0 || source.vpcId !== void 0) && destination.vmId !== void 0;
5776
+ if (internalService && transforms.length > 0) {
5777
+ fail2(`${prefix}transform`, "a VM-to-VM rule cannot carry a transform yet \u2014 remove it (only public ingress/egress rules run transforms)");
5778
+ }
5779
+ if (protocol === "tcp") {
5780
+ const destinationIsWorld = destination.public === true || destination.host !== void 0;
5781
+ const sourceIsIdentity = source.vmId !== void 0 || source.vpcId !== void 0;
5782
+ if (destinationIsWorld || sourceIsIdentity) {
5783
+ fail2(`${prefix}protocol`, "a `tcp` rule is public ingress only \u2014 `{ public: true } -> { vmId, port }`. The edge splices the session without terminating it, so outbound there is nothing to broker (an outbound allow is a firewall rule, and injecting into one needs `http` or `postgres`) and inward there is no certificate to answer the source with. Give the source `public: true`, or drop `protocol` to be served as HTTPS");
5784
+ }
5785
+ } else if (protocol === "imap" || protocol === "imaps") {
5786
+ const destinationIsWorld = destination.public === true || destination.host !== void 0;
5787
+ const sourceIsIdentity = source.vmId !== void 0 || source.vpcId !== void 0;
5788
+ if (destinationIsWorld || sourceIsIdentity) {
5789
+ fail2(`${prefix}protocol`, `an \`${protocol}\` rule is public ingress only \u2014 \`{ public: true } -> { vmId, port }\`. The 993 front serves mail clients reaching a VM's IMAP server; outbound there is nothing to broker (an outbound allow is a firewall rule) and inward there is no certificate to answer the source with`);
5790
+ }
5791
+ } else if (protocol === "socks5") {
5792
+ const destinationIsWorld = destination.public === true || destination.host !== void 0;
5793
+ const sourceIsIdentity = source.public !== true;
5794
+ if (!sourceIsIdentity || !destinationIsWorld) {
5795
+ fail2(`${prefix}protocol`, 'a `socks5` rule is egress only \u2014 `{ vmId } -> { host: "gate.provider.com", port: 1080 }`. The front exists so a guest can open sessions through a proxy whose credentials it never holds; inbound there is nothing to serve, and publishing one would be an open proxy on a platform address');
5796
+ }
5797
+ if (destination.host === void 0) {
5798
+ fail2(`${prefix}destination`, "a `socks5` rule has to name the proxy it goes through: give the destination a `host` and the `port` the provider listens on. `{ public: true }` on its own asks the edge to be the proxy, which would let the guest out on our address rather than through yours");
5799
+ }
5800
+ } else if (protocol === "postgres") {
5801
+ const injects = transforms.some((transform) => transform !== null && typeof transform === "object" && "postgres" in transform);
5802
+ const destinationIsWorld = destination.public === true || destination.host !== void 0;
5803
+ if (injects && !destinationIsWorld) {
5804
+ fail2(`${prefix}transform`, "a `postgres` transform belongs on an egress rule: the edge spends a credential the guest never holds, so pointing it at your own VM would hand every client a logged-in session. Give the destination `public: true` or a `host`, or drop the transform");
5805
+ }
5806
+ } else if (protocol === "smtp") {
5807
+ const injects = transforms.some((transform) => transform !== null && typeof transform === "object" && "smtp" in transform);
5808
+ const destinationIsWorld = destination.public === true || destination.host !== void 0;
5809
+ if (injects && !destinationIsWorld) {
5810
+ fail2(`${prefix}transform`, "an `smtp` transform belongs on an egress rule: the edge spends a credential the guest never holds, so pointing it at your own VM would turn it into an open relay. Give the destination `public: true` or a `host`, or drop the transform");
5811
+ }
5812
+ if (typeof typed.domain === "string" && typed.domain.includes("@") && source.public !== true) {
5813
+ fail2(`${prefix}domain`, `\`${typed.domain}\` names a mailbox, which only makes sense on a rule that receives mail. An egress or VM-to-VM rule names the host the guest submits through \u2014 drop the part before \`@\``);
5814
+ }
5815
+ }
5816
+ return { ...typed, source, destination };
5817
+ }
5818
+ function missingIdentityMessage(side, endpoint) {
5819
+ if (side === "source") {
5820
+ return isEmptyEndpoint(endpoint) ? "source is empty. Say who opens the session: `vmId`, `vpcId`, or `public: true`" : "source only narrows the session without saying who opens it. Add `vmId`, `vpcId`, or `public: true`";
5821
+ }
5822
+ return "destination is empty. Say where the session lands: `vmId` + `port`, `host` + `port`, or `public: true` for the domain's own origin";
5823
+ }
5824
+ function validateTlsRule(rule) {
5825
+ const typed = validateRuleShape2(rule, "");
5826
+ for (const side of ["source", "destination"]) {
5827
+ const endpoint = typed[side];
5828
+ if (!hasTlsIdentity(endpoint)) {
5829
+ fail2(side, missingIdentityMessage(side, endpoint));
5830
+ }
5831
+ }
5832
+ if (typed.source.public === true && typed.destination.public === true) {
5833
+ fail2("destination", "a rule from the public Internet to the domain's own origin allows nothing the world cannot already do. Did you mean a `vmId` destination (ingress) or a `vmId` source (egress)?");
5834
+ }
5835
+ return typed;
5836
+ }
5837
+ function validateTlsSpec(spec) {
5838
+ if (!isPlainObject2(spec)) {
5839
+ fail2("tls", "tls must be an object with a `rules` array");
5840
+ }
5841
+ for (const field of Object.keys(spec)) {
5842
+ if (field !== "rules") {
5843
+ fail2(`tls.${field}`, `tls has unknown field \`${field}\`. Expected: rules`);
5844
+ }
5845
+ }
5846
+ const rules = spec.rules;
5847
+ if (!Array.isArray(rules)) {
5848
+ fail2("tls.rules", "tls.rules must be an array");
5849
+ }
5850
+ rules.forEach((rule, index) => {
5851
+ const prefix = `tls.rules[${index}].`;
5852
+ const typed = validateRuleShape2(rule, prefix);
5853
+ const sourceNames = hasTlsIdentity(typed.source);
5854
+ const destinationNames = hasTlsIdentity(typed.destination);
5855
+ if (!sourceNames && !destinationNames) {
5856
+ fail2(`tls.rules[${index}]`, `tls.rules[${index}]: both source and destination refer to the VM being created, which allows nothing. Give one of them a \`vmId\`, \`vpcId\`, or \`public: true\``);
5857
+ }
5858
+ if (sourceNames && destinationNames) {
5859
+ fail2(`tls.rules[${index}]`, `tls.rules[${index}]: neither end refers to the VM being created. Leave one endpoint's identity unset so it means this VM, or create the rule with tls.rules.create instead`);
5860
+ }
5861
+ if (!destinationNames && typed.destination.port === void 0) {
5862
+ fail2(`tls.rules[${index}].destination.port`, `tls.rules[${index}]: destination.port is required: say which port on this VM the session lands on`);
5863
+ }
5864
+ });
5865
+ return spec;
5866
+ }
5867
+ function tlsRulePath(ruleId) {
5868
+ return `/v5/tls/${segment(ruleId)}`;
5869
+ }
5870
+ function tlsForwardAuthPath(id2) {
5871
+ return `/v5/tls/forward-auth/${segment(id2)}`;
5872
+ }
5873
+ var TlsForwardAuthNamespace = class {
5874
+ client;
5875
+ constructor(client) {
5876
+ this.client = client;
5877
+ }
5878
+ /** List your forward-auth configurations, newest first. */
5879
+ list() {
5880
+ return this.client.get("/v5/tls/forward-auth");
5881
+ }
5882
+ /** Create a forward-auth configuration. Static request-header values are write-only. */
5883
+ async create(options) {
5884
+ const validated = validateTlsForwardAuth(options);
5885
+ return this.client.post("/v5/tls/forward-auth", validated);
5886
+ }
5887
+ /** Fetch one forward-auth configuration by id. */
5888
+ get(id2) {
5889
+ return this.client.get(tlsForwardAuthPath(id2));
5890
+ }
5891
+ /** Replace a configuration in place, including rotating its static headers. */
5892
+ async update(id2, options) {
5893
+ const validated = validateTlsForwardAuth(options);
5894
+ return this.client.put(tlsForwardAuthPath(id2), validated);
5895
+ }
5896
+ /** Delete an unused forward-auth configuration. */
5897
+ delete(id2) {
5898
+ return this.client.delete(tlsForwardAuthPath(id2));
5899
+ }
5900
+ };
5901
+ var TlsRulesNamespace = class {
5902
+ client;
5903
+ constructor(client) {
5904
+ this.client = client;
5905
+ }
5906
+ /**
5907
+ * Allow a named TLS session: `domain`, from `source`, to `destination`.
5908
+ *
5909
+ * An ingress `domain` is a name you have verified, or any unused subdomain
5910
+ * of `style.dev`, which is free and needs no verification or DNS records.
5911
+ *
5912
+ * @example
5913
+ * // Public HTTPS ingress: anyone dialing app.acme.com lands on a VM's port.
5914
+ * await freestyle.tls.rules.create({
5915
+ * action: "allow",
5916
+ * domain: "app.acme.com",
5917
+ * source: { public: true },
5918
+ * destination: { vmId: "vm-123456", port: 8000 },
5919
+ * });
5920
+ *
5921
+ * @example
5922
+ * // Egress with a secret injected: a VM reaches OpenAI at its real origin,
5923
+ * // and the edge adds the Authorization header the guest never held.
5924
+ * await freestyle.tls.rules.create({
5925
+ * action: "allow",
5926
+ * domain: "api.openai.com",
5927
+ * source: { vmId: "vm-123456" },
5928
+ * destination: { public: true },
5929
+ * transform: [{ headers: { authorization: "Bearer sk-…" } }],
5930
+ * });
5931
+ *
5932
+ * @example
5933
+ * // Internal service: one VM reaches another by a name. The source opens
5934
+ * // https://api.internal; the edge terminates it and forwards to the target
5935
+ * // VM's port. Steered at the edge, HTTP-only, no transform.
5936
+ * await freestyle.tls.rules.create({
5937
+ * action: "allow",
5938
+ * domain: "api.internal",
5939
+ * source: { vmId: "vm-123456" },
5940
+ * destination: { vmId: "vm-654321", port: 8000 },
5941
+ * });
5942
+ *
5943
+ * @throws {TlsRuleValidationError} before any request, when the rule cannot be
5944
+ * well formed.
5945
+ */
5946
+ // `async` so a validation failure arrives as a rejected promise like every
5947
+ // other error from this client, instead of a synchronous throw that a
5948
+ // `.catch()` chain would sail straight past.
5949
+ async create(options) {
5950
+ const validated = validateTlsRule(options);
5951
+ return this.client.post("/v5/tls", validated);
5952
+ }
5953
+ /**
5954
+ * List your rules, newest first. Pass `vmId` for the rules that apply to one
5955
+ * VM — those naming it, plus those naming a private network it is on.
5956
+ */
5957
+ list(options = {}) {
5958
+ return this.client.get("/v5/tls", options);
5959
+ }
5960
+ get(ruleId) {
5961
+ return this.client.get(tlsRulePath(ruleId));
5962
+ }
5963
+ /**
5964
+ * Replace a rule in place. The one mutation the firewall does not allow: a
5965
+ * transform carries a rotating secret, and delete-then-create would drop
5966
+ * traffic or trip the account limit mid-swap. The rule keeps its id and
5967
+ * creation time; everything in `options` is replaced.
5968
+ *
5969
+ * @throws {TlsRuleValidationError} before any request, when the replacement
5970
+ * cannot be well formed.
5971
+ */
5972
+ async update(ruleId, options) {
5973
+ const validated = validateTlsRule(options);
5974
+ return this.client.put(tlsRulePath(ruleId), validated);
5975
+ }
5976
+ /** Delete a rule. The resources it named are untouched. */
5977
+ delete(ruleId) {
5978
+ return this.client.delete(tlsRulePath(ruleId));
5979
+ }
5980
+ };
5981
+ var TlsNamespace = class {
5982
+ client;
5983
+ rules;
5984
+ forwardAuth;
5985
+ constructor(client) {
5986
+ this.client = client;
5987
+ this.rules = new TlsRulesNamespace(client);
5988
+ this.forwardAuth = new TlsForwardAuthNamespace(client);
5989
+ }
5990
+ };
5991
+
5992
+ // ../../node_modules/.bun/freestyle@0.2.16/node_modules/freestyle/dist/tunnel.js
5993
+ function tunnelPath(idOrSlug) {
5994
+ return `/v5/tunnels/${segment(idOrSlug)}`;
5995
+ }
5996
+ function withTunnelId(tunnel) {
5997
+ tunnel.tunnelId ??= tunnel.id;
5998
+ return tunnel;
5999
+ }
6000
+ var TunnelsNamespace = class {
6001
+ client;
6002
+ constructor(client) {
6003
+ this.client = client;
6004
+ }
6005
+ /** List your tunnels, newest first. Listings never include a private key. */
6006
+ list() {
6007
+ return this.client.get("/v5/tunnels").then((result) => {
6008
+ result.tunnels.forEach(withTunnelId);
6009
+ return result;
6010
+ });
6011
+ }
6012
+ /**
6013
+ * Create a tunnel, optionally attaching networks in the same call
6014
+ * (`vpcs`, all-or-nothing). This is a one-time setup: the tunnel lives
6015
+ * until you delete it and its config never changes — save it, bring it up
6016
+ * once, and manage which networks it reaches by attaching and detaching
6017
+ * them.
6018
+ */
6019
+ create(options = {}) {
6020
+ return this.client.post("/v5/tunnels", options).then(withTunnelId);
6021
+ }
6022
+ /** Read one tunnel by its id or its slug. Its config comes back with a blank `PrivateKey`. */
6023
+ get(tunnelIdOrSlug) {
6024
+ return this.client.get(tunnelPath(tunnelIdOrSlug)).then(withTunnelId);
6025
+ }
6026
+ /**
6027
+ * Rename a tunnel or change its slug. Only the labels move: the keys,
6028
+ * addresses, routes and attached networks are untouched, so a connected
6029
+ * client sees nothing change and the config you saved keeps working.
6030
+ */
6031
+ update(tunnelIdOrSlug, options) {
6032
+ return this.client.patch(tunnelPath(tunnelIdOrSlug), options).then(withTunnelId);
6033
+ }
6034
+ /**
6035
+ * Replace a tunnel's keys, keeping its id and every attached network's
6036
+ * address. Use this if you lost the config or need to revoke one that
6037
+ * leaked.
6038
+ */
6039
+ rotateKey(tunnelIdOrSlug, options = {}) {
6040
+ return this.client.post(`${tunnelPath(tunnelIdOrSlug)}/rotate-key`, options).then(withTunnelId);
6041
+ }
6042
+ /** Delete a tunnel, detaching every network as it goes. */
6043
+ delete(tunnelIdOrSlug) {
6044
+ return this.client.delete(tunnelPath(tunnelIdOrSlug));
6045
+ }
6046
+ /**
6047
+ * Attach a private network to a tunnel. The tunnel gains an address inside
6048
+ * the network — what that network's VMs see as your address — and a
6049
+ * connected client can reach it immediately, with no config change. The
6050
+ * network's CIDRs must fall inside the tunnel's routes and must not
6051
+ * overlap another attached network's.
6052
+ */
6053
+ attachVpc(tunnelIdOrSlug, vpcIdOrSlug, options = {}) {
6054
+ return this.client.post(`${tunnelPath(tunnelIdOrSlug)}/vpcs/${segment(vpcIdOrSlug)}`, options).then(withTunnelId);
6055
+ }
6056
+ /**
6057
+ * Detach a private network from a tunnel: it stops being reachable and the
6058
+ * tunnel's address inside it is released. The tunnel — and any connected
6059
+ * client — carries on untouched.
6060
+ */
6061
+ detachVpc(tunnelIdOrSlug, vpcIdOrSlug) {
6062
+ return this.client.delete(`${tunnelPath(tunnelIdOrSlug)}/vpcs/${segment(vpcIdOrSlug)}`).then(withTunnelId);
6063
+ }
6064
+ };
6065
+
6066
+ // ../../node_modules/.bun/freestyle@0.2.16/node_modules/freestyle/dist/vms/fs.js
6067
+ var RESUMABLE_THRESHOLD_BYTES = 8 * 1024 * 1024;
6068
+ var DEFAULT_CHUNK_BYTES = 16 * 1024 * 1024;
6069
+ var DEFAULT_MAX_ATTEMPTS = 5;
6070
+ var VmFilesystem = class {
6071
+ client;
6072
+ basePath;
6073
+ constructor(client, basePath) {
6074
+ this.client = client;
6075
+ this.basePath = basePath;
6076
+ }
6077
+ /**
6078
+ * Read a whole file, or with `offset`/`length` just part of one, into memory.
6079
+ *
6080
+ * The result is a plain `Uint8Array` on every runtime — never a Node
6081
+ * `Buffer`, even on Node. Don't coerce it with `String(bytes)` or
6082
+ * `bytes.toString()`: those go through the typed-array `toString` and
6083
+ * produce the byte values joined by commas ("123,10,32,…"), not the file's
6084
+ * text. For text use {@link readTextFile}, or decode explicitly with
6085
+ * `new TextDecoder().decode(bytes)`.
6086
+ */
6087
+ async readFile(path, options = {}) {
6088
+ const response = await this.openRead(path, options);
6089
+ return new Uint8Array(await response.arrayBuffer());
6090
+ }
6091
+ /** Read a file as UTF-8 text. Use this, not `String(await readFile(...))`. */
6092
+ async readTextFile(path, options = {}) {
6093
+ const response = await this.openRead(path, options);
6094
+ return await response.text();
6095
+ }
6096
+ /**
6097
+ * Stream a file out of the VM without holding it in memory — pipe it to disk,
6098
+ * to an HTTP response, or through a parser.
6099
+ *
6100
+ * ```ts
6101
+ * const bytes = await vm.fs.readFileStream("/var/log/big.log");
6102
+ * await pipeline(Readable.fromWeb(bytes), createWriteStream("big.log"));
6103
+ * ```
6104
+ */
6105
+ async readFileStream(path, options = {}) {
6106
+ const response = await this.openRead(path, options);
6107
+ if (!response.body)
6108
+ throw new Error(`no response body reading ${path}`);
6109
+ return response.body;
6110
+ }
6111
+ /**
6112
+ * Write a file into the VM, whatever its size.
6113
+ *
6114
+ * ```ts
6115
+ * await vm.fs.writeFile("/etc/app.conf", "debug = true");
6116
+ * await vm.fs.writeFile("/data/model.bin", await openAsBlob("model.bin"), {
6117
+ * onProgress: ({ completedBytes, totalBytes }) =>
6118
+ * console.log(`${completedBytes}/${totalBytes}`),
6119
+ * });
6120
+ * ```
6121
+ *
6122
+ * A `Blob` (`node:fs`'s `openAsBlob`, or a browser `File`) is read one chunk at
6123
+ * a time and never held in memory whole.
6124
+ */
6125
+ async writeFile(path, content, options = {}) {
6126
+ const body = typeof content === "string" ? new TextEncoder().encode(content) : content;
6127
+ const size = body instanceof Uint8Array ? body.byteLength : body.size;
6128
+ if (size > RESUMABLE_THRESHOLD_BYTES) {
6129
+ return this.uploadInChunks(path, body, size, options);
6130
+ }
6131
+ const bytes = body instanceof Uint8Array ? body : new Uint8Array(await body.arrayBuffer());
6132
+ await this.client.putBytes(`${this.basePath}/fs/write`, bytes, { path, mode: options.mode, sha256: await sha256Hex(bytes) }, options.signal);
6133
+ options.onProgress?.({ completedBytes: size, totalBytes: size });
6134
+ }
6135
+ /** Write text. Identical to {@link writeFile} with a string. */
6136
+ writeTextFile(path, content, options = {}) {
6137
+ return this.writeFile(path, content, options);
6138
+ }
6139
+ async readDir(path) {
6140
+ const { entries } = await this.client.get(`${this.basePath}/fs/dir`, { path });
6141
+ return entries;
6142
+ }
6143
+ async mkdir(path) {
6144
+ await this.client.post(`${this.basePath}/fs/mkdir`, { path });
6145
+ }
6146
+ async remove(path) {
6147
+ await this.client.delete(`${this.basePath}/fs/remove`, { path });
6148
+ }
6149
+ async exists(path) {
6150
+ const { exists } = await this.client.get(`${this.basePath}/fs/exists`, { path });
6151
+ return exists;
6152
+ }
6153
+ stat(path) {
6154
+ return this.client.get(`${this.basePath}/fs/stat`, { path });
6155
+ }
6156
+ openRead(path, options) {
6157
+ const { offset, length } = options;
6158
+ let range;
6159
+ if (offset !== void 0 || length !== void 0) {
6160
+ const start = offset ?? 0;
6161
+ range = length === void 0 ? `bytes=${start}-` : `bytes=${start}-${start + length - 1}`;
6162
+ }
6163
+ return this.client.getBytes(`${this.basePath}/fs/read`, { path }, { range, signal: options.signal });
6164
+ }
6165
+ /**
6166
+ * Send a large file as an upload session: ordered chunks, each retried
6167
+ * independently with backoff, so one dropped connection costs one chunk rather
6168
+ * than the whole transfer. The file appears at `path` only on commit.
6169
+ */
6170
+ async uploadInChunks(path, content, size, options) {
6171
+ const chunkSize = options.chunkSize ?? DEFAULT_CHUNK_BYTES;
6172
+ const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
6173
+ const sha256 = content instanceof Uint8Array ? await sha256Hex(content) : void 0;
6174
+ const session = await this.client.post(`${this.basePath}/fs/uploads`, {
6175
+ path,
6176
+ size,
6177
+ sha256,
6178
+ mode: options.mode
6179
+ });
6180
+ const uploadPath = `${this.basePath}/fs/uploads/${session.uploadId}`;
6181
+ try {
6182
+ let offset = session.receivedBytes;
6183
+ while (offset < size) {
6184
+ const end = Math.min(offset + chunkSize, size);
6185
+ const chunk = content instanceof Uint8Array ? content.subarray(offset, end) : content.slice(offset, end);
6186
+ const status = await this.putChunkWithRetry(uploadPath, chunk, offset, maxAttempts, options.signal);
6187
+ offset = status.receivedBytes;
6188
+ options.onProgress?.({ completedBytes: Math.min(offset, size), totalBytes: size });
6189
+ }
6190
+ await this.client.post(`${uploadPath}/commit`);
6191
+ } catch (error) {
6192
+ await this.client.delete(uploadPath).catch(() => {
6193
+ });
6194
+ throw error;
6195
+ }
6196
+ }
6197
+ /**
6198
+ * PUT one chunk, retrying transient failures. After any failure the session
6199
+ * is re-fetched: the server only counts fully-stored chunks, so its
6200
+ * `receivedBytes` tells us whether this chunk actually landed (return) or
6201
+ * must be resent (retry).
6202
+ */
6203
+ async putChunkWithRetry(uploadPath, chunk, offset, maxAttempts, signal) {
6204
+ let lastError;
6205
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
6206
+ signal?.throwIfAborted?.();
6207
+ try {
6208
+ return await this.client.putBytes(uploadPath, chunk, { offset }, signal);
6209
+ } catch (error) {
6210
+ lastError = error;
6211
+ const retryable = !(error instanceof FreestyleApiError) || error.status >= 500 || error.status === 408 || error.status === 409 || error.status === 429;
6212
+ if (!retryable || signal?.aborted || attempt === maxAttempts)
6213
+ throw error;
6214
+ await sleep(Math.min(500 * 2 ** (attempt - 1), 8e3));
6215
+ const status = await this.client.get(uploadPath).catch(() => void 0);
6216
+ if (status && status.receivedBytes > offset)
6217
+ return status;
6218
+ }
6219
+ }
6220
+ throw lastError;
6221
+ }
6222
+ };
6223
+ async function sha256Hex(bytes) {
6224
+ const subtle = globalThis.crypto?.subtle;
6225
+ if (!subtle)
6226
+ return void 0;
6227
+ const digest = await subtle.digest("SHA-256", bytes);
6228
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
6229
+ }
6230
+ function sleep(ms) {
6231
+ return new Promise((resolve) => setTimeout(resolve, ms));
6232
+ }
6233
+
6234
+ // ../../node_modules/.bun/freestyle@0.2.16/node_modules/freestyle/dist/vms/pty.js
6235
+ function isNodeRuntime2() {
6236
+ return typeof process !== "undefined" && Boolean(process.versions?.node);
6237
+ }
6238
+ async function openSocket(url, headers) {
6239
+ if (isNodeRuntime2()) {
6240
+ let wsModule;
6241
+ try {
6242
+ wsModule = await import("./wrapper-GH5LMTDE.js");
6243
+ } catch {
6244
+ throw new Error("PTY sessions need the `ws` package in Node. Install it with `npm install ws`.");
6245
+ }
6246
+ return new wsModule.default(url, { headers });
6247
+ }
6248
+ const NativeWebSocket = globalThis.WebSocket;
6249
+ if (!NativeWebSocket) {
6250
+ throw new Error("PTY sessions need a WebSocket client; no global WebSocket is available in this environment.");
6251
+ }
6252
+ return new NativeWebSocket(url);
6253
+ }
6254
+ function onSocketEvent(socket, event, listener) {
6255
+ if (socket.on) {
6256
+ socket.on(event, listener);
6257
+ return;
6258
+ }
6259
+ socket.addEventListener?.(event, listener);
6260
+ }
6261
+ var utf8Decoder = new TextDecoder();
6262
+ function normalizeMessage(raw, isBinary) {
6263
+ if (raw && typeof raw === "object" && "data" in raw) {
6264
+ const data = raw.data;
6265
+ if (typeof data === "string")
6266
+ return { text: data };
6267
+ if (data instanceof ArrayBuffer)
6268
+ return { binary: new Uint8Array(data) };
6269
+ return {};
6270
+ }
6271
+ if (raw instanceof Uint8Array)
6272
+ return isBinary ? { binary: raw } : { text: utf8Decoder.decode(raw) };
6273
+ if (typeof raw === "string")
6274
+ return { text: raw };
6275
+ return {};
6276
+ }
6277
+ var PtySession = class {
6278
+ _sessionId;
6279
+ _slug;
6280
+ _created = false;
6281
+ socket;
6282
+ constructor(socket, sessionId, events = {}) {
6283
+ this.socket = socket;
6284
+ this._sessionId = sessionId;
6285
+ onSocketEvent(socket, "message", (raw, isBinary) => {
6286
+ const { text, binary } = normalizeMessage(raw, isBinary);
6287
+ if (binary) {
6288
+ events.onData?.(binary);
6289
+ return;
6290
+ }
6291
+ if (!text)
6292
+ return;
6293
+ let msg;
6294
+ try {
6295
+ msg = JSON.parse(text);
6296
+ } catch {
6297
+ return;
6298
+ }
6299
+ if (msg.type === "exited")
6300
+ events.onExit?.(msg.exitCode);
6301
+ else if (msg.type === "error")
6302
+ events.onError?.(new Error(msg.message));
6303
+ else if (msg.type === "sessionInfo") {
6304
+ this._sessionId = msg.sessionId;
6305
+ this._slug = msg.slug;
6306
+ this._created = msg.created ?? true;
6307
+ events.onSessionInfo?.(msg.sessionId);
6308
+ }
6309
+ });
6310
+ onSocketEvent(socket, "close", (raw) => {
6311
+ const info = raw;
6312
+ events.onClose?.({
6313
+ wasClean: info?.wasClean ?? true,
6314
+ code: info?.code ?? 1e3,
6315
+ reason: info?.reason ?? ""
6316
+ });
6317
+ });
6318
+ onSocketEvent(socket, "error", (error) => events.onError?.(error));
6319
+ }
6320
+ /** Send raw bytes (or UTF-8 text) to the guest's stdin. */
6321
+ write(data) {
6322
+ this.socket.send(typeof data === "string" ? new TextEncoder().encode(data) : data);
6323
+ }
6324
+ resize(options) {
6325
+ this.sendControl({ type: "resize", cols: options.cols, rows: options.rows });
6326
+ }
6327
+ signal(sig) {
6328
+ this.sendControl({ type: "signal", signal: sig });
6329
+ }
6330
+ sendControl(msg) {
6331
+ this.socket.send(JSON.stringify(msg));
6332
+ }
6333
+ /** Close the connection. The shell keeps running; a later `attach()` can reconnect. */
6334
+ detach() {
6335
+ this.socket.close();
6336
+ }
6337
+ get sessionId() {
6338
+ return this._sessionId;
6339
+ }
6340
+ /**
6341
+ * The name this session answers to, from the server's `sessionInfo` frame.
6342
+ * `undefined` for a session opened without a slug — reattach to that one by
6343
+ * `sessionId` instead.
6344
+ */
6345
+ get slug() {
6346
+ return this._slug;
6347
+ }
6348
+ /**
6349
+ * Whether `open()` started this shell, or handed back one that was already
6350
+ * running under the same `slug`. `false` means your `exec` never ran — check
6351
+ * it before waiting on output from a command you thought you started.
6352
+ */
6353
+ get created() {
6354
+ return this._created;
6355
+ }
6356
+ get readyState() {
6357
+ return this.socket.readyState;
6358
+ }
6359
+ };
6360
+ function waitForOpen(socket) {
6361
+ return new Promise((resolve, reject) => {
6362
+ if (socket.readyState === 1) {
6363
+ resolve();
6364
+ return;
6365
+ }
6366
+ onSocketEvent(socket, "open", () => resolve());
6367
+ onSocketEvent(socket, "error", (error) => reject(error));
6368
+ });
6369
+ }
6370
+ var VmPty = class {
6371
+ client;
6372
+ basePath;
6373
+ constructor(client, basePath) {
6374
+ this.client = client;
6375
+ this.basePath = basePath;
6376
+ }
6377
+ /**
6378
+ * Open a new terminal on the VM. The returned session's data/exit
6379
+ * listeners are live from the moment the socket connects — installed
6380
+ * before we even know the session id — so a command that produces output
6381
+ * (or exits) immediately can never race past them.
6382
+ */
6383
+ async open(options = {}) {
6384
+ const { exec, cols, rows, linuxUser, slug, replaceOnExit, onData, onExit, onClose, onError } = options;
6385
+ const query = {};
6386
+ if (exec !== void 0)
6387
+ query.exec = exec;
6388
+ if (cols !== void 0)
6389
+ query.cols = cols;
6390
+ if (rows !== void 0)
6391
+ query.rows = rows;
6392
+ if (linuxUser !== void 0)
6393
+ query.linuxUser = linuxUser;
6394
+ if (slug !== void 0)
6395
+ query.slug = slug;
6396
+ if (replaceOnExit !== void 0)
6397
+ query.replaceOnExit = String(replaceOnExit);
6398
+ const url = wsUrl(this.client, `${this.basePath}/pty`, query);
6399
+ const headers = this.client.authHeaders();
6400
+ const socket = await openSocket(url, headers);
6401
+ return new Promise((resolve, reject) => {
6402
+ let settled = false;
6403
+ const session = new PtySession(socket, -1, {
6404
+ onData,
6405
+ onExit,
6406
+ onClose: (info) => {
6407
+ if (!settled) {
6408
+ settled = true;
6409
+ reject(new Error("connection closed before sessionInfo"));
6410
+ }
6411
+ onClose?.(info);
6412
+ },
6413
+ onError: (error) => {
6414
+ if (!settled) {
6415
+ settled = true;
6416
+ reject(error);
6417
+ }
6418
+ onError?.(error);
6419
+ },
6420
+ onSessionInfo: () => {
6421
+ settled = true;
6422
+ resolve(session);
6423
+ }
6424
+ });
6425
+ });
6426
+ }
6427
+ /** Reattach to an existing session; retained scrollback replays first. */
6428
+ async attach(options) {
6429
+ const { session: selector, linuxUser, onData, onExit, onClose, onError } = options;
6430
+ const url = wsUrl(this.client, `${this.basePath}/pty/sessions/${segment(String(selector))}`, {
6431
+ ...linuxUser !== void 0 ? { linuxUser } : {}
6432
+ });
6433
+ const headers = this.client.authHeaders();
6434
+ const socket = await openSocket(url, headers);
6435
+ const session = new PtySession(socket, typeof selector === "number" ? selector : -1, {
6436
+ onData,
6437
+ onExit,
6438
+ onClose,
6439
+ onError
6440
+ });
6441
+ await waitForOpen(socket);
6442
+ return session;
6443
+ }
6444
+ /** List the VM's terminal sessions, running and recently exited. */
6445
+ list(options = {}) {
6446
+ return this.client.get(`${this.basePath}/pty/sessions`, options);
6447
+ }
6448
+ /** Kill a terminal session and remove it. */
6449
+ close(session, options = {}) {
6450
+ return this.client.delete(`${this.basePath}/pty/sessions/${segment(String(session))}`, options);
6451
+ }
6452
+ };
6453
+ var VmLinuxUserPty = class {
6454
+ pty;
6455
+ linuxUser;
6456
+ constructor(pty, linuxUser) {
6457
+ this.pty = pty;
6458
+ this.linuxUser = linuxUser;
6459
+ }
6460
+ open(options = {}) {
6461
+ return this.pty.open({ ...options, linuxUser: this.linuxUser });
6462
+ }
6463
+ attach(options) {
6464
+ return this.pty.attach({ ...options, linuxUser: this.linuxUser });
6465
+ }
6466
+ list() {
6467
+ return this.pty.list({ linuxUser: this.linuxUser });
6468
+ }
6469
+ close(session) {
6470
+ return this.pty.close(session, { linuxUser: this.linuxUser });
6471
+ }
6472
+ };
6473
+ function wsUrl(client, path, query) {
6474
+ const httpUrl = client.url(path, query);
6475
+ return httpUrl.replace(/^http/, "ws");
6476
+ }
6477
+
6478
+ // ../../node_modules/.bun/freestyle@0.2.16/node_modules/freestyle/dist/vms/snapshots.js
6479
+ var VmSnapshotsNamespace = class {
6480
+ client;
6481
+ constructor(client) {
6482
+ this.client = client;
6483
+ }
6484
+ list(options = {}) {
6485
+ return this.client.get("/v5/snapshots", options);
6486
+ }
6487
+ get(snapshotId) {
6488
+ return this.client.get(`/v5/snapshots/${segment(snapshotId)}`);
6489
+ }
6490
+ update(snapshotId, options) {
6491
+ return this.client.patch(`/v5/snapshots/${segment(snapshotId)}`, options);
6492
+ }
6493
+ delete(snapshotId) {
6494
+ return this.client.delete(`/v5/snapshots/${segment(snapshotId)}`);
6495
+ }
6496
+ };
6497
+
6498
+ // ../../node_modules/.bun/freestyle@0.2.16/node_modules/freestyle/dist/vms/index.js
6499
+ function vmPath(id2) {
6500
+ return `/v5/vms/${segment(id2)}`;
6501
+ }
6502
+ var Vm = class {
6503
+ id;
6504
+ fs;
6505
+ pty;
6506
+ client;
6507
+ basePath;
6508
+ constructor(client, id2) {
6509
+ this.client = client;
6510
+ this.id = id2;
6511
+ this.basePath = vmPath(id2);
6512
+ this.fs = new VmFilesystem(client, this.basePath);
6513
+ this.pty = new VmPty(client, this.basePath);
6514
+ }
6515
+ /** Fetch the current VM record. */
6516
+ data() {
6517
+ return this.client.get(this.basePath);
6518
+ }
6519
+ /**
6520
+ * The firewall rules that apply to this VM: those naming it, plus those
6521
+ * naming a private network it is attached to.
6522
+ *
6523
+ * Deleting the VM deletes the rules that name it, so this never reports a
6524
+ * rule over a machine that no longer exists.
6525
+ */
6526
+ firewallRules() {
6527
+ return this.client.get("/v5/firewall/rules", { vmId: this.id });
6528
+ }
6529
+ /** Rename the VM, change its slug or idle timeout, or merge in metadata. */
6530
+ update(options) {
6531
+ return this.client.patch(this.basePath, options);
6532
+ }
6533
+ /** Boot a stopped VM, or resume a paused one. */
6534
+ start() {
6535
+ return this.client.post(`${this.basePath}/start`);
6536
+ }
6537
+ /** Freeze a running VM, keeping its memory so a later start resumes it exactly. */
6538
+ pause() {
6539
+ return this.client.post(`${this.basePath}/pause`);
6540
+ }
6541
+ /**
6542
+ * Change vCPU, memory, or disk. Every axis is grow-only. vCPU and memory
6543
+ * apply live to a running VM, on resume for a paused one, and at the next
6544
+ * boot for a stopped one. Growing the disk needs a running VM.
6545
+ */
6546
+ resize(options) {
6547
+ return this.client.post(`${this.basePath}/resize`, options);
6548
+ }
6549
+ /** Permanently destroy the VM */
6550
+ delete() {
6551
+ return this.client.delete(this.basePath);
6552
+ }
6553
+ /**
6554
+ * Run a command in the guest and wait for it to finish. A non-zero exit
6555
+ * status is still a successful call — check `statusCode`, which is null if
6556
+ * the command was killed by its timeout.
6557
+ */
6558
+ exec(options) {
6559
+ const body = typeof options === "string" ? { command: options } : options;
6560
+ return this.client.post(`${this.basePath}/exec-await`, body);
6561
+ }
6562
+ /** Scope process and terminal operations to one existing in-guest Linux user. */
6563
+ linuxUser(linuxUser) {
6564
+ return new VmLinuxUser(this, linuxUser);
6565
+ }
6566
+ /**
6567
+ * Capture the VM's exact state, memory and disk. The VM must be running or
6568
+ * paused. The new snapshot is private and fully materialized when this
6569
+ * resolves.
6570
+ */
6571
+ snapshot(options = {}) {
6572
+ return this.client.post(`${this.basePath}/snapshot`, options);
6573
+ }
6574
+ };
6575
+ var VmLinuxUser = class {
6576
+ vm;
6577
+ linuxUser;
6578
+ pty;
6579
+ constructor(vm, linuxUser) {
6580
+ this.vm = vm;
6581
+ this.linuxUser = linuxUser;
6582
+ this.pty = new VmLinuxUserPty(vm.pty, linuxUser);
6583
+ }
6584
+ exec(options) {
6585
+ const request = typeof options === "string" ? { command: options } : options;
6586
+ return this.vm.exec({ ...request, linuxUser: this.linuxUser });
6587
+ }
6588
+ };
6589
+ var VmsNamespace = class {
6590
+ client;
6591
+ snapshots;
6592
+ constructor(client) {
6593
+ this.client = client;
6594
+ this.snapshots = new VmSnapshotsNamespace(client);
6595
+ }
6596
+ /**
6597
+ * Boot a new VM.
6598
+ *
6599
+ * `firewall` is required: a VM gets nothing implicitly, so state what it may
6600
+ * reach. `{ rules: [] }` is a legitimate answer — a VM reachable only through
6601
+ * a mapped domain or SSH.
6602
+ *
6603
+ * @example
6604
+ * const { vm, vmId } = await freestyle.vms.create({
6605
+ * firewall: {
6606
+ * rules: [
6607
+ * // This VM can reach the Internet. Without it, it cannot.
6608
+ * { action: "allow", source: {}, destination: { public: true } },
6609
+ * ],
6610
+ * },
6611
+ * });
6612
+ * await vm.exec("echo hello");
6613
+ */
6614
+ async create(options) {
6615
+ validateFirewallSpec(options.firewall, "VM");
6616
+ if (options.tls !== void 0) {
6617
+ validateTlsSpec(options.tls);
6618
+ }
6619
+ const data = await this.client.post("/v5/vms", options);
6620
+ return {
6621
+ vm: new Vm(this.client, data.id),
6622
+ vmId: data.id,
6623
+ data,
6624
+ // The ids of the rules the create declared — nothing else names them.
6625
+ firewallRules: data.firewallRules ?? [],
6626
+ tlsRules: data.tlsRules ?? []
6627
+ };
6628
+ }
6629
+ /**
6630
+ * List your VMs.
6631
+ *
6632
+ * @example
6633
+ * const { vms } = await freestyle.vms.list();
6634
+ */
6635
+ list(options = {}) {
6636
+ return this.client.get("/v5/vms", options);
6637
+ }
6638
+ /**
6639
+ * Fetch a VM by its id or its slug.
6640
+ *
6641
+ * @example
6642
+ * const vm = await freestyle.vms.get("vmId");
6643
+ */
6644
+ get(vmIdOrSlug) {
6645
+ return this.client.get(vmPath(vmIdOrSlug));
6646
+ }
6647
+ /** A handle to a VM, without a network call. */
6648
+ ref(vmIdOrSlug) {
6649
+ return new Vm(this.client, vmIdOrSlug);
6650
+ }
6651
+ delete(vmIdOrSlug) {
6652
+ return this.client.delete(vmPath(vmIdOrSlug));
6653
+ }
6654
+ };
6655
+
6656
+ // ../../node_modules/.bun/freestyle@0.2.16/node_modules/freestyle/dist/vpc.js
6657
+ function vpcPath(id2) {
6658
+ return `/v5/vpcs/${segment(id2)}`;
6659
+ }
6660
+ var VpcTunnelNamespace = class {
6661
+ client;
6662
+ basePath;
6663
+ constructor(client, basePath) {
6664
+ this.client = client;
6665
+ this.basePath = basePath;
6666
+ }
6667
+ /**
6668
+ * List the tunnels attached to this network, newest first. Listings never
6669
+ * include a private key: one is returned only by
6670
+ * `freestyle.tunnels.create` and `freestyle.tunnels.rotateKey`.
6671
+ */
6672
+ list() {
6673
+ return this.client.get(`${this.basePath}/tunnels`);
6674
+ }
6675
+ };
6676
+ var Vpc = class {
6677
+ id;
6678
+ tunnels;
6679
+ client;
6680
+ basePath;
6681
+ constructor(client, id2) {
6682
+ this.client = client;
6683
+ this.id = id2;
6684
+ this.basePath = vpcPath(id2);
6685
+ this.tunnels = new VpcTunnelNamespace(client, this.basePath);
6686
+ }
6687
+ data() {
6688
+ return this.client.get(this.basePath);
6689
+ }
6690
+ update(options) {
6691
+ return this.client.patch(this.basePath, options);
6692
+ }
6693
+ delete() {
6694
+ return this.client.delete(this.basePath);
6695
+ }
6696
+ };
6697
+ var VpcNamespace = class {
6698
+ client;
6699
+ constructor(client) {
6700
+ this.client = client;
6701
+ }
6702
+ /** List your private networks. */
6703
+ list() {
6704
+ return this.client.get("/v5/vpcs");
6705
+ }
6706
+ /** Create a private network, optionally with the rules that say what it is for. */
6707
+ async create(options = {}) {
6708
+ if (options.firewall !== void 0)
6709
+ validateFirewallSpec(options.firewall, "network");
6710
+ const data = await this.client.post("/v5/vpcs", options);
6711
+ return {
6712
+ vpc: new Vpc(this.client, data.id),
6713
+ vpcId: data.id,
6714
+ data,
6715
+ firewallRules: data.firewallRules ?? []
6716
+ };
6717
+ }
6718
+ /** Fetch a private network by its id or its slug. */
6719
+ get(vpcIdOrSlug) {
6720
+ return this.client.get(vpcPath(vpcIdOrSlug));
6721
+ }
6722
+ /** A handle to a private network, without a network call. */
6723
+ ref(vpcIdOrSlug) {
6724
+ return new Vpc(this.client, vpcIdOrSlug);
6725
+ }
6726
+ delete(vpcIdOrSlug) {
6727
+ return this.client.delete(vpcPath(vpcIdOrSlug));
6728
+ }
6729
+ };
6730
+
6731
+ // ../../node_modules/.bun/freestyle@0.2.16/node_modules/freestyle/dist/index.js
6732
+ var Freestyle = class {
6733
+ client;
6734
+ vms;
6735
+ vpc;
6736
+ tunnels;
6737
+ /** Allowed network communication, stated as `source -> destination` intent. */
6738
+ firewall;
6739
+ /** Named TLS sessions, stated as `domain, source -> destination` intent. */
6740
+ tls;
6741
+ domains;
6742
+ identities;
6743
+ constructor(options = {}) {
6744
+ this.client = new FreestyleClient(options);
6745
+ this.vms = new VmsNamespace(this.client);
6746
+ this.vpc = new VpcNamespace(this.client);
6747
+ this.tunnels = new TunnelsNamespace(this.client);
6748
+ this.firewall = new FirewallNamespace(this.client);
6749
+ this.tls = new TlsNamespace(this.client);
6750
+ this.domains = new DomainsNamespace(this.client);
6751
+ this.identities = new IdentitiesNamespace(this.client);
6752
+ }
6753
+ /** Low-level escape hatch: an authenticated `fetch` against the API. Pass the whole path, `/v5` included. */
6754
+ fetch(path, init) {
6755
+ return this.client.fetch(path, init);
6756
+ }
6757
+ };
6758
+
6759
+ // ../sandbox/package.json
6760
+ var package_default = {
6761
+ name: "@decocms/sandbox",
6762
+ version: "1.80.0",
6763
+ private: true,
6764
+ type: "module",
6765
+ description: "Sandbox runtime for isolated Studio tool execution",
6766
+ scripts: {
6767
+ check: "tsc --noEmit",
6768
+ test: "bun test",
6769
+ build: "bun run build:typegen && bun run build:harness-runner",
6770
+ "build:typegen": "mkdir -p dist && bun run --cwd=../typegen build && bun --cwd=../typegen pm pack --filename $PWD/dist/typegen.tgz --quiet",
6771
+ "build:harness-runner": "mkdir -p dist && bun --cwd=../harness-runner pm pack --filename $PWD/dist/harness-runner.tgz --quiet"
6772
+ },
6773
+ exports: {
6774
+ "./shared": "./shared.ts",
6775
+ "./provider": "./server/provider/index.ts",
6776
+ "./provider/agent-sandbox": "./server/provider/agent-sandbox/index.ts",
6777
+ "./provider/postgres-state-store": "./server/provider/postgres-state-store.ts",
6778
+ "./provider/freestyle": "./server/provider/freestyle.ts",
6779
+ "./provider/remote": "./server/provider/remote.ts",
6780
+ "./provider/router": "./server/provider/router.ts",
6781
+ "./provider/sandbox-api": "./server/provider/sandbox-api.ts",
6782
+ "./provider/sandbox-server": "./server/provider/sandbox-server.ts",
6783
+ "./daemon-client": "./server/daemon-client.ts",
6784
+ "./org-fs": "./org-fs.ts",
6785
+ "./dispatch": "./dispatch/index.ts",
6786
+ "./dispatch/*": "./dispatch/*.ts",
6787
+ "./proxy/http": "./proxy/http.ts",
6788
+ "./proxy/websocket": "./proxy/websocket.ts"
6789
+ },
6790
+ dependencies: {
6791
+ "@decocms/shared": "workspace:*",
6792
+ "@kubernetes/client-node": "^1.4.0",
6793
+ "@modelcontextprotocol/sdk": "1.29.0",
6794
+ "@opentelemetry/api": "^1.9.0",
6795
+ freestyle: "0.2.16",
6796
+ zod: "4.3.6"
6797
+ },
6798
+ devDependencies: {
6799
+ "@types/bun": "latest",
6800
+ postgres: "^3.4.5",
6801
+ typescript: "^7.0.2"
6802
+ }
6803
+ };
6804
+
6805
+ // ../sandbox/server/provider/shared/proxy-metrics.ts
6806
+ var RUNNER_KIND = {
6807
+ kubernetes: "agent-sandbox",
6808
+ freestyle: "freestyle"
6809
+ };
6810
+ function daemonProxyTimer(meter) {
6811
+ const histogram = meter?.createHistogram("studio.sandbox.proxy.duration_ms", {
6812
+ description: "Wall-clock latency of studio-mediated requests to the sandbox daemon: tool exec proxies (source=daemon) and preview iframe traffic (source=preview).",
6813
+ unit: "ms"
6814
+ });
6815
+ return async (runnerKind, send) => {
6816
+ if (!histogram) return send();
6817
+ const start = performance.now();
6818
+ const record = (statusCode) => histogram.record(performance.now() - start, {
6819
+ runner_kind: runnerKind,
6820
+ source: "daemon",
6821
+ status_code: statusCode
6822
+ });
6823
+ try {
6824
+ const res = await send();
6825
+ record(res.status);
6826
+ return res;
6827
+ } catch (err) {
6828
+ record(0);
6829
+ throw err;
6830
+ }
6831
+ };
6832
+ }
6833
+
6834
+ // ../sandbox/server/provider/freestyle.ts
6835
+ var LOG_LABEL2 = "FreestyleSandboxProvider";
6836
+ var DAEMON_PORT = 9e3;
6837
+ var WORKDIR = "/app";
6838
+ var DEFAULT_DEV_PORT2 = 3e3;
6839
+ var CONTAINER = "sandbox";
6840
+ var SIDECAR = "orgfs";
6841
+ var DEFAULT_SIDECAR_IMAGE = "ghcr.io/decocms/studio/orgfs-sidecar:0.1.0";
6842
+ var HOST_ORG_DIR = "/srv/sandbox/org";
6843
+ var HOST_CTL_DIR = "/srv/sandbox/orgfs-ctl";
6844
+ var CTL_DIR = "/run/orgfs";
6845
+ var HOST_SETUP = [
6846
+ `mkdir -p ${HOST_ORG_DIR} ${HOST_CTL_DIR}`,
6847
+ `grep -q ' ${HOST_ORG_DIR} ' /etc/fstab || printf '%s\\n' '${HOST_ORG_DIR} ${HOST_ORG_DIR} none bind,rshared 0 0' 'tmpfs ${HOST_CTL_DIR} tmpfs uid=1000,gid=1000,mode=0755,size=1m 0 0' >> /etc/fstab`,
6848
+ `mountpoint -q ${HOST_ORG_DIR} || mount ${HOST_ORG_DIR}`,
6849
+ `mountpoint -q ${HOST_CTL_DIR} || mount ${HOST_CTL_DIR}`
6850
+ ].join(" && ");
6851
+ var TOKEN_KEY = "studio-daemon-token";
6852
+ var ORG_KEY = "studio-org-id";
6853
+ var USER_KEY = "studio-user-id";
6854
+ var DAEMON_BOOT_TIMEOUT_MS = 12e4;
6855
+ var HEALTH_POLL_MS = 500;
6856
+ var HEALTH_PROBE_TIMEOUT_MS2 = 5e3;
6857
+ var IMAGE_PULL_TIMEOUT_MS = 3e5;
6858
+ var LOOKUP_TTL_MS = 5 * 6e4;
6859
+ var MISS_TTL_MS = 1e4;
6860
+ var LOOKUP_MAX = 5e3;
6861
+ var WATCH_TIMEOUT_MS = 5 * 6e4;
6862
+ var LOOKUP_TIMEOUT_MS = 3e3;
6863
+ var FreestyleSandboxProvider = class {
6864
+ freestyle;
6865
+ lookupClient;
6866
+ image;
6867
+ sidecarImage;
6868
+ idleTimeoutSeconds;
6869
+ autoDeleteSeconds;
6870
+ domainSuffix;
6871
+ timeDaemonRequest;
6872
+ inflight = new Inflight();
6873
+ snapshotInflight = new Inflight();
6874
+ lookups = /* @__PURE__ */ new Map();
6875
+ releaseTimers = /* @__PURE__ */ new Map();
6876
+ constructor(opts) {
6877
+ this.freestyle = opts.client ?? new Freestyle({ apiKey: opts.apiKey });
6878
+ this.lookupClient = opts.client ?? new Freestyle({
6879
+ apiKey: opts.apiKey,
6880
+ fetch: Object.assign(
6881
+ (input, init) => fetch(input, {
6882
+ ...init,
6883
+ signal: init?.signal ? AbortSignal.any([
6884
+ init.signal,
6885
+ AbortSignal.timeout(LOOKUP_TIMEOUT_MS)
6886
+ ]) : AbortSignal.timeout(LOOKUP_TIMEOUT_MS)
6887
+ }),
6888
+ { preconnect: fetch.preconnect }
6889
+ )
6890
+ });
6891
+ this.image = opts.image ?? `ghcr.io/decocms/studio/studio-sandbox-go:${package_default.version}`;
6892
+ this.sidecarImage = opts.orgFsSidecarImage ?? DEFAULT_SIDECAR_IMAGE;
6893
+ this.idleTimeoutSeconds = opts.idleTimeoutSeconds ?? 15 * 60;
6894
+ this.autoDeleteSeconds = opts.autoDeleteSeconds ?? 3 * 24 * 60 * 60;
6895
+ this.domainSuffix = opts.domainSuffix ?? "style.dev";
6896
+ this.timeDaemonRequest = daemonProxyTimer(opts.meter);
6897
+ }
6898
+ /** Whether this handle is a Freestyle sandbox. Cached; see `MISS_TTL_MS`. */
6899
+ async owns(handle) {
6900
+ return await this.find(handle) !== null;
6901
+ }
6902
+ // ---- Lookup ---------------------------------------------------------------
6903
+ daemonUrl(handle) {
6904
+ return `https://${handle}.${this.domainSuffix}`;
6905
+ }
6906
+ async find(handle, opts = {}) {
6907
+ const hit = this.lookups.get(handle);
6908
+ if (!opts.refresh && hit && Date.now() - hit.at < (hit.found ? LOOKUP_TTL_MS : MISS_TTL_MS)) {
6909
+ return hit.found;
6910
+ }
6911
+ let found = null;
6912
+ try {
6913
+ const data = await this.lookupClient.vms.get(handle);
6914
+ const token = data.metadata[TOKEN_KEY];
6915
+ if (token) {
6916
+ found = {
6917
+ vm: this.freestyle.vms.ref(data.id),
6918
+ daemon: { url: this.daemonUrl(handle), token }
6919
+ };
6920
+ }
6921
+ } catch (err) {
6922
+ if (!(err instanceof FreestyleApiError && err.status === 404)) throw err;
6923
+ }
6924
+ this.remember(handle, found);
6925
+ return found;
6926
+ }
6927
+ remember(handle, found) {
6928
+ this.lookups.delete(handle);
6929
+ if (this.lookups.size >= LOOKUP_MAX) {
6930
+ const oldest = this.lookups.keys().next();
6931
+ if (!oldest.done) this.lookups.delete(oldest.value);
6932
+ }
6933
+ this.lookups.set(handle, { found, at: Date.now() });
6934
+ }
6935
+ // ---- Base snapshot --------------------------------------------------------
6936
+ /**
6937
+ * A snapshot of a VM with both images already pulled, so a sandbox boots in
6938
+ * seconds instead of pulling ~2GB. One per image pair; built on first use.
6939
+ */
6940
+ baseSnapshot() {
6941
+ const { image, sidecarImage } = this;
6942
+ const slug = `studio-sandbox-${createHash3("sha256").update(`${image}
6943
+ ${sidecarImage}`).digest("hex").slice(0, 16)}`;
6944
+ return this.snapshotInflight.run(slug, async () => {
6945
+ if (await this.snapshotExists(slug)) return slug;
6946
+ console.log(`[${LOG_LABEL2}] building base snapshot ${slug} for ${image}`);
6947
+ const { vm } = await this.freestyle.vms.create({
6948
+ ttlSeconds: 1800,
6949
+ firewall: {
6950
+ rules: [
6951
+ { action: "allow", source: {}, destination: { public: true } }
6952
+ ]
6953
+ }
6954
+ });
6955
+ try {
6956
+ await this.run(
6957
+ vm,
6958
+ `docker pull -q ${shellQuote(image)} && docker pull -q ${shellQuote(sidecarImage)}`,
6959
+ { timeoutMs: IMAGE_PULL_TIMEOUT_MS }
6960
+ );
6961
+ await vm.snapshot({ slug });
6962
+ } catch (err) {
6963
+ if (!await this.snapshotExists(slug)) throw err;
6964
+ } finally {
6965
+ await vm.delete().catch(() => {
6966
+ });
6967
+ }
6968
+ return slug;
6969
+ });
6970
+ }
6971
+ async snapshotExists(slug) {
6972
+ try {
6973
+ await this.freestyle.vms.snapshots.get(slug);
6974
+ return true;
6975
+ } catch (err) {
6976
+ if (err instanceof FreestyleApiError && err.status === 404) return false;
6977
+ throw err;
6978
+ }
6979
+ }
6980
+ async run(vm, command, opts = {}) {
6981
+ const res = await vm.exec({ command, ...opts });
6982
+ if (res.statusCode !== 0) {
6983
+ throw new Error(
6984
+ `${command.split(" ", 2).join(" ")} failed (${res.statusCode ?? "timeout"}): ${(res.stderr || res.stdout || "").slice(-500)}`
6985
+ );
6986
+ }
6987
+ return res.stdout ?? "";
6988
+ }
6989
+ // ---- Lifecycle ------------------------------------------------------------
6990
+ async ensure(id2, opts = {}) {
6991
+ const handle = computeHandle(id2);
6992
+ if (handle.startsWith("s-")) {
6993
+ throw new Error(
6994
+ `ensure: projectRef has no slug source: ${id2.projectRef}`
6995
+ );
6996
+ }
6997
+ return this.inflight.run(handle, async () => {
6998
+ this.cancelRelease(handle);
6999
+ const existing = await this.find(handle, { refresh: true });
7000
+ if (existing) {
7001
+ await this.resume(existing, opts);
7002
+ } else {
7003
+ await this.create(handle, opts);
7004
+ }
7005
+ return {
7006
+ handle,
7007
+ workdir: WORKDIR,
7008
+ previewUrl: this.daemonUrl(handle),
7009
+ warmPoolAdopted: false,
7010
+ provider: "freestyle"
7011
+ };
7012
+ });
7013
+ }
7014
+ async resume(found, opts) {
7015
+ const { state } = await found.vm.data();
7016
+ if (state !== "running") await found.vm.start();
7017
+ await this.waitForDaemon(found.daemon.url);
7018
+ if (opts.orgFsConfigJson) {
7019
+ await postOrgFsConfig(
7020
+ found.daemon.url,
7021
+ found.daemon.token,
7022
+ opts.orgFsConfigJson
7023
+ ).catch((err) => console.warn(`[${LOG_LABEL2}] org-fs relay failed`, err));
7024
+ }
7025
+ const patch = gitCredentialRefreshPatch(opts);
7026
+ if (patch) {
7027
+ await postConfig(found.daemon.url, found.daemon.token, patch).catch(
7028
+ (err) => console.warn(
7029
+ `[${LOG_LABEL2}] git credential refresh failed: ${errMsg2(err)}`
7030
+ )
7031
+ );
7032
+ }
7033
+ }
7034
+ async create(handle, opts) {
7035
+ const snapshotId = await this.baseSnapshot();
7036
+ const token = randomBytes2(24).toString("hex");
7037
+ const { vm } = await this.createVm({
7038
+ snapshotId,
7039
+ slug: handle,
7040
+ idleTimeoutSeconds: this.idleTimeoutSeconds,
7041
+ metadata: {
7042
+ [TOKEN_KEY]: token,
7043
+ ...opts.tenant ? { [ORG_KEY]: opts.tenant.orgId, [USER_KEY]: opts.tenant.userId } : {}
7044
+ },
7045
+ firewall: {
7046
+ rules: [{ action: "allow", source: {}, destination: { public: true } }]
7047
+ },
7048
+ tls: {
7049
+ rules: [
7050
+ {
7051
+ action: "allow",
7052
+ domain: `${handle}.${this.domainSuffix}`,
7053
+ source: { public: true },
7054
+ destination: { port: DAEMON_PORT }
7055
+ }
7056
+ ]
7057
+ }
7058
+ });
7059
+ const daemon = { url: this.daemonUrl(handle), token };
7060
+ try {
7061
+ const env = {
7062
+ ...opts.env,
7063
+ DAEMON_TOKEN: token,
7064
+ DAEMON_BOOT_ID: crypto.randomUUID(),
7065
+ APP_ROOT: WORKDIR,
7066
+ PROXY_PORT: String(DAEMON_PORT),
7067
+ ORGFS_SIDECAR_CONFIG_PATH: `${CTL_DIR}/config.json`,
7068
+ ORGFS_SIDECAR_STATUS_PATH: `${CTL_DIR}/status.json`
7069
+ };
7070
+ const envFlags = Object.keys(env).map((k) => `-e ${shellQuote(k)}`).join(" ");
7071
+ await this.run(
7072
+ vm,
7073
+ `sudo sh -c ${shellQuote(HOST_SETUP)} && docker run -d --name ${SIDECAR} --restart=always --privileged --device /dev/fuse -e APP_ROOT=${WORKDIR} -v ${HOST_ORG_DIR}:${WORKDIR}/org:rshared -v ${HOST_CTL_DIR}:${CTL_DIR} ${shellQuote(this.sidecarImage)}`,
7074
+ { timeoutMs: IMAGE_PULL_TIMEOUT_MS }
7075
+ );
7076
+ await this.run(
7077
+ vm,
7078
+ `docker run -d --name ${CONTAINER} --restart=always -p ${DAEMON_PORT}:${DAEMON_PORT} -v ${HOST_ORG_DIR}:${WORKDIR}/org:rslave -v ${HOST_CTL_DIR}:${CTL_DIR} ${envFlags} ${shellQuote(this.image)}`,
7079
+ { env, timeoutMs: IMAGE_PULL_TIMEOUT_MS }
7080
+ );
7081
+ await this.waitForDaemon(daemon.url);
7082
+ await postConfig(daemon.url, token, this.configPayload(opts) ?? {});
7083
+ if (opts.orgFsConfigJson) {
7084
+ await postOrgFsConfig(daemon.url, token, opts.orgFsConfigJson).catch(
7085
+ (err) => console.warn(`[${LOG_LABEL2}] org-fs relay failed`, err)
7086
+ );
7087
+ }
7088
+ } catch (err) {
7089
+ await vm.delete().catch(() => {
7090
+ });
7091
+ this.lookups.delete(handle);
7092
+ throw err;
7093
+ }
7094
+ this.remember(handle, { vm, daemon });
7095
+ }
7096
+ /** A plan that caps autoDelete lower answers 400; omitting it gets the cap. */
7097
+ async createVm(opts) {
7098
+ try {
7099
+ return await this.freestyle.vms.create({
7100
+ ...opts,
7101
+ autoDeleteSeconds: this.autoDeleteSeconds
7102
+ });
7103
+ } catch (err) {
7104
+ if (!(err instanceof FreestyleApiError && err.status === 400)) throw err;
7105
+ console.warn(
7106
+ `[${LOG_LABEL2}] autoDeleteSeconds=${this.autoDeleteSeconds} refused (${err.message}); creating with the plan's cap`
7107
+ );
7108
+ return this.freestyle.vms.create(opts);
7109
+ }
7110
+ }
7111
+ configPayload(opts) {
7112
+ return buildConfigPayload({
7113
+ runtime: opts.workload?.runtime ?? "node",
7114
+ packageManager: opts.workload?.packageManager ? {
7115
+ name: opts.workload.packageManager,
7116
+ ...opts.workload.packageManagerPath ? { path: opts.workload.packageManagerPath } : {}
7117
+ } : null,
7118
+ repo: opts.repo ?? null,
7119
+ extraRepos: opts.extraRepos ?? [],
7120
+ port: opts.workload?.devPort ?? DEFAULT_DEV_PORT2,
7121
+ tenant: opts.tenant,
7122
+ cloneOnly: opts.cloneOnly === true
7123
+ });
7124
+ }
7125
+ async waitForDaemon(url) {
7126
+ const deadline = Date.now() + DAEMON_BOOT_TIMEOUT_MS;
7127
+ while (Date.now() < deadline) {
7128
+ if (await daemonAnswers(url)) return;
7129
+ await delay(HEALTH_POLL_MS);
7130
+ }
7131
+ throw new Error(
7132
+ `sandbox daemon at ${url} did not answer /health within ${DAEMON_BOOT_TIMEOUT_MS / 1e3}s`
7133
+ );
7134
+ }
7135
+ async delete(handle) {
7136
+ this.cancelRelease(handle);
7137
+ const found = await this.find(handle, { refresh: true });
7138
+ this.lookups.delete(handle);
7139
+ if (!found) return;
7140
+ await found.vm.exec({
7141
+ command: `docker stop -t 30 ${CONTAINER}; docker stop -t 10 ${SIDECAR}`,
7142
+ timeoutMs: 6e4
7143
+ }).catch(() => {
7144
+ });
7145
+ await found.vm.delete();
7146
+ }
7147
+ async lastTermination(_handle) {
7148
+ return null;
7149
+ }
7150
+ /** A paused VM is alive: the next request resumes it. */
7151
+ async alive(handle) {
7152
+ return await this.find(handle, { refresh: true }) !== null;
7153
+ }
7154
+ /** `ensure` only returns once the daemon answers, so this only has to see that. */
7155
+ async *watchClaimLifecycle(handle, signal) {
7156
+ const since2 = Date.now();
7157
+ yield { kind: "claiming", since: since2 };
7158
+ let announced = false;
7159
+ while (!signal?.aborted) {
7160
+ const found = await this.find(handle, { refresh: true }).catch(
7161
+ () => null
7162
+ );
7163
+ if (found) {
7164
+ if (!announced) {
7165
+ announced = true;
7166
+ yield { kind: "warming-daemon", since: Date.now() };
7167
+ }
7168
+ if (await daemonAnswers(found.daemon.url)) {
7169
+ yield { kind: "ready" };
7170
+ return;
7171
+ }
7172
+ }
7173
+ if (Date.now() - since2 > WATCH_TIMEOUT_MS) {
7174
+ yield {
7175
+ kind: "failed",
7176
+ reason: found ? "crash-loop-backoff" : "claim-never-created",
7177
+ message: found ? "the sandbox VM exists but its daemon never answered" : "no sandbox VM was created"
7178
+ };
7179
+ return;
7180
+ }
7181
+ await delay(1e3);
7182
+ }
7183
+ }
7184
+ async hasSchedulableCapacity() {
7185
+ return true;
7186
+ }
7187
+ async getPreviewUrl(handle) {
7188
+ return await this.find(handle) ? this.daemonUrl(handle) : null;
7189
+ }
7190
+ async proxyDaemonRequest(handle, path, init) {
7191
+ const found = await this.find(handle);
7192
+ if (!found) {
7193
+ return Response.json({ error: "sandbox not found" }, { status: 404 });
7194
+ }
7195
+ const reread = async () => (await this.find(handle, { refresh: true }))?.daemon ?? null;
7196
+ return this.timeDaemonRequest(
7197
+ RUNNER_KIND.freestyle,
7198
+ () => proxyDaemonWithRetry(found.daemon, path, init, {
7199
+ unauthorized: async () => {
7200
+ const fresh = await reread();
7201
+ return fresh && fresh.token !== found.daemon.token ? fresh : null;
7202
+ },
7203
+ unreachable: reread
7204
+ })
7205
+ );
7206
+ }
7207
+ /** The daemon's public address and bearer, for a host serving `sandbox-api`. */
7208
+ async daemonEndpoint(handle) {
7209
+ return (await this.find(handle))?.daemon ?? null;
7210
+ }
7211
+ async adoptLiveClaim(_id, handle) {
7212
+ return await this.find(handle, { refresh: true }) !== null;
7213
+ }
7214
+ async resolvePreviewUpstreamUrl(handle) {
7215
+ return (await this.find(handle))?.daemon.url ?? null;
7216
+ }
7217
+ proxyPreviewRequest(handle, request) {
7218
+ return proxyPreview(
7219
+ request,
7220
+ {
7221
+ base: () => this.resolvePreviewUpstreamUrl(handle).catch(() => null),
7222
+ invalidate: () => this.lookups.delete(handle),
7223
+ retryBase: () => this.find(handle, { refresh: true }).then((f) => f?.daemon.url ?? null).catch(() => null)
7224
+ },
7225
+ LOG_LABEL2
7226
+ );
7227
+ }
7228
+ async markTenantPoolsDirty(_repoUrl, _ref) {
7229
+ return [];
7230
+ }
7231
+ /**
7232
+ * Pause after `graceMs`. Pausing keeps memory and disk, so this costs the
7233
+ * sandbox nothing but a resume.
7234
+ *
7235
+ * ponytail: in-process timer, lost on restart; the idle timeout pauses the
7236
+ * VM anyway, only later.
7237
+ */
7238
+ async releaseAfter(handle, graceMs) {
7239
+ this.cancelRelease(handle);
7240
+ const timer = setTimeout(() => {
7241
+ this.releaseTimers.delete(handle);
7242
+ void this.find(handle).then((f) => f?.vm.pause()).catch(
7243
+ (err) => console.warn(`[${LOG_LABEL2}] pause ${handle} failed: ${errMsg2(err)}`)
7244
+ );
7245
+ }, graceMs);
7246
+ timer.unref?.();
7247
+ this.releaseTimers.set(handle, timer);
7248
+ }
7249
+ /** The VM's idle timeout already counts every request as activity. */
7250
+ async renewTtl(handle) {
7251
+ this.cancelRelease(handle);
7252
+ }
7253
+ cancelRelease(handle) {
7254
+ const timer = this.releaseTimers.get(handle);
7255
+ if (timer) clearTimeout(timer);
7256
+ this.releaseTimers.delete(handle);
7257
+ }
7258
+ close() {
7259
+ for (const timer of this.releaseTimers.values()) clearTimeout(timer);
7260
+ this.releaseTimers.clear();
7261
+ this.lookups.clear();
7262
+ }
7263
+ };
7264
+ async function daemonAnswers(url) {
7265
+ try {
7266
+ const res = await fetch(`${url}/health`, {
7267
+ signal: AbortSignal.timeout(HEALTH_PROBE_TIMEOUT_MS2)
7268
+ });
7269
+ await res.body?.cancel();
7270
+ return res.ok;
7271
+ } catch {
7272
+ return false;
7273
+ }
7274
+ }
7275
+ function shellQuote(s) {
7276
+ return `'${s.replaceAll("'", `'\\''`)}'`;
7277
+ }
7278
+ function errMsg2(err) {
7279
+ return err instanceof Error ? err.message : String(err);
7280
+ }
7281
+
7282
+ // ../sandbox/server/provider/router.ts
7283
+ import { createHash as createHash4 } from "crypto";
7284
+
7285
+ // ../sandbox/server/provider/shared/provider-tag.ts
7286
+ var KEY = "sandboxProvider";
7287
+ function tagSandboxProvider(err, kind) {
7288
+ if (err instanceof Error) Object.assign(err, { [KEY]: kind });
7289
+ return err;
7290
+ }
7291
+ function sandboxProviderOfError(err) {
7292
+ for (let e = err, depth = 0; e instanceof Error && depth < 5; depth++) {
7293
+ const kind = e[KEY];
7294
+ if (kind === "kubernetes" || kind === "freestyle") return kind;
7295
+ e = e.cause;
7296
+ }
7297
+ return void 0;
7298
+ }
7299
+
7300
+ // ../sandbox/server/provider/router.ts
7301
+ function handleShare(handle) {
7302
+ const hex = createHash4("sha256").update(handle).digest("hex").slice(0, 8);
7303
+ return Number.parseInt(hex, 16) / 2 ** 32;
7304
+ }
7305
+ var PICKED_MAX = 5e3;
7306
+ var other = (kind) => kind === "kubernetes" ? "freestyle" : "kubernetes";
7307
+ function repeatsElsewhere(err) {
7308
+ return err instanceof ConfigRequestError;
7309
+ }
7310
+ var SandboxProviderRouter = class {
7311
+ constructor(providers, opts = {}) {
7312
+ this.providers = providers;
7313
+ if (!providers.kubernetes && !providers.freestyle) {
7314
+ throw new Error("SandboxProviderRouter needs at least one provider");
7315
+ }
7316
+ this.freestyleShare = Math.min(1, Math.max(0, opts.freestyleShare ?? 0));
7317
+ }
7318
+ providers;
7319
+ picked = /* @__PURE__ */ new Map();
7320
+ /**
7321
+ * Handles whose sandbox on this provider outlived a fallback because its
7322
+ * delete failed. Retried when `place` next sees the handle.
7323
+ *
7324
+ * ponytail: in-process, lost on restart; Freestyle's autoDeleteSeconds
7325
+ * reclaims a stray paused VM anyway, only later.
7326
+ */
7327
+ strays = /* @__PURE__ */ new Map();
7328
+ freestyleShare;
7329
+ get(kind) {
7330
+ const provider = this.providers[kind];
7331
+ if (!provider)
7332
+ throw new Error(`sandbox provider ${kind} is not configured`);
7333
+ return provider;
7334
+ }
7335
+ remember(handle, kind) {
7336
+ this.picked.delete(handle);
7337
+ if (this.picked.size >= PICKED_MAX) {
7338
+ const oldest = this.picked.keys().next();
7339
+ if (!oldest.done) this.picked.delete(oldest.value);
7340
+ }
7341
+ this.picked.set(handle, kind);
7342
+ }
7343
+ /** Who serves an existing handle. Kubernetes is the fallback: it has no cheap ownership check. */
7344
+ async providerOf(handle) {
7345
+ const { kubernetes, freestyle } = this.providers;
7346
+ if (!freestyle) return "kubernetes";
7347
+ if (!kubernetes) return "freestyle";
7348
+ const picked = this.picked.get(handle);
7349
+ if (picked) return picked;
7350
+ const owned = await freestyle.owns(handle).catch(() => false);
7351
+ return owned ? "freestyle" : "kubernetes";
7352
+ }
7353
+ async owner(handle) {
7354
+ return this.get(await this.providerOf(handle));
7355
+ }
7356
+ /** Where `ensure` puts this sandbox, and why. */
7357
+ async place(handle, opts) {
7358
+ const { kubernetes, freestyle } = this.providers;
7359
+ const image = opts.sandboxImage ?? "default";
7360
+ if (image !== "default") {
7361
+ if (!kubernetes) {
7362
+ throw new Error(`sandbox image ${image} needs the kubernetes provider`);
7363
+ }
7364
+ return { kind: "kubernetes", reason: freestyle ? "image" : void 0 };
7365
+ }
7366
+ if (!kubernetes) return { kind: "freestyle" };
7367
+ if (!freestyle) return { kind: "kubernetes" };
7368
+ const stray = this.strays.get(handle);
7369
+ if (stray) this.retire(stray, handle);
7370
+ if (stray !== "kubernetes" && await kubernetes.alive(handle).catch(() => false)) {
7371
+ return { kind: "kubernetes", reason: "existing" };
7372
+ }
7373
+ const owned = stray === "freestyle" ? false : await freestyle.owns(handle).catch(() => null);
7374
+ if (owned) return { kind: "freestyle", reason: "existing" };
7375
+ if (owned === null) {
7376
+ return { kind: "kubernetes", reason: "freestyle-unavailable" };
7377
+ }
7378
+ if (opts.provider) return { kind: opts.provider, reason: "requested" };
7379
+ if (this.hasWarmPool(opts, image)) {
7380
+ return { kind: "kubernetes", reason: "warm-pool" };
7381
+ }
7382
+ if (handleShare(handle) < this.freestyleShare) {
7383
+ return { kind: "freestyle", reason: "split" };
7384
+ }
7385
+ const roomy = await kubernetes.hasSchedulableCapacity().catch(() => false);
7386
+ return roomy ? { kind: "kubernetes", reason: "split" } : { kind: "freestyle", reason: "capacity" };
7387
+ }
7388
+ /** The org has pods already cloned and running for this image. */
7389
+ hasWarmPool(opts, image) {
7390
+ const tenant = claimTenantKey(opts);
7391
+ if (!tenant) return false;
7392
+ const pools = this.providers.kubernetes?.listTenantPools?.() ?? [];
7393
+ return pools.some((p) => p.tenant === tenant && p.image === image);
7394
+ }
7395
+ async ensure(id2, opts = {}) {
7396
+ const { provider: _requested, ...rest } = opts;
7397
+ const handle = computeHandle(id2);
7398
+ const placed = await this.place(handle, opts);
7399
+ this.remember(handle, placed.kind);
7400
+ if (this.strays.get(handle) === placed.kind) this.strays.delete(handle);
7401
+ let failure;
7402
+ try {
7403
+ const sandbox2 = await this.get(placed.kind).ensure(id2, rest);
7404
+ return placed.reason ? { ...sandbox2, placement: { reason: placed.reason } } : sandbox2;
7405
+ } catch (err) {
7406
+ failure = tagSandboxProvider(err, placed.kind);
7407
+ }
7408
+ const fallback = other(placed.kind);
7409
+ if (!placed.reason || placed.reason === "image" || repeatsElsewhere(failure)) {
7410
+ throw failure;
7411
+ }
7412
+ console.warn(
7413
+ `[SandboxProviderRouter] ensure ${handle} failed on ${placed.kind}, trying ${fallback}:`,
7414
+ failure instanceof Error ? failure.message : String(failure)
7415
+ );
7416
+ this.remember(handle, fallback);
7417
+ let sandbox;
7418
+ try {
7419
+ sandbox = await this.get(fallback).ensure(id2, rest);
7420
+ } catch (err) {
7421
+ this.picked.delete(handle);
7422
+ throw tagSandboxProvider(err, fallback);
7423
+ }
7424
+ if (placed.reason === "existing") this.retire(placed.kind, handle);
7425
+ return {
7426
+ ...sandbox,
7427
+ placement: { reason: "fallback", fallbackFrom: placed.kind }
7428
+ };
7429
+ }
7430
+ /** Delete the sandbox a fallback left behind on `kind`, in the background. */
7431
+ retire(kind, handle) {
7432
+ this.strays.set(handle, kind);
7433
+ void retry(
7434
+ async () => {
7435
+ if (this.strays.get(handle) === kind)
7436
+ await this.get(kind).delete(handle);
7437
+ },
7438
+ { maxAttempts: 3 }
7439
+ ).then(() => {
7440
+ if (this.strays.get(handle) === kind) this.strays.delete(handle);
7441
+ }).catch(
7442
+ (err) => console.warn(
7443
+ `[SandboxProviderRouter] delete of ${kind} sandbox ${handle} after fallback failed:`,
7444
+ err instanceof Error ? err.message : String(err)
7445
+ )
7446
+ );
7447
+ }
7448
+ async delete(handle) {
7449
+ const stray = this.strays.get(handle);
7450
+ this.strays.delete(handle);
7451
+ await Promise.all([
7452
+ (await this.owner(handle)).delete(handle),
7453
+ stray && this.get(stray).delete(handle).catch(() => {
7454
+ })
7455
+ ]);
7456
+ this.picked.delete(handle);
7457
+ }
7458
+ async lastTermination(handle) {
7459
+ return (await this.owner(handle)).lastTermination(handle);
7460
+ }
7461
+ async alive(handle) {
7462
+ return (await this.owner(handle)).alive(handle);
7463
+ }
7464
+ async *watchClaimLifecycle(handle, signal) {
7465
+ yield* (await this.owner(handle)).watchClaimLifecycle(handle, signal);
7466
+ }
7467
+ /** Room anywhere: an ensure without room on Kubernetes lands on Freestyle. */
7468
+ async hasSchedulableCapacity() {
7469
+ const { kubernetes, freestyle } = this.providers;
7470
+ if (!kubernetes || freestyle) return true;
7471
+ return kubernetes.hasSchedulableCapacity();
7472
+ }
7473
+ async getPreviewUrl(handle) {
7474
+ return (await this.owner(handle)).getPreviewUrl(handle);
7475
+ }
7476
+ async proxyDaemonRequest(handle, path, init) {
7477
+ return (await this.owner(handle)).proxyDaemonRequest(handle, path, init);
7478
+ }
7479
+ async adoptLiveClaim(id2, handle) {
7480
+ return (await this.owner(handle)).adoptLiveClaim(id2, handle);
7481
+ }
7482
+ async resolvePreviewUpstreamUrl(handle) {
7483
+ return (await this.owner(handle)).resolvePreviewUpstreamUrl(handle);
7484
+ }
7485
+ async proxyPreviewRequest(handle, request) {
7486
+ return (await this.owner(handle)).proxyPreviewRequest(handle, request);
7487
+ }
7488
+ async markTenantPoolsDirty(repoUrl, ref) {
7489
+ return await this.providers.kubernetes?.markTenantPoolsDirty(repoUrl, ref) ?? [];
7490
+ }
7491
+ async releaseAfter(handle, graceMs) {
7492
+ await (await this.owner(handle)).releaseAfter(handle, graceMs);
7493
+ }
7494
+ async renewTtl(handle) {
7495
+ await (await this.owner(handle)).renewTtl(handle);
7496
+ }
7497
+ async daemonEndpoint(handle) {
7498
+ const owner = this.providers[await this.providerOf(handle)];
7499
+ if (!owner?.daemonEndpoint) {
7500
+ throw new Error("sandbox provider has no daemon endpoint to serve");
7501
+ }
7502
+ return owner.daemonEndpoint(handle);
7503
+ }
7504
+ /** Only Kubernetes sandboxes take pushed credentials. */
7505
+ listSandboxes() {
7506
+ return this.providers.kubernetes?.listSandboxes?.() ?? [];
7507
+ }
7508
+ listTenantPools() {
7509
+ return this.providers.kubernetes?.listTenantPools?.() ?? [];
7510
+ }
7511
+ close() {
7512
+ this.providers.kubernetes?.close();
7513
+ this.providers.freestyle?.close();
7514
+ this.picked.clear();
7515
+ this.strays.clear();
7516
+ }
7517
+ };
7518
+
7519
+ // ../sandbox/server/provider/sandbox-api.ts
7520
+ import { z as z4 } from "zod";
7521
+ var SANDBOX_TOOLS = {
7522
+ ensure: "SANDBOX_ENSURE",
7523
+ status: "SANDBOX_STATUS",
7524
+ delete: "SANDBOX_DELETE",
7525
+ lifetime: "SANDBOX_LIFETIME",
7526
+ capacity: "SANDBOX_CAPACITY",
7527
+ tenantPoolsPush: "SANDBOX_TENANT_POOLS_PUSH",
7528
+ credentialsPush: "SANDBOX_CREDENTIALS_PUSH",
7529
+ list: "SANDBOX_LIST"
7530
+ };
7531
+ var SANDBOX_WATCH_PATH = "/api/sandbox/watch";
7532
+ var SANDBOX_WATCH_KEEPALIVE_MS = 15e3;
7533
+ var id = z4.string().min(1).max(512);
7534
+ var CLONE_URL_MAX = 4096;
7535
+ var ORG_FS_CONFIG_MAX = 65536;
7536
+ var expiresAt = z4.number().int().positive();
7537
+ var sandboxIdSchema = z4.object({
7538
+ userId: id,
7539
+ projectRef: id
7540
+ });
7541
+ var tenantSchema = z4.object({
7542
+ orgId: id,
7543
+ userId: id,
7544
+ orgSlug: z4.string().optional(),
7545
+ orgName: z4.string().optional(),
7546
+ userEmail: z4.string().optional(),
7547
+ userName: z4.string().optional()
7548
+ });
7549
+ var repoSchema = z4.object({
7550
+ cloneUrl: z4.string().min(1).max(CLONE_URL_MAX),
7551
+ connectionId: z4.string().optional(),
7552
+ repositoryId: z4.string().optional(),
7553
+ userName: z4.string(),
7554
+ userEmail: z4.string(),
7555
+ branch: z4.string().optional(),
7556
+ displayName: z4.string().optional(),
7557
+ submoduleCredentials: z4.array(z4.object({ host: z4.string(), token: z4.string() })).optional(),
7558
+ directoryName: z4.string().optional(),
7559
+ credentialExpiresAt: expiresAt.optional()
7560
+ });
7561
+ var providerKindSchema = z4.enum(["kubernetes", "freestyle"]);
7562
+ var ensureOptionsSchema = z4.object({
7563
+ provider: providerKindSchema.optional(),
7564
+ purpose: z4.enum(["interactive", "harness-run"]).optional(),
7565
+ sandboxImage: SandboxImageSchema.optional(),
7566
+ branch: z4.string().optional(),
7567
+ repo: repoSchema.optional(),
7568
+ extraRepos: z4.array(repoSchema).optional(),
7569
+ workload: z4.object({
7570
+ runtime: z4.enum(["node", "bun", "deno"]),
7571
+ packageManager: z4.enum(["npm", "pnpm", "yarn", "bun", "deno"]),
7572
+ devPort: z4.number().int().optional(),
7573
+ packageManagerPath: z4.string().optional()
7574
+ }).optional(),
7575
+ cloneOnly: z4.boolean().optional(),
7576
+ env: z4.record(z4.string(), z4.string()).optional(),
7577
+ tenant: tenantSchema.optional(),
7578
+ orgFsConfigJson: z4.string().max(ORG_FS_CONFIG_MAX).optional()
7579
+ });
7580
+ var daemonSchema = z4.object({
7581
+ url: z4.url({ protocol: /^https?$/ }),
7582
+ token: z4.string().min(1)
7583
+ });
7584
+ var ensureInputSchema = z4.object({
7585
+ id: sandboxIdSchema,
7586
+ opts: ensureOptionsSchema,
7587
+ /**
7588
+ * Until when `opts.orgFsConfigJson` stays valid, at least; unset keeps it
7589
+ * out of the host's store. Clone credentials carry their own
7590
+ * `credentialExpiresAt`.
7591
+ */
7592
+ credentialsValidUntil: z4.object({ orgFsConfig: expiresAt.optional() }).optional()
7593
+ });
7594
+ var ensureOutputSchema = z4.object({
7595
+ handle: z4.string().min(1),
7596
+ workdir: z4.string().min(1),
7597
+ previewUrl: z4.string().nullable(),
7598
+ warmPoolAdopted: z4.boolean(),
7599
+ provider: providerKindSchema.default("kubernetes"),
7600
+ // A reason this build doesn't know drops the label, not the ensure.
7601
+ placement: z4.object({
7602
+ reason: z4.enum(SANDBOX_PLACEMENT_REASONS),
7603
+ fallbackFrom: providerKindSchema.optional()
7604
+ }).optional().catch(void 0),
7605
+ daemon: daemonSchema
7606
+ });
7607
+ var handleInputSchema = z4.object({ handle: z4.string().min(1) });
7608
+ var statusInputSchema = z4.object({
7609
+ handle: z4.string().min(1),
7610
+ /** Re-provision an evicted claim before answering. */
7611
+ resurrect: z4.boolean().optional()
7612
+ });
7613
+ var podTerminationSchema = z4.object({
7614
+ reason: z4.string(),
7615
+ oomKilled: z4.boolean(),
7616
+ exitCode: z4.number().int().optional(),
7617
+ memoryLimit: z4.string().optional(),
7618
+ evictionMessage: z4.string().optional()
7619
+ });
7620
+ var statusOutputSchema = z4.object({
7621
+ alive: z4.boolean(),
7622
+ previewUrl: z4.string().nullable(),
7623
+ daemon: daemonSchema.nullable(),
7624
+ lastTermination: podTerminationSchema.nullable(),
7625
+ provider: providerKindSchema.default("kubernetes")
7626
+ });
7627
+ var lifetimeInputSchema = z4.object({
7628
+ handle: z4.string().min(1),
7629
+ graceMs: z4.number().int().min(0).optional()
7630
+ });
7631
+ var emptySchema = z4.object({});
7632
+ var capacityOutputSchema = z4.object({ schedulable: z4.boolean() });
7633
+ var tenantPoolsPushInputSchema = z4.object({
7634
+ repoUrl: z4.string().min(1).max(CLONE_URL_MAX),
7635
+ ref: z4.string().min(1)
7636
+ });
7637
+ var tenantPoolsPushOutputSchema = z4.object({
7638
+ pools: z4.array(z4.string())
7639
+ });
7640
+ var toolErrorSchema = z4.object({
7641
+ code: z4.enum(["bootstrap-rejected", "internal"]),
7642
+ error: z4.string(),
7643
+ status: z4.number().int().optional(),
7644
+ /** The provider that failed, when the host knows it. */
7645
+ provider: providerKindSchema.optional()
7646
+ });
7647
+ var since = z4.number();
7648
+ var claimPhaseSchema = z4.discriminatedUnion(
7649
+ "kind",
7650
+ [
7651
+ z4.object({ kind: z4.literal("claiming"), since }),
7652
+ z4.object({
7653
+ kind: z4.literal("waiting-for-capacity"),
7654
+ since,
7655
+ message: z4.string().optional(),
7656
+ nodeClaim: z4.string().optional()
7657
+ }),
7658
+ z4.object({ kind: z4.literal("pulling-image"), since }),
7659
+ z4.object({ kind: z4.literal("starting-container"), since }),
7660
+ z4.object({ kind: z4.literal("warming-daemon"), since }),
7661
+ z4.object({ kind: z4.literal("ready") }),
7662
+ z4.object({
7663
+ kind: z4.literal("failed"),
7664
+ reason: z4.enum([
7665
+ "image-pull-backoff",
7666
+ "crash-loop-backoff",
7667
+ "scheduling-timeout",
7668
+ "claim-never-created",
7669
+ "reconciler-error",
7670
+ "unknown"
7671
+ ]),
7672
+ message: z4.string()
7673
+ })
7674
+ ]
7675
+ );
7676
+ var tenantIdsSchema = z4.object({ orgId: id, userId: id });
7677
+ var repoIdentitySchema = z4.union([
7678
+ z4.object({ repositoryId: id }).strict(),
7679
+ z4.object({ connectionId: id, repo: id }).strict()
7680
+ ]);
7681
+ var CREDENTIALS_PUSH_MAX = 2e3;
7682
+ var pushedCloneUrl = z4.url({ protocol: /^https?$/ }).max(CLONE_URL_MAX);
7683
+ var credentialsPushInputSchema = z4.object({
7684
+ cloneUrls: z4.array(
7685
+ z4.object({
7686
+ tenant: tenantIdsSchema,
7687
+ repo: repoIdentitySchema,
7688
+ cloneUrl: pushedCloneUrl,
7689
+ expiresAt
7690
+ })
7691
+ ).max(CREDENTIALS_PUSH_MAX),
7692
+ /** Clone URLs for tenant pool allocations, keyed by the pool's tenant and repo. */
7693
+ poolCloneUrls: z4.array(
7694
+ z4.object({
7695
+ tenant: id,
7696
+ repoUrl: z4.string().min(1).max(CLONE_URL_MAX),
7697
+ cloneUrl: pushedCloneUrl,
7698
+ expiresAt
7699
+ })
7700
+ ).max(CREDENTIALS_PUSH_MAX).default([]),
7701
+ orgFsConfigs: z4.array(
7702
+ z4.object({
7703
+ tenant: tenantIdsSchema,
7704
+ orgFsConfigJson: z4.string().min(1).max(ORG_FS_CONFIG_MAX),
7705
+ expiresAt
7706
+ })
7707
+ ).max(CREDENTIALS_PUSH_MAX)
7708
+ });
7709
+ var credentialsPushOutputSchema = z4.object({
7710
+ stored: z4.number().int().nonnegative(),
7711
+ kept: z4.number().int().nonnegative()
7712
+ });
7713
+ var SANDBOX_LIST_MAX = 5e3;
7714
+ var SANDBOX_LIST_POOLS_MAX = 500;
7715
+ var SANDBOX_LIST_POOL_REPOS_MAX = 100;
7716
+ var listOutputSchema = z4.object({
7717
+ sandboxes: z4.array(
7718
+ z4.object({
7719
+ handle: id,
7720
+ tenant: tenantIdsSchema.nullable(),
7721
+ repos: z4.array(repoIdentitySchema).max(16),
7722
+ /** The sandbox mounts org-fs. */
7723
+ orgFs: z4.boolean(),
7724
+ /** When the org-fs config the host holds for its tenant expires. */
7725
+ orgFsConfigExpiresAt: expiresAt.nullable()
7726
+ })
7727
+ ).max(SANDBOX_LIST_MAX),
7728
+ /** Defaulted so a host that predates pool listing still lists its sandboxes. */
7729
+ pools: z4.array(
7730
+ z4.object({
7731
+ name: id,
7732
+ tenant: id,
7733
+ image: SandboxImageSchema,
7734
+ repos: z4.array(
7735
+ z4.object({
7736
+ repoUrl: z4.string().min(1).max(CLONE_URL_MAX),
7737
+ branch: z4.string().min(1)
7738
+ })
7739
+ ).max(SANDBOX_LIST_POOL_REPOS_MAX)
7740
+ })
7741
+ ).max(SANDBOX_LIST_POOLS_MAX).default([])
7742
+ });
7743
+
7744
+ // ../sandbox/server/provider/sandbox-server.ts
7745
+ function messageChain(err) {
7746
+ const parts = [];
7747
+ for (let e = err, depth = 0; e && depth < 5; depth++) {
7748
+ parts.push(e instanceof Error ? e.message : String(e));
7749
+ e = e instanceof Error ? e.cause : void 0;
7750
+ }
7751
+ return parts.join(": ");
7752
+ }
7753
+ function tool(def) {
7754
+ return {
7755
+ ...def,
7756
+ // A thrown message is all an MCP error carries, so it carries JSON.
7757
+ execute: (input) => def.execute(input).catch((err) => {
7758
+ const provider = sandboxProviderOfError(err);
7759
+ const body = {
7760
+ ...err instanceof ConfigRequestError ? {
7761
+ code: "bootstrap-rejected",
7762
+ error: err.message,
7763
+ status: err.status
7764
+ } : { code: "internal", error: messageChain(err) },
7765
+ ...provider ? { provider } : {}
7766
+ };
4697
7767
  throw new Error(JSON.stringify(body));
4698
7768
  })
4699
7769
  };
@@ -4732,7 +7802,8 @@ function sandboxTools(provider, credentials) {
4732
7802
  alive: daemon !== null,
4733
7803
  previewUrl,
4734
7804
  daemon,
4735
- lastTermination
7805
+ lastTermination,
7806
+ provider: await provider().providerOf?.(handle) ?? "kubernetes"
4736
7807
  };
4737
7808
  }
4738
7809
  }),
@@ -4873,6 +7944,7 @@ function kubeConfigForServer(server) {
4873
7944
  export {
4874
7945
  AgentSandboxProvider,
4875
7946
  CREDENTIALS_PUSH_MAX,
7947
+ FreestyleSandboxProvider,
4876
7948
  PushedCredentials,
4877
7949
  SANDBOX_LIST_MAX,
4878
7950
  SANDBOX_LIST_POOLS_MAX,
@@ -4880,6 +7952,7 @@ export {
4880
7952
  SANDBOX_TOOLS,
4881
7953
  SANDBOX_WATCH_KEEPALIVE_MS,
4882
7954
  SANDBOX_WATCH_PATH,
7955
+ SandboxProviderRouter,
4883
7956
  TenantPoolCapacityError,
4884
7957
  TenantPoolConflictError,
4885
7958
  capacityOutputSchema,