@aerol-ai/aerolvm-sdk 0.6.4 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/MicroVM.d.ts CHANGED
@@ -1,11 +1,40 @@
1
1
  import { type APIVersion, type RetryConfig } from "./internal/client.js";
2
2
  import { Sandbox } from "./Sandbox.js";
3
3
  import { Image } from "./Image.js";
4
- import type { BuildImageOptions, BuildImageResult, CloneGeneration, CreateOptions, CreateSessionOptions, CreateTemplateOptions, CreateWasmModuleOptions, PushWasmModuleOptions, PushWasmModuleResult, ExecStreamHandle, ExecStreamOptions, HealthStatus, EgressProfile, EgressProfileList, EgressProfileOptions, ListEgressProfilesOptions, NetworkPolicyCheckOptions, NetworkPolicyCheckResult, IngressTarget, Lifecycle, GetOptions, ListOptions, MountSpecRedacted, RegisterSnapshotOptions, ResizeOptions, SandboxSnapshot, Session, SessionAttachHandle, SessionAttachOptions, Template, WasmModule } from "./types.js";
4
+ import type { APITokenList, BuildImageOptions, BuildImageResult, CloneGeneration, CreateAPITokenOptions, CreatedAPIToken, CreateOptions, CreateSessionOptions, CreateTemplateOptions, CreateWasmModuleOptions, PushWasmModuleOptions, PushWasmModuleResult, ExecStreamHandle, ExecStreamOptions, HealthStatus, EgressProfile, EgressProfileList, EgressProfileOptions, ListEgressProfilesOptions, NetworkPolicyCheckOptions, NetworkPolicyCheckResult, IngressTarget, Lifecycle, GetOptions, ListOptions, MountSpecRedacted, RegisterSnapshotOptions, ListSnapshotsOptions, SnapshotList, ResizeOptions, SandboxSnapshot, Session, SessionAttachHandle, SessionAttachOptions, Template, WasmModule, WhoAmI } from "./types.js";
5
5
  type FetchLike = typeof fetch;
6
+ /** Scoped API token calls, reached as `microvm.tokens`. */
7
+ export interface Tokens {
8
+ /**
9
+ * Mint a token (admin only). The result's `token` is the secret, returned
10
+ * this once and never again. A 409 means the name is taken, possibly by an
11
+ * earlier attempt whose reply was lost; that secret cannot be recovered,
12
+ * so revoke the token and create it again.
13
+ */
14
+ create(options: CreateAPITokenOptions): Promise<CreatedAPIToken>;
15
+ /** List every token (auditor or admin); expired ones stay listed for 30 days. */
16
+ list(): Promise<APITokenList>;
17
+ /** Revoke a token by id (admin only). Revoking one already gone succeeds. */
18
+ revoke(id: string): Promise<void>;
19
+ }
6
20
  export interface MicroVMConfig {
21
+ /**
22
+ * The bearer every request carries: the operator PAT, an `avm_` API token
23
+ * or an OIDC ID token. Defaults to `SB_PAT_TOKEN`, then its alias
24
+ * `SB_API_TOKEN`; when both are set, `SB_PAT_TOKEN` wins.
25
+ */
7
26
  patToken?: string;
8
27
  apiUrl?: string;
28
+ /**
29
+ * The team requests act for, sent as the `X-AerolVM-Team` header on every
30
+ * HTTP request. Defaults to `SB_TEAM`; with neither, no header is sent and
31
+ * the server uses the credential's first matching binding. The header only
32
+ * chooses among teams the credential is already bound to (a team it does
33
+ * not hold is refused with 403), and the operator PAT ignores it.
34
+ * WebSocket streams (`execStream`, `attachSession`) cannot carry custom
35
+ * headers from a browser or Node, so they go without it.
36
+ */
37
+ team?: string;
9
38
  fetch?: FetchLike;
10
39
  /**
11
40
  * Wire version of the sandbox daemon API to call. Defaults to the SDK's
@@ -17,14 +46,27 @@ export interface MicroVMConfig {
17
46
  /**
18
47
  * Retry policy for transient transport errors (socket closed, connection
19
48
  * reset) and retryable HTTP status codes (421, 429, 502, 503, 504). The SDK
20
- * retries up to 3 times with exponential backoff by default. Pass
21
- * `{ maxRetries: 0 }` to disable.
49
+ * retries up to 3 times with exponential backoff by default. `exec` retries
50
+ * only failures that prove the command never reached the server
51
+ * (connection refused, connect timeout, DNS) and HTTP 429/503, so a command
52
+ * never runs twice. Pass `{ maxRetries: 0 }` to disable. A 429 or 503
53
+ * waits as long as the server's `Retry-After` asks, up to
54
+ * `retry.maxRetryAfterMs` (30 s by default).
22
55
  */
23
56
  retry?: RetryConfig;
57
+ /**
58
+ * Cancels the client's HTTP requests: aborting it ends the request in
59
+ * flight and any retry wait, so a call never sits out a long
60
+ * `Retry-After` once aborted. Pass `AbortSignal.timeout(ms)` to bound
61
+ * how long the client may run. WebSocket streams are not affected.
62
+ */
63
+ signal?: AbortSignal;
24
64
  }
25
65
  export declare class MicroVM {
26
66
  readonly apiUrl: string;
27
67
  readonly patToken: string;
68
+ /** The team sent as `X-AerolVM-Team`, or `""` when none is. */
69
+ readonly team: string;
28
70
  private readonly client;
29
71
  constructor(config?: MicroVMConfig);
30
72
  create(options: CreateOptions): Promise<Sandbox>;
@@ -46,6 +88,12 @@ export declare class MicroVM {
46
88
  createSnapshot(id: string, name: string): Promise<SandboxSnapshot>;
47
89
  registerSnapshot(options: RegisterSnapshotOptions): Promise<SandboxSnapshot>;
48
90
  registerSnapshotFromImage(name: string, image: Image, options?: Omit<RegisterSnapshotOptions, "name" | "image" | "dockerfileContent">): Promise<SandboxSnapshot>;
91
+ /** Look a snapshot up by name. In a cluster the request is routed to the node holding the image. */
92
+ getSnapshot(name: string): Promise<SandboxSnapshot>;
93
+ /** List the caller's snapshots, one name-ordered page at a time. */
94
+ listSnapshots(options?: ListSnapshotsOptions): Promise<SnapshotList>;
95
+ /** Delete a snapshot by name (idempotent 404 if it does not exist). */
96
+ deleteSnapshot(name: string): Promise<void>;
49
97
  destroy(id: string): Promise<void>;
50
98
  resize(id: string, options: ResizeOptions): Promise<Sandbox>;
51
99
  updateLifecycle(id: string, lifecycle: Lifecycle): Promise<Sandbox>;
@@ -143,6 +191,17 @@ export declare class MicroVM {
143
191
  get dns(): {
144
192
  target(): Promise<IngressTarget>;
145
193
  };
194
+ /**
195
+ * Who the server says the caller is: principal, role, team and when the
196
+ * credential expires. Every valid credential may ask.
197
+ */
198
+ whoami(): Promise<WhoAmI>;
199
+ /**
200
+ * Scoped API tokens (`avm_…`) for services and CI, as
201
+ * `microvm.tokens.create/list/revoke`. Same namespaced shape as
202
+ * {@link MicroVM.dns}.
203
+ */
204
+ get tokens(): Tokens;
146
205
  private wrap;
147
206
  }
148
207
  export {};
package/dist/MicroVM.js CHANGED
@@ -1,25 +1,31 @@
1
1
  import { APIClient, stripTrailingSlashes } from "./internal/client.js";
2
2
  import { Sandbox } from "./Sandbox.js";
3
3
  const defaultAPIURL = "http://127.0.0.1:21212";
4
- const authRequiredErrorMessage = "PAT token is required. Set patToken or SB_PAT_TOKEN.";
4
+ const authRequiredErrorMessage = "PAT token is required. Set patToken, SB_PAT_TOKEN or SB_API_TOKEN.";
5
5
  export class MicroVM {
6
6
  apiUrl;
7
7
  patToken;
8
+ /** The team sent as `X-AerolVM-Team`, or `""` when none is. */
9
+ team;
8
10
  client;
9
11
  constructor(config = {}) {
10
- const patToken = config.patToken ?? readEnv("SB_PAT_TOKEN") ?? "";
12
+ const patToken = config.patToken ?? readEnv("SB_PAT_TOKEN") ?? readEnv("SB_API_TOKEN") ?? "";
11
13
  const apiUrl = normalizeURL(config.apiUrl ?? readEnv("SB_API_URL") ?? defaultAPIURL);
14
+ const team = (config.team ?? readEnv("SB_TEAM") ?? "").trim();
12
15
  if (patToken === "") {
13
16
  throw new Error(authRequiredErrorMessage);
14
17
  }
15
18
  this.apiUrl = apiUrl;
16
19
  this.patToken = patToken;
20
+ this.team = team;
17
21
  this.client = new APIClient({
18
22
  baseURL: apiUrl,
19
23
  patToken,
24
+ team,
20
25
  fetch: config.fetch,
21
26
  apiVersion: config.apiVersion,
22
27
  retry: config.retry,
28
+ signal: config.signal,
23
29
  });
24
30
  }
25
31
  async create(options) {
@@ -69,6 +75,18 @@ export class MicroVM {
69
75
  async registerSnapshotFromImage(name, image, options = {}) {
70
76
  return this.client.registerSnapshotFromImage(name, image, options);
71
77
  }
78
+ /** Look a snapshot up by name. In a cluster the request is routed to the node holding the image. */
79
+ async getSnapshot(name) {
80
+ return this.client.getSnapshot(name);
81
+ }
82
+ /** List the caller's snapshots, one name-ordered page at a time. */
83
+ async listSnapshots(options = {}) {
84
+ return this.client.listSnapshots(options);
85
+ }
86
+ /** Delete a snapshot by name (idempotent 404 if it does not exist). */
87
+ async deleteSnapshot(name) {
88
+ return this.client.deleteSnapshot(name);
89
+ }
72
90
  async destroy(id) {
73
91
  await this.client.destroy(id);
74
92
  }
@@ -237,6 +255,26 @@ export class MicroVM {
237
255
  target: () => client.ingressDNS(),
238
256
  };
239
257
  }
258
+ /**
259
+ * Who the server says the caller is: principal, role, team and when the
260
+ * credential expires. Every valid credential may ask.
261
+ */
262
+ async whoami() {
263
+ return this.client.whoami();
264
+ }
265
+ /**
266
+ * Scoped API tokens (`avm_…`) for services and CI, as
267
+ * `microvm.tokens.create/list/revoke`. Same namespaced shape as
268
+ * {@link MicroVM.dns}.
269
+ */
270
+ get tokens() {
271
+ const client = this.client;
272
+ return {
273
+ create: (options) => client.createAPIToken(options),
274
+ list: () => client.listAPITokens(),
275
+ revoke: (id) => client.revokeAPIToken(id),
276
+ };
277
+ }
240
278
  wrap(sandbox) {
241
279
  return new Sandbox(this.client, sandbox);
242
280
  }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { MicroVM } from "./MicroVM.js";
2
2
  export { Sandbox } from "./Sandbox.js";
3
3
  export { Image } from "./Image.js";
4
- export type { MicroVMConfig } from "./MicroVM.js";
4
+ export type { MicroVMConfig, Tokens } from "./MicroVM.js";
5
5
  export type { RetryConfig } from "./internal/client.js";
6
- export type { AddCustomDomainOptions, AuditEvent, AuditOptions, AuditPage, EgressProfile, EgressProfileList, EgressProfileOptions, EgressProfileRef, EgressInject, EgressRule, ListEgressProfilesOptions, NetworkLearned, NetworkLearnedEntry, NetworkPolicy, NetworkPolicyCheckOptions, NetworkPolicyCheckResult, NetworkPolicyOptions, BinaryLike, BuildImageOptions, BuildImagePushOptions, BuildImageResult, CloneGeneration, CreateOptions, CreateSessionOptions, CreateTemplateOptions, CreateWasmModuleOptions, CustomDomain, CustomDomainDNSRecords, CustomDomainStatus, DNSRecord, DNSRecordType, ExecRequest, ExecResult, ExecExitInfo, ExposedPort, ExposePortOptions, ExposeProtocol, ExposeResult, Failover, FailoverPolicy, GetOptions, HealthStatus, IngressTarget, IngressTargetSource, Lifecycle, ListOptions, MountSpec, MountSpecRedacted, MountType, PlatformVolumeMount, RegisterSnapshotOptions, RegistryAuth, ResizeOptions, Sandbox as SandboxData, SandboxSnapshot, SandboxStatus, Session, SessionAttachHandle, SessionAttachOptions, SessionStatus, Template, TemplatePushState, TemplateStatus, WasmModule, WasmModuleStatus, PushWasmModuleOptions, PushWasmModuleResult, UpdateLifecycleOptions, } from "./types.js";
6
+ export type { AddCustomDomainOptions, APIToken, APITokenList, CreateAPITokenOptions, CreatedAPIToken, WhoAmI, WhoAmIPrincipal, AuditEvent, AuditOptions, AuditPage, EgressProfile, EgressProfileList, EgressProfileOptions, EgressProfileRef, EgressInject, EgressRule, ListEgressProfilesOptions, NetworkLearned, NetworkLearnedEntry, NetworkPolicy, NetworkPolicyCheckOptions, NetworkPolicyCheckResult, NetworkPolicyOptions, BinaryLike, BuildImageOptions, BuildImagePushOptions, BuildImageResult, CloneGeneration, CreateOptions, CreateSessionOptions, CreateTemplateOptions, CreateWasmModuleOptions, CustomDomain, CustomDomainDNSRecords, CustomDomainStatus, DNSRecord, DNSRecordType, ExecRequest, ExecResult, ExecExitInfo, ExposedPort, ExposePortOptions, ExposeProtocol, ExposeResult, Failover, FailoverPolicy, GetOptions, HealthStatus, IngressTarget, IngressTargetSource, Lifecycle, ListOptions, MountSpec, MountSpecRedacted, MountType, PlatformVolumeMount, RegisterSnapshotOptions, ListSnapshotsOptions, SnapshotList, RegistryAuth, ResizeOptions, Sandbox as SandboxData, SandboxSnapshot, SandboxStatus, Session, SessionAttachHandle, SessionAttachOptions, SessionStatus, Template, TemplatePushState, TemplateStatus, WasmModule, WasmModuleStatus, PushWasmModuleOptions, PushWasmModuleResult, UpdateLifecycleOptions, } from "./types.js";
@@ -1,6 +1,11 @@
1
1
  import { Image } from "../Image.js";
2
- import type { AddCustomDomainOptions, BinaryLike, BuildImageOptions, BuildImageResult, CreateOptions, CreateSessionOptions, CreateTemplateOptions, CreateWasmModuleOptions, PushWasmModuleOptions, PushWasmModuleResult, CustomDomain, CloneGeneration, CustomDomainDNSRecords, ExecRequest, ExecResult, ExecStreamHandle, ExecStreamOptions, ExposedPort, ExposePortOptions, ExposeResult, Failover, HealthStatus, IngressTarget, Lifecycle, GetOptions, ListOptions, MountSpecRedacted, NetworkUsage, AuditOptions, AuditPage, EgressProfile, EgressProfileList, EgressProfileOptions, EgressProfileRef, EgressRule, ListEgressProfilesOptions, NetworkLearned, NetworkPolicy, NetworkPolicyCheckOptions, NetworkPolicyCheckResult, NetworkPolicyOptions, RegisterSnapshotOptions, SetNetworkLimitsOptions, ResizeOptions, Sandbox, SandboxSnapshot, Session, SessionAttachHandle, SessionAttachOptions, Template, WasmModule } from "../types.js";
2
+ import type { AddCustomDomainOptions, APITokenList, BinaryLike, BuildImageOptions, BuildImageResult, CreateAPITokenOptions, CreatedAPIToken, CreateOptions, CreateSessionOptions, CreateTemplateOptions, CreateWasmModuleOptions, PushWasmModuleOptions, PushWasmModuleResult, CustomDomain, CloneGeneration, CustomDomainDNSRecords, ExecRequest, ExecResult, ExecStreamHandle, ExecStreamOptions, ExposedPort, ExposePortOptions, ExposeResult, Failover, HealthStatus, IngressTarget, Lifecycle, GetOptions, ListOptions, MountSpecRedacted, NetworkUsage, AuditOptions, AuditPage, EgressProfile, EgressProfileList, EgressProfileOptions, EgressProfileRef, EgressRule, ListEgressProfilesOptions, NetworkLearned, NetworkPolicy, NetworkPolicyCheckOptions, NetworkPolicyCheckResult, NetworkPolicyOptions, ListSnapshotsOptions, RegisterSnapshotOptions, SnapshotList, SetNetworkLimitsOptions, ResizeOptions, Sandbox, SandboxSnapshot, Session, SessionAttachHandle, SessionAttachOptions, Template, WasmModule, WhoAmI } from "../types.js";
3
3
  type FetchLike = typeof fetch;
4
+ /**
5
+ * The request header that selects which of a credential's teams a request
6
+ * acts for. Keep in sync with pkg/api.HeaderTeam on the server.
7
+ */
8
+ export declare const TEAM_HEADER = "X-AerolVM-Team";
4
9
  /**
5
10
  * Wire version of the v1-style sandbox endpoints (`/v1/sandboxes`, etc.) this
6
11
  * client speaks. The SDK and the server version independently — bumping the
@@ -36,10 +41,24 @@ export interface RetryConfig {
36
41
  * waits when `maxRetries` is high. Defaults to `5000`.
37
42
  */
38
43
  maxDelayMs?: number;
44
+ /**
45
+ * Cap in milliseconds on the wait before re-sending a 429 or 503. The SDK
46
+ * waits as long as the server's `Retry-After` header asks (seconds or an
47
+ * HTTP date), never less than its own backoff, plus up to a fifth more so
48
+ * clients told the same value spread out; a longer ask is cut to this. A
49
+ * missing or malformed header uses the backoff above. Defaults to
50
+ * `30000`; `0` ignores `Retry-After`.
51
+ */
52
+ maxRetryAfterMs?: number;
39
53
  }
40
54
  export interface APIClientConfig {
41
55
  baseURL: string;
42
56
  patToken?: string;
57
+ /**
58
+ * Sent as the {@link TEAM_HEADER} header on every HTTP request. Empty or
59
+ * absent sends no header.
60
+ */
61
+ team?: string;
43
62
  fetch?: FetchLike;
44
63
  /**
45
64
  * Wire version to call. Defaults to "v1". Pin this if you need a specific
@@ -49,11 +68,37 @@ export interface APIClientConfig {
49
68
  apiVersion?: APIVersion;
50
69
  /**
51
70
  * Retry policy for transient transport errors (socket closed, connection
52
- * reset) and retryable HTTP status codes (421, 429, 502, 503, 504). Pass
53
- * `{ maxRetries: 0 }` to disable retry entirely.
71
+ * reset) and retryable HTTP status codes (421, 429, 502, 503, 504). `exec`
72
+ * is narrower: it retries only failures that prove the command never
73
+ * reached the server (connection refused, connect timeout, DNS) and HTTP
74
+ * 429/503. Pass `{ maxRetries: 0 }` to disable retry entirely.
54
75
  */
55
76
  retry?: RetryConfig;
77
+ /**
78
+ * Cancels the client's HTTP requests: aborting it ends the request in
79
+ * flight and any retry wait, so a call never sits out a long
80
+ * `Retry-After` once aborted. WebSocket streams are not affected.
81
+ */
82
+ signal?: AbortSignal;
56
83
  }
84
+ /**
85
+ * Reads a `Retry-After` value in either form RFC 9110 allows: delta-seconds
86
+ * (`"120"`) or an HTTP date. Returns the wait in milliseconds (0 for a date
87
+ * already past), or `undefined` for an absent or malformed value.
88
+ */
89
+ export declare function parseRetryAfter(value: string | null | undefined, nowMs: number): number | undefined;
90
+ /**
91
+ * How long {@link APIClient.request} waits before re-sending. `attempt`
92
+ * counts from 0, `retryAfter` is a 429/503's `Retry-After` header (null
93
+ * otherwise) and `random` is uniform in [0, 1).
94
+ *
95
+ * Without a usable header it is the exponential backoff ±25%. With one, it
96
+ * never retries sooner than the server asked (nor sooner than the backoff)
97
+ * and adds up to a fifth so clients told the same value spread out.
98
+ * `maxRetryAfterMs` is a hard ceiling: when the server asks for at least
99
+ * that long the client is early anyway, so the spread goes below it instead.
100
+ */
101
+ export declare function retryDelayMs(config: Required<RetryConfig>, attempt: number, retryAfter: string | null, nowMs: number, random: number): number;
57
102
  /**
58
103
  * Trim trailing slashes from a URL without a backtracking regex. The natural
59
104
  * `value.replace(/\/+$/, "")` is an anchored `+` quantifier — a polynomial
@@ -64,9 +109,11 @@ export declare class APIClient {
64
109
  readonly baseURL: string;
65
110
  readonly apiVersion: APIVersion;
66
111
  private readonly patToken;
112
+ private readonly team;
67
113
  private readonly fetchFn;
68
114
  private readonly versionPrefix;
69
115
  private readonly retryConfig;
116
+ private readonly signal;
70
117
  constructor(config: APIClientConfig);
71
118
  /**
72
119
  * Build a versioned API path. Pass the suffix beginning with "/" (e.g.
@@ -106,6 +153,9 @@ export declare class APIClient {
106
153
  createSnapshot(id: string, name: string): Promise<SandboxSnapshot>;
107
154
  registerSnapshot(options: RegisterSnapshotOptions): Promise<SandboxSnapshot>;
108
155
  registerSnapshotFromImage(name: string, image: Image, options?: Omit<RegisterSnapshotOptions, "name" | "image" | "dockerfileContent">): Promise<SandboxSnapshot>;
156
+ getSnapshot(name: string): Promise<SandboxSnapshot>;
157
+ listSnapshots(options?: ListSnapshotsOptions): Promise<SnapshotList>;
158
+ deleteSnapshot(name: string): Promise<void>;
109
159
  destroy(id: string): Promise<void>;
110
160
  resize(id: string, options: ResizeOptions): Promise<SandboxResource>;
111
161
  updateLifecycle(id: string, lifecycle: Lifecycle): Promise<SandboxResource>;
@@ -181,19 +231,31 @@ export declare class APIClient {
181
231
  deleteWasmModule(id: string): Promise<void>;
182
232
  pushWasmModule(options: PushWasmModuleOptions): Promise<PushWasmModuleResult>;
183
233
  rebuildTemplate(id: string): Promise<Template>;
234
+ whoami(): Promise<WhoAmI>;
235
+ createAPIToken(options: CreateAPITokenOptions): Promise<CreatedAPIToken>;
236
+ listAPITokens(): Promise<APITokenList>;
237
+ revokeAPIToken(id: string): Promise<void>;
184
238
  private wrap;
185
239
  private doJSON;
186
240
  private doBytes;
187
241
  /**
188
242
  * Low-level HTTP request with automatic retry for transient failures.
189
243
  *
190
- * Transport-level errors (socket closed, connection reset, DNS blip) are
191
- * always retried regardless of HTTP method — the request never reached the
192
- * server so there is no idempotency concern.
244
+ * Transport-level errors (socket closed, connection reset, timeouts, DNS
245
+ * blip) and HTTP 421/429/502/503/504 are retried regardless of HTTP method.
246
+ * Several of those failures can happen after the server received the
247
+ * request, so a retry may deliver it twice; that is safe only because every
248
+ * mutating endpoint in the daemon is designed for idempotent retry (e.g.
249
+ * INSERT OR IGNORE + disambiguation).
250
+ *
251
+ * A request passed `{ idempotent: false }` (exec) retries only transport
252
+ * failures that prove the request never left the client and HTTP 429/503;
253
+ * everything else is surfaced to the caller rather than risk running the
254
+ * command twice.
193
255
  *
194
- * HTTP-level retryable codes (421, 429, 502, 503, 504) are retried for ALL
195
- * methods because every mutating endpoint in the daemon is already
196
- * designed for idempotent retry (e.g. INSERT OR IGNORE + disambiguation).
256
+ * A 429 or 503 waits as long as its `Retry-After` asks (see
257
+ * {@link retryDelayMs}). The client's `signal` ends the request and any
258
+ * wait.
197
259
  */
198
260
  private request;
199
261
  }
@@ -1,10 +1,40 @@
1
1
  import { basename } from "node:path";
2
2
  import { PATH_PREFIX as V1_PATH_PREFIX, ingressDNSPath as v1IngressDNSPath, sandboxCustomDomainDNSPath as v1SandboxCustomDomainDNSPath, sandboxCustomDomainPath as v1SandboxCustomDomainPath, sandboxCustomDomainsPath as v1SandboxCustomDomainsPath, } from "./api/v1/paths.js";
3
3
  import { Image } from "../Image.js";
4
+ /**
5
+ * The request header that selects which of a credential's teams a request
6
+ * acts for. Keep in sync with pkg/api.HeaderTeam on the server.
7
+ */
8
+ export const TEAM_HEADER = "X-AerolVM-Team";
4
9
  const DEFAULT_API_VERSION = "v1";
5
10
  const PATH_PREFIXES = {
6
11
  v1: V1_PATH_PREFIX,
7
12
  };
13
+ function fromApiWhoAmI(w) {
14
+ const out = {
15
+ principal: { kind: w.principal.kind, id: w.principal.id },
16
+ role: w.role,
17
+ };
18
+ if (w.principal.name)
19
+ out.principal.name = w.principal.name;
20
+ if (w.team)
21
+ out.team = w.team;
22
+ if (w.expires_at)
23
+ out.expiresAt = w.expires_at;
24
+ return out;
25
+ }
26
+ function fromApiAPIToken(t) {
27
+ return {
28
+ id: t.id,
29
+ name: t.name,
30
+ role: t.role,
31
+ team: t.team,
32
+ createdBy: t.created_by,
33
+ createdAt: t.created_at,
34
+ expiresAt: t.expires_at,
35
+ expired: t.expired === true,
36
+ };
37
+ }
8
38
  function fromApiEgressProfile(p) {
9
39
  const out = {
10
40
  name: p.name,
@@ -29,17 +59,76 @@ function fromApiEgressRule({ inject, ...rule }) {
29
59
  // The server closes the connection, so the retry reconnects and the ingress
30
60
  // re-routes it (plans/ingress-proxy-routing.md, review 2A).
31
61
  const RETRYABLE_STATUS_CODES = new Set([421, 429, 502, 503, 504]);
62
+ // The subset a non-idempotent request (exec) may retry: 429 and 503 are the
63
+ // server turning the request away before doing any work. 421, 502 and 504 come
64
+ // from forwarding/proxy hops, and the status alone can't prove that no hop
65
+ // already handed the command to the sandbox, so exec surfaces them. They are
66
+ // also the statuses whose Retry-After paces clients: 429 when this caller is
67
+ // over its share, 503 when the node sheds load. A Retry-After on a 421, 502 or
68
+ // 504 is a hop's, not the server's, so those use the normal backoff.
69
+ const REFUSED_STATUS_CODES = new Set([429, 503]);
32
70
  /** Default retry settings when the caller doesn't supply a RetryConfig. */
33
71
  const DEFAULT_RETRY = {
34
72
  maxRetries: 3,
35
73
  baseDelayMs: 200,
36
74
  maxDelayMs: 5_000,
75
+ // An overloaded node asks for about a second, so the cap only bites on a
76
+ // long ask (maintenance, a misconfigured proxy), which should not hold a
77
+ // call for minutes per retry.
78
+ maxRetryAfterMs: 30_000,
37
79
  };
80
+ // IMF-fixdate, the HTTP-date form servers send ("Sun, 06 Nov 1994 08:49:37
81
+ // GMT"). Date.parse alone would also take "1" or a date with no zone, read in
82
+ // local time.
83
+ const IMF_FIXDATE = /^[A-Za-z]{3}, \d{2} [A-Za-z]{3} \d{4} \d{2}:\d{2}:\d{2} GMT$/;
84
+ /**
85
+ * Reads a `Retry-After` value in either form RFC 9110 allows: delta-seconds
86
+ * (`"120"`) or an HTTP date. Returns the wait in milliseconds (0 for a date
87
+ * already past), or `undefined` for an absent or malformed value.
88
+ */
89
+ export function parseRetryAfter(value, nowMs) {
90
+ const trimmed = (value ?? "").trim();
91
+ if (/^[0-9]+$/.test(trimmed)) {
92
+ return Number(trimmed) * 1000;
93
+ }
94
+ if (!IMF_FIXDATE.test(trimmed)) {
95
+ return undefined;
96
+ }
97
+ const when = Date.parse(trimmed);
98
+ return Number.isNaN(when) ? undefined : Math.max(when - nowMs, 0);
99
+ }
100
+ /**
101
+ * How long {@link APIClient.request} waits before re-sending. `attempt`
102
+ * counts from 0, `retryAfter` is a 429/503's `Retry-After` header (null
103
+ * otherwise) and `random` is uniform in [0, 1).
104
+ *
105
+ * Without a usable header it is the exponential backoff ±25%. With one, it
106
+ * never retries sooner than the server asked (nor sooner than the backoff)
107
+ * and adds up to a fifth so clients told the same value spread out.
108
+ * `maxRetryAfterMs` is a hard ceiling: when the server asks for at least
109
+ * that long the client is early anyway, so the spread goes below it instead.
110
+ */
111
+ export function retryDelayMs(config, attempt, retryAfter, nowMs, random) {
112
+ const backoff = Math.min(config.baseDelayMs * 2 ** attempt, config.maxDelayMs);
113
+ const limit = config.maxRetryAfterMs;
114
+ const hint = limit > 0 ? parseRetryAfter(retryAfter, nowMs) : undefined;
115
+ if (hint === undefined) {
116
+ return backoff * (0.75 + 0.5 * random);
117
+ }
118
+ const target = Math.max(hint, backoff);
119
+ if (target >= limit) {
120
+ return limit - limit * 0.2 * random;
121
+ }
122
+ const upper = Math.min(target * 1.2, limit);
123
+ return target + (upper - target) * random;
124
+ }
38
125
  /**
39
126
  * Returns `true` when the thrown `error` looks like a transport-level failure
40
127
  * that vanishes on retry (connection reset, socket closed by peer, DNS
41
128
  * blip). `undici` and Node core use several `cause.code` values; we check
42
- * the full cause chain.
129
+ * the full cause chain. A reset, EPIPE, read/headers/body timeout or "other
130
+ * side closed" can fire after the server already received the request, so
131
+ * this classification is only safe for idempotent requests.
43
132
  */
44
133
  function isTransientTransportError(error) {
45
134
  const codes = new Set([
@@ -69,6 +158,45 @@ function isTransientTransportError(error) {
69
158
  }
70
159
  return false;
71
160
  }
161
+ /**
162
+ * Returns `true` only for transport failures that prove the request never
163
+ * left the client: the connection was refused, did not open before the
164
+ * connect timeout, or the host name did not resolve (transiently; ENOTFOUND
165
+ * is not retried for any request). Any other code in the cause chain means
166
+ * bytes may have reached the server, so the answer is `false`.
167
+ */
168
+ function isUnsentTransportError(error) {
169
+ let current = error;
170
+ for (let depth = 0; depth < 5 && current instanceof Error; depth++) {
171
+ const err = current;
172
+ switch (err.code) {
173
+ case undefined:
174
+ break;
175
+ case "ECONNREFUSED":
176
+ case "EAI_AGAIN":
177
+ case "UND_ERR_CONNECT_TIMEOUT":
178
+ return true;
179
+ case "ETIMEDOUT":
180
+ // Node uses ETIMEDOUT both for a connect that never completed and for
181
+ // an open socket that stopped answering; only the first is unsent.
182
+ return failedWhileConnecting(err);
183
+ default:
184
+ return false;
185
+ }
186
+ current = err.cause;
187
+ }
188
+ return false;
189
+ }
190
+ function failedWhileConnecting(err) {
191
+ if (err.syscall === "connect")
192
+ return true;
193
+ // With autoSelectFamily (Node's default), a host with several addresses
194
+ // fails with an AggregateError holding one error per connect attempt and no
195
+ // syscall of its own.
196
+ return (err instanceof AggregateError &&
197
+ err.errors.length > 0 &&
198
+ err.errors.every((inner) => inner?.syscall === "connect"));
199
+ }
72
200
  /**
73
201
  * Trim trailing slashes from a URL without a backtracking regex. The natural
74
202
  * `value.replace(/\/+$/, "")` is an anchored `+` quantifier — a polynomial
@@ -81,21 +209,37 @@ export function stripTrailingSlashes(value) {
81
209
  }
82
210
  return value.slice(0, end);
83
211
  }
84
- /** Sleep with ±25% jitter so concurrent callers don't thundering-herd. */
85
- function jitteredDelay(baseMs) {
86
- const jitter = 1 + (Math.random() - 0.5) * 0.5; // 0.75 – 1.25
87
- return new Promise((resolve) => setTimeout(resolve, Math.round(baseMs * jitter)));
212
+ /** Waits `ms`, or rejects with the signal's reason as soon as it aborts. */
213
+ function sleep(ms, signal) {
214
+ return new Promise((resolve, reject) => {
215
+ if (signal?.aborted) {
216
+ reject(signal.reason);
217
+ return;
218
+ }
219
+ const onAbort = () => {
220
+ clearTimeout(timer);
221
+ reject(signal?.reason);
222
+ };
223
+ const timer = setTimeout(() => {
224
+ signal?.removeEventListener("abort", onAbort);
225
+ resolve();
226
+ }, ms);
227
+ signal?.addEventListener("abort", onAbort, { once: true });
228
+ });
88
229
  }
89
230
  export class APIClient {
90
231
  baseURL;
91
232
  apiVersion;
92
233
  patToken;
234
+ team;
93
235
  fetchFn;
94
236
  versionPrefix;
95
237
  retryConfig;
238
+ signal;
96
239
  constructor(config) {
97
240
  this.baseURL = stripTrailingSlashes(config.baseURL);
98
241
  this.patToken = config.patToken ?? "";
242
+ this.team = (config.team ?? "").trim();
99
243
  this.fetchFn = config.fetch ?? fetch;
100
244
  this.apiVersion = config.apiVersion ?? DEFAULT_API_VERSION;
101
245
  this.versionPrefix = PATH_PREFIXES[this.apiVersion];
@@ -103,7 +247,9 @@ export class APIClient {
103
247
  maxRetries: config.retry?.maxRetries ?? DEFAULT_RETRY.maxRetries,
104
248
  baseDelayMs: config.retry?.baseDelayMs ?? DEFAULT_RETRY.baseDelayMs,
105
249
  maxDelayMs: config.retry?.maxDelayMs ?? DEFAULT_RETRY.maxDelayMs,
250
+ maxRetryAfterMs: config.retry?.maxRetryAfterMs ?? DEFAULT_RETRY.maxRetryAfterMs,
106
251
  };
252
+ this.signal = config.signal;
107
253
  }
108
254
  /**
109
255
  * Build a versioned API path. Pass the suffix beginning with "/" (e.g.
@@ -272,6 +418,25 @@ export class APIClient {
272
418
  dockerfileContent: image.dockerfile,
273
419
  });
274
420
  }
421
+ async getSnapshot(name) {
422
+ return fromApiSandboxSnapshot(await this.doJSON("GET", this.versioned(`/snapshots/${encodeURIComponent(name)}`)));
423
+ }
424
+ async listSnapshots(options = {}) {
425
+ const params = new URLSearchParams();
426
+ if (options.limit)
427
+ params.set("limit", String(options.limit));
428
+ if (options.startingAfter)
429
+ params.set("starting_after", options.startingAfter);
430
+ const query = params.toString();
431
+ const response = await this.doJSON("GET", this.versioned(`/snapshots${query ? `?${query}` : ""}`));
432
+ return {
433
+ snapshots: (response.snapshots ?? []).map(fromApiSandboxSnapshot),
434
+ hasMore: response.has_more ?? false,
435
+ };
436
+ }
437
+ async deleteSnapshot(name) {
438
+ await this.doJSON("DELETE", this.versioned(`/snapshots/${encodeURIComponent(name)}`));
439
+ }
275
440
  async destroy(id) {
276
441
  await this.doJSON("DELETE", `${this.versionPrefix}/sandboxes/${id}`);
277
442
  }
@@ -284,7 +449,8 @@ export class APIClient {
284
449
  return this.wrap(response);
285
450
  }
286
451
  async exec(id, request) {
287
- const response = await this.doJSON("POST", `${this.versionPrefix}/sandboxes/${id}/toolbox/process/execute`, toApiExecRequest(request));
452
+ // Not idempotent: a re-sent exec runs the caller's command a second time.
453
+ const response = await this.doJSON("POST", `${this.versionPrefix}/sandboxes/${id}/toolbox/process/execute`, toApiExecRequest(request), { idempotent: false });
288
454
  return fromApiExecResult(response);
289
455
  }
290
456
  execStream(id, options) {
@@ -571,10 +737,33 @@ export class APIClient {
571
737
  const response = await this.doJSON("POST", `${this.versionPrefix}/templates/${id}/rebuild`);
572
738
  return fromApiTemplate(response);
573
739
  }
740
+ async whoami() {
741
+ return fromApiWhoAmI(await this.doJSON("GET", this.versioned("/whoami")));
742
+ }
743
+ async createAPIToken(options) {
744
+ const response = await this.doJSON("POST", this.versioned("/tokens"), {
745
+ name: options.name,
746
+ role: options.role,
747
+ team: options.team,
748
+ expires_in: options.expiresIn,
749
+ });
750
+ return { ...fromApiAPIToken(response), token: response.token };
751
+ }
752
+ async listAPITokens() {
753
+ const response = await this.doJSON("GET", this.versioned("/tokens"));
754
+ return { tokens: (response.tokens ?? []).map(fromApiAPIToken) };
755
+ }
756
+ async revokeAPIToken(id) {
757
+ const trimmed = id.trim();
758
+ if (trimmed === "") {
759
+ throw new Error("token id is required");
760
+ }
761
+ await this.doJSON("DELETE", this.versioned(`/tokens/${encodeURIComponent(trimmed)}`));
762
+ }
574
763
  wrap(sandbox) {
575
764
  return new SandboxResource(this, fromApiSandbox(sandbox));
576
765
  }
577
- async doJSON(method, path, body) {
766
+ async doJSON(method, path, body, options) {
578
767
  const init = {};
579
768
  if (body !== undefined) {
580
769
  init.body = JSON.stringify(body);
@@ -582,7 +771,7 @@ export class APIClient {
582
771
  "Content-Type": "application/json",
583
772
  };
584
773
  }
585
- const response = await this.request(method, path, init);
774
+ const response = await this.request(method, path, init, options);
586
775
  if (!response.ok) {
587
776
  throw await decodeError(response);
588
777
  }
@@ -601,46 +790,59 @@ export class APIClient {
601
790
  /**
602
791
  * Low-level HTTP request with automatic retry for transient failures.
603
792
  *
604
- * Transport-level errors (socket closed, connection reset, DNS blip) are
605
- * always retried regardless of HTTP method — the request never reached the
606
- * server so there is no idempotency concern.
793
+ * Transport-level errors (socket closed, connection reset, timeouts, DNS
794
+ * blip) and HTTP 421/429/502/503/504 are retried regardless of HTTP method.
795
+ * Several of those failures can happen after the server received the
796
+ * request, so a retry may deliver it twice; that is safe only because every
797
+ * mutating endpoint in the daemon is designed for idempotent retry (e.g.
798
+ * INSERT OR IGNORE + disambiguation).
607
799
  *
608
- * HTTP-level retryable codes (421, 429, 502, 503, 504) are retried for ALL
609
- * methods because every mutating endpoint in the daemon is already
610
- * designed for idempotent retry (e.g. INSERT OR IGNORE + disambiguation).
800
+ * A request passed `{ idempotent: false }` (exec) retries only transport
801
+ * failures that prove the request never left the client and HTTP 429/503;
802
+ * everything else is surfaced to the caller rather than risk running the
803
+ * command twice.
804
+ *
805
+ * A 429 or 503 waits as long as its `Retry-After` asks (see
806
+ * {@link retryDelayMs}). The client's `signal` ends the request and any
807
+ * wait.
611
808
  */
612
- async request(method, path, init = {}) {
809
+ async request(method, path, init = {}, options = {}) {
613
810
  const headers = new Headers(init.headers);
614
811
  if (this.patToken !== "") {
615
812
  headers.set("Authorization", `Bearer ${this.patToken}`);
616
813
  }
814
+ if (this.team !== "") {
815
+ headers.set(TEAM_HEADER, this.team);
816
+ }
617
817
  const url = `${this.baseURL}${path}`;
618
- const requestInit = { ...init, method, headers };
619
- const { maxRetries, baseDelayMs, maxDelayMs } = this.retryConfig;
620
- let lastError;
621
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
818
+ const signal = init.signal ?? this.signal;
819
+ const requestInit = { ...init, method, headers, ...(signal ? { signal } : {}) };
820
+ const { maxRetries } = this.retryConfig;
821
+ const idempotent = options.idempotent ?? true;
822
+ const retryableStatusCodes = idempotent ? RETRYABLE_STATUS_CODES : REFUSED_STATUS_CODES;
823
+ const isRetryableError = idempotent ? isTransientTransportError : isUnsentTransportError;
824
+ for (let attempt = 0;; attempt++) {
825
+ let retryAfter = null;
622
826
  try {
623
827
  const response = await this.fetchFn(url, requestInit);
624
- // Retry on transient HTTP status codes.
625
- if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < maxRetries) {
626
- const delay = Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
627
- await jitteredDelay(delay);
628
- continue;
828
+ if (!retryableStatusCodes.has(response.status) || attempt >= maxRetries) {
829
+ return response;
830
+ }
831
+ if (REFUSED_STATUS_CODES.has(response.status)) {
832
+ retryAfter = response.headers.get("retry-after");
629
833
  }
630
- return response;
834
+ // Free the connection instead of holding it through the wait.
835
+ response.body?.cancel().catch(() => undefined);
631
836
  }
632
837
  catch (error) {
633
- lastError = error;
634
- // Only retry transport-level failures; anything else propagates.
635
- if (!isTransientTransportError(error) || attempt >= maxRetries) {
838
+ // Only retry transport-level failures; anything else propagates. An
839
+ // aborted signal surfaces whatever fetch threw for it.
840
+ if (signal?.aborted || !isRetryableError(error) || attempt >= maxRetries) {
636
841
  throw error;
637
842
  }
638
- const delay = Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
639
- await jitteredDelay(delay);
640
843
  }
844
+ await sleep(retryDelayMs(this.retryConfig, attempt, retryAfter, Date.now(), Math.random()), signal);
641
845
  }
642
- // Should be unreachable — the loop always throws or returns.
643
- throw lastError;
644
846
  }
645
847
  }
646
848
  export class SandboxResource {
@@ -826,6 +1028,15 @@ function toApiCreateOptions(options) {
826
1028
  module_ref: options.moduleRef,
827
1029
  tenant_id: options.tenantId,
828
1030
  custom_domains: options.customDomains,
1031
+ gpus: options.gpus ? toApiGPUOptions(options.gpus) : undefined,
1032
+ template_id: options.templateId,
1033
+ };
1034
+ }
1035
+ function toApiGPUOptions(gpus) {
1036
+ return {
1037
+ vendor: gpus.vendor,
1038
+ count: gpus.count,
1039
+ device_ids: gpus.deviceIDs,
829
1040
  };
830
1041
  }
831
1042
  function toApiRegisterSnapshotOptions(options) {
package/dist/types.d.ts CHANGED
@@ -194,9 +194,10 @@ export interface CreateOptions {
194
194
  */
195
195
  networkEgressRules?: EgressRule[];
196
196
  /**
197
- * Whether the sandbox may be exposed to the public internet. Omitted defaults
198
- * to private (no public URL, `exposePort` fails). Set `true` to opt in to
199
- * public exposure; `false` permanently refuses it for this sandbox.
197
+ * Whether the sandbox is public from boot. Omitted or `false` creates it
198
+ * private: no public URL and no ingress route. `true` makes it public from
199
+ * boot. Either way the first `exposePort` call makes the sandbox public, so
200
+ * `false` does not stop a later `exposePort`.
200
201
  */
201
202
  allowPublicTraffic?: boolean;
202
203
  /**
@@ -269,6 +270,13 @@ export interface CreateOptions {
269
270
  * `sandbox.customDomains.add(host)` for each entry after create.
270
271
  */
271
272
  customDomains?: string[];
273
+ /**
274
+ * Firecracker template id (from `createTemplate`); the template must be
275
+ * ready. Requires `runtime: "firecracker"` and `image`: pass the image the
276
+ * template was built from. The sandbox boots from the template's prepared
277
+ * rootfs instead of building one from `image` on every create.
278
+ */
279
+ templateId?: string;
272
280
  }
273
281
  export interface ResizeOptions {
274
282
  /** Number of CPU cores. Fractional values supported (e.g. 0.5). */
@@ -445,6 +453,19 @@ export interface SandboxSnapshot {
445
453
  memoryMB?: number;
446
454
  diskGB?: number;
447
455
  }
456
+ /** Options for {@link MicroVM.listSnapshots}: one name-ordered page. */
457
+ export interface ListSnapshotsOptions {
458
+ /** Maximum snapshots to return in this page. */
459
+ limit?: number;
460
+ /** Exclusive name cursor — pass the last name of the previous page. */
461
+ startingAfter?: string;
462
+ }
463
+ /** One page of snapshots from {@link MicroVM.listSnapshots}. */
464
+ export interface SnapshotList {
465
+ snapshots: SandboxSnapshot[];
466
+ /** True when more snapshots remain after this page. */
467
+ hasMore: boolean;
468
+ }
448
469
  /**
449
470
  * Discriminated result from `exposePort`. Branch on `protocol` to access the
450
471
  * fields that are meaningful for the chosen wire protocol — only the raw-TCP
@@ -482,8 +503,9 @@ export interface Sandbox {
482
503
  networkBlockAll: boolean;
483
504
  /**
484
505
  * Hostname-egress state on `get` for a sandbox whose allow list names
485
- * hosts (container runtimes): "active", "held" or "unavailable". Absent
486
- * otherwise, and on list results.
506
+ * hosts (container runtimes): "active", "held" or "unavailable";
507
+ * "unenforced" when the server's firewall is off and nothing filters the
508
+ * sandbox. Absent otherwise, and on list results.
487
509
  */
488
510
  egressStatus?: string;
489
511
  /** Egress profiles this sandbox references. */
@@ -629,7 +651,10 @@ export interface NetworkPolicy {
629
651
  networkEgressRules: EgressRule[];
630
652
  /** Hostname entries in force: inline plus every profile's (at most 1024). */
631
653
  effectiveHostnameCount: number;
632
- /** "active", "held" or "unavailable" for hostname rules on a container. */
654
+ /**
655
+ * "active", "held" or "unavailable" for hostname rules on a container;
656
+ * "unenforced" when the server's firewall is off.
657
+ */
633
658
  egressStatus?: string;
634
659
  }
635
660
  /**
@@ -722,6 +747,59 @@ export interface EgressProfileList {
722
747
  /** Pass back as `cursor` for the next page; absent on the last one. */
723
748
  nextCursor?: string;
724
749
  }
750
+ /** Who the caller is, as `microvm.whoami()` reports it. */
751
+ export interface WhoAmIPrincipal {
752
+ /** `"operator"`, `"token"`, `"oidc"`, `"controlplane"`, ... */
753
+ kind: string;
754
+ /** Stable id: `"operator"`, `"tok_…"`, `"oidc:<issuer>:<sub>"`, ... */
755
+ id: string;
756
+ /** Display name; absent when the server has none. */
757
+ name?: string;
758
+ }
759
+ /** The body of `microvm.whoami()`: the caller's own resolved identity. */
760
+ export interface WhoAmI {
761
+ principal: WhoAmIPrincipal;
762
+ /** `"viewer"`, `"runner"`, `"auditor"` or `"admin"`. */
763
+ role: string;
764
+ /** `"*"` for every team, a team name, or absent for a managed account. */
765
+ team?: string;
766
+ /** When the credential stops working (RFC 3339); absent if it never does. */
767
+ expiresAt?: string;
768
+ }
769
+ /** One scoped API token as `microvm.tokens.list()` returns it. Never the secret. */
770
+ export interface APIToken {
771
+ /** Public id (`tok_…`): what `tokens.revoke` takes and audit records name. */
772
+ id: string;
773
+ name: string;
774
+ role: string;
775
+ /** A team name, or `"*"` for every team. */
776
+ team: string;
777
+ createdBy: string;
778
+ createdAt: string;
779
+ expiresAt: string;
780
+ /** Expired tokens no longer work; they stay listed for 30 days. */
781
+ expired: boolean;
782
+ }
783
+ /** `microvm.tokens.create()`'s result: the token plus its secret. */
784
+ export interface CreatedAPIToken extends APIToken {
785
+ /** The secret (`avm_…`), returned this once and never stored by the server. */
786
+ token: string;
787
+ }
788
+ /** The body of `microvm.tokens.create()`. */
789
+ export interface CreateAPITokenOptions {
790
+ /** 1 to 64 of `A-Z a-z 0-9 . _ -`, unique among the team's tokens. */
791
+ name: string;
792
+ /** `"viewer"`, `"runner"`, `"auditor"` or `"admin"`. */
793
+ role: string;
794
+ /** A team name, or `"*"` for every team. `auditor` and `admin` require `"*"`. */
795
+ team: string;
796
+ /** Required lifetime: `"90d"`, `"720h"` or any Go duration. */
797
+ expiresIn: string;
798
+ }
799
+ /** `microvm.tokens.list()`'s result. */
800
+ export interface APITokenList {
801
+ tokens: APIToken[];
802
+ }
725
803
  /** One destination a learn-mode sandbox reached. */
726
804
  export interface NetworkLearnedEntry {
727
805
  host: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aerol-ai/aerolvm-sdk",
3
- "version": "0.6.4",
3
+ "version": "0.7.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,7 +26,7 @@
26
26
  "test": "tsc -p tsconfig.test.json --outDir dist-test && node --test dist-test/*.test.js dist-test/internal/*.test.js"
27
27
  },
28
28
  "devDependencies": {
29
- "@types/node": "^26.6.3",
29
+ "@types/node": "^26.6.6",
30
30
  "typescript": "^7.0.2",
31
31
  "tsx": "^4.23.15"
32
32
  }