@aerol-ai/aerolvm-sdk 0.7.0 → 0.8.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.
package/dist/MicroVM.d.ts CHANGED
@@ -1,7 +1,7 @@
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 { 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";
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, CreateSnapshotOptions, RegisterSnapshotOptions, ListSnapshotsOptions, SnapshotList, ResizeOptions, SandboxSnapshot, Session, SessionAttachHandle, SessionAttachOptions, Template, WasmModule, WhoAmI } from "./types.js";
5
5
  type FetchLike = typeof fetch;
6
6
  /** Scoped API token calls, reached as `microvm.tokens`. */
7
7
  export interface Tokens {
@@ -85,7 +85,7 @@ export declare class MicroVM {
85
85
  getByName(name: string, options?: GetOptions): Promise<Sandbox | null>;
86
86
  start(id: string): Promise<Sandbox>;
87
87
  stop(id: string): Promise<Sandbox>;
88
- createSnapshot(id: string, name: string): Promise<SandboxSnapshot>;
88
+ createSnapshot(id: string, name: string, options?: CreateSnapshotOptions): Promise<SandboxSnapshot>;
89
89
  registerSnapshot(options: RegisterSnapshotOptions): Promise<SandboxSnapshot>;
90
90
  registerSnapshotFromImage(name: string, image: Image, options?: Omit<RegisterSnapshotOptions, "name" | "image" | "dockerfileContent">): Promise<SandboxSnapshot>;
91
91
  /** Look a snapshot up by name. In a cluster the request is routed to the node holding the image. */
package/dist/MicroVM.js CHANGED
@@ -66,8 +66,8 @@ export class MicroVM {
66
66
  const sandbox = await this.client.stop(id);
67
67
  return this.wrap(sandbox.toJSON());
68
68
  }
69
- async createSnapshot(id, name) {
70
- return this.client.createSnapshot(id, name);
69
+ async createSnapshot(id, name, options = {}) {
70
+ return this.client.createSnapshot(id, name, options);
71
71
  }
72
72
  async registerSnapshot(options) {
73
73
  return this.client.registerSnapshot(options);
@@ -0,0 +1,28 @@
1
+ /**
2
+ * An HTTP error response from the AerolVM server. `message` is the server's
3
+ * message, or `request failed with status N` when the body carries none (a
4
+ * proxy's HTML error page, say): the same text the SDK threw before this
5
+ * class existed, so code that printed or matched it sees no change.
6
+ *
7
+ * Branch on `code`, not on `message`: the code is a stable identifier, the
8
+ * message is wording for a person.
9
+ */
10
+ export declare class MicroVMApiError extends Error {
11
+ /** The HTTP status of the final attempt, such as 422 or 503. */
12
+ readonly status: number;
13
+ /**
14
+ * The server's stable error code, such as `"idempotency_key_reused"` or
15
+ * `"overloaded"`. Undefined when the response has none: many errors are
16
+ * only ever shown to a person, and a proxy's error page has no code.
17
+ */
18
+ readonly code: string | undefined;
19
+ /**
20
+ * How long the response's `Retry-After` header asked the caller to wait,
21
+ * in milliseconds. Undefined without a usable header.
22
+ */
23
+ readonly retryAfterMs: number | undefined;
24
+ constructor(status: number, message: string, options?: {
25
+ code?: string;
26
+ retryAfterMs?: number;
27
+ });
28
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * An HTTP error response from the AerolVM server. `message` is the server's
3
+ * message, or `request failed with status N` when the body carries none (a
4
+ * proxy's HTML error page, say): the same text the SDK threw before this
5
+ * class existed, so code that printed or matched it sees no change.
6
+ *
7
+ * Branch on `code`, not on `message`: the code is a stable identifier, the
8
+ * message is wording for a person.
9
+ */
10
+ export class MicroVMApiError extends Error {
11
+ /** The HTTP status of the final attempt, such as 422 or 503. */
12
+ status;
13
+ /**
14
+ * The server's stable error code, such as `"idempotency_key_reused"` or
15
+ * `"overloaded"`. Undefined when the response has none: many errors are
16
+ * only ever shown to a person, and a proxy's error page has no code.
17
+ */
18
+ code;
19
+ /**
20
+ * How long the response's `Retry-After` header asked the caller to wait,
21
+ * in milliseconds. Undefined without a usable header.
22
+ */
23
+ retryAfterMs;
24
+ constructor(status, message, options = {}) {
25
+ super(message);
26
+ this.name = "MicroVMApiError";
27
+ this.status = status;
28
+ this.code = options.code;
29
+ this.retryAfterMs = options.retryAfterMs;
30
+ }
31
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { MicroVM } from "./MicroVM.js";
2
2
  export { Sandbox } from "./Sandbox.js";
3
3
  export { Image } from "./Image.js";
4
+ export { MicroVMApiError } from "./errors.js";
4
5
  export type { MicroVMConfig, Tokens } from "./MicroVM.js";
5
6
  export type { RetryConfig } from "./internal/client.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";
7
+ 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, CreateSnapshotOptions, 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";
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { MicroVM } from "./MicroVM.js";
2
2
  export { Sandbox } from "./Sandbox.js";
3
3
  export { Image } from "./Image.js";
4
+ export { MicroVMApiError } from "./errors.js";
@@ -1,5 +1,5 @@
1
1
  import { Image } from "../Image.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";
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, CreateSnapshotOptions, RegisterSnapshotOptions, SnapshotList, SetNetworkLimitsOptions, ResizeOptions, Sandbox, SandboxSnapshot, Session, SessionAttachHandle, SessionAttachOptions, Template, WasmModule, WhoAmI } from "../types.js";
3
3
  type FetchLike = typeof fetch;
4
4
  /**
5
5
  * The request header that selects which of a credential's teams a request
@@ -81,6 +81,13 @@ export interface APIClientConfig {
81
81
  */
82
82
  signal?: AbortSignal;
83
83
  }
84
+ /**
85
+ * The header a sandbox create carries so the server can recognise a retry of
86
+ * it. Keep in sync with clustercreate.HeaderIdempotencyKey on the server.
87
+ */
88
+ export declare const IDEMPOTENCY_KEY_HEADER = "Idempotency-Key";
89
+ /** A fresh create key: 128 random bits as 32 lowercase hex characters. */
90
+ export declare function newIdempotencyKey(): string;
84
91
  /**
85
92
  * Reads a `Retry-After` value in either form RFC 9110 allows: delta-seconds
86
93
  * (`"120"`) or an HTTP date. Returns the wait in milliseconds (0 for a date
@@ -150,7 +157,7 @@ export declare class APIClient {
150
157
  get(id: string, options?: GetOptions): Promise<SandboxResource>;
151
158
  start(id: string): Promise<SandboxResource>;
152
159
  stop(id: string): Promise<SandboxResource>;
153
- createSnapshot(id: string, name: string): Promise<SandboxSnapshot>;
160
+ createSnapshot(id: string, name: string, options?: CreateSnapshotOptions): Promise<SandboxSnapshot>;
154
161
  registerSnapshot(options: RegisterSnapshotOptions): Promise<SandboxSnapshot>;
155
162
  registerSnapshotFromImage(name: string, image: Image, options?: Omit<RegisterSnapshotOptions, "name" | "image" | "dockerfileContent">): Promise<SandboxSnapshot>;
156
163
  getSnapshot(name: string): Promise<SandboxSnapshot>;
@@ -330,7 +337,7 @@ export declare class SandboxResource implements Sandbox {
330
337
  list(): Promise<CustomDomain[]>;
331
338
  dns(): Promise<CustomDomainDNSRecords>;
332
339
  };
333
- createSnapshot(name: string): Promise<SandboxSnapshot>;
340
+ createSnapshot(name: string, options?: CreateSnapshotOptions): Promise<SandboxSnapshot>;
334
341
  start(): Promise<this>;
335
342
  stop(): Promise<this>;
336
343
  destroy(): Promise<void>;
@@ -1,5 +1,6 @@
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
+ import { MicroVMApiError } from "../errors.js";
3
4
  import { Image } from "../Image.js";
4
5
  /**
5
6
  * The request header that selects which of a credential's teams a request
@@ -67,6 +68,19 @@ const RETRYABLE_STATUS_CODES = new Set([421, 429, 502, 503, 504]);
67
68
  // over its share, 503 when the node sheds load. A Retry-After on a 421, 502 or
68
69
  // 504 is a hop's, not the server's, so those use the normal backoff.
69
70
  const REFUSED_STATUS_CODES = new Set([429, 503]);
71
+ /**
72
+ * The header a sandbox create carries so the server can recognise a retry of
73
+ * it. Keep in sync with clustercreate.HeaderIdempotencyKey on the server.
74
+ */
75
+ export const IDEMPOTENCY_KEY_HEADER = "Idempotency-Key";
76
+ /** A fresh create key: 128 random bits as 32 lowercase hex characters. */
77
+ export function newIdempotencyKey() {
78
+ // getRandomValues, unlike randomUUID, also works on a page served over
79
+ // plain http.
80
+ const bytes = new Uint8Array(16);
81
+ globalThis.crypto.getRandomValues(bytes);
82
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
83
+ }
70
84
  /** Default retry settings when the caller doesn't supply a RetryConfig. */
71
85
  const DEFAULT_RETRY = {
72
86
  maxRetries: 3,
@@ -262,7 +276,13 @@ export class APIClient {
262
276
  }
263
277
  async create(options) {
264
278
  const resolved = await this.resolveImage(options);
265
- const response = await this.doJSON("POST", this.versioned("/sandboxes"), toApiCreateOptions(resolved));
279
+ // One key per call, sent unchanged on each of its automatic retries: a
280
+ // create retried after a timeout gets back the sandbox the first attempt
281
+ // made instead of making a second one.
282
+ const key = options.idempotencyKey?.trim() || newIdempotencyKey();
283
+ const response = await this.doJSON("POST", this.versioned("/sandboxes"), toApiCreateOptions(resolved), {
284
+ headers: { [IDEMPOTENCY_KEY_HEADER]: key },
285
+ });
266
286
  return new SandboxResource(this, fromApiCreateSandboxResponse(response));
267
287
  }
268
288
  /**
@@ -385,8 +405,12 @@ export class APIClient {
385
405
  const response = await this.doJSON("POST", `${this.versionPrefix}/sandboxes/${id}/stop`);
386
406
  return this.wrap(response);
387
407
  }
388
- async createSnapshot(id, name) {
389
- const response = await this.doJSON("POST", `${this.versionPrefix}/sandboxes/${id}/snapshot`, { name });
408
+ async createSnapshot(id, name, options = {}) {
409
+ const body = { name };
410
+ if (options.allowPause !== undefined) {
411
+ body.allow_pause = options.allowPause;
412
+ }
413
+ const response = await this.doJSON("POST", `${this.versionPrefix}/sandboxes/${id}/snapshot`, body);
390
414
  return fromApiSandboxSnapshot(response);
391
415
  }
392
416
  async registerSnapshot(options) {
@@ -764,12 +788,10 @@ export class APIClient {
764
788
  return new SandboxResource(this, fromApiSandbox(sandbox));
765
789
  }
766
790
  async doJSON(method, path, body, options) {
767
- const init = {};
791
+ const init = { headers: { ...options?.headers } };
768
792
  if (body !== undefined) {
769
793
  init.body = JSON.stringify(body);
770
- init.headers = {
771
- "Content-Type": "application/json",
772
- };
794
+ init.headers = { ...init.headers, "Content-Type": "application/json" };
773
795
  }
774
796
  const response = await this.request(method, path, init, options);
775
797
  if (!response.ok) {
@@ -928,8 +950,8 @@ export class SandboxResource {
928
950
  dns: () => client.customDomainDNS(id),
929
951
  };
930
952
  }
931
- async createSnapshot(name) {
932
- return this.client.createSnapshot(this.id, name);
953
+ async createSnapshot(name, options = {}) {
954
+ return this.client.createSnapshot(this.id, name, options);
933
955
  }
934
956
  async start() {
935
957
  const updated = await this.client.start(this.id);
@@ -1432,16 +1454,19 @@ function toBlob(data) {
1432
1454
  return new Blob([Uint8Array.from(data)]);
1433
1455
  }
1434
1456
  async function decodeError(response) {
1457
+ const retryAfterMs = parseRetryAfter(response.headers.get("retry-after"), Date.now());
1435
1458
  try {
1436
1459
  const payload = (await response.json());
1437
1460
  if (typeof payload.error === "string" && payload.error !== "") {
1438
- return new Error(payload.error);
1461
+ const code = typeof payload.code === "string" && payload.code !== "" ? payload.code : undefined;
1462
+ return new MicroVMApiError(response.status, payload.error, { code, retryAfterMs });
1439
1463
  }
1440
1464
  }
1441
1465
  catch {
1442
- // Fall through to status-based error.
1466
+ // Not JSON (a proxy's error page) or no body: fall through to the
1467
+ // status-based message, with no code.
1443
1468
  }
1444
- return new Error(`request failed with status ${response.status}`);
1469
+ return new MicroVMApiError(response.status, `request failed with status ${response.status}`, { retryAfterMs });
1445
1470
  }
1446
1471
  const STREAM_PREFIX_STDOUT = 0x01;
1447
1472
  const STREAM_PREFIX_STDERR = 0x02;
package/dist/types.d.ts CHANGED
@@ -29,6 +29,16 @@ export interface BuildImageResult {
29
29
  /** Pushed reference, e.g. "ghcr.io/my-org/my-image:v1.2.3". */
30
30
  pushed?: string;
31
31
  }
32
+ export interface CreateSnapshotOptions {
33
+ /**
34
+ * Whether the snapshot may pause a running sandbox. Defaults to true: a
35
+ * running sandbox is paused while it is copied (its disk writes wait;
36
+ * Docker pauses the container) and keeps running afterwards. Set false to
37
+ * have a sandbox that is not stopped refused with HTTP 409 instead, for a
38
+ * workload that must never stall. A stopped sandbox needs no pause.
39
+ */
40
+ allowPause?: boolean;
41
+ }
32
42
  export interface RegisterSnapshotOptions {
33
43
  /** Human-readable identifier other callers use in `CreateOptions.snapshot`. */
34
44
  name: string;
@@ -135,6 +145,19 @@ export interface GPUOptions {
135
145
  }
136
146
  import type { Image } from "./Image.js";
137
147
  export interface CreateOptions {
148
+ /**
149
+ * Sent as the create's `Idempotency-Key` header, never in the body. A create
150
+ * sent again with the same key and the same options returns the sandbox the
151
+ * first one made instead of making a second; the same key with different
152
+ * options fails with a 422 `MicroVMApiError` whose `code` is
153
+ * `"idempotency_key_reused"`. Keys are scoped to the caller's account.
154
+ *
155
+ * Leave it unset and the SDK makes a random key for each `create` call and
156
+ * reuses it on that call's automatic retries, so a retry after a timeout
157
+ * cannot make a second sandbox. Set your own to make your own retries of
158
+ * the whole call safe too, such as after your process restarts.
159
+ */
160
+ idempotencyKey?: string;
138
161
  /**
139
162
  * Optional name, unique among the caller's sandboxes (another account may
140
163
  * use the same name). A repeat create with a name the caller already holds
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aerol-ai/aerolvm-sdk",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",