@decocms/sandbox-controller 0.6.0 → 0.7.1

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +2422 -6
  2. package/dist/index.js +191 -29
  3. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -60,6 +60,15 @@ interface Sandbox {
60
60
  warmPoolAdopted: boolean;
61
61
  /** Where the sandbox runs. */
62
62
  provider: SandboxProviderKind;
63
+ /** Why it runs there, when `SandboxProviderRouter` had a choice to make. */
64
+ placement?: SandboxPlacement;
65
+ }
66
+ declare const SANDBOX_PLACEMENT_REASONS: readonly ["existing", "requested", "split", "capacity", "fallback", "image", "warm-pool", "freestyle-unavailable"];
67
+ type SandboxPlacementReason = (typeof SANDBOX_PLACEMENT_REASONS)[number];
68
+ interface SandboxPlacement {
69
+ reason: SandboxPlacementReason;
70
+ /** The provider whose ensure failed, when `reason` is `fallback`. */
71
+ fallbackFrom?: SandboxProviderKind;
63
72
  }
64
73
  /** When omitted, no dev server is started; the default tool image is used. */
65
74
  interface Workload {
@@ -490,6 +499,22 @@ declare const ensureOutputSchema: z.ZodObject<{
490
499
  kubernetes: "kubernetes";
491
500
  freestyle: "freestyle";
492
501
  }>>;
502
+ placement: z.ZodCatch<z.ZodOptional<z.ZodObject<{
503
+ reason: z.ZodEnum<{
504
+ existing: "existing";
505
+ requested: "requested";
506
+ split: "split";
507
+ capacity: "capacity";
508
+ fallback: "fallback";
509
+ image: "image";
510
+ "warm-pool": "warm-pool";
511
+ "freestyle-unavailable": "freestyle-unavailable";
512
+ }>;
513
+ fallbackFrom: z.ZodOptional<z.ZodEnum<{
514
+ kubernetes: "kubernetes";
515
+ freestyle: "freestyle";
516
+ }>>;
517
+ }, z.core.$strip>>>;
493
518
  daemon: z.ZodObject<{
494
519
  url: z.ZodURL;
495
520
  token: z.ZodString;
@@ -787,7 +812,7 @@ interface AgentSandboxProviderOptions {
787
812
  * API key from first provision, and Better Auth deletes that key once it
788
813
  * expires — so replaying the persisted config on recovery mounts org-fs with
789
814
  * a credential that no longer exists. Every WebDAV call then 401s and the
790
- * agent sees `Input/output error` on every `org/` path, silently, for the
815
+ * agent sees `Input/output error` on every `/app/org/` path, silently, for the
791
816
  * life of the sandbox. Returns null when it can't mint (no org slug, mint
792
817
  * error); the runner then falls back to the persisted config. Must never
793
818
  * throw.
@@ -1310,6 +1335,2364 @@ interface TenantPoolReader {
1310
1335
  /** A cached pool list for `AgentSandboxProvider`'s `tenantPools`; a failed load keeps the last one. */
1311
1336
  declare function tenantPoolReader(store: Pick<TenantPoolStore, "list">, cluster: string, { refreshMs }?: TenantPoolReaderOptions): TenantPoolReader;
1312
1337
 
1338
+ /** Options every credential mode shares. */
1339
+ interface TransportOptions {
1340
+ baseUrl?: string;
1341
+ /**
1342
+ * Transport to use instead of the built-in one, which speaks HTTP/2 on Node
1343
+ * and defers to the runtime's `fetch` everywhere else. Pass one to route
1344
+ * through a proxy, size the connection pool yourself, or add retries.
1345
+ */
1346
+ fetch?: typeof fetch;
1347
+ }
1348
+ type FreestyleOptions = ({
1349
+ apiKey?: string;
1350
+ identityAccessToken?: never;
1351
+ } & TransportOptions) | ({
1352
+ identityAccessToken: string;
1353
+ apiKey?: never;
1354
+ } & TransportOptions) | ({
1355
+ /** Stack access token used by the CLI; resolved to `teamId` at the gateway. */
1356
+ stackAccessToken: string;
1357
+ teamId: string;
1358
+ apiKey?: never;
1359
+ identityAccessToken?: never;
1360
+ } & TransportOptions);
1361
+ /** `undefined`/`null` query values are dropped; everything else is stringified. */
1362
+ type QueryValue = string | number | boolean | undefined | null;
1363
+ /**
1364
+ * Thin HTTP client for the Freestyle public API: attaches the configured
1365
+ * credential, resolves the base URL, and turns non-2xx responses into
1366
+ * {@link FreestyleApiError}.
1367
+ *
1368
+ * Every request runs as a background request: the API detaches the work from
1369
+ * the HTTP connection, so a call that outlives the connection (a long
1370
+ * snapshot materialization, a cold VM boot — including one triggered by a
1371
+ * read that wakes the VM, a client/proxy timeout) still runs to completion.
1372
+ * Calls that finish within a few seconds return inline exactly as usual;
1373
+ * longer ones answer `202 Accepted` and the client transparently polls
1374
+ * `GET /v5/background-requests/{id}` until the result — same value, same
1375
+ * errors — is ready. If a finished result was too large to store (the API
1376
+ * caps stored bodies), idempotent GETs are re-fetched once directly: by then
1377
+ * the slow part (e.g. waking the VM) has already happened. Identity-token
1378
+ * clients are the one exception: their scope cannot reach the poll route,
1379
+ * so none of their requests are backgrounded.
1380
+ */
1381
+ declare class FreestyleClient {
1382
+ readonly baseUrl: string;
1383
+ private readonly authenticationHeaders;
1384
+ private readonly fetchImpl;
1385
+ /** Identity-token callers cannot poll `/v5/background-requests`, so their requests are never backgrounded. */
1386
+ private readonly backgroundEligible;
1387
+ constructor(options?: FreestyleOptions);
1388
+ /** Headers this client authenticates every request with, e.g. for a raw WebSocket handshake. */
1389
+ authHeaders(): Record<string, string>;
1390
+ /** Absolute URL for an API path, e.g. `/v5/vms/{id}` → `https://.../v5/vms/{id}`. */
1391
+ url(path: string, query?: Record<string, QueryValue>): string;
1392
+ /**
1393
+ * Low-level escape hatch: an authenticated `fetch` against the API, relative
1394
+ * to `baseUrl`. Pass the whole path, `/v5` included — the same paths the
1395
+ * methods above use, and the same ones the API declares. It is also how the
1396
+ * client follows a server-supplied URL (a 202's `resultUrl`), and the only
1397
+ * way to reach the unversioned `/status` and `/openapi.json`.
1398
+ */
1399
+ fetch(path: string, init?: RequestInit): Promise<Response>;
1400
+ /** A path or URL as this client's `fetch` resolves it: absolute URLs are
1401
+ * followed as given, everything else hangs off `baseUrl` with no version
1402
+ * segment added — the caller already supplied whatever version it wants. */
1403
+ private absoluteUrl;
1404
+ private request;
1405
+ /**
1406
+ * Follow a 202 from a backgrounded request: poll its result URL until the
1407
+ * stored response — success or error, identical to what the inline call
1408
+ * would have produced — is ready.
1409
+ */
1410
+ private pollBackgroundRequest;
1411
+ /** Turn a terminal API response into the call's value, or throw its error. */
1412
+ private finalize;
1413
+ get<T>(path: string, query?: Record<string, QueryValue>): Promise<T>;
1414
+ post<T>(path: string, body?: unknown): Promise<T>;
1415
+ patch<T>(path: string, body?: unknown): Promise<T>;
1416
+ put<T>(path: string, body?: unknown): Promise<T>;
1417
+ putEmpty<T>(path: string): Promise<T>;
1418
+ /** PUT raw bytes as `application/octet-stream`: a whole file, or one chunk of one. */
1419
+ putBytes<T>(path: string, rawBody: Uint8Array | Blob, query?: Record<string, QueryValue>, signal?: AbortSignal): Promise<T>;
1420
+ /**
1421
+ * GET a response whose body is bytes rather than JSON, returned unread so the
1422
+ * caller can stream it. Errors still surface as {@link FreestyleApiError}.
1423
+ */
1424
+ getBytes(path: string, query?: Record<string, QueryValue>, init?: {
1425
+ range?: string;
1426
+ signal?: AbortSignal;
1427
+ }): Promise<Response>;
1428
+ delete<T = void>(path: string, query?: Record<string, QueryValue>): Promise<T>;
1429
+ }
1430
+
1431
+ type VerificationState = "pending" | "verified";
1432
+ interface DomainVerification {
1433
+ id: string;
1434
+ domain: string;
1435
+ verificationCode: string;
1436
+ /** Place `verificationCode` in a TXT record at this name, then verify. */
1437
+ recordName: string;
1438
+ state: VerificationState;
1439
+ /** When this challenge proved control; null while pending. */
1440
+ verifiedAt: string | null;
1441
+ createdAt: string;
1442
+ }
1443
+ interface DomainOwnership {
1444
+ domain: string;
1445
+ createdAt: string;
1446
+ }
1447
+ /** A successful verification: ownership, plus which challenge proved it. */
1448
+ interface DomainVerified {
1449
+ domain: string;
1450
+ createdAt: string;
1451
+ verifiedBy: string;
1452
+ }
1453
+ interface CertificateInfo {
1454
+ domain: string;
1455
+ wildcard: boolean;
1456
+ active: boolean;
1457
+ notAfter: string;
1458
+ generation: number;
1459
+ }
1460
+ /**
1461
+ * What your placeholder pages wear. A placeholder page is the edge's 404 for
1462
+ * a hostname under one of your verified domains that none of your TLS rules
1463
+ * serve; it reloads itself until a route appears.
1464
+ */
1465
+ interface PlaceholderBranding {
1466
+ /** An https URL of the logo. The visitor's browser loads it directly. */
1467
+ logoUrl: string;
1468
+ /** Named on the page in place of Freestyle; null = logo only. */
1469
+ productName: string | null;
1470
+ createdAt: string;
1471
+ updatedAt: string;
1472
+ }
1473
+ interface SetPlaceholderBrandingOptions {
1474
+ /** An https URL of the logo, at most 2048 characters. */
1475
+ logoUrl: string;
1476
+ /** Named on the page in place of Freestyle, at most 64 characters. Omit for logo only. */
1477
+ productName?: string;
1478
+ }
1479
+ interface ListVerificationsOptions {
1480
+ /** Narrow to one state. Omit for every challenge the account holds, verified records included. */
1481
+ state?: VerificationState;
1482
+ }
1483
+ /** The `freestyle.domains` namespace. */
1484
+ declare class DomainsNamespace {
1485
+ private readonly client;
1486
+ readonly verifications: DomainVerificationsNamespace;
1487
+ readonly certificates: DomainCertificatesNamespace;
1488
+ /** The branding on the 404 page for hostnames under your domains that nothing serves. */
1489
+ readonly placeholder: DomainPlaceholderNamespace;
1490
+ constructor(client: FreestyleClient);
1491
+ /**
1492
+ * List the domains you hold: those you verified, and any Freestyle
1493
+ * subdomain you have taken by publishing it.
1494
+ */
1495
+ list(): Promise<DomainOwnership[]>;
1496
+ }
1497
+ declare class DomainVerificationsNamespace {
1498
+ private readonly client;
1499
+ constructor(client: FreestyleClient);
1500
+ /**
1501
+ * Issue a challenge. Publish the returned `verificationCode` as a TXT
1502
+ * record at `recordName`, then `complete()` it.
1503
+ */
1504
+ create(domain: string): Promise<DomainVerification>;
1505
+ list(options?: ListVerificationsOptions): Promise<DomainVerification[]>;
1506
+ /** Fetch a verification challenge by its id or its domain. */
1507
+ get(domainOrId: string): Promise<DomainVerification>;
1508
+ /**
1509
+ * Check DNS and record ownership. Addressed by id, only that challenge's
1510
+ * own code counts; use this when several people are verifying the same
1511
+ * domain.
1512
+ */
1513
+ complete(domainOrId: string): Promise<DomainVerified>;
1514
+ /**
1515
+ * Withdraw outstanding challenges; by domain this drops all of them. A
1516
+ * challenge that already verified is a permanent record and cannot be
1517
+ * deleted.
1518
+ */
1519
+ delete(domainOrId: string): Promise<void>;
1520
+ }
1521
+ declare class DomainCertificatesNamespace {
1522
+ private readonly client;
1523
+ constructor(client: FreestyleClient);
1524
+ list(): Promise<CertificateInfo[]>;
1525
+ /**
1526
+ * Request an ongoing `*.domain` certificate. Requires
1527
+ * `_acme-challenge.<domain>` to be NS-delegated to Freestyle's
1528
+ * nameservers, since wildcards can only be proven over DNS.
1529
+ */
1530
+ createWildcard(domain: string): Promise<CertificateInfo>;
1531
+ }
1532
+ /** The `freestyle.domains.placeholder` namespace. */
1533
+ declare class DomainPlaceholderNamespace {
1534
+ private readonly client;
1535
+ constructor(client: FreestyleClient);
1536
+ /** Your placeholder branding, or `null` when pages carry the Freestyle default. */
1537
+ get(): Promise<PlaceholderBranding | null>;
1538
+ /**
1539
+ * Set the branding for every placeholder page under your verified domains,
1540
+ * including domains you verify later. Replaces any earlier setting.
1541
+ */
1542
+ set(options: SetPlaceholderBrandingOptions): Promise<PlaceholderBranding>;
1543
+ /** Remove your branding; pages go back to the Freestyle default. */
1544
+ delete(): Promise<void>;
1545
+ }
1546
+
1547
+ /** The transport a matcher is restricted to. */
1548
+ type FirewallProtocol = "tcp" | "udp" | "icmp";
1549
+ /**
1550
+ * What a rule does with the traffic it matches.
1551
+ *
1552
+ * Only `allow` exists today, and every rule states it rather than defaulting:
1553
+ * a default would mean that the day `deny` arrives, every rule written before
1554
+ * it silently acquires an action it never stated — and "what does this rule
1555
+ * do" is the one question a firewall rule must never answer by omission.
1556
+ */
1557
+ type FirewallAction = "allow";
1558
+ /**
1559
+ * One end of a rule: which traffic this side matches.
1560
+ *
1561
+ * Fields intersect. `{ vpcId: "vpc-db", port: 5432, protocol: "tcp" }` is
1562
+ * "traffic in that network, on 5432, over TCP" — not three alternatives.
1563
+ *
1564
+ * Matchers select two different ways. `vmId`/`vpcId`/`tunnelId` select by
1565
+ * **identity**: they match that resource however it is addressed. `cidr`/
1566
+ * `public` select by **address**: `public: true` is every publicly routable
1567
+ * address, whoever owns it — another Freestyle VM reached at its public address
1568
+ * is reached over the public Internet like any other host. The two overlap, and
1569
+ * allow rules union, so that is simply more traffic allowed.
1570
+ *
1571
+ * The public Internet is `public: true`, never an empty matcher. An empty
1572
+ * matcher is the absence of a statement, and that is what
1573
+ * {@link VmFirewallSpec} uses to mean "the VM being created"; if emptiness
1574
+ * also meant the Internet, the most dangerous rule you can write would be the
1575
+ * one you get by forgetting a field.
1576
+ *
1577
+ * New selectors arrive as new optional fields, so this type only ever grows.
1578
+ */
1579
+ interface FirewallEndpoint {
1580
+ /** A single VM, by id or by your slug for it. Answers hold its id. */
1581
+ vmId?: string;
1582
+ /** Everything on one private network, by id or by your slug for it. Answers hold its id. */
1583
+ vpcId?: string;
1584
+ /**
1585
+ * Whatever is on the far side of one tunnel — the client that dials it, and
1586
+ * any remote range routed over it. Tunnels have no slug, so this is a tunnel
1587
+ * id: `tun-…`, or `wg-…` for one carried over from the per-VPC VPN records
1588
+ * tunnels replaced.
1589
+ *
1590
+ * The one selector that names traffic arriving from outside the platform by
1591
+ * identity rather than by address: the peer's addresses are the peer's
1592
+ * business, and can change without the rules admitting it changing too.
1593
+ */
1594
+ tunnelId?: string;
1595
+ /** An address range, IPv4 or IPv6, in canonical `network/prefix` form. */
1596
+ cidr?: string;
1597
+ /**
1598
+ * Every publicly routable address, whoever owns it — including another
1599
+ * Freestyle VM reached at its public address. Only `true` is meaningful —
1600
+ * omit the field otherwise — and it is not combined with `vmId` or `vpcId`.
1601
+ */
1602
+ public?: true;
1603
+ /** A single port, 1–65535. Requires {@link FirewallEndpoint.protocol}. */
1604
+ port?: number;
1605
+ protocol?: FirewallProtocol;
1606
+ }
1607
+ /**
1608
+ * A resource a rule cannot outlive. Deleting the resource deletes the rule, so
1609
+ * no rule is ever left naming something that no longer exists.
1610
+ */
1611
+ interface FirewallDependency {
1612
+ /** `"vm"`, `"vpc"`, or `"tunnel"` today; new selectors add new kinds. */
1613
+ kind: string;
1614
+ id: string;
1615
+ /**
1616
+ * Your handle for the named resource, when it has one — so a rule can say
1617
+ * what it points at without a lookup per id.
1618
+ */
1619
+ slug?: string | null;
1620
+ /** Your label for the named resource, under the same rules as `slug`. */
1621
+ displayName?: string | null;
1622
+ }
1623
+ /** An allowed path through the network: traffic matching `source` may reach `destination`. */
1624
+ interface FirewallRuleData {
1625
+ id: string;
1626
+ action: FirewallAction;
1627
+ source: FirewallEndpoint;
1628
+ destination: FirewallEndpoint;
1629
+ description?: string | null;
1630
+ /** What this rule dies with. See {@link FirewallDependency}. */
1631
+ dependencies: FirewallDependency[];
1632
+ createdAt: string;
1633
+ updatedAt: string;
1634
+ }
1635
+ interface CreateFirewallRuleOptions {
1636
+ /** Required. `"allow"` is the only accepted value. */
1637
+ action: FirewallAction;
1638
+ source: FirewallEndpoint;
1639
+ destination: FirewallEndpoint;
1640
+ /** A free-form note, up to 1024 characters. */
1641
+ description?: string;
1642
+ }
1643
+ /**
1644
+ * A rule declared inside a create, where the resource being created has no id
1645
+ * yet.
1646
+ *
1647
+ * Exactly one endpoint may be left without an identity; that one *is* the new
1648
+ * VM. So `{ source: { public: true }, destination: { port: 443, protocol:
1649
+ * "tcp" } }` is inbound HTTPS to it, and `{ source: {}, destination: { public:
1650
+ * true, port: 443, protocol: "tcp" } }` is outbound HTTPS from it.
1651
+ */
1652
+ type InlineFirewallRule = CreateFirewallRuleOptions;
1653
+ /** The `firewall` block of a create. */
1654
+ interface FirewallSpec {
1655
+ rules: InlineFirewallRule[];
1656
+ }
1657
+ interface ListFirewallRulesOptions {
1658
+ /**
1659
+ * Rules that apply to this VM: those naming it, plus those naming a private
1660
+ * network it is attached to.
1661
+ */
1662
+ vmId?: string;
1663
+ /** Rules naming this private network. */
1664
+ vpcId?: string;
1665
+ /** Rules naming this tunnel, by its id. */
1666
+ tunnelId?: string;
1667
+ limit?: number;
1668
+ offset?: number;
1669
+ }
1670
+ interface ListFirewallRulesResult {
1671
+ /** Newest first. */
1672
+ rules: FirewallRuleData[];
1673
+ totalCount: number;
1674
+ }
1675
+ /** One end of a connection to ask about: everything true about the party. */
1676
+ interface TrafficParty {
1677
+ /**
1678
+ * Freestyle's own ingress — the web proxy serving a mapped domain, or the
1679
+ * SSH proxy. Always allowed, whatever your rules say.
1680
+ */
1681
+ platform?: boolean;
1682
+ vmId?: string;
1683
+ /** Every private network this party is on; a rule naming one of them matches. */
1684
+ vpcIds?: string[];
1685
+ tunnelId?: string;
1686
+ /** The address in use, when there is one — what `cidr` rules are tested against. */
1687
+ address?: string;
1688
+ /** Whether that address is publicly routable — what `public: true` matches. */
1689
+ public?: boolean;
1690
+ port?: number;
1691
+ }
1692
+ /** A connection to decide about. */
1693
+ interface TrafficDescription {
1694
+ source: TrafficParty;
1695
+ destination: TrafficParty;
1696
+ protocol?: FirewallProtocol;
1697
+ }
1698
+ /**
1699
+ * Why a connection would be allowed, or that it would not.
1700
+ *
1701
+ * `allowedByPlatform` means Freestyle itself delivered it — a mapped domain,
1702
+ * an SSH session — which no rule can block. `allowedByRule` names the rule.
1703
+ * `deniedByPlatform` runs the other way: platform policy closed the connection
1704
+ * and no rule opens it — today, outbound mail, which goes through an `smtp`
1705
+ * TLS rule instead.
1706
+ */
1707
+ type FirewallDecision = {
1708
+ outcome: "allowedByPlatform";
1709
+ side: "source" | "destination" | "both";
1710
+ } | {
1711
+ outcome: "allowedByRule";
1712
+ ruleId: string;
1713
+ } | {
1714
+ outcome: "deniedByPlatform";
1715
+ reason: "outboundMail";
1716
+ } | {
1717
+ outcome: "denied";
1718
+ };
1719
+ /** Firewall rules: `freestyle.firewall.rules`. */
1720
+ declare class FirewallRulesNamespace {
1721
+ private readonly client;
1722
+ constructor(client: FreestyleClient);
1723
+ /**
1724
+ * Allow traffic from one place to another.
1725
+ *
1726
+ * @example
1727
+ * // Public Internet -> a VM, on HTTPS.
1728
+ * await freestyle.firewall.rules.create({
1729
+ * action: "allow",
1730
+ * source: { public: true },
1731
+ * destination: { vmId: "vm-123456", port: 443, protocol: "tcp" },
1732
+ * });
1733
+ *
1734
+ * @example
1735
+ * // One private network -> another, on Postgres.
1736
+ * await freestyle.firewall.rules.create({
1737
+ * action: "allow",
1738
+ * source: { vpcId: "vpc-frontend" },
1739
+ * destination: { vpcId: "vpc-backend", port: 5432, protocol: "tcp" },
1740
+ * });
1741
+ *
1742
+ * @throws {FirewallRuleValidationError} before any request, when the rule
1743
+ * cannot be well formed.
1744
+ */
1745
+ create(options: CreateFirewallRuleOptions): Promise<FirewallRuleData>;
1746
+ /**
1747
+ * List your rules, newest first. Pass `vmId` for the rules that apply to one
1748
+ * VM — those naming it, plus those naming a private network it is on.
1749
+ */
1750
+ list(options?: ListFirewallRulesOptions): Promise<ListFirewallRulesResult>;
1751
+ get(ruleId: string): Promise<FirewallRuleData>;
1752
+ /** Delete a rule. The resources it named are untouched. */
1753
+ delete(ruleId: string): Promise<void>;
1754
+ }
1755
+ /** The `freestyle.firewall` namespace. */
1756
+ declare class FirewallNamespace {
1757
+ private readonly client;
1758
+ readonly rules: FirewallRulesNamespace;
1759
+ constructor(client: FreestyleClient);
1760
+ /**
1761
+ * Ask whether a connection would be allowed, without changing anything.
1762
+ *
1763
+ * Useful for checking a rule set before you rely on it — and for confirming
1764
+ * what you already believe, e.g. that a mapped domain reaches a VM with no
1765
+ * rules at all.
1766
+ *
1767
+ * @example
1768
+ * // A mapped domain: allowed whatever your rules say.
1769
+ * await freestyle.firewall.evaluate({
1770
+ * source: { platform: true },
1771
+ * destination: { vmId: "vm-123456", port: 3000 },
1772
+ * protocol: "tcp",
1773
+ * });
1774
+ * // => { outcome: "allowedByPlatform", side: "source" }
1775
+ */
1776
+ evaluate(traffic: TrafficDescription): Promise<FirewallDecision>;
1777
+ }
1778
+
1779
+ interface IdentityData {
1780
+ id: string;
1781
+ managed: boolean;
1782
+ }
1783
+ interface IdentityInfo {
1784
+ id: string;
1785
+ accountId: string;
1786
+ managed: boolean;
1787
+ }
1788
+ interface CreatedToken {
1789
+ id: string;
1790
+ /** Returned once, in full. */
1791
+ token: string;
1792
+ }
1793
+ interface AccessTokenInfo {
1794
+ id: string;
1795
+ }
1796
+ interface ListIdentitiesResult {
1797
+ identities: IdentityData[];
1798
+ total: number;
1799
+ }
1800
+ interface ListIdentitiesOptions {
1801
+ limit?: number;
1802
+ offset?: number;
1803
+ includeManaged?: boolean;
1804
+ }
1805
+ interface VmPermission {
1806
+ id: string;
1807
+ identityId: string;
1808
+ vmId: string;
1809
+ allowedLinuxUsers: string[] | null;
1810
+ grantedAt: string;
1811
+ grantedBy: string;
1812
+ }
1813
+ interface GrantVmPermissionOptions {
1814
+ vmId: string;
1815
+ /** Restrict the grant to these Linux users; omit (or null) for unrestricted access. */
1816
+ allowedLinuxUsers?: string[] | null;
1817
+ }
1818
+ /** An identity's access tokens: `identity.tokens`. */
1819
+ declare class IdentityTokensNamespace {
1820
+ private readonly client;
1821
+ private readonly basePath;
1822
+ constructor(client: FreestyleClient, basePath: string);
1823
+ /** Mint an access token for the identity. */
1824
+ create(): Promise<CreatedToken>;
1825
+ list(): Promise<AccessTokenInfo[]>;
1826
+ revoke(tokenId: string): Promise<void>;
1827
+ }
1828
+ /** An identity's per-VM permission grants: `identity.permissions.vm`. */
1829
+ declare class IdentityVmPermissionsNamespace {
1830
+ private readonly client;
1831
+ private readonly basePath;
1832
+ constructor(client: FreestyleClient, basePath: string);
1833
+ /** List the VMs this identity has been granted access to. */
1834
+ list(): Promise<VmPermission[]>;
1835
+ /** Grant this identity access to a VM. */
1836
+ grant(options: GrantVmPermissionOptions): Promise<VmPermission>;
1837
+ /** Fetch this identity's grant on a VM. */
1838
+ get(vmId: string): Promise<VmPermission>;
1839
+ /** Change which Linux users this identity may act as on a VM. */
1840
+ update(vmId: string, allowedLinuxUsers: string[] | null): Promise<VmPermission>;
1841
+ /** Revoke this identity's access to a VM. */
1842
+ revoke(vmId: string): Promise<void>;
1843
+ }
1844
+ /** A handle to one identity. */
1845
+ declare class Identity {
1846
+ readonly id: string;
1847
+ readonly tokens: IdentityTokensNamespace;
1848
+ readonly permissions: {
1849
+ vm: IdentityVmPermissionsNamespace;
1850
+ };
1851
+ private readonly client;
1852
+ private readonly basePath;
1853
+ constructor(client: FreestyleClient, id: string);
1854
+ data(): Promise<IdentityInfo>;
1855
+ /** Delete the identity, along with its tokens and grants. Managed identities cannot be deleted. */
1856
+ delete(): Promise<void>;
1857
+ }
1858
+ /** The `freestyle.identities` namespace: scoped access for your end users. */
1859
+ declare class IdentitiesNamespace {
1860
+ private readonly client;
1861
+ constructor(client: FreestyleClient);
1862
+ /** Create an identity. */
1863
+ create(options?: {
1864
+ managed?: boolean;
1865
+ }): Promise<{
1866
+ identity: Identity;
1867
+ identityId: string;
1868
+ data: IdentityData;
1869
+ }>;
1870
+ list(options?: ListIdentitiesOptions): Promise<ListIdentitiesResult>;
1871
+ get(identityId: string): Promise<IdentityInfo>;
1872
+ /** A handle to an identity, without a network call. */
1873
+ ref(identityId: string): Identity;
1874
+ delete(identityId: string): Promise<void>;
1875
+ }
1876
+
1877
+ /**
1878
+ * How the edge treats the session's bytes.
1879
+ *
1880
+ * **`http` is the default.** A rule without a protocol is HTTP; the edge
1881
+ * terminates the session so a {@link TlsTransform} (header injection) can act on
1882
+ * it.
1883
+ *
1884
+ * `postgres` is served on **5432**. Two shapes use it. **Ingress** publishes a
1885
+ * database by name: the edge terminates the TLS your certificate covers and
1886
+ * splices the session to the VM's Postgres port, so your own server still
1887
+ * authenticates the client end to end. **Egress** is the interesting one — with a
1888
+ * {@link PostgresTransform} the guest connects to a vendor's database with *no
1889
+ * password at all*, and the edge completes the real handshake upstream
1890
+ * (SCRAM-SHA-256, MD5, or cleartext) with credentials the guest has never held.
1891
+ * Connect with `sslmode=require` or stricter: an unencrypted session offers no
1892
+ * SNI, and the name is what selects a rule.
1893
+ *
1894
+ * `minecraft` is served on **25565** for Minecraft: Java Edition. The edge reads
1895
+ * the server address out of the client's handshake — the routing key, exactly as
1896
+ * `Host` is over HTTP — and then splices the session through to the VM. Point the
1897
+ * domain's A/AAAA records at the edge as you would for a web domain and players
1898
+ * dial it directly; no SRV record is needed while they use the default port. It
1899
+ * terminates nothing, so it carries no transform and needs no certificate. A
1900
+ * **status ping** (the multiplayer list) never starts a stopped VM — it is
1901
+ * answered with a "sleeping" line instead; a player **joining** is what wakes it.
1902
+ *
1903
+ * `tcp` is opaque passthrough, and it shares **443** with `http` — the client
1904
+ * dials HTTPS either way, and the edge tells them apart by matching the SNI in
1905
+ * the ClientHello against your rules. Where an `http` rule has the edge
1906
+ * terminate the session, a `tcp` rule has it splice: the bytes go to your VM
1907
+ * untouched and **your own certificate** answers the handshake. Ask for it when
1908
+ * the session is yours to end — mutual TLS you verify yourself, a protocol over
1909
+ * TLS that is not HTTP, a certificate you would rather issue than delegate. The
1910
+ * cost is everything the edge could otherwise have done: no transform, no
1911
+ * certificate issued for the name, and no HTTP/3 (there is no TCP to splice).
1912
+ * It is **public ingress only** — `{ public: true } -> { vmId, port }`.
1913
+ *
1914
+ * `imap` and `imaps` are served on **993**, and share it the way `http` and
1915
+ * `tcp` share 443 — one door, one route table, and the rule decides whether the
1916
+ * edge is in the session. `imap` terminates: the edge answers the handshake
1917
+ * with a certificate issued for the name and forwards *plain* IMAP to your
1918
+ * server, so the rule lands on 143 by default and you run an unadorned mail
1919
+ * server. `imaps` splices: your server terminates its own TLS on its own
1920
+ * certificate and the platform holds no key to a byte of the mail, at the price
1921
+ * of your obtaining that certificate and renewing it. Neither carries a
1922
+ * transform — the credential in an IMAP session is your user's own. Both are
1923
+ * public ingress only.
1924
+ *
1925
+ * `smtp` is served on **25** (MX delivery), **587** (submission) and **465**
1926
+ * (implicit TLS). It is the one protocol whose `domain` is not a hostname: mail
1927
+ * is routed by the **envelope recipient**, so a rule's name is a mail route and
1928
+ * may carry a mailbox. `bot@acme.com` serves that one address, `acme.com` serves
1929
+ * every mailbox at the name, `bot@*.acme.com` serves that mailbox at any
1930
+ * subdomain and `*.acme.com` serves every mailbox at any subdomain — most
1931
+ * specific wins. That is what lets a single wildcard MX record at `*.acme.com`
1932
+ * give every subdomain its own VM, and what lets `agent-7@acme.com` land
1933
+ * somewhere different from `sales@acme.com`; no SNI could express either, which
1934
+ * is why the envelope and not the handshake is the routing key. Point an **MX**
1935
+ * record at the edge rather than an A/AAAA one. The edge speaks SMTP as far as
1936
+ * the first `RCPT TO`, then replays the envelope to your VM and splices — your
1937
+ * own server still accepts or refuses the mail itself. **Egress** works like
1938
+ * `postgres`: with an {@link SmtpTransform} the guest submits mail through a
1939
+ * provider with *no password at all*, and the edge completes `AUTH` upstream
1940
+ * with credentials the guest has never held. Your VM sees the edge as the
1941
+ * connecting peer, so a guest that scores senders by IP will score the edge.
1942
+ *
1943
+ * `socks5` (1080) is the mirror image of all of them and the only **egress-only**
1944
+ * protocol. Your VM opens SOCKS5 to the platform edge offering *no
1945
+ * authentication*, names its destination inside the request, and the edge opens
1946
+ * the session onward through your own proxy provider using the credentials a
1947
+ * {@link Socks5Transform} carries — so the workload can use the subscription
1948
+ * without ever being able to read it. Nothing is terminated: after the proxy
1949
+ * answers, the edge copies bytes it cannot decrypt, and your TLS runs end to end
1950
+ * to the real origin. Because the destination rides the SOCKS request rather
1951
+ * than DNS, a `socks5` rule's `domain` is an **allow-list** and not a name
1952
+ * anything resolves — `*` ("everything this VM opens goes through my proxy") is
1953
+ * the shape most people want and is accepted here alone. The destination names
1954
+ * the proxy: `{ vmId } -> { host: "gate.provider.com", port: 1080 }`.
1955
+ *
1956
+ * A **VM-to-VM internal service** (`{ vmId | vpcId } -> { vmId }`) is
1957
+ * edge-brokered like every other shape (the source opens the name at the edge's
1958
+ * front door for its protocol, and the edge forwards to the target VM's port) —
1959
+ * and it cannot carry a transform yet.
1960
+ */
1961
+ type TlsProtocol = "tcp" | "http" | "postgres" | "minecraft" | "smtp" | "imap" | "imaps" | "socks5";
1962
+ /**
1963
+ * What a rule does with the session it matches.
1964
+ *
1965
+ * Only `allow` exists today, and every rule states it rather than defaulting —
1966
+ * the same discipline the firewall keeps, and for the same reason. A default
1967
+ * would mean that the day `deny` arrives (here it plausibly does: "allow every
1968
+ * domain except this one" is a real thing to want, given the `*` wildcard), every
1969
+ * rule written before it silently acquires an action it never stated.
1970
+ */
1971
+ type TlsAction = "allow";
1972
+ /**
1973
+ * One end of a rule: which session this side matches.
1974
+ *
1975
+ * The vocabulary is the firewall's — identity (`vmId`/`vpcId`) or address
1976
+ * (`public`) — because a reader who knows the firewall should read this by
1977
+ * reflex. What differs is that the two ends are *not* symmetric, and the
1978
+ * asymmetries fall out of what each side means:
1979
+ *
1980
+ * - **A source never carries a landing coordinate** (`host`/`port`). A source is
1981
+ * *who opens* the session, not where it goes. The edge authenticates a source
1982
+ * by identity — its switch port, its anti-spoofed address — never by the name
1983
+ * it dials, because the name a client offers proves nothing about the client.
1984
+ * - **A destination is never a `vpcId`.** "Which member of a network does this
1985
+ * land on" has no answer; a landing is one place. A whole network may be a
1986
+ * *source* — every VM on it may open the session — but not a destination.
1987
+ *
1988
+ * `public` also means different things by side. On a source it is the firewall's
1989
+ * meaning exactly: every publicly routable client. On a destination it is *the
1990
+ * domain's own public origin* — wherever the world's DNS says the name lives — so
1991
+ * egress to a vendor is `{ public: true }`, never a blank matcher. The blank
1992
+ * matcher never means "the Internet" here, for the same reason it never does in
1993
+ * the firewall: the most dangerous statement you can write must not be the one
1994
+ * you get by forgetting a field. Every rule states both ends.
1995
+ *
1996
+ * New selectors arrive as new optional fields, so this type only ever grows.
1997
+ */
1998
+ interface TlsEndpoint {
1999
+ /**
2000
+ * A single VM, by id or by your slug for it. On a source, the VM that may open
2001
+ * the session; on a destination, the VM the session lands on. Answers hold its
2002
+ * id.
2003
+ */
2004
+ vmId?: string;
2005
+ /**
2006
+ * Everything on one private network, by id or by your slug for it. **Source
2007
+ * only** — every member may open the session. A landing is one place, so a
2008
+ * network cannot be a destination. Answers hold its id.
2009
+ */
2010
+ vpcId?: string;
2011
+ /**
2012
+ * On a source, every publicly routable client — the open Internet dials the
2013
+ * name. On a destination, the domain's own public origin: wherever the world's
2014
+ * DNS resolves the name. Only `true` is meaningful — omit the field otherwise.
2015
+ */
2016
+ public?: true;
2017
+ /**
2018
+ * **Destination only.** Pin the session to this host instead of resolving the
2019
+ * domain — an alias, or an address reachable over a tunnel — while still
2020
+ * presenting `domain` as the SNI.
2021
+ */
2022
+ host?: string;
2023
+ /**
2024
+ * **Destination only.** The port the session lands on. Required when the
2025
+ * destination is a `vmId` or a `host`; defaults to 443 for a `public` origin.
2026
+ */
2027
+ port?: number;
2028
+ }
2029
+ /**
2030
+ * Credentials injected into a Postgres startup handshake. The guest connects with
2031
+ * no password (or any); the edge completes the real handshake upstream — so a
2032
+ * compromised guest never holds a credential it could exfiltrate.
2033
+ */
2034
+ interface PostgresTransform {
2035
+ /** The upstream role to authenticate as. */
2036
+ username: string;
2037
+ /** The upstream password. Write-only: sealed at rest, read back as `"***"`. */
2038
+ password: string;
2039
+ /** The database to select. Optional: omitted leaves the guest's choice. */
2040
+ database?: string;
2041
+ }
2042
+ /**
2043
+ * Credentials injected into an SMTP submission handshake. The guest submits mail
2044
+ * with no password; the edge completes `AUTH` upstream on its behalf — so a
2045
+ * compromised guest never holds a submission credential it could exfiltrate, and
2046
+ * cannot be turned into a spam relay by whoever took it.
2047
+ */
2048
+ interface SmtpTransform {
2049
+ /** The submission username to authenticate as. */
2050
+ username: string;
2051
+ /** The submission password. Write-only: sealed at rest, read back as `"***"`. */
2052
+ password: string;
2053
+ }
2054
+ /**
2055
+ * Credentials presented to your upstream SOCKS5 proxy. Your VM connects to the
2056
+ * platform's SOCKS5 front offering *no authentication at all* and the edge
2057
+ * authenticates to the provider on its behalf — so the subscription is never
2058
+ * inside the VM to be read out of its environment, its disk or a snapshot of it.
2059
+ * A guest that is fully compromised can use the proxy; it cannot take it.
2060
+ *
2061
+ * Optional. A provider that allow-lists its customers by source address needs no
2062
+ * pair, and a `socks5` rule with no transform is an ordinary shape.
2063
+ */
2064
+ interface Socks5Transform {
2065
+ /** The username to present to the upstream proxy. */
2066
+ username: string;
2067
+ /** The password to present to the upstream proxy. Write-only: sealed at rest, read back as `"***"`. */
2068
+ password: string;
2069
+ }
2070
+ /**
2071
+ * One rewrite applied to a session in flight — something only the platform can
2072
+ * do, because the whole point is to do what the guest could not do itself.
2073
+ *
2074
+ * Externally tagged: the wire is `{ headers: { … } }`, `{ postgres: { … } }`,
2075
+ * `{ smtp: { … } }`, `{ socks5: { … } }` or `{ s3: { … } }`, exactly one key — `headers` injects
2076
+ * request headers into an `http` session, `postgres` completes a `postgres`
2077
+ * handshake, `smtp` completes a submission `AUTH`, and `socks5` authenticates the
2078
+ * hop to your proxy provider. Each names the protocol it needs, so a rule's
2079
+ * `protocol` is inferred from its transform when you omit it. Credential transforms are
2080
+ * **egress** acts: the platform spends a credential the guest never holds, so a
2081
+ * `postgres` or `smtp` transform on a rule landing on your own VM is refused —
2082
+ * pointed inward the first hands every client a logged-in session and the second
2083
+ * turns your mail server into an open relay. (`socks5` has no inward form at all
2084
+ * to refuse.) Secrets here are
2085
+ * **write-only**: sealed at rest and
2086
+ * read back as `"***"`, with the record marked {@link TlsRuleData.redacted}.
2087
+ * Header *names* survive a read — they are not secret, and seeing which headers a
2088
+ * rule sets is how you audit it.
2089
+ */
2090
+ type TlsTransform = {
2091
+ headers: Record<string, string>;
2092
+ } | {
2093
+ jsonPatch: JsonPatchOperation[];
2094
+ } | {
2095
+ postgres: PostgresTransform;
2096
+ } | {
2097
+ smtp: SmtpTransform;
2098
+ } | {
2099
+ socks5: Socks5Transform;
2100
+ } | {
2101
+ s3: S3Transform;
2102
+ };
2103
+ /** An S3 operation understood by the edge. Other operations fail closed. */
2104
+ type S3Operation = "GetObject" | "HeadObject" | "PutObject" | "DeleteObject" | "ListObjectsV2" | "CreateMultipartUpload" | "UploadPart" | "ListParts" | "CompleteMultipartUpload" | "AbortMultipartUpload" | "ListMultipartUploads";
2105
+ /** Credentials are sealed at rest, redacted on reads, and never sent to the VM. */
2106
+ interface S3Credentials {
2107
+ accessKeyId: string;
2108
+ secretAccessKey: string;
2109
+ /** Optional temporary session token. Rotate with tls.rules.update before expiry. */
2110
+ sessionToken?: string;
2111
+ }
2112
+ interface S3Scope {
2113
+ bucket: string;
2114
+ /** Literal key prefix. Use a trailing / for a directory boundary; "" permits all keys. */
2115
+ prefix: string;
2116
+ operations: S3Operation[];
2117
+ }
2118
+ /** Authorize then SigV4-sign HTTP egress. Must be the sole transform, without match.
2119
+ * Supports virtual-hosted and path-style bucket addressing at a concrete origin.
2120
+ * Listing requires an explicit prefix within scope. Copy, versioned
2121
+ * requests, ACLs, and aws-chunked uploads are rejected. Bodies stream over HTTPS
2122
+ * using UNSIGNED-PAYLOAD; policies requiring signed payload hashes are unsupported.
2123
+ */
2124
+ interface S3Transform {
2125
+ region: string;
2126
+ credentials: S3Credentials;
2127
+ scope: S3Scope;
2128
+ }
2129
+ /** A JSON value. Patch values are write-only and read back as `"***"`. */
2130
+ type JsonPatchValue = null | boolean | number | string | JsonPatchValue[] | {
2131
+ [key: string]: JsonPatchValue;
2132
+ };
2133
+ /** RFC 6902 operation. Paths use RFC 6901 JSON Pointer, including /~0 and /~1
2134
+ * escaping. `add` overwrites object members but inserts array elements; /-
2135
+ * appends to an existing array. Parent containers must already exist.
2136
+ * A rule accepts one patch of up to 128 operations / 64 KiB. All operations must
2137
+ * succeed before forwarding. Applies to every JSON HTTP egress request on the
2138
+ * rule; there is no endpoint filtering, provider translation, or wildcard path.
2139
+ */
2140
+ type JsonPatchOperation = {
2141
+ op: "add" | "replace" | "test";
2142
+ path: string;
2143
+ value: JsonPatchValue;
2144
+ } | {
2145
+ op: "remove";
2146
+ path: string;
2147
+ } | {
2148
+ op: "move" | "copy";
2149
+ from: string;
2150
+ path: string;
2151
+ };
2152
+ /** A reusable forward-auth configuration attached to one public HTTP ingress rule. */
2153
+ interface TlsForwardAuthReference {
2154
+ /** The forward-auth configuration id returned by {@link TlsForwardAuthNamespace.create}. */
2155
+ id: string;
2156
+ }
2157
+ /** The customer-owned service Freestyle asks before forwarding a request to a VM. */
2158
+ interface CreateTlsForwardAuthOptions {
2159
+ /** Absolute HTTPS endpoint that receives the authorization subrequest. */
2160
+ url: string;
2161
+ /** Static headers added to authorization subrequests. Values are write-only. */
2162
+ headers?: Record<string, string>;
2163
+ /** Authorization deadline in milliseconds. Defaults server-side; accepted range is 100-5000. */
2164
+ timeoutMs?: number;
2165
+ /** Cookies withheld from the destination VM and protected from being overwritten by it. */
2166
+ protectedCookies?: string[];
2167
+ /** Headers copied from an allowed authorization response onto the request sent to the VM. */
2168
+ authResponseHeaders?: string[];
2169
+ }
2170
+ /** A stored forward-auth configuration. Static header values are returned as `"***"`. */
2171
+ interface TlsForwardAuthData {
2172
+ id: string;
2173
+ /** The account that owns the configuration. Taken from your credential, never a request body. */
2174
+ accountId: string;
2175
+ url: string;
2176
+ /** Static authorization-request headers, with every value replaced by `"***"`. */
2177
+ headers?: Record<string, string>;
2178
+ timeoutMs: number;
2179
+ protectedCookies?: string[];
2180
+ authResponseHeaders?: string[];
2181
+ /** Whether any static header value was redacted out of `headers`. */
2182
+ redacted: boolean;
2183
+ createdAt: string;
2184
+ updatedAt: string;
2185
+ }
2186
+ interface ListTlsForwardAuthResult {
2187
+ /** Newest first. */
2188
+ configs: TlsForwardAuthData[];
2189
+ totalCount: number;
2190
+ }
2191
+ /**
2192
+ * A resource a rule cannot outlive. Deleting the resource deletes the rule, so no
2193
+ * rule is ever left naming something that no longer exists — the same cascade the
2194
+ * firewall uses.
2195
+ */
2196
+ interface TlsDependency {
2197
+ /** `"vm"` or `"vpc"` today; new selectors add new kinds. */
2198
+ kind: string;
2199
+ id: string;
2200
+ /**
2201
+ * Your handle for the named resource, when it has one — so a rule can say
2202
+ * what it points at without a lookup per id.
2203
+ */
2204
+ slug?: string | null;
2205
+ /** Your label for the named resource, under the same rules as `slug`. */
2206
+ displayName?: string | null;
2207
+ }
2208
+ /**
2209
+ * A stored TLS rule: the name a session is addressed to, who may open it, where
2210
+ * it lands, and what the edge does to it in flight.
2211
+ *
2212
+ * `transform` is always redacted here — the sealed secrets live only in storage
2213
+ * and reach only the dataplane. When any secret was redacted out, `redacted` is
2214
+ * `true`, so a caller knows the shape it sees is not the whole story.
2215
+ */
2216
+ /**
2217
+ * Method/exact-path subset of Vercel Sandbox matchers.
2218
+ * https://vercel.com/docs/sandbox/concepts/firewall#matchers
2219
+ * Dimensions are ANDed; methods are ORed. Non-matches skip all transforms.
2220
+ */
2221
+ interface HttpRequestMatch {
2222
+ /** Case-sensitive HTTP method tokens. Omitted matches any; [] matches none. */
2223
+ method?: string[];
2224
+ /** Exact path, excluding query. No decoding or trailing-slash normalization. */
2225
+ path?: {
2226
+ exact: string;
2227
+ };
2228
+ }
2229
+ interface TlsRuleData {
2230
+ id: string;
2231
+ /** The account that owns the rule. Taken from your credential, never a request body. */
2232
+ accountId: string;
2233
+ action: TlsAction;
2234
+ /** The name the session is addressed to: exact, `*.suffix`, or `*`. */
2235
+ domain: string;
2236
+ protocol: TlsProtocol;
2237
+ source: TlsEndpoint;
2238
+ destination: TlsEndpoint;
2239
+ /** The rewrites this rule applies, with every secret replaced by `"***"`. */
2240
+ transform?: TlsTransform[];
2241
+ /** Select which HTTP egress requests receive all of this rule's transforms. */
2242
+ match?: HttpRequestMatch;
2243
+ /** Authorization Freestyle performs before forwarding this public HTTP ingress. */
2244
+ forwardAuth?: TlsForwardAuthReference;
2245
+ /** Whether any transform secret was redacted out of `transform`. */
2246
+ redacted?: boolean;
2247
+ /**
2248
+ * Whether the platform owns this rule. Managed rules are Freestyle's own (the
2249
+ * SSH edge's standing grant, say), invisible to and undeletable by the account.
2250
+ */
2251
+ managed?: boolean;
2252
+ /** What this rule dies with. See {@link TlsDependency}. */
2253
+ dependencies: TlsDependency[];
2254
+ createdAt: string;
2255
+ updatedAt: string;
2256
+ }
2257
+ interface CreateTlsRuleOptions {
2258
+ /** Required. `"allow"` is the only accepted value. */
2259
+ action: TlsAction;
2260
+ /**
2261
+ * The name the session is addressed to: exact (`app.acme.com`), `*.suffix`, or
2262
+ * `*`. For `smtp` this is a mail route instead, and may name a mailbox —
2263
+ * `bot@acme.com`, `acme.com`, `bot@*.acme.com` or `*.acme.com`.
2264
+ */
2265
+ domain: string;
2266
+ /**
2267
+ * How the edge treats the bytes. Optional and usually omitted: it is inferred
2268
+ * from `transform` when one is present, and defaults to `http` otherwise. Set
2269
+ * it to `"imap"` to publish a mailbox on 993 (the edge terminates and forwards
2270
+ * plain IMAP to your server; `"imaps"` splices to a server holding its own
2271
+ * certificate), `"postgres"` to publish a database on 5432, `"minecraft"` to publish a
2272
+ * Minecraft server on 25565, `"smtp"` to receive mail on 25/587/465, or
2273
+ * `"tcp"` to have the edge splice the session on 443 without terminating it,
2274
+ * so your VM's own certificate answers the handshake. See
2275
+ * {@link TlsProtocol}.
2276
+ */
2277
+ protocol?: TlsProtocol;
2278
+ /** Who may open the session. */
2279
+ source: TlsEndpoint;
2280
+ /** Where the session lands. */
2281
+ destination: TlsEndpoint;
2282
+ /** Rewrites applied in flight, in order. Secrets here are write-only. */
2283
+ transform?: TlsTransform[];
2284
+ /** Select which HTTP egress requests receive all of this rule's transforms. */
2285
+ match?: HttpRequestMatch;
2286
+ /** Ask this reusable configuration before forwarding a public HTTP request to the VM. */
2287
+ forwardAuth?: TlsForwardAuthReference;
2288
+ }
2289
+ /**
2290
+ * A rule declared inside a create, where the resource being created has no id
2291
+ * yet.
2292
+ *
2293
+ * Exactly one endpoint may be left without an identity; that one *is* the new VM.
2294
+ * So `{ source: { public: true }, destination: { port: 8000 } }` is inbound to
2295
+ * it, and `{ source: {}, destination: { public: true } }` is egress from it. The
2296
+ * identity-less destination still owes its `port`: a landing without a port is
2297
+ * not a landing.
2298
+ */
2299
+ type InlineTlsRule = CreateTlsRuleOptions;
2300
+ /** The `tls` block of a create. */
2301
+ interface TlsSpec {
2302
+ rules: InlineTlsRule[];
2303
+ }
2304
+ interface ListTlsRulesOptions {
2305
+ /** Rules that apply to this VM: those naming it, plus those naming a private network it is on. */
2306
+ vmId?: string;
2307
+ /** Rules naming this private network. */
2308
+ vpcId?: string;
2309
+ limit?: number;
2310
+ offset?: number;
2311
+ }
2312
+ interface ListTlsRulesResult {
2313
+ /** Newest first. */
2314
+ rules: TlsRuleData[];
2315
+ totalCount: number;
2316
+ }
2317
+ /** Reusable authorization checks for public HTTP ingress: `freestyle.tls.forwardAuth`. */
2318
+ declare class TlsForwardAuthNamespace {
2319
+ private readonly client;
2320
+ constructor(client: FreestyleClient);
2321
+ /** List your forward-auth configurations, newest first. */
2322
+ list(): Promise<ListTlsForwardAuthResult>;
2323
+ /** Create a forward-auth configuration. Static request-header values are write-only. */
2324
+ create(options: CreateTlsForwardAuthOptions): Promise<TlsForwardAuthData>;
2325
+ /** Fetch one forward-auth configuration by id. */
2326
+ get(id: string): Promise<TlsForwardAuthData>;
2327
+ /** Replace a configuration in place, including rotating its static headers. */
2328
+ update(id: string, options: CreateTlsForwardAuthOptions): Promise<TlsForwardAuthData>;
2329
+ /** Delete an unused forward-auth configuration. */
2330
+ delete(id: string): Promise<void>;
2331
+ }
2332
+ /** TLS rules: `freestyle.tls.rules`. */
2333
+ declare class TlsRulesNamespace {
2334
+ private readonly client;
2335
+ constructor(client: FreestyleClient);
2336
+ /**
2337
+ * Allow a named TLS session: `domain`, from `source`, to `destination`.
2338
+ *
2339
+ * An ingress `domain` is a name you have verified, or any unused subdomain
2340
+ * of `style.dev`, which is free and needs no verification or DNS records.
2341
+ *
2342
+ * @example
2343
+ * // Public HTTPS ingress: anyone dialing app.acme.com lands on a VM's port.
2344
+ * await freestyle.tls.rules.create({
2345
+ * action: "allow",
2346
+ * domain: "app.acme.com",
2347
+ * source: { public: true },
2348
+ * destination: { vmId: "vm-123456", port: 8000 },
2349
+ * });
2350
+ *
2351
+ * @example
2352
+ * // Egress with a secret injected: a VM reaches OpenAI at its real origin,
2353
+ * // and the edge adds the Authorization header the guest never held.
2354
+ * await freestyle.tls.rules.create({
2355
+ * action: "allow",
2356
+ * domain: "api.openai.com",
2357
+ * source: { vmId: "vm-123456" },
2358
+ * destination: { public: true },
2359
+ * transform: [{ headers: { authorization: "Bearer sk-…" } }],
2360
+ * });
2361
+ *
2362
+ * @example
2363
+ * // Internal service: one VM reaches another by a name. The source opens
2364
+ * // https://api.internal; the edge terminates it and forwards to the target
2365
+ * // VM's port. Steered at the edge, HTTP-only, no transform.
2366
+ * await freestyle.tls.rules.create({
2367
+ * action: "allow",
2368
+ * domain: "api.internal",
2369
+ * source: { vmId: "vm-123456" },
2370
+ * destination: { vmId: "vm-654321", port: 8000 },
2371
+ * });
2372
+ *
2373
+ * @throws {TlsRuleValidationError} before any request, when the rule cannot be
2374
+ * well formed.
2375
+ */
2376
+ create(options: CreateTlsRuleOptions): Promise<TlsRuleData>;
2377
+ /**
2378
+ * List your rules, newest first. Pass `vmId` for the rules that apply to one
2379
+ * VM — those naming it, plus those naming a private network it is on.
2380
+ */
2381
+ list(options?: ListTlsRulesOptions): Promise<ListTlsRulesResult>;
2382
+ get(ruleId: string): Promise<TlsRuleData>;
2383
+ /**
2384
+ * Replace a rule in place. The one mutation the firewall does not allow: a
2385
+ * transform carries a rotating secret, and delete-then-create would drop
2386
+ * traffic or trip the account limit mid-swap. The rule keeps its id and
2387
+ * creation time; everything in `options` is replaced.
2388
+ *
2389
+ * @throws {TlsRuleValidationError} before any request, when the replacement
2390
+ * cannot be well formed.
2391
+ */
2392
+ update(ruleId: string, options: CreateTlsRuleOptions): Promise<TlsRuleData>;
2393
+ /** Delete a rule. The resources it named are untouched. */
2394
+ delete(ruleId: string): Promise<void>;
2395
+ }
2396
+ /** The `freestyle.tls` namespace. */
2397
+ declare class TlsNamespace {
2398
+ private readonly client;
2399
+ readonly rules: TlsRulesNamespace;
2400
+ readonly forwardAuth: TlsForwardAuthNamespace;
2401
+ constructor(client: FreestyleClient);
2402
+ }
2403
+
2404
+ /**
2405
+ * One private network on the far side of a tunnel: the network, your address
2406
+ * inside it, and the `[Peer]` key that carries it.
2407
+ */
2408
+ interface TunnelAttachment {
2409
+ vpcId: string;
2410
+ /**
2411
+ * Your IPv4 address inside the private network, absent on a v6-only
2412
+ * attachment. It belongs to this tunnel for as long as the network stays
2413
+ * attached, so you can write it into configs and rules.
2414
+ */
2415
+ ipv4?: string | null;
2416
+ /** Your IPv6 address inside the private network, under the same rules. */
2417
+ ipv6?: string | null;
2418
+ /** @deprecated The primary address, whichever family. Read `ipv4`/`ipv6`. */
2419
+ address: string;
2420
+ /** @deprecated The second-family address. Read `ipv4`/`ipv6`. */
2421
+ secondaryAddress?: string | null;
2422
+ vpcCidr: string;
2423
+ /** The CIDRs the gateway routes to this network. */
2424
+ allowedIps: string[];
2425
+ /**
2426
+ * Ranges behind your client that this network routes through the tunnel (a
2427
+ * site-to-site grant). VMs reach them with an ordinary route via `address`.
2428
+ * Absent when the attachment has none.
2429
+ */
2430
+ remoteCidrs?: string[];
2431
+ /**
2432
+ * Whether this attachment is a network exit: your client forwards for
2433
+ * destinations that are not its own address, and members route through it
2434
+ * with an ordinary route via the attachment's address. Absent when false.
2435
+ */
2436
+ exit?: boolean;
2437
+ createdAt: string;
2438
+ }
2439
+ /**
2440
+ * A tunnel: your WireGuard identity on the platform. It exists until you
2441
+ * delete it: connect, disconnect and reconnect as often as you like — a
2442
+ * session is set up on the handshake that needs it, so an idle tunnel costs
2443
+ * nothing and never expires. A tunnel routes nowhere until you attach a
2444
+ * private network to it; attach several and one WireGuard interface reaches
2445
+ * them all.
2446
+ */
2447
+ interface TunnelData {
2448
+ /** @deprecated The tunnel's id under its old name. Read `tunnelId`. */
2449
+ id: string;
2450
+ /**
2451
+ * The tunnel's id, under the name the rest of the API uses for it — a
2452
+ * firewall rule says `tunnelId`, so the tunnel does too.
2453
+ */
2454
+ tunnelId: string;
2455
+ /**
2456
+ * Your handle for this tunnel, if you gave it one. Usable anywhere the id
2457
+ * is, in place of it.
2458
+ */
2459
+ slug?: string | null;
2460
+ /** Your label for this tunnel, if you gave it one. Shown in place of the slug. */
2461
+ displayName?: string | null;
2462
+ /**
2463
+ * A complete WireGuard config file, fixed for the life of the tunnel:
2464
+ * attaching and detaching networks never changes it, so bring it up once
2465
+ * and manage what it reaches through the API. `PrivateKey` is blank except
2466
+ * on {@link TunnelsNamespace.create} and {@link TunnelsNamespace.rotateKey},
2467
+ * the only two calls that mint a keypair.
2468
+ */
2469
+ clientConfig: string;
2470
+ /** Host to dial. */
2471
+ endpointHost?: string | null;
2472
+ endpointPort: number;
2473
+ clientPublicKey: string;
2474
+ /** The one `[Peer]` public key in your config. */
2475
+ serverPublicKey: string;
2476
+ /**
2477
+ * Your fixed addresses inside the tunnel. Only the gateway ever sees them:
2478
+ * each attached network sees an address inside its own subnet instead
2479
+ * (`attachments[].address`).
2480
+ */
2481
+ clientAddressV4: string;
2482
+ clientAddressV6: string;
2483
+ /**
2484
+ * The ranges your client routes through the tunnel — its `AllowedIPs`,
2485
+ * fixed at create. A network can only be attached if its CIDRs fall inside
2486
+ * these.
2487
+ */
2488
+ routes: string[];
2489
+ /** The private networks on the far side, oldest attachment first. */
2490
+ attachments: TunnelAttachment[];
2491
+ createdAt: string;
2492
+ updatedAt: string;
2493
+ }
2494
+ /**
2495
+ * A tunnel plus a client private key — returned only by the two calls that
2496
+ * mint a keypair. The key is never stored and cannot be read back. Empty when
2497
+ * you supplied your own public key.
2498
+ */
2499
+ interface CreatedTunnel extends TunnelData {
2500
+ /** Returned once. Already embedded in `clientConfig`. */
2501
+ clientPrivateKey: string;
2502
+ }
2503
+ interface CreateTunnelOptions {
2504
+ /**
2505
+ * A URL-safe handle, unique within your account: lowercase letters, digits
2506
+ * and single hyphens, up to 63 characters. Address the tunnel by it instead
2507
+ * of by id.
2508
+ */
2509
+ slug?: string;
2510
+ /**
2511
+ * A label for a slug that does not read as a name — a tunnel per device id,
2512
+ * per session. Shown in place of the slug.
2513
+ */
2514
+ displayName?: string;
2515
+ /**
2516
+ * The public key of a keypair you already hold. Omit to have one minted for
2517
+ * you; supply it and the platform never sees a private key at all.
2518
+ */
2519
+ clientPublicKey?: string;
2520
+ /**
2521
+ * The ranges your client will route through the tunnel — its `AllowedIPs`,
2522
+ * fixed for the tunnel's life so attaching and detaching networks never
2523
+ * changes your config. Omit for the default (`10.0.0.0/8` and `fd00::/8`),
2524
+ * which covers every network with default addressing. Only networks whose
2525
+ * CIDRs fall inside the routes can be attached.
2526
+ */
2527
+ routes?: string[];
2528
+ /**
2529
+ * Networks to attach in the same call, so one request yields a tunnel that
2530
+ * already routes somewhere. All-or-nothing: if any attachment is refused,
2531
+ * nothing is created.
2532
+ */
2533
+ vpcs?: CreateTunnelVpc[];
2534
+ }
2535
+ /** One network named at create, with the same address choices as an attach. */
2536
+ interface CreateTunnelVpc {
2537
+ /** The network to attach: a VPC id, or your slug for it. */
2538
+ vpcId?: string;
2539
+ /** @deprecated The network to attach, under its old name. Write `vpcId`. */
2540
+ vpc?: string;
2541
+ /** The address you want inside it; omit to be assigned one. */
2542
+ ipv4?: string;
2543
+ /** An explicit IPv6 address instead; only one may be set. */
2544
+ ipv6?: string;
2545
+ /** Ranges behind your client this network may route to; see {@link AttachVpcOptions.remoteCidrs}. */
2546
+ remoteCidrs?: string[];
2547
+ /** See {@link AttachVpcOptions.exit}. */
2548
+ exit?: boolean;
2549
+ }
2550
+ interface AttachVpcOptions {
2551
+ /**
2552
+ * The address you want inside the private network. Omit to be assigned one.
2553
+ * It must be free: a VM already holding it makes this a conflict, and once
2554
+ * yours, no VM can be given it.
2555
+ */
2556
+ ipv4?: string;
2557
+ /**
2558
+ * An explicit IPv6 address, equivalent to passing one in `ipv4`; only one
2559
+ * may be set.
2560
+ */
2561
+ ipv6?: string;
2562
+ /**
2563
+ * Ranges behind your client that this network routes through the tunnel — a
2564
+ * site-to-site setup where your client machine forwards for other hosts on
2565
+ * its side. Hosts inside these ranges keep their real addresses inside the
2566
+ * network. A range carved out of the network's own CIDR just works: VMs
2567
+ * reach it with no configuration, and the range is excluded from VM address
2568
+ * allocation (it must be free when granted). A range outside the network's
2569
+ * CIDRs is reached by giving VMs a route via the attachment's `address`. A
2570
+ * range may not straddle the network's CIDR boundary, nor overlap a range
2571
+ * another tunnel already grants this network.
2572
+ */
2573
+ remoteCidrs?: string[];
2574
+ /**
2575
+ * Make your client this network's exit — a router members can name as a
2576
+ * next hop. A VM routes through it with an ordinary NIC route via the
2577
+ * attachment's address, and everything it sends the exit — whatever the
2578
+ * destination — is forwarded to your client, which is expected to NAT it
2579
+ * onward. Several attachments per network may be exits: members choose per
2580
+ * route, so two exits and two same-metric routes are a redundant pair, and
2581
+ * a dead exit stops being ARP/NDP-answered so the others take over by
2582
+ * themselves.
2583
+ */
2584
+ exit?: boolean;
2585
+ }
2586
+ interface UpdateTunnelOptions {
2587
+ /** An empty string clears it. */
2588
+ slug?: string;
2589
+ /** An empty string clears it. See {@link CreateTunnelOptions.displayName}. */
2590
+ displayName?: string;
2591
+ }
2592
+ interface ListTunnelsResult {
2593
+ tunnels: TunnelData[];
2594
+ totalCount: number;
2595
+ }
2596
+ declare class TunnelsNamespace {
2597
+ private readonly client;
2598
+ constructor(client: FreestyleClient);
2599
+ /** List your tunnels, newest first. Listings never include a private key. */
2600
+ list(): Promise<ListTunnelsResult>;
2601
+ /**
2602
+ * Create a tunnel, optionally attaching networks in the same call
2603
+ * (`vpcs`, all-or-nothing). This is a one-time setup: the tunnel lives
2604
+ * until you delete it and its config never changes — save it, bring it up
2605
+ * once, and manage which networks it reaches by attaching and detaching
2606
+ * them.
2607
+ */
2608
+ create(options?: CreateTunnelOptions): Promise<CreatedTunnel>;
2609
+ /** Read one tunnel by its id or its slug. Its config comes back with a blank `PrivateKey`. */
2610
+ get(tunnelIdOrSlug: string): Promise<TunnelData>;
2611
+ /**
2612
+ * Rename a tunnel or change its slug. Only the labels move: the keys,
2613
+ * addresses, routes and attached networks are untouched, so a connected
2614
+ * client sees nothing change and the config you saved keeps working.
2615
+ */
2616
+ update(tunnelIdOrSlug: string, options: UpdateTunnelOptions): Promise<TunnelData>;
2617
+ /**
2618
+ * Replace a tunnel's keys, keeping its id and every attached network's
2619
+ * address. Use this if you lost the config or need to revoke one that
2620
+ * leaked.
2621
+ */
2622
+ rotateKey(tunnelIdOrSlug: string, options?: {
2623
+ clientPublicKey?: string;
2624
+ }): Promise<CreatedTunnel>;
2625
+ /** Delete a tunnel, detaching every network as it goes. */
2626
+ delete(tunnelIdOrSlug: string): Promise<void>;
2627
+ /**
2628
+ * Attach a private network to a tunnel. The tunnel gains an address inside
2629
+ * the network — what that network's VMs see as your address — and a
2630
+ * connected client can reach it immediately, with no config change. The
2631
+ * network's CIDRs must fall inside the tunnel's routes and must not
2632
+ * overlap another attached network's.
2633
+ */
2634
+ attachVpc(tunnelIdOrSlug: string, vpcIdOrSlug: string, options?: AttachVpcOptions): Promise<TunnelData>;
2635
+ /**
2636
+ * Detach a private network from a tunnel: it stops being reachable and the
2637
+ * tunnel's address inside it is released. The tunnel — and any connected
2638
+ * client — carries on untouched.
2639
+ */
2640
+ detachVpc(tunnelIdOrSlug: string, vpcIdOrSlug: string): Promise<TunnelData>;
2641
+ }
2642
+
2643
+ type VmState = "starting" | "running" | "pausing" | "paused" | "stopped";
2644
+ /**
2645
+ * The base snapshots the platform publishes — the shared starting points every
2646
+ * account can boot from, qualified `{owner}/{slug}` because they belong to the
2647
+ * `freestyle` account rather than yours. Mirrors `catalog/snapshots.json`.
2648
+ *
2649
+ * A slug is a name, not a pointer: these are reassigned to a freshly built
2650
+ * snapshot on every catalog rebuild, which is the point — booting
2651
+ * `freestyle/ubuntu` gets you the current build. Pin a `sh-…` id instead when
2652
+ * you need the exact image to stay put.
2653
+ */
2654
+ type BaseSnapshotId =
2655
+ /** Ubuntu 24.04 LTS · 2 vCPU · 4 GiB · 16 GB. */
2656
+ "freestyle/ubuntu-sm"
2657
+ /** Ubuntu 24.04 LTS · 4 vCPU · 8 GiB · 32 GB. The platform default. */
2658
+ | "freestyle/ubuntu"
2659
+ /** Ubuntu 24.04 LTS · 8 vCPU · 16 GiB · 64 GB. */
2660
+ | "freestyle/ubuntu-lg"
2661
+ /** Ubuntu 24.04 LTS · 16 vCPU · 32 GiB · 128 GB. */
2662
+ | "freestyle/ubuntu-xl"
2663
+ /** Ubuntu 24.04 LTS · 32 vCPU · 64 GiB · 128 GB. The largest size any plan can actually create. */
2664
+ | "freestyle/ubuntu-2xl"
2665
+ /** Ubuntu 24.04 LTS · 64 vCPU · 128 GiB · 256 GB. Above every plan's per-VM cap. */
2666
+ | "freestyle/ubuntu-3xl"
2667
+ /** BusyBox · 1 vCPU · 128 MiB · 1 GB. For when Ubuntu is too big to fit. */
2668
+ | "freestyle/busybox";
2669
+ /**
2670
+ * Anywhere a snapshot is named: a `sh-…` id, your own slug for one, or a
2671
+ * public `{owner}/{slug}`. The {@link BaseSnapshotId} arm exists purely so
2672
+ * editors suggest the platform's catalog; `(string & {})` keeps every other
2673
+ * snapshot just as valid, and stops TypeScript from collapsing the union back
2674
+ * to a bare `string` and losing those suggestions.
2675
+ */
2676
+ type SnapshotIdOrSlug = BaseSnapshotId | (string & {});
2677
+ interface VmResources {
2678
+ /** vCPU count. */
2679
+ cpu: number;
2680
+ /** Memory, MiB. */
2681
+ memory: number;
2682
+ /** Disk, MiB. */
2683
+ storage: number;
2684
+ }
2685
+ /** An in-guest static route: reach `cidr` via the gateway `via`, inside the network's CIDR. */
2686
+ interface NetworkRoute {
2687
+ cidr: string;
2688
+ via: string;
2689
+ /**
2690
+ * Route priority, as `ip route`'s `metric`. Omit for the platform default.
2691
+ * Several routes to the same `cidr` at the same metric are installed as one
2692
+ * multipath route — the guest spreads flows across their gateways (turn on
2693
+ * `net.ipv4.fib_multipath_use_neigh` in the guest so a dead gateway drops
2694
+ * out) — while different metrics make a primary and a backup.
2695
+ */
2696
+ metric?: number;
2697
+ }
2698
+ /** A VM's place on a private network. */
2699
+ interface VmNetwork {
2700
+ /** @deprecated The network's id under its old name. Read `vpcId`. */
2701
+ vpc: string;
2702
+ /** The network's id, under the name the rest of the API uses. */
2703
+ vpcId?: string;
2704
+ /** The VM's IPv4 address inside the network; absent unless it asked for one. */
2705
+ ipv4?: string | null;
2706
+ /** The network's CIDR, alongside `ipv4`. */
2707
+ cidr?: string | null;
2708
+ /** The VM's IPv6 address, when the network has an IPv6 CIDR. */
2709
+ ipv6?: string | null;
2710
+ /** The network's IPv6 CIDR, alongside `ipv6`. */
2711
+ cidrV6?: string | null;
2712
+ routes: NetworkRoute[];
2713
+ }
2714
+ /** A VM as its owning account sees it. */
2715
+ interface VmData {
2716
+ id: string;
2717
+ state: VmState;
2718
+ /** URL-safe identifier, unique within the account; usable anywhere an id is. */
2719
+ slug?: string | null;
2720
+ /** The VM's label, if it has one. Shown in place of the slug. */
2721
+ displayName?: string | null;
2722
+ resources: VmResources;
2723
+ /** The snapshot the VM booted from, when it booted from one. */
2724
+ snapshotId?: string | null;
2725
+ /**
2726
+ * What that snapshot was called when the VM was created, qualified
2727
+ * `{owner}/{slug}` when it belongs to another account. Recorded once, so it
2728
+ * still names the origin after the snapshot is renamed or deleted.
2729
+ *
2730
+ * A name, not a pointer. Slugs can be reassigned, so boot from `snapshotId`.
2731
+ */
2732
+ sourceSnapshotSlugAtCreate?: string | null;
2733
+ /**
2734
+ * What that snapshot is called *now*, resolved when the VM is read. Absent
2735
+ * when it has no slug, and when it has been deleted — so against
2736
+ * `sourceSnapshotSlugAtCreate` it is what tells a renamed origin from a gone
2737
+ * one, without a second request.
2738
+ */
2739
+ sourceSnapshotSlug?: string | null;
2740
+ /** The snapshot's display name now, under the same rules. */
2741
+ sourceSnapshotDisplayName?: string | null;
2742
+ idleTimeoutSeconds?: number | null;
2743
+ /**
2744
+ * Delete the VM once it has gone this long without running. Absent means
2745
+ * never; 0 means the VM is ephemeral — deleted the moment it stops. Note
2746
+ * this can be set for you: some plans cap how long an unused VM is kept,
2747
+ * and the cap shows up here.
2748
+ */
2749
+ autoDeleteSeconds?: number | null;
2750
+ /**
2751
+ * Whether {@link Vm.autoDeleteSeconds} is your plan's doing rather than
2752
+ * yours. A window your plan set goes away by itself if you move to a plan
2753
+ * that does not reclaim unused VMs; one you set is yours and stays.
2754
+ */
2755
+ autoDeleteFromPlan?: boolean;
2756
+ /** Delete the VM this long after it was created, whatever it is doing. */
2757
+ ttlSeconds?: number | null;
2758
+ /** Pause the VM once one run has lasted this long, however busy it is. */
2759
+ maxRunSeconds?: number | null;
2760
+ /**
2761
+ * The VM's lifetime runtime budget in seconds. Spending it pauses the VM and
2762
+ * makes every later start a 409 until you raise this.
2763
+ */
2764
+ maxRunTotalSeconds?: number | null;
2765
+ /**
2766
+ * Seconds this VM has spent running, across every run it has ever had —
2767
+ * what {@link Vm.maxRunTotalSeconds} is measured against.
2768
+ */
2769
+ totalRunSeconds?: number;
2770
+ /** Whether the VM is booted again when it stops without being asked to. */
2771
+ automaticRestart?: boolean;
2772
+ /**
2773
+ * The VM's stable public IPv6 address — assigned at create and kept for the
2774
+ * VM's life, across restarts and moves between machines.
2775
+ */
2776
+ publicIpv6?: string | null;
2777
+ /**
2778
+ * The public IPv4 address the VM's outbound traffic is seen as coming from.
2779
+ * It belongs to your account rather than to this VM: every VM you own shares
2780
+ * it, and it survives restarts and moves between machines.
2781
+ *
2782
+ * Optional, and worth reading as optional even though a VM in service has
2783
+ * one: nothing promises an account keeps a single outbound address.
2784
+ */
2785
+ egressIpv4?: string | null;
2786
+ /**
2787
+ * The public IPv6 address the VM's outbound traffic is seen as coming from.
2788
+ * Today it is the VM's own {@link Vm.publicIpv6} — it appears from the
2789
+ * address it answers on — but read it as its own field: where a VM answers
2790
+ * and where it appears from are two questions, and {@link Vm.egressIpv4}
2791
+ * already answers them differently.
2792
+ */
2793
+ egressIpv6?: string | null;
2794
+ metadata: Record<string, string>;
2795
+ /** The placement constraints the VM was created with, when any. */
2796
+ placement?: VmPlacement | null;
2797
+ /** Total CPU time consumed, seconds. */
2798
+ cpuTimeSeconds?: number | null;
2799
+ lastNetworkActivity?: string | null;
2800
+ /** The private networks the VM is on (at most one today). */
2801
+ vpcs: VmNetwork[];
2802
+ /** @deprecated The same list as `vpcs`, kept for old readers. */
2803
+ networks: VmNetwork[];
2804
+ createdAt: string;
2805
+ updatedAt: string;
2806
+ }
2807
+ /**
2808
+ * Placement constraints for a VM, fixed at create time and honored again
2809
+ * whenever the platform relocates the VM.
2810
+ */
2811
+ interface VmPlacement {
2812
+ /**
2813
+ * Hard rules: the VM will not land on a topology domain already hosting a
2814
+ * VM matched by any rule's selector. Creation fails with a 409 when no
2815
+ * domain can satisfy every rule. At most 8 rules.
2816
+ */
2817
+ antiAffinity: VmAntiAffinityRule[];
2818
+ }
2819
+ /** One anti-affinity rule: "not on the same `topology` domain as any VM matching `selector`". */
2820
+ interface VmAntiAffinityRule {
2821
+ /** The domain the rule spreads across. `node` — the only domain today — means "not on the same host machine". */
2822
+ topology: "node";
2823
+ selector: VmLabelSelector;
2824
+ }
2825
+ /**
2826
+ * Selects VMs by their metadata: a VM matches when its metadata contains
2827
+ * every `matchLabels` entry. 1–8 entries; keys and values are limited to 63
2828
+ * characters, like the metadata they match.
2829
+ */
2830
+ interface VmLabelSelector {
2831
+ matchLabels: Record<string, string>;
2832
+ }
2833
+ /** Attach a VM to one of your private networks. A VM may be on at most one. */
2834
+ interface AttachNetwork {
2835
+ /** The network to join, by id or by your slug for it. */
2836
+ vpcId?: string;
2837
+ /** @deprecated The network to join, under its old name. Write `vpcId`. */
2838
+ vpc?: string;
2839
+ /**
2840
+ * The IPv4 address to take inside the network: an explicit address, `true`
2841
+ * to auto-allocate, or `false` to opt out. Omitted = allocate one, since
2842
+ * networks are dual-stack.
2843
+ */
2844
+ ipv4?: string | boolean;
2845
+ /**
2846
+ * The IPv6 address to take: an explicit address, `true` to auto-allocate
2847
+ * (the default), or `false` to opt out.
2848
+ */
2849
+ ipv6?: string | boolean;
2850
+ /** Static routes to install in the guest. */
2851
+ routes?: NetworkRoute[];
2852
+ }
2853
+ interface CreateVmOptions {
2854
+ /** Boot from this snapshot: its id, your slug for it, or a public `{owner}/{slug}`. Omit for the platform default. */
2855
+ snapshotId?: SnapshotIdOrSlug | null;
2856
+ /**
2857
+ * URL-safe identifier, unique within your account: 1–63 chars of
2858
+ * `[a-z0-9-]`, no leading, trailing, or repeated hyphens.
2859
+ *
2860
+ * How you address the VM everywhere — the API, the CLI, the dashboard.
2861
+ */
2862
+ slug?: string | null;
2863
+ /** Take `slug` from whichever VM currently holds it; the holder keeps running, slugless. Requires `slug`. */
2864
+ reassignSlug?: boolean;
2865
+ /**
2866
+ * A label for a slug that does not read as a name — one minted per run,
2867
+ * per commit or per tenant (`run-7f3a91c2`). Shown in place of the slug.
2868
+ */
2869
+ displayName?: string | null;
2870
+ /**
2871
+ * Pause the VM after this many seconds without network activity. Traffic
2872
+ * over its public address counts, as does anything typed in a terminal
2873
+ * session; -1 (or omitting this) means it is never paused for being idle.
2874
+ */
2875
+ idleTimeoutSeconds?: number | null;
2876
+ /**
2877
+ * Delete the VM once it has gone this many seconds without running. Every
2878
+ * start resets the clock, and a running VM is never deleted for this.
2879
+ * -1 (or omitting this) means never; 0 makes the VM ephemeral — deleted
2880
+ * the moment it stops.
2881
+ *
2882
+ * Some plans cap how long an unused VM is kept. On those, omitting this (or
2883
+ * sending -1) gets you the cap rather than "keep forever", and asking for
2884
+ * longer is a 400.
2885
+ */
2886
+ autoDeleteSeconds?: number | null;
2887
+ /**
2888
+ * Delete the VM this many seconds after it is created, whatever it is doing
2889
+ * at the time. A deadline, not an idle window: nothing resets it. -1 (or
2890
+ * omitting this) means no deadline.
2891
+ */
2892
+ ttlSeconds?: number | null;
2893
+ /**
2894
+ * Pause the VM once a single run has lasted this many seconds, however busy
2895
+ * it is. Starting it again gives it a fresh budget — unlike
2896
+ * {@link CreateVmOptions.maxRunTotalSeconds}, which never resets. -1 (or
2897
+ * omitting this) means no cap.
2898
+ */
2899
+ maxRunSeconds?: number | null;
2900
+ /**
2901
+ * How many seconds this VM may run in total, ever. Spending the budget
2902
+ * pauses the VM, and every later start is a 409 until you raise it. -1 (or
2903
+ * omitting this) means no budget.
2904
+ */
2905
+ maxRunTotalSeconds?: number | null;
2906
+ /**
2907
+ * Boot the VM again if it stops without you asking it to — the VMM crashes,
2908
+ * the guest kernel panics, the machine it runs on loses power. Defaults to
2909
+ * `true`.
2910
+ *
2911
+ * It never fights you: shutting the VM down, pausing it, running `poweroff`
2912
+ * inside it, or hitting an idle timeout all leave it off. This only decides
2913
+ * who gets the last word when a *failure* stops it.
2914
+ */
2915
+ automaticRestart?: boolean;
2916
+ /** Put the VM on your private networks. Omit to leave it off them. At most one is accepted today. */
2917
+ vpcs?: AttachNetwork[];
2918
+ /** @deprecated The same list as `vpcs`; write `vpcs`. */
2919
+ networks?: AttachNetwork[];
2920
+ /** Up to 64 metadata entries; keys and values are limited to 63 characters. */
2921
+ metadata?: Record<string, string>;
2922
+ /**
2923
+ * Placement constraints (anti-affinity), fixed for the VM's life. Selectors
2924
+ * match the metadata of your other VMs.
2925
+ */
2926
+ placement?: VmPlacement | null;
2927
+ /**
2928
+ * Firewall rules created with the VM and deleted with it.
2929
+ *
2930
+ * Inside this block an endpoint with no identity means the VM being created,
2931
+ * which is how a rule names a VM whose id does not exist yet. See
2932
+ * {@link FirewallSpec}.
2933
+ *
2934
+ * @example
2935
+ * firewall: {
2936
+ * rules: [
2937
+ * // Inbound HTTPS from anywhere.
2938
+ * { action: "allow", source: { public: true }, destination: { port: 443, protocol: "tcp" } },
2939
+ * // Outbound HTTPS to anywhere.
2940
+ * { action: "allow", source: {}, destination: { public: true, port: 443, protocol: "tcp" } },
2941
+ * ],
2942
+ * }
2943
+ *
2944
+ * Required. A VM gets nothing implicitly — no outbound Internet, no inbound —
2945
+ * so a VM that wants either says so here. `{ rules: [] }` is a legitimate
2946
+ * answer: a VM reachable only through a mapped domain or SSH.
2947
+ */
2948
+ firewall: FirewallSpec;
2949
+ /**
2950
+ * TLS rules created with the VM and deleted with it — named sessions the VM
2951
+ * may serve or open. Optional, unlike `firewall`: a TLS rule is a grant
2952
+ * layered on top of the firewall's packet decision, never a baseline a VM
2953
+ * must state.
2954
+ *
2955
+ * Inside this block an endpoint with no identity means the VM being created,
2956
+ * exactly as in `firewall`. See {@link TlsSpec}.
2957
+ *
2958
+ * @example
2959
+ * tls: {
2960
+ * rules: [
2961
+ * // Publish app.acme.com to port 8000 on this VM.
2962
+ * { action: "allow", domain: "app.acme.com", source: { public: true }, destination: { port: 8000 } },
2963
+ * ],
2964
+ * }
2965
+ */
2966
+ tls?: TlsSpec;
2967
+ }
2968
+ interface UpdateVmOptions {
2969
+ /** Change the slug; an empty string clears it. */
2970
+ slug?: string;
2971
+ /** Take `slug` from whichever VM currently holds it. */
2972
+ reassignSlug?: boolean;
2973
+ /**
2974
+ * Change the display label; an empty string clears it. See
2975
+ * {@link CreateVmOptions.displayName}.
2976
+ */
2977
+ displayName?: string;
2978
+ /** Seconds of network idleness before the VM is paused; -1 removes the timeout. */
2979
+ idleTimeoutSeconds?: number;
2980
+ /**
2981
+ * Seconds of not running before the VM is deleted; -1 removes the window
2982
+ * (on a plan that caps it, -1 puts you back on the cap rather than off it),
2983
+ * and 0 makes the VM ephemeral — deleted the moment it stops, or right away
2984
+ * if it is already stopped.
2985
+ */
2986
+ autoDeleteSeconds?: number;
2987
+ /**
2988
+ * Seconds from creation — not from now — before the VM is deleted; -1
2989
+ * removes the deadline. A value already in the past deletes it shortly.
2990
+ */
2991
+ ttlSeconds?: number;
2992
+ /** Seconds one run may last before the VM is paused; -1 removes the cap. */
2993
+ maxRunSeconds?: number;
2994
+ /**
2995
+ * The lifetime runtime budget in seconds; -1 removes it. Raising this past
2996
+ * `totalRunSeconds` is how you start a VM that has spent its budget.
2997
+ */
2998
+ maxRunTotalSeconds?: number;
2999
+ /**
3000
+ * Turn automatic restart on or off. Applies to the next failure: this neither
3001
+ * boots a stopped VM nor stops a running one.
3002
+ */
3003
+ automaticRestart?: boolean;
3004
+ /** Metadata merged into the VM. Only the keys you send are touched; send an empty value to remove one. */
3005
+ metadata?: Record<string, string>;
3006
+ }
3007
+ interface ResizeVmOptions {
3008
+ /** vCPU count. Grow-only. Applies live to a running VM. */
3009
+ cpu?: number;
3010
+ /** Memory, MiB. Grow-only. Applies live to a running VM. */
3011
+ memory?: number;
3012
+ /** Disk, MiB. Grow-only, and only while the VM is running. */
3013
+ storage?: number;
3014
+ }
3015
+ interface ExecOptions {
3016
+ /** The command line, run through the guest's shell. */
3017
+ command: string;
3018
+ /**
3019
+ * Guest Linux user to run as. Omit for the VM's default user: the account
3020
+ * holding uid 1000, or `root` in an image that has no such account. Required
3021
+ * when an identity grant restricts users.
3022
+ */
3023
+ linuxUser?: string;
3024
+ /** Wall-clock limit, milliseconds. 1–300000; defaults to the guest's own. */
3025
+ timeoutMs?: number;
3026
+ /** Extra environment variables. Keys must be valid POSIX names. */
3027
+ env?: Record<string, string>;
3028
+ /** Base64-encoded stdin, up to 1 MiB decoded. */
3029
+ stdin?: string;
3030
+ }
3031
+ interface ExecResult {
3032
+ stdout?: string | null;
3033
+ stderr?: string | null;
3034
+ /** The command's exit status; null if it was killed by the timeout. */
3035
+ statusCode?: number | null;
3036
+ }
3037
+ interface ListVmsOptions {
3038
+ /** Only VMs in this state. */
3039
+ state?: VmState;
3040
+ /** Exact slug match. */
3041
+ slug?: string;
3042
+ /** Only VMs booted from this snapshot. */
3043
+ snapshotId?: SnapshotIdOrSlug;
3044
+ /** Comma-separated `key:value` pairs, e.g. `env:prod,region:us-west`. */
3045
+ metadata?: string;
3046
+ limit?: number;
3047
+ offset?: number;
3048
+ }
3049
+ interface ListVmsResult {
3050
+ vms: VmData[];
3051
+ totalCount: number;
3052
+ runningCount: number;
3053
+ startingCount: number;
3054
+ pausingCount: number;
3055
+ pausedCount: number;
3056
+ stoppedCount: number;
3057
+ }
3058
+ interface DirEntry {
3059
+ name: string;
3060
+ /** `file`, `directory`, or `symlink`. */
3061
+ kind: string;
3062
+ }
3063
+ interface FileStat {
3064
+ /** Size in bytes. */
3065
+ size: number;
3066
+ isFile: boolean;
3067
+ isDirectory: boolean;
3068
+ isSymlink: boolean;
3069
+ /** Octal mode, e.g. `0644`. */
3070
+ permissions: string;
3071
+ owner: string;
3072
+ group: string;
3073
+ modified: string;
3074
+ }
3075
+
3076
+ interface ReadFileOptions {
3077
+ /** First byte to read. Default 0. */
3078
+ offset?: number;
3079
+ /** How many bytes to read. Default: to the end of the file. */
3080
+ length?: number;
3081
+ signal?: AbortSignal;
3082
+ }
3083
+ /**
3084
+ * How far a transfer has got. An object rather than positional arguments so it
3085
+ * can grow — a phase, a message — without breaking callers, and so progress
3086
+ * reads the same way wherever the SDK reports it.
3087
+ */
3088
+ interface TransferProgress {
3089
+ /** Bytes confirmed stored so far. */
3090
+ completedBytes: number;
3091
+ /** Bytes in the whole transfer. */
3092
+ totalBytes: number;
3093
+ }
3094
+ interface WriteFileOptions {
3095
+ /** Final file mode bits, e.g. 0o755. Defaults to the target's existing mode, or 0o600. */
3096
+ mode?: number;
3097
+ /**
3098
+ * Called as bytes are confirmed stored. A chunked upload reports after each
3099
+ * chunk; a single-request write reports once, on completion.
3100
+ */
3101
+ onProgress?: (progress: TransferProgress) => void;
3102
+ signal?: AbortSignal;
3103
+ /** Bytes per chunk for large files (default 16 MiB; the server caps a chunk at 256 MiB). */
3104
+ chunkSize?: number;
3105
+ /** Attempts per chunk before giving up (default 5). */
3106
+ maxAttempts?: number;
3107
+ }
3108
+ /**
3109
+ * The guest filesystem of one VM: `GET/PUT /vms/{id}/fs/*`.
3110
+ *
3111
+ * Paths must be absolute, free of `..`, and at most 4096 characters. Files cross
3112
+ * the wire as raw bytes in both directions, so there is no size limit short of
3113
+ * the 16 GiB per-file ceiling and nothing to encode or decode.
3114
+ *
3115
+ * `writeFile` picks its own transport: small files go in one request, large ones
3116
+ * as a resumable chunked upload whose failed chunks are retried individually.
3117
+ * Either way the write is atomic — a reader in the guest sees the old file until
3118
+ * the new one has arrived intact — and verified by sha256 end to end.
3119
+ */
3120
+ declare class VmFilesystem {
3121
+ private readonly client;
3122
+ private readonly basePath;
3123
+ constructor(client: FreestyleClient, basePath: string);
3124
+ /**
3125
+ * Read a whole file, or with `offset`/`length` just part of one, into memory.
3126
+ *
3127
+ * The result is a plain `Uint8Array` on every runtime — never a Node
3128
+ * `Buffer`, even on Node. Don't coerce it with `String(bytes)` or
3129
+ * `bytes.toString()`: those go through the typed-array `toString` and
3130
+ * produce the byte values joined by commas ("123,10,32,…"), not the file's
3131
+ * text. For text use {@link readTextFile}, or decode explicitly with
3132
+ * `new TextDecoder().decode(bytes)`.
3133
+ */
3134
+ readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
3135
+ /** Read a file as UTF-8 text. Use this, not `String(await readFile(...))`. */
3136
+ readTextFile(path: string, options?: ReadFileOptions): Promise<string>;
3137
+ /**
3138
+ * Stream a file out of the VM without holding it in memory — pipe it to disk,
3139
+ * to an HTTP response, or through a parser.
3140
+ *
3141
+ * ```ts
3142
+ * const bytes = await vm.fs.readFileStream("/var/log/big.log");
3143
+ * await pipeline(Readable.fromWeb(bytes), createWriteStream("big.log"));
3144
+ * ```
3145
+ */
3146
+ readFileStream(path: string, options?: ReadFileOptions): Promise<ReadableStream<Uint8Array>>;
3147
+ /**
3148
+ * Write a file into the VM, whatever its size.
3149
+ *
3150
+ * ```ts
3151
+ * await vm.fs.writeFile("/etc/app.conf", "debug = true");
3152
+ * await vm.fs.writeFile("/data/model.bin", await openAsBlob("model.bin"), {
3153
+ * onProgress: ({ completedBytes, totalBytes }) =>
3154
+ * console.log(`${completedBytes}/${totalBytes}`),
3155
+ * });
3156
+ * ```
3157
+ *
3158
+ * A `Blob` (`node:fs`'s `openAsBlob`, or a browser `File`) is read one chunk at
3159
+ * a time and never held in memory whole.
3160
+ */
3161
+ writeFile(path: string, content: string | Uint8Array | Blob, options?: WriteFileOptions): Promise<void>;
3162
+ /** Write text. Identical to {@link writeFile} with a string. */
3163
+ writeTextFile(path: string, content: string, options?: WriteFileOptions): Promise<void>;
3164
+ readDir(path: string): Promise<DirEntry[]>;
3165
+ mkdir(path: string): Promise<void>;
3166
+ remove(path: string): Promise<void>;
3167
+ exists(path: string): Promise<boolean>;
3168
+ stat(path: string): Promise<FileStat>;
3169
+ private openRead;
3170
+ /**
3171
+ * Send a large file as an upload session: ordered chunks, each retried
3172
+ * independently with backoff, so one dropped connection costs one chunk rather
3173
+ * than the whole transfer. The file appears at `path` only on commit.
3174
+ */
3175
+ private uploadInChunks;
3176
+ /**
3177
+ * PUT one chunk, retrying transient failures. After any failure the session
3178
+ * is re-fetched: the server only counts fully-stored chunks, so its
3179
+ * `receivedBytes` tells us whether this chunk actually landed (return) or
3180
+ * must be resent (retry).
3181
+ */
3182
+ private putChunkWithRetry;
3183
+ }
3184
+
3185
+ interface PtySessionInfo {
3186
+ /** Per-VM session id, for attaching or closing this session. */
3187
+ sessionId: number;
3188
+ /** `"running"` while the shell is alive, `"exited"` once it has terminated. */
3189
+ state: string;
3190
+ /** Unix timestamp, in seconds, when the session was opened. */
3191
+ createdUnix: number;
3192
+ cols: number;
3193
+ rows: number;
3194
+ /** Exit code, present only once the session has exited. */
3195
+ exitCode?: number | null;
3196
+ /**
3197
+ * Linux user the session runs as, named even when the open left the choice to
3198
+ * the VM's default. Absent only for a session opened by a guest agent that
3199
+ * predates that default, which ran it as root.
3200
+ */
3201
+ linuxUser?: string | null;
3202
+ /**
3203
+ * The name this session was opened with, while it still answers to it.
3204
+ * Released the moment the shell exits, so an exited session reports none and
3205
+ * is addressable only by `sessionId`.
3206
+ */
3207
+ slug?: string | null;
3208
+ }
3209
+ interface ListPtySessionsResult {
3210
+ sessions: PtySessionInfo[];
3211
+ }
3212
+ interface ClosePtySessionResult {
3213
+ sessionId: number;
3214
+ /** Exit code if the session had already exited. Absent when it was still running. */
3215
+ exitCode?: number | null;
3216
+ }
3217
+ interface PtyOpenOptions {
3218
+ /** Command to run; omit for a login shell. */
3219
+ exec?: string;
3220
+ /** Initial width in columns (default 80). */
3221
+ cols?: number;
3222
+ /** Initial height in rows (default 24). */
3223
+ rows?: number;
3224
+ /**
3225
+ * Guest Linux user to run as. Omit for the VM's default user: the account
3226
+ * holding uid 1000, or `root` in an image that has no such account.
3227
+ */
3228
+ linuxUser?: string;
3229
+ /**
3230
+ * Name this session, so you can reattach to it later without storing the id
3231
+ * we mint — `vm.pty.attach({ session: "main" })` from anywhere, any time.
3232
+ *
3233
+ * Get-or-create: if a session already has this name you get *that* one back —
3234
+ * alive, or exited with its final output and exit code still readable — and
3235
+ * `exec` is not run. `session.created` tells you which happened. Starting
3236
+ * over is `close()` then `open()` again, which is deliberate: it destroys the
3237
+ * dead session's output, and that output is usually why you came looking.
3238
+ *
3239
+ * Lowercase letters, digits and hyphens, and not all digits (session ids are
3240
+ * bare integers, so an all-digit name could not be told apart from one).
3241
+ */
3242
+ slug?: string;
3243
+ /**
3244
+ * Respawn the shell in place when it exits, keeping this session's id, name
3245
+ * and scrollback — for a terminal that should outlive whatever runs in it.
3246
+ * A command that dies at startup is not respawned forever: after a few exits
3247
+ * in quick succession the session is left dead so the error stays readable.
3248
+ *
3249
+ * Leave this off if you are waiting on a command's exit code. A session that
3250
+ * comes back to life reports `running` again, and its `exitCode` becomes the
3251
+ * next shell's.
3252
+ */
3253
+ replaceOnExit?: boolean;
3254
+ }
3255
+ /** A session's id, or the slug it was opened with. */
3256
+ type PtySessionSelector = number | string;
3257
+ interface PtyAttachOptions {
3258
+ /** The session to reattach to: its id, or the slug it was opened with. */
3259
+ session: PtySessionSelector;
3260
+ /** Guest Linux user that must own the session. */
3261
+ linuxUser?: string;
3262
+ }
3263
+ interface PtySessionEvents {
3264
+ onData?: (data: Uint8Array) => void;
3265
+ onExit?: (exitCode: number) => void;
3266
+ onClose?: (info: {
3267
+ wasClean: boolean;
3268
+ code: number;
3269
+ reason: string;
3270
+ }) => void;
3271
+ onError?: (err: unknown) => void;
3272
+ }
3273
+ /** Minimal surface both `ws` (Node) and the browser's native `WebSocket` provide. */
3274
+ interface Socket {
3275
+ readonly readyState: number;
3276
+ send(data: string | Uint8Array): void;
3277
+ close(code?: number, reason?: string): void;
3278
+ on?(event: "open" | "message" | "close" | "error", listener: (...args: unknown[]) => void): void;
3279
+ addEventListener?(event: string, listener: (event: unknown) => void): void;
3280
+ }
3281
+ /**
3282
+ * One interactive terminal session. Sessions are owned by the guest agent and
3283
+ * outlive the connection that opened them: closing this only detaches, so the
3284
+ * shell keeps running and a later `attach()` can pick it back up.
3285
+ */
3286
+ /** Internal-only: lets {@link VmPty.open} learn the session id from the very
3287
+ * same listener that goes on to handle data/exit frames, so there's never a
3288
+ * gap between a throwaway listener and the real one where a fast command's
3289
+ * output (or even its exit) could arrive and be dropped. */
3290
+ interface InternalPtySessionEvents extends PtySessionEvents {
3291
+ onSessionInfo?: (sessionId: number) => void;
3292
+ }
3293
+ declare class PtySession {
3294
+ private _sessionId;
3295
+ private _slug;
3296
+ private _created;
3297
+ private readonly socket;
3298
+ constructor(socket: Socket, sessionId: number, events?: InternalPtySessionEvents);
3299
+ /** Send raw bytes (or UTF-8 text) to the guest's stdin. */
3300
+ write(data: Uint8Array | string): void;
3301
+ resize(options: {
3302
+ cols: number;
3303
+ rows: number;
3304
+ }): void;
3305
+ signal(sig: "sigint" | "sigkill"): void;
3306
+ private sendControl;
3307
+ /** Close the connection. The shell keeps running; a later `attach()` can reconnect. */
3308
+ detach(): void;
3309
+ get sessionId(): number;
3310
+ /**
3311
+ * The name this session answers to, from the server's `sessionInfo` frame.
3312
+ * `undefined` for a session opened without a slug — reattach to that one by
3313
+ * `sessionId` instead.
3314
+ */
3315
+ get slug(): string | undefined;
3316
+ /**
3317
+ * Whether `open()` started this shell, or handed back one that was already
3318
+ * running under the same `slug`. `false` means your `exec` never ran — check
3319
+ * it before waiting on output from a command you thought you started.
3320
+ */
3321
+ get created(): boolean;
3322
+ get readyState(): number;
3323
+ }
3324
+ /** The `vm.pty` surface: open/attach terminal WebSockets, and list/close sessions over REST. */
3325
+ declare class VmPty {
3326
+ private readonly client;
3327
+ private readonly basePath;
3328
+ constructor(client: FreestyleClient, basePath: string);
3329
+ /**
3330
+ * Open a new terminal on the VM. The returned session's data/exit
3331
+ * listeners are live from the moment the socket connects — installed
3332
+ * before we even know the session id — so a command that produces output
3333
+ * (or exits) immediately can never race past them.
3334
+ */
3335
+ open(options?: PtyOpenOptions & PtySessionEvents): Promise<PtySession>;
3336
+ /** Reattach to an existing session; retained scrollback replays first. */
3337
+ attach(options: PtyAttachOptions & PtySessionEvents): Promise<PtySession>;
3338
+ /** List the VM's terminal sessions, running and recently exited. */
3339
+ list(options?: {
3340
+ linuxUser?: string;
3341
+ }): Promise<ListPtySessionsResult>;
3342
+ /** Kill a terminal session and remove it. */
3343
+ close(session: PtySessionSelector, options?: {
3344
+ linuxUser?: string;
3345
+ }): Promise<ClosePtySessionResult>;
3346
+ }
3347
+ /** PTY operations scoped to one existing Linux user by `vm.linuxUser(...)`. */
3348
+ declare class VmLinuxUserPty {
3349
+ private readonly pty;
3350
+ private readonly linuxUser;
3351
+ constructor(pty: VmPty, linuxUser: string);
3352
+ open(options?: Omit<PtyOpenOptions, "linuxUser"> & PtySessionEvents): Promise<PtySession>;
3353
+ attach(options: Omit<PtyAttachOptions, "linuxUser"> & PtySessionEvents): Promise<PtySession>;
3354
+ list(): Promise<ListPtySessionsResult>;
3355
+ close(session: PtySessionSelector): Promise<ClosePtySessionResult>;
3356
+ }
3357
+
3358
+ /** A snapshot as its owner sees it. */
3359
+ interface SnapshotData {
3360
+ id: string;
3361
+ sourceVmId?: string | null;
3362
+ /** Your handle for this snapshot; boot from it anywhere an id is taken. */
3363
+ slug?: string | null;
3364
+ /** The snapshot's label, if it has one. Shown in place of the slug. */
3365
+ displayName?: string | null;
3366
+ /** Owning account; public snapshots from other accounts are read-only. */
3367
+ accountId?: string | null;
3368
+ /** Public snapshots are bootable by any account. */
3369
+ public: boolean;
3370
+ /**
3371
+ * Delete the snapshot once this long has passed without a VM being created
3372
+ * from it. Absent means never; some plans cap it and the cap shows up here.
3373
+ */
3374
+ autoDeleteSeconds?: number | null;
3375
+ /**
3376
+ * Whether {@link SnapshotData.autoDeleteSeconds} is your plan's doing rather
3377
+ * than yours; one your plan set goes away by itself if you move to a plan
3378
+ * that does not reclaim unused snapshots.
3379
+ */
3380
+ autoDeleteFromPlan?: boolean;
3381
+ /**
3382
+ * Delete the snapshot this long after it was taken, however recently
3383
+ * anything booted it.
3384
+ */
3385
+ ttlSeconds?: number | null;
3386
+ /**
3387
+ * When a VM was last created from this snapshot. Absent if none ever has
3388
+ * been — which is not the same as untouched, since renaming a snapshot is
3389
+ * not using it.
3390
+ */
3391
+ lastUsedAt?: string | null;
3392
+ createdAt: string;
3393
+ updatedAt: string;
3394
+ }
3395
+ interface CreateSnapshotOptions {
3396
+ /**
3397
+ * A URL-safe handle you can boot from directly, in place of the id.
3398
+ */
3399
+ slug?: string;
3400
+ /**
3401
+ * A label for a slug that does not read as a name — a build id, a commit
3402
+ * sha. Shown in place of the slug.
3403
+ */
3404
+ displayName?: string;
3405
+ /**
3406
+ * Delete the snapshot once this many seconds pass without a VM being created
3407
+ * from it. Every create from it resets the clock. -1 (or omitting this)
3408
+ * means never; some plans cap this, and on those omitting it (or sending
3409
+ * -1) gets you the cap.
3410
+ */
3411
+ autoDeleteSeconds?: number;
3412
+ /**
3413
+ * Delete the snapshot this many seconds after it is taken, whatever has
3414
+ * booted it since.
3415
+ */
3416
+ ttlSeconds?: number;
3417
+ }
3418
+ interface UpdateSnapshotOptions {
3419
+ /** An empty string clears it. */
3420
+ slug?: string;
3421
+ /** An empty string clears it. See {@link CreateSnapshotOptions.displayName}. */
3422
+ displayName?: string;
3423
+ /**
3424
+ * Seconds of nothing being created from it before the snapshot is deleted;
3425
+ * -1 removes the window. On a plan that caps it, -1 puts you back on the
3426
+ * cap.
3427
+ */
3428
+ autoDeleteSeconds?: number;
3429
+ /**
3430
+ * Seconds from when the snapshot was taken — not from now — before it is
3431
+ * deleted; -1 removes the deadline.
3432
+ */
3433
+ ttlSeconds?: number;
3434
+ }
3435
+ interface SnapshotCreated {
3436
+ snapshotId: string;
3437
+ sourceVmId: string | null;
3438
+ snapshot: SnapshotData;
3439
+ }
3440
+ interface ListSnapshotsOptions {
3441
+ /** Only snapshots taken from this VM. */
3442
+ sourceVmId?: string;
3443
+ limit?: number;
3444
+ offset?: number;
3445
+ }
3446
+ interface ListSnapshotsResult {
3447
+ snapshots: SnapshotData[];
3448
+ totalCount: number;
3449
+ }
3450
+ /** The `freestyle.vms.snapshots` namespace: `GET /v5/snapshots`, `/v5/snapshots/{id}`. */
3451
+ declare class VmSnapshotsNamespace {
3452
+ private readonly client;
3453
+ constructor(client: FreestyleClient);
3454
+ list(options?: ListSnapshotsOptions): Promise<ListSnapshotsResult>;
3455
+ get(snapshotId: string): Promise<SnapshotData>;
3456
+ update(snapshotId: string, options: UpdateSnapshotOptions): Promise<SnapshotData>;
3457
+ delete(snapshotId: string): Promise<void>;
3458
+ }
3459
+
3460
+ /**
3461
+ * A handle to one VM, keyed by its id or slug. Every method hits the API
3462
+ * fresh (nothing is cached); `data()` re-fetches and returns the current
3463
+ * record.
3464
+ */
3465
+ declare class Vm {
3466
+ readonly id: string;
3467
+ readonly fs: VmFilesystem;
3468
+ readonly pty: VmPty;
3469
+ private readonly client;
3470
+ private readonly basePath;
3471
+ constructor(client: FreestyleClient, id: string);
3472
+ /** Fetch the current VM record. */
3473
+ data(): Promise<VmData>;
3474
+ /**
3475
+ * The firewall rules that apply to this VM: those naming it, plus those
3476
+ * naming a private network it is attached to.
3477
+ *
3478
+ * Deleting the VM deletes the rules that name it, so this never reports a
3479
+ * rule over a machine that no longer exists.
3480
+ */
3481
+ firewallRules(): Promise<ListFirewallRulesResult>;
3482
+ /** Rename the VM, change its slug or idle timeout, or merge in metadata. */
3483
+ update(options: UpdateVmOptions): Promise<VmData>;
3484
+ /** Boot a stopped VM, or resume a paused one. */
3485
+ start(): Promise<VmData>;
3486
+ /** Freeze a running VM, keeping its memory so a later start resumes it exactly. */
3487
+ pause(): Promise<VmData>;
3488
+ /**
3489
+ * Change vCPU, memory, or disk. Every axis is grow-only. vCPU and memory
3490
+ * apply live to a running VM, on resume for a paused one, and at the next
3491
+ * boot for a stopped one. Growing the disk needs a running VM.
3492
+ */
3493
+ resize(options: ResizeVmOptions): Promise<VmData>;
3494
+ /** Permanently destroy the VM */
3495
+ delete(): Promise<void>;
3496
+ /**
3497
+ * Run a command in the guest and wait for it to finish. A non-zero exit
3498
+ * status is still a successful call — check `statusCode`, which is null if
3499
+ * the command was killed by its timeout.
3500
+ */
3501
+ exec(options: string | ExecOptions): Promise<ExecResult>;
3502
+ /** Scope process and terminal operations to one existing in-guest Linux user. */
3503
+ linuxUser(linuxUser: string): VmLinuxUser;
3504
+ /**
3505
+ * Capture the VM's exact state, memory and disk. The VM must be running or
3506
+ * paused. The new snapshot is private and fully materialized when this
3507
+ * resolves.
3508
+ */
3509
+ snapshot(options?: CreateSnapshotOptions): Promise<SnapshotCreated>;
3510
+ }
3511
+ /** A VM handle whose process-spawning operations run as one Linux user. */
3512
+ declare class VmLinuxUser {
3513
+ private readonly vm;
3514
+ private readonly linuxUser;
3515
+ readonly pty: VmLinuxUserPty;
3516
+ constructor(vm: Vm, linuxUser: string);
3517
+ exec(options: string | Omit<ExecOptions, "linuxUser">): Promise<ExecResult>;
3518
+ }
3519
+ /** The `freestyle.vms` namespace: create, list, and manage VMs. */
3520
+ declare class VmsNamespace {
3521
+ private readonly client;
3522
+ readonly snapshots: VmSnapshotsNamespace;
3523
+ constructor(client: FreestyleClient);
3524
+ /**
3525
+ * Boot a new VM.
3526
+ *
3527
+ * `firewall` is required: a VM gets nothing implicitly, so state what it may
3528
+ * reach. `{ rules: [] }` is a legitimate answer — a VM reachable only through
3529
+ * a mapped domain or SSH.
3530
+ *
3531
+ * @example
3532
+ * const { vm, vmId } = await freestyle.vms.create({
3533
+ * firewall: {
3534
+ * rules: [
3535
+ * // This VM can reach the Internet. Without it, it cannot.
3536
+ * { action: "allow", source: {}, destination: { public: true } },
3537
+ * ],
3538
+ * },
3539
+ * });
3540
+ * await vm.exec("echo hello");
3541
+ */
3542
+ create(options: CreateVmOptions): Promise<{
3543
+ vm: Vm;
3544
+ vmId: string;
3545
+ data: VmData;
3546
+ firewallRules: FirewallRuleData[];
3547
+ /** The rules the create's `tls` block produced; empty without one. */
3548
+ tlsRules: TlsRuleData[];
3549
+ }>;
3550
+ /**
3551
+ * List your VMs.
3552
+ *
3553
+ * @example
3554
+ * const { vms } = await freestyle.vms.list();
3555
+ */
3556
+ list(options?: ListVmsOptions): Promise<ListVmsResult>;
3557
+ /**
3558
+ * Fetch a VM by its id or its slug.
3559
+ *
3560
+ * @example
3561
+ * const vm = await freestyle.vms.get("vmId");
3562
+ */
3563
+ get(vmIdOrSlug: string): Promise<VmData>;
3564
+ /** A handle to a VM, without a network call. */
3565
+ ref(vmIdOrSlug: string): Vm;
3566
+ delete(vmIdOrSlug: string): Promise<void>;
3567
+ }
3568
+
3569
+ /** A private network. */
3570
+ interface VpcData {
3571
+ id: string;
3572
+ /** The network's IPv4 CIDR block, when it has one. */
3573
+ cidr?: string | null;
3574
+ /** The network's IPv6 CIDR block — always present. */
3575
+ cidrV6: string;
3576
+ /** Your handle for this network; usable anywhere its id is. */
3577
+ slug?: string | null;
3578
+ /** The network's label, if it has one. Shown in place of the slug. */
3579
+ displayName?: string | null;
3580
+ createdAt: string;
3581
+ }
3582
+ interface CreateVpcOptions {
3583
+ /**
3584
+ * Firewall rules created with the network and deleted with it.
3585
+ *
3586
+ * A bare endpoint means the network being created, so `{ source: {},
3587
+ * destination: {} }` is the most common thing anyone wants to say about a
3588
+ * network: its members can reach each other.
3589
+ *
3590
+ * @example
3591
+ * firewall: {
3592
+ * rules: [
3593
+ * // Members reach each other.
3594
+ * { action: "allow", source: {}, destination: {} },
3595
+ * // Members reach the Internet.
3596
+ * { action: "allow", source: {}, destination: { public: true } },
3597
+ * ],
3598
+ * }
3599
+ */
3600
+ firewall?: FirewallSpec;
3601
+ /**
3602
+ * The network's IPv4 CIDR. Networks are dual-stack; a `/24` out of
3603
+ * `10.0.0.0/8` is derived for you when omitted. Name one to pick the range
3604
+ * yourself — worth doing when the network must not overlap an estate you
3605
+ * reach over a tunnel, or needs more than 254 IPv4 members.
3606
+ */
3607
+ cidr?: string;
3608
+ /** The network's IPv6 CIDR; a unique-local /64 is derived when omitted. */
3609
+ cidrV6?: string;
3610
+ /** A URL-safe handle, unique within your account, usable anywhere its id is. */
3611
+ slug?: string;
3612
+ /**
3613
+ * A label for a slug that does not read as a name — one network per tenant
3614
+ * or per environment id. Shown in place of the slug.
3615
+ */
3616
+ displayName?: string;
3617
+ }
3618
+ interface UpdateVpcOptions {
3619
+ /** An empty string clears it. */
3620
+ slug?: string;
3621
+ /** An empty string clears it. See {@link CreateVpcOptions.displayName}. */
3622
+ displayName?: string;
3623
+ }
3624
+ interface ListVpcsResult {
3625
+ vpcs: VpcData[];
3626
+ totalCount: number;
3627
+ }
3628
+ /** Tunnels attached to one private network: `vpc.tunnels`. */
3629
+ declare class VpcTunnelNamespace {
3630
+ private readonly client;
3631
+ private readonly basePath;
3632
+ constructor(client: FreestyleClient, basePath: string);
3633
+ /**
3634
+ * List the tunnels attached to this network, newest first. Listings never
3635
+ * include a private key: one is returned only by
3636
+ * `freestyle.tunnels.create` and `freestyle.tunnels.rotateKey`.
3637
+ */
3638
+ list(): Promise<ListTunnelsResult>;
3639
+ }
3640
+ /** A handle to one private network. */
3641
+ declare class Vpc {
3642
+ readonly id: string;
3643
+ readonly tunnels: VpcTunnelNamespace;
3644
+ private readonly client;
3645
+ private readonly basePath;
3646
+ constructor(client: FreestyleClient, id: string);
3647
+ data(): Promise<VpcData>;
3648
+ update(options: UpdateVpcOptions): Promise<VpcData>;
3649
+ delete(): Promise<void>;
3650
+ }
3651
+ /** The `freestyle.vpc` namespace: create and manage private networks. */
3652
+ declare class VpcNamespace {
3653
+ private readonly client;
3654
+ constructor(client: FreestyleClient);
3655
+ /** List your private networks. */
3656
+ list(): Promise<ListVpcsResult>;
3657
+ /** Create a private network, optionally with the rules that say what it is for. */
3658
+ create(options?: CreateVpcOptions): Promise<{
3659
+ vpc: Vpc;
3660
+ vpcId: string;
3661
+ data: VpcData;
3662
+ firewallRules: FirewallRuleData[];
3663
+ }>;
3664
+ /** Fetch a private network by its id or its slug. */
3665
+ get(vpcIdOrSlug: string): Promise<VpcData>;
3666
+ /** A handle to a private network, without a network call. */
3667
+ ref(vpcIdOrSlug: string): Vpc;
3668
+ delete(vpcIdOrSlug: string): Promise<void>;
3669
+ }
3670
+
3671
+ /**
3672
+ * The Freestyle API client. See https://freestyle.sh/docs and
3673
+ * https://api.freestyle.sh/openapi.json for the underlying HTTP contract.
3674
+ *
3675
+ * @example
3676
+ * const freestyle = new Freestyle({ apiKey: process.env.FREESTYLE_API_KEY });
3677
+ * const { vm } = await freestyle.vms.create();
3678
+ * const { stdout } = await vm.exec("echo hello");
3679
+ */
3680
+ declare class Freestyle {
3681
+ private readonly client;
3682
+ readonly vms: VmsNamespace;
3683
+ readonly vpc: VpcNamespace;
3684
+ readonly tunnels: TunnelsNamespace;
3685
+ /** Allowed network communication, stated as `source -> destination` intent. */
3686
+ readonly firewall: FirewallNamespace;
3687
+ /** Named TLS sessions, stated as `domain, source -> destination` intent. */
3688
+ readonly tls: TlsNamespace;
3689
+ readonly domains: DomainsNamespace;
3690
+ readonly identities: IdentitiesNamespace;
3691
+ constructor(options?: FreestyleOptions);
3692
+ /** Low-level escape hatch: an authenticated `fetch` against the API. Pass the whole path, `/v5` included. */
3693
+ fetch(path: string, init?: RequestInit): Promise<Response>;
3694
+ }
3695
+
1313
3696
  interface DaemonAddress {
1314
3697
  url: string;
1315
3698
  token: string;
@@ -1322,24 +3705,37 @@ interface DaemonAddress {
1322
3705
  *
1323
3706
  * The VM is the whole state: its slug is the handle and its metadata holds the
1324
3707
  * daemon bearer, so any Studio replica can find a sandbox without a database.
1325
- * An idle VM is paused, not deleted, and traffic to its domain resumes it.
3708
+ * An idle VM is paused, and traffic to its domain resumes it; one paused for
3709
+ * `autoDeleteSeconds` is deleted.
1326
3710
  */
1327
3711
 
1328
3712
  interface FreestyleSandboxProviderOptions {
1329
3713
  apiKey: string;
1330
3714
  /** The sandbox image. Default: the release built from this package's version. */
1331
3715
  image?: string;
3716
+ /** The org-fs sidecar image. Default: the chart's `orgFs.image`. */
3717
+ orgFsSidecarImage?: string;
1332
3718
  /** Pause a VM after this long without network activity. Default 15 min. */
1333
3719
  idleTimeoutSeconds?: number;
3720
+ /**
3721
+ * Delete a VM after this long without running. Default 3 days. A plan
3722
+ * that caps it lower gets its cap.
3723
+ */
3724
+ autoDeleteSeconds?: number;
1334
3725
  /** Where daemon ports are published. Default `style.dev`, free on every account. */
1335
3726
  domainSuffix?: string;
1336
3727
  /** Records daemon request latency; see `daemonProxyTimer`. */
1337
3728
  meter?: Meter;
3729
+ /** Replaces the API client, for tests. */
3730
+ client?: Freestyle;
1338
3731
  }
1339
3732
  declare class FreestyleSandboxProvider implements SandboxProvider {
1340
3733
  private readonly freestyle;
3734
+ private readonly lookupClient;
1341
3735
  private readonly image;
3736
+ private readonly sidecarImage;
1342
3737
  private readonly idleTimeoutSeconds;
3738
+ private readonly autoDeleteSeconds;
1343
3739
  private readonly domainSuffix;
1344
3740
  private readonly timeDaemonRequest;
1345
3741
  private readonly inflight;
@@ -1353,8 +3749,8 @@ declare class FreestyleSandboxProvider implements SandboxProvider {
1353
3749
  private find;
1354
3750
  private remember;
1355
3751
  /**
1356
- * A snapshot of a VM with the image already pulled, so a sandbox boots in
1357
- * seconds instead of pulling ~2GB. One per image; built on first use.
3752
+ * A snapshot of a VM with both images already pulled, so a sandbox boots in
3753
+ * seconds instead of pulling ~2GB. One per image pair; built on first use.
1358
3754
  */
1359
3755
  private baseSnapshot;
1360
3756
  private snapshotExists;
@@ -1362,6 +3758,8 @@ declare class FreestyleSandboxProvider implements SandboxProvider {
1362
3758
  ensure(id: SandboxId, opts?: EnsureOptions): Promise<Sandbox>;
1363
3759
  private resume;
1364
3760
  private create;
3761
+ /** A plan that caps autoDelete lower answers 400; omitting it gets the cap. */
3762
+ private createVm;
1365
3763
  private configPayload;
1366
3764
  private waitForDaemon;
1367
3765
  delete(handle: string): Promise<void>;
@@ -1398,6 +3796,7 @@ declare class FreestyleSandboxProvider implements SandboxProvider {
1398
3796
  * where `EnsureOptions.provider` asks, when that provider is configured and
1399
3797
  * can run it; else `freestyleShare` of new sandboxes go to Freestyle, and the
1400
3798
  * rest to Kubernetes while it has room. An existing one stays where it is.
3799
+ * When the chosen provider's ensure fails, the other one gets a try.
1401
3800
  * Handle-only calls go to the provider that owns the handle, because a handle
1402
3801
  * carries no provider tag.
1403
3802
  */
@@ -1420,9 +3819,22 @@ interface SandboxProviderRouterOptions {
1420
3819
  */
1421
3820
  freestyleShare?: number;
1422
3821
  }
3822
+ interface Placement {
3823
+ kind: SandboxProviderKind;
3824
+ /** Absent when only one provider could take it. */
3825
+ reason?: SandboxPlacementReason;
3826
+ }
1423
3827
  declare class SandboxProviderRouter implements SandboxProvider {
1424
3828
  readonly providers: SandboxProviders;
1425
3829
  private readonly picked;
3830
+ /**
3831
+ * Handles whose sandbox on this provider outlived a fallback because its
3832
+ * delete failed. Retried when `place` next sees the handle.
3833
+ *
3834
+ * ponytail: in-process, lost on restart; Freestyle's autoDeleteSeconds
3835
+ * reclaims a stray paused VM anyway, only later.
3836
+ */
3837
+ private readonly strays;
1426
3838
  private readonly freestyleShare;
1427
3839
  constructor(providers: SandboxProviders, opts?: SandboxProviderRouterOptions);
1428
3840
  private get;
@@ -1430,9 +3842,13 @@ declare class SandboxProviderRouter implements SandboxProvider {
1430
3842
  /** Who serves an existing handle. Kubernetes is the fallback: it has no cheap ownership check. */
1431
3843
  providerOf(handle: string): Promise<SandboxProviderKind>;
1432
3844
  private owner;
1433
- /** Where `ensure` puts this sandbox. */
1434
- place(handle: string, opts: EnsureOptions): Promise<SandboxProviderKind>;
3845
+ /** Where `ensure` puts this sandbox, and why. */
3846
+ place(handle: string, opts: EnsureOptions): Promise<Placement>;
3847
+ /** The org has pods already cloned and running for this image. */
3848
+ private hasWarmPool;
1435
3849
  ensure(id: SandboxId, opts?: EnsureOptions): Promise<Sandbox>;
3850
+ /** Delete the sandbox a fallback left behind on `kind`, in the background. */
3851
+ private retire;
1436
3852
  delete(handle: string): Promise<void>;
1437
3853
  lastTermination(handle: string): Promise<PodTermination | null>;
1438
3854
  alive(handle: string): Promise<boolean>;