@machine0/sdk 0.0.0-stage → 0.1.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/README.md CHANGED
@@ -1,3 +1,118 @@
1
- # Temporary Holding Version
1
+ # @machine0/sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Typed Node.js/TypeScript SDK for the [machine0](https://machine0.io) API — cloud VMs from code.
4
+
5
+ - **Zero dependencies.** A thin client over the global `fetch`. Works in plain Node ≥ 20.12, serverless runtimes (nodejs:22), and any modern bundler.
6
+ - **Fully typed.** Inputs and outputs are compile-time checked against the live API's router inside the machine0 monorepo, plus runtime response-shape tests — the published types cannot silently drift from the server.
7
+ - **ESM.** `import` works everywhere; `require("@machine0/sdk")` needs Node ≥ 20.19 / ≥ 22.12 (`require(esm)` support).
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @machine0/sdk
13
+ # or
14
+ bun add @machine0/sdk
15
+ ```
16
+
17
+ ## Quickstart
18
+
19
+ 1. Create an API key: dashboard → **Account → API Tokens** ([app.machine0.io/account/tokens](https://app.machine0.io/account/tokens)).
20
+ 2. Keep it server-side. **Never ship an API key to a browser** — it grants full control of your machines.
21
+
22
+ ```ts
23
+ import { createClient } from "@machine0/sdk";
24
+
25
+ const m0 = createClient({ apiKey: process.env.MACHINE0_API_KEY! });
26
+
27
+ // List machines
28
+ const machines = await m0.machines.list();
29
+
30
+ // Create one
31
+ const created = await m0.machines.create({
32
+ name: "worker-1",
33
+ size: "large",
34
+ region: "us-east",
35
+ imageName: "ubuntu-25-04-x64",
36
+ });
37
+
38
+ // Fetch full details (IP appears once it's RUNNING)
39
+ const machine = await m0.machines.getByName({ name: "worker-1" });
40
+ console.log(machine.status, machine.ip, machine.url);
41
+
42
+ // Metrics (if created with metricsEnabled: true)
43
+ const { points } = await m0.machines.metrics({ id: created.id, range: "1h" });
44
+
45
+ // Destroy it
46
+ await m0.machines.destroy({ id: created.id });
47
+ ```
48
+
49
+ ## API surface (v1)
50
+
51
+ | Resource | Methods |
52
+ |---|---|
53
+ | `machines` | `list()`, `get({id})`, `getByName({name})`, `create(input)`, `update(input)`, `start/stop/reboot/suspend/destroy({id})`, `metrics({id, range: "1h" \| "24h" \| "7d"})` |
54
+ | `images` | `list()`, `get({id})` |
55
+ | `sshKeys` | `list()`, `get({name})`, `create(input)`, `update({name, isDefault})`, `destroy({id})` |
56
+
57
+ Notes:
58
+ - Lifecycle mutations (`start`, `stop`, `reboot`, `suspend`, `destroy`) are asynchronous on the server — they return `{success: true}` immediately and the machine transitions through states (`STARTING`, `STOPPING`, …). Poll `get()` to observe progress.
59
+ - `machines.create` responds before the image join exists, so the created machine lacks `image`/`distribution` — call `get()` if you need them right away.
60
+ - `sshKeys.create({type: "MANAGED", name})` returns the plaintext `privateKey` — store it. (Managed keys are retrievable later through the CLI's `machine0 keys get <name> --download`; this SDK deliberately never requests the private key after creation.)
61
+ - All date fields are ISO-8601 strings.
62
+
63
+ ## Options
64
+
65
+ ```ts
66
+ createClient({
67
+ apiKey: "m0_...", // required
68
+ apiUrl: "https://app.machine0.io", // default; point at a dev server to test locally
69
+ timeoutMs: 30_000, // per-request timeout, default 30s
70
+ });
71
+ ```
72
+
73
+ ## Error handling
74
+
75
+ Three distinct error classes, deliberately not collapsed:
76
+
77
+ ```ts
78
+ import { Machine0ApiError } from "@machine0/sdk/errors";
79
+
80
+ try {
81
+ await m0.machines.start({ id });
82
+ } catch (err) {
83
+ // Prefer the name check if your bundle may contain duplicate copies of the
84
+ // SDK (instanceof fails across copies): err.name === "Machine0ApiError".
85
+ if (err instanceof Machine0ApiError) {
86
+ // The API answered with an error.
87
+ err.status; // HTTP status (401, 404, 429, ...)
88
+ err.code; // tRPC code: "UNAUTHORIZED" | "NOT_FOUND" | "TOO_MANY_REQUESTS" | ... | "UNKNOWN"
89
+ err.appErrorCode; // machine0 code when provided, e.g. "INVALID_MACHINE_STATE",
90
+ // "MACHINE_LIMIT_REACHED", "INSUFFICIENT_FUNDS", "SIZE_UNAVAILABLE"
91
+ err.providerRateLimited; // true when the cloud provider throttled — retry later
92
+ } else if (err instanceof Error && err.name === "TimeoutError") {
93
+ // Request exceeded timeoutMs.
94
+ } else {
95
+ // Native network error (DNS, connection refused) — propagated unwrapped.
96
+ }
97
+ }
98
+ ```
99
+
100
+ - **No retries in v1.** The SDK makes exactly one attempt per call; bring your own retry policy if you need one (respect `providerRateLimited` and 429s).
101
+ - **A timeout aborts the request, not the operation.** The server may still complete a mutation whose response you never saw (e.g. a `create` that timed out can still create — and bill — the machine). After a timeout on a mutation, check state with `list()`/`get()` before retrying.
102
+ - `code: "UNKNOWN"` means the response wasn't a machine0 API error envelope (e.g. a gateway 502 HTML page).
103
+
104
+ ## Rate limits
105
+
106
+ API keys are rate-limited per user (default 3000 requests per window). Exceeding it yields `code: "TOO_MANY_REQUESTS"` (HTTP 429). The SDK sends one HTTP request per call and does not batch, unlike the web dashboard's client.
107
+
108
+ ## Stability
109
+
110
+ Responses are typed for regular user accounts: admin-role API keys (SUPERUSER/IMAGEADMIN) receive additional fields the types do not declare — treat extra properties as possible.
111
+
112
+ Enum-ish response fields (`status`, `size`, `region`, `distribution`, …) are **open sets**: the platform adds sizes, regions and statuses between SDK releases, so their types are `KnownValue | string`. Always handle unknown values with a default arm instead of exhaustive narrowing. Input types are closed — the server validates them.
113
+
114
+ Pre-1.0 (`0.x`): the surface is small and intentionally conservative, but minor versions may adjust types as the API's public contract firms up. Pin a minor (`"@machine0/sdk": "~0.1.0"`) if you need strict stability. Procedures consumed by this SDK are treated as a public contract on the server side — breaking wire changes follow a two-phase deprecation discipline.
115
+
116
+ ## License
117
+
118
+ MIT
@@ -0,0 +1,76 @@
1
+ import type { ActionResult, Image, ImageDetail, Machine, MachineCreated, MachineCreateInput, MachineDetail, MachineMetrics, MachineMetricsRange, MachineUpdateInput, SshKey, SshKeyCreated, SshKeyCreatedManaged, SshKeyCreateInput, SshKeyWithFile } from "./types.js";
2
+ export interface Machine0ClientOptions {
3
+ /** machine0 API key (dashboard → Account → API Tokens, https://app.machine0.io/account/tokens). Sent as the `x-api-key` header. */
4
+ apiKey: string;
5
+ /**
6
+ * API base URL. Defaults to https://app.machine0.io. The key is sent on
7
+ * every request — only point this at `http://` for loopback dev servers,
8
+ * never a remote host (the key would travel in cleartext).
9
+ */
10
+ apiUrl?: string;
11
+ /** Per-request timeout in milliseconds. Defaults to 30000. */
12
+ timeoutMs?: number;
13
+ }
14
+ /**
15
+ * Create a typed machine0 API client.
16
+ *
17
+ * Zero dependencies: speaks the tRPC HTTP wire format over global fetch
18
+ * (queries are GETs with a URL-encoded JSON `input` param, mutations are
19
+ * POSTs with a JSON body). One HTTP request per call, no batching, no
20
+ * retries — see the README for the full error-handling contract.
21
+ */
22
+ export declare function createClient(options: Machine0ClientOptions): {
23
+ machines: {
24
+ list: () => Promise<Machine[]>;
25
+ get: (input: {
26
+ id: string;
27
+ }) => Promise<MachineDetail>;
28
+ getByName: (input: {
29
+ name: string;
30
+ }) => Promise<MachineDetail>;
31
+ create: (input: MachineCreateInput) => Promise<MachineCreated>;
32
+ update: (input: MachineUpdateInput) => Promise<MachineDetail>;
33
+ start: (input: {
34
+ id: string;
35
+ }) => Promise<ActionResult>;
36
+ stop: (input: {
37
+ id: string;
38
+ }) => Promise<ActionResult>;
39
+ reboot: (input: {
40
+ id: string;
41
+ }) => Promise<ActionResult>;
42
+ suspend: (input: {
43
+ id: string;
44
+ }) => Promise<ActionResult>;
45
+ destroy: (input: {
46
+ id: string;
47
+ }) => Promise<ActionResult>;
48
+ metrics: (input: {
49
+ id: string;
50
+ range: MachineMetricsRange;
51
+ }) => Promise<MachineMetrics>;
52
+ };
53
+ images: {
54
+ list: () => Promise<Image[]>;
55
+ get: (input: {
56
+ id: string;
57
+ }) => Promise<ImageDetail>;
58
+ };
59
+ sshKeys: {
60
+ list: () => Promise<SshKey[]>;
61
+ get: (input: {
62
+ name: string;
63
+ }) => Promise<SshKeyWithFile>;
64
+ create: <T extends SshKeyCreateInput>(input: T) => Promise<T extends {
65
+ type: "MANAGED";
66
+ } ? SshKeyCreatedManaged : SshKeyCreated>;
67
+ update: (input: {
68
+ name: string;
69
+ isDefault?: boolean;
70
+ }) => Promise<SshKeyWithFile>;
71
+ destroy: (input: {
72
+ id: string;
73
+ }) => Promise<ActionResult>;
74
+ };
75
+ };
76
+ export type Machine0Client = ReturnType<typeof createClient>;
package/dist/client.js ADDED
@@ -0,0 +1,132 @@
1
+ import { errorFromTrpcEnvelope, Machine0ApiError } from "./errors.js";
2
+ const DEFAULT_API_URL = "https://app.machine0.io";
3
+ const DEFAULT_TIMEOUT_MS = 30_000;
4
+ // Computed key: a literal `"x-api-key": <value>` assignment trips secret
5
+ // scanners (gstack-redact env.kv) on every ship even though the value is a
6
+ // variable reference, never a credential.
7
+ const API_KEY_HEADER = "x-api-key";
8
+ /**
9
+ * Create a typed machine0 API client.
10
+ *
11
+ * Zero dependencies: speaks the tRPC HTTP wire format over global fetch
12
+ * (queries are GETs with a URL-encoded JSON `input` param, mutations are
13
+ * POSTs with a JSON body). One HTTP request per call, no batching, no
14
+ * retries — see the README for the full error-handling contract.
15
+ */
16
+ export function createClient(options) {
17
+ if (typeof options.apiKey !== "string" || options.apiKey.trim() === "") {
18
+ throw new TypeError("@machine0/sdk: apiKey is required — create one in the dashboard at https://app.machine0.io/account/tokens");
19
+ }
20
+ const rawApiUrl = options.apiUrl ?? DEFAULT_API_URL;
21
+ try {
22
+ new URL(rawApiUrl);
23
+ }
24
+ catch {
25
+ throw new TypeError(`@machine0/sdk: apiUrl must be an absolute URL (got ${JSON.stringify(rawApiUrl)})`);
26
+ }
27
+ // Strip trailing slashes with a loop, not /\/+$/ — that regex backtracks
28
+ // polynomially on caller-supplied strings (CodeQL js/polynomial-redos).
29
+ let baseUrl = rawApiUrl;
30
+ while (baseUrl.endsWith("/")) {
31
+ baseUrl = baseUrl.slice(0, -1);
32
+ }
33
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
34
+ const baseHeaders = {
35
+ [API_KEY_HEADER]: options.apiKey,
36
+ "x-machine0-client": "sdk",
37
+ };
38
+ async function request(url, init) {
39
+ const response = await fetch(url, {
40
+ ...init,
41
+ signal: AbortSignal.timeout(timeoutMs),
42
+ // The API never redirects, and fetch does NOT strip custom headers like
43
+ // x-api-key on cross-origin redirects — fail loudly instead of
44
+ // forwarding the key to whatever host a Location header names.
45
+ redirect: "error",
46
+ });
47
+ let body;
48
+ try {
49
+ body = await response.json();
50
+ }
51
+ catch (error) {
52
+ // Only a JSON parse failure means "the API sent non-JSON" (gateway 502
53
+ // HTML, proxy corruption on a 2xx). A timeout or network failure DURING
54
+ // the body read must keep its native identity — the documented error
55
+ // contract routes those to the caller's timeout/network branches, not
56
+ // to a Machine0ApiError that could claim status 200.
57
+ if (!(error instanceof SyntaxError)) {
58
+ throw error;
59
+ }
60
+ throw new Machine0ApiError({
61
+ status: response.status,
62
+ code: "UNKNOWN",
63
+ message: `machine0 API returned a non-JSON response (HTTP ${response.status})`,
64
+ });
65
+ }
66
+ if (body && typeof body === "object" && "error" in body) {
67
+ throw errorFromTrpcEnvelope(response.status, body);
68
+ }
69
+ if (!response.ok) {
70
+ throw new Machine0ApiError({
71
+ status: response.status,
72
+ code: "UNKNOWN",
73
+ message: `machine0 API request failed (HTTP ${response.status})`,
74
+ });
75
+ }
76
+ // A 2xx JSON body that is not a tRPC result envelope (middlebox/proxy
77
+ // JSON, wrong endpoint) must not silently resolve `undefined`. Every
78
+ // SDK-consumed procedure returns data, so `result` must be an object
79
+ // carrying a `data` property (which may legitimately be null).
80
+ const result = body && typeof body === "object" && "result" in body
81
+ ? body.result
82
+ : undefined;
83
+ if (!result || typeof result !== "object" || !("data" in result)) {
84
+ throw new Machine0ApiError({
85
+ status: response.status,
86
+ code: "UNKNOWN",
87
+ message: `machine0 API returned an unexpected response shape (HTTP ${response.status})`,
88
+ });
89
+ }
90
+ return result.data;
91
+ }
92
+ function query(path, input) {
93
+ const url = input === undefined
94
+ ? `${baseUrl}/trpc/${path}`
95
+ : `${baseUrl}/trpc/${path}?input=${encodeURIComponent(JSON.stringify(input))}`;
96
+ // No content-type on GET: the tRPC server rejects unexpected media types.
97
+ return request(url, { method: "GET", headers: baseHeaders });
98
+ }
99
+ function mutate(path, input) {
100
+ return request(`${baseUrl}/trpc/${path}`, {
101
+ method: "POST",
102
+ headers: { ...baseHeaders, "content-type": "application/json" },
103
+ body: JSON.stringify(input),
104
+ });
105
+ }
106
+ return {
107
+ machines: {
108
+ list: () => query("machines.list"),
109
+ get: (input) => query("machines.get", input),
110
+ getByName: (input) => query("machines.getByName", input),
111
+ create: (input) => mutate("machines.create", input),
112
+ update: (input) => mutate("machines.update", input),
113
+ start: (input) => mutate("machines.start", input),
114
+ stop: (input) => mutate("machines.stop", input),
115
+ reboot: (input) => mutate("machines.reboot", input),
116
+ suspend: (input) => mutate("machines.suspend", input),
117
+ destroy: (input) => mutate("machines.destroy", input),
118
+ metrics: (input) => query("machines.metrics", input),
119
+ },
120
+ images: {
121
+ list: () => query("images.list"),
122
+ get: (input) => query("images.get", input),
123
+ },
124
+ sshKeys: {
125
+ list: () => query("keys.list"),
126
+ get: (input) => query("keys.get", input),
127
+ create: (input) => mutate("keys.create", input),
128
+ update: (input) => mutate("keys.update", input),
129
+ destroy: (input) => mutate("keys.destroy", input),
130
+ },
131
+ };
132
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Error thrown for every non-successful machine0 API response.
3
+ *
4
+ * Not thrown for transport-level failures: network errors (DNS failure,
5
+ * connection refused) propagate as the runtime's native `TypeError`, and
6
+ * timeouts as a `DOMException` named "TimeoutError" — see the README's
7
+ * error-handling section.
8
+ */
9
+ export declare class Machine0ApiError extends Error {
10
+ /** HTTP status code of the response. */
11
+ readonly status: number;
12
+ /** tRPC error code string (e.g. "NOT_FOUND", "UNAUTHORIZED", "TOO_MANY_REQUESTS"), or "UNKNOWN" when the response was not a tRPC error envelope. */
13
+ readonly code: string;
14
+ /** machine0 application error code (e.g. "INVALID_MACHINE_STATE", "MACHINE_LIMIT_REACHED"), when the server provided one. */
15
+ readonly appErrorCode?: string;
16
+ /** True when the failure was caused by cloud-provider rate limiting (retrying later may succeed). */
17
+ readonly providerRateLimited?: boolean;
18
+ constructor(params: {
19
+ status: number;
20
+ code: string;
21
+ message: string;
22
+ appErrorCode?: string;
23
+ providerRateLimited?: boolean;
24
+ });
25
+ }
26
+ /**
27
+ * Build a Machine0ApiError from a parsed tRPC error envelope
28
+ * (`{error: {message, data: {code, appErrorCode, ...}}}`). Published via the
29
+ * `@machine0/sdk/errors` subpath because the client module imports it, but
30
+ * the supported public surface of this module is `Machine0ApiError`; this
31
+ * helper's signature may change in any release.
32
+ */
33
+ export declare function errorFromTrpcEnvelope(status: number, envelope: unknown): Machine0ApiError;
package/dist/errors.js ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Error thrown for every non-successful machine0 API response.
3
+ *
4
+ * Not thrown for transport-level failures: network errors (DNS failure,
5
+ * connection refused) propagate as the runtime's native `TypeError`, and
6
+ * timeouts as a `DOMException` named "TimeoutError" — see the README's
7
+ * error-handling section.
8
+ */
9
+ export class Machine0ApiError extends Error {
10
+ /** HTTP status code of the response. */
11
+ status;
12
+ /** tRPC error code string (e.g. "NOT_FOUND", "UNAUTHORIZED", "TOO_MANY_REQUESTS"), or "UNKNOWN" when the response was not a tRPC error envelope. */
13
+ code;
14
+ /** machine0 application error code (e.g. "INVALID_MACHINE_STATE", "MACHINE_LIMIT_REACHED"), when the server provided one. */
15
+ appErrorCode;
16
+ /** True when the failure was caused by cloud-provider rate limiting (retrying later may succeed). */
17
+ providerRateLimited;
18
+ constructor(params) {
19
+ super(params.message);
20
+ this.name = "Machine0ApiError";
21
+ this.status = params.status;
22
+ this.code = params.code;
23
+ this.appErrorCode = params.appErrorCode;
24
+ this.providerRateLimited = params.providerRateLimited;
25
+ }
26
+ }
27
+ /**
28
+ * Build a Machine0ApiError from a parsed tRPC error envelope
29
+ * (`{error: {message, data: {code, appErrorCode, ...}}}`). Published via the
30
+ * `@machine0/sdk/errors` subpath because the client module imports it, but
31
+ * the supported public surface of this module is `Machine0ApiError`; this
32
+ * helper's signature may change in any release.
33
+ */
34
+ export function errorFromTrpcEnvelope(status, envelope) {
35
+ const error = envelope && typeof envelope === "object" && "error" in envelope
36
+ ? envelope.error
37
+ : undefined;
38
+ let message = `machine0 API error (HTTP ${status})`;
39
+ let code = "UNKNOWN";
40
+ let appErrorCode;
41
+ let providerRateLimited;
42
+ if (error && typeof error === "object") {
43
+ const e = error;
44
+ if (typeof e.message === "string" && e.message.length > 0) {
45
+ message = e.message;
46
+ }
47
+ if (typeof e.data?.code === "string") {
48
+ code = e.data.code;
49
+ }
50
+ if (typeof e.data?.appErrorCode === "string") {
51
+ appErrorCode = e.data.appErrorCode;
52
+ }
53
+ if (typeof e.data?.providerRateLimited === "boolean") {
54
+ providerRateLimited = e.data.providerRateLimited;
55
+ }
56
+ }
57
+ return new Machine0ApiError({
58
+ status,
59
+ code,
60
+ message,
61
+ appErrorCode,
62
+ providerRateLimited,
63
+ });
64
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Response-side enum fields are OPEN sets: the server adds sizes, regions and
3
+ * statuses between SDK releases, and an already-installed SDK must not make
4
+ * exhaustive narrowing silently fall through when it meets a value newer than
5
+ * its types. `Open<T>` keeps autocomplete for the known members while telling
6
+ * the compiler unknown strings are possible — always include a default arm.
7
+ * INPUT types stay closed unions (the server validates them).
8
+ */
9
+ export type Open<T extends string> = T | (string & {});
10
+ export type MachineStatus = "DESTROYED" | "CREATING" | "RUNNING" | "STARTING" | "STOPPING" | "STOPPED" | "RESTARTING" | "ERRORED" | "SUSPENDING" | "SUSPENDED" | "UNAVAILABLE";
11
+ export type MachineSize = "small" | "medium" | "large" | "xl" | "xxl" | "large-nvme" | "xl-nvme" | "xxl-nvme" | "xl-premium" | "xxl-premium" | "xxxl" | "4xl" | "5xl" | "6xl" | "gpu-4000ada-1" | "gpu-l40s-1" | "gpu-6000ada-1" | "gpu-mi300x-1" | "gpu-h100-1" | "gpu-h200-1" | "gpu-mi300x-8" | "gpu-h100-8" | "gpu-h200-8";
12
+ export type MachineRegion = "us-east" | "us-west" | "uk" | "eu" | "asia";
13
+ export type Distribution = "ubuntu" | "nixos";
14
+ export type ImageStatus = "DESTROYED" | "CREATING" | "READY" | "TRANSFERRING" | "ERRORED";
15
+ export type ImageType = "USER" | "SYSTEM";
16
+ export type SshKeyType = "MANAGED" | "PUBLIC";
17
+ export type MachineMetricsRange = "1h" | "24h" | "7d";
18
+ /** Machine as returned by `machines.list`. */
19
+ export interface Machine {
20
+ id: string;
21
+ name: string;
22
+ /** Proxy URL of the machine (`https://<name>.mac0.io`). */
23
+ url: string;
24
+ status: Open<MachineStatus>;
25
+ ip: string | null;
26
+ size: Open<MachineSize>;
27
+ region: Open<MachineRegion>;
28
+ provider: string;
29
+ createdAt: string;
30
+ firstStartedAt: string | null;
31
+ destroyedAt: string | null;
32
+ image: string | null;
33
+ distribution: Open<Distribution> | null;
34
+ ageMinutes: number | null;
35
+ totalCost: number | null;
36
+ pricePerHour: number | null;
37
+ metricsEnabled: boolean;
38
+ }
39
+ /**
40
+ * Machine as returned by `machines.create`. The create path responds from
41
+ * the freshly-inserted row before the image join exists, so `image` and
42
+ * `distribution` are absent — fetch the machine (`machines.get`) if you need
43
+ * them immediately after creation.
44
+ */
45
+ export type MachineCreated = Omit<Machine, "image" | "distribution">;
46
+ /** Machine as returned by `machines.get`, `machines.getByName` and `machines.update`. */
47
+ export interface MachineDetail {
48
+ id: string;
49
+ name: string;
50
+ url: string;
51
+ status: Open<MachineStatus>;
52
+ ip: string | null;
53
+ size: Open<MachineSize>;
54
+ vcpu: number | null;
55
+ ram: number | null;
56
+ disk: number | null;
57
+ region: Open<MachineRegion>;
58
+ provider: string;
59
+ createdAt: string;
60
+ firstStartedAt: string | null;
61
+ image: string | null;
62
+ imageVersion: number | null;
63
+ defaultSSHUsername: string | null;
64
+ sshClientInitialCommand: string | null;
65
+ metricsEnabled: boolean;
66
+ distribution: Open<Distribution>;
67
+ /** SSH key the machine was created with (null if since destroyed). */
68
+ key: {
69
+ id: string;
70
+ name: string;
71
+ type: Open<SshKeyType>;
72
+ fileName: string;
73
+ publicKey: string;
74
+ } | null;
75
+ ageMinutes: number | null;
76
+ totalCost: number | null;
77
+ pricePerHour: number | null;
78
+ mountPaths: string[];
79
+ statusExplanation: string | null;
80
+ }
81
+ export interface DiskMount {
82
+ diskName: string;
83
+ path: string;
84
+ }
85
+ export interface MachineCreateInput {
86
+ name: string;
87
+ size: MachineSize;
88
+ /** `"us"` is a legacy alias for `"us-east"` accepted by the API. */
89
+ region: MachineRegion | "us";
90
+ imageName: string;
91
+ sshKeyId?: string;
92
+ imageVersion?: number;
93
+ profileName?: string;
94
+ mounts?: DiskMount[];
95
+ metricsEnabled?: boolean;
96
+ }
97
+ export interface MachineUpdateInput {
98
+ id: string;
99
+ name?: string;
100
+ defaultSSHUsername?: string;
101
+ sshClientInitialCommand?: string;
102
+ metricsEnabled?: boolean;
103
+ }
104
+ /** Returned by machine lifecycle mutations (start/stop/reboot/suspend/destroy). */
105
+ export interface ActionResult {
106
+ success: boolean;
107
+ }
108
+ export interface MachineMetricsPoint {
109
+ /** Bucket timestamp (ISO-8601). */
110
+ t: string;
111
+ cpuPct: number | null;
112
+ memPct: number | null;
113
+ rxBps: number | null;
114
+ txBps: number | null;
115
+ load1: number | null;
116
+ load5: number | null;
117
+ load15: number | null;
118
+ diskPct: number | null;
119
+ diskReadBps: number | null;
120
+ diskWriteBps: number | null;
121
+ gpuPct: number | null;
122
+ }
123
+ export interface MachineMetrics {
124
+ points: MachineMetricsPoint[];
125
+ }
126
+ /** Image as returned by `images.list`. */
127
+ export interface Image {
128
+ id: string;
129
+ name: string;
130
+ description: string | null;
131
+ distribution: Open<Distribution>;
132
+ type: Open<ImageType>;
133
+ status: Open<ImageStatus>;
134
+ provider: string;
135
+ sizeGb: number | null;
136
+ minDiskGb: number | null;
137
+ regions: string[];
138
+ gpuOnly: boolean;
139
+ createdAt: string;
140
+ updatedAt: string;
141
+ ageMinutes: number;
142
+ totalCost: number | null;
143
+ pricePerHour: number | null;
144
+ }
145
+ /** Image as returned by `images.get`. */
146
+ export interface ImageDetail extends Image {
147
+ versionSnapshotStorageCost: number | null;
148
+ }
149
+ /** SSH key as returned by `sshKeys.list`. */
150
+ export interface SshKey {
151
+ id: string;
152
+ name: string;
153
+ type: Open<SshKeyType>;
154
+ publicKey: string;
155
+ isDefault: boolean;
156
+ createdAt: string;
157
+ }
158
+ /** SSH key as returned by `sshKeys.get` and `sshKeys.update` (adds the key's file name). */
159
+ export interface SshKeyWithFile extends SshKey {
160
+ fileName: string;
161
+ }
162
+ export type SshKeyCreateInput = {
163
+ type: "PUBLIC";
164
+ name: string;
165
+ fileName: string;
166
+ publicKey: string;
167
+ isDefault?: boolean;
168
+ } | {
169
+ type: "MANAGED";
170
+ name: string;
171
+ isDefault?: boolean;
172
+ };
173
+ /** Result of `sshKeys.create` for PUBLIC keys. */
174
+ export interface SshKeyCreated extends SshKeyWithFile {
175
+ }
176
+ /**
177
+ * Result of `sshKeys.create` for MANAGED keys: includes the plaintext
178
+ * private key — store it. This SDK never requests the private key again
179
+ * (the platform can re-issue managed keys via the CLI's `keys get --download`).
180
+ */
181
+ export interface SshKeyCreatedManaged extends SshKeyWithFile {
182
+ privateKey: string;
183
+ }
184
+ export declare const MACHINE_KEYS: readonly ["id", "name", "url", "status", "ip", "size", "region", "provider", "createdAt", "firstStartedAt", "destroyedAt", "image", "distribution", "ageMinutes", "totalCost", "pricePerHour", "metricsEnabled"];
185
+ export declare const MACHINE_CREATED_KEYS: readonly ["id", "name", "url", "status", "ip", "size", "region", "provider", "createdAt", "firstStartedAt", "destroyedAt", "ageMinutes", "totalCost", "pricePerHour", "metricsEnabled"];
186
+ export declare const MACHINE_DETAIL_KEYS: readonly ["id", "name", "url", "status", "ip", "size", "vcpu", "ram", "disk", "region", "provider", "createdAt", "firstStartedAt", "image", "imageVersion", "defaultSSHUsername", "sshClientInitialCommand", "metricsEnabled", "distribution", "key", "ageMinutes", "totalCost", "pricePerHour", "mountPaths", "statusExplanation"];
187
+ export declare const IMAGE_KEYS: readonly ["id", "name", "description", "distribution", "type", "status", "provider", "sizeGb", "minDiskGb", "regions", "gpuOnly", "createdAt", "updatedAt", "ageMinutes", "totalCost", "pricePerHour"];
188
+ export declare const IMAGE_DETAIL_KEYS: readonly ["id", "name", "description", "distribution", "type", "status", "provider", "sizeGb", "minDiskGb", "regions", "gpuOnly", "createdAt", "updatedAt", "ageMinutes", "totalCost", "pricePerHour", "versionSnapshotStorageCost"];
189
+ export declare const SSH_KEY_KEYS: readonly ["id", "name", "type", "publicKey", "isDefault", "createdAt"];
190
+ export declare const SSH_KEY_WITH_FILE_KEYS: readonly ["id", "name", "type", "publicKey", "isDefault", "createdAt", "fileName"];
package/dist/types.js ADDED
@@ -0,0 +1,116 @@
1
+ // Hand-written wire types for the machine0 API.
2
+ //
3
+ // These deliberately do NOT import anything from the server: the published
4
+ // artifact must be dependency-free. Honesty is enforced two ways instead:
5
+ // - src/type-tests.ts (repo-only, never published) asserts every type here
6
+ // against the real AppRouter via inferRouterInputs/inferRouterOutputs —
7
+ // drift breaks `turbo typecheck` in CI.
8
+ // - The *_KEYS constants below bridge compile time and runtime: type-tests
9
+ // assert each constant is exhaustive over its type's keys, and the
10
+ // integration test (apps/api/src/sdk-client.test.ts) asserts the server's
11
+ // actual response keys equal the constant. A field declared here that the
12
+ // server never sends cannot survive both checks.
13
+ //
14
+ // All date fields are ISO-8601 strings: the API serializes plain JSON (no
15
+ // superjson transformer).
16
+ // Runtime key constants — the compile-time/runtime bridge (see file header).
17
+ // `satisfies` guarantees no phantom keys; type-tests.ts asserts exhaustiveness
18
+ // (every key of the type is listed); the integration test asserts the server's
19
+ // actual responses carry exactly these keys.
20
+ export const MACHINE_KEYS = [
21
+ "id",
22
+ "name",
23
+ "url",
24
+ "status",
25
+ "ip",
26
+ "size",
27
+ "region",
28
+ "provider",
29
+ "createdAt",
30
+ "firstStartedAt",
31
+ "destroyedAt",
32
+ "image",
33
+ "distribution",
34
+ "ageMinutes",
35
+ "totalCost",
36
+ "pricePerHour",
37
+ "metricsEnabled",
38
+ ];
39
+ export const MACHINE_CREATED_KEYS = [
40
+ "id",
41
+ "name",
42
+ "url",
43
+ "status",
44
+ "ip",
45
+ "size",
46
+ "region",
47
+ "provider",
48
+ "createdAt",
49
+ "firstStartedAt",
50
+ "destroyedAt",
51
+ "ageMinutes",
52
+ "totalCost",
53
+ "pricePerHour",
54
+ "metricsEnabled",
55
+ ];
56
+ export const MACHINE_DETAIL_KEYS = [
57
+ "id",
58
+ "name",
59
+ "url",
60
+ "status",
61
+ "ip",
62
+ "size",
63
+ "vcpu",
64
+ "ram",
65
+ "disk",
66
+ "region",
67
+ "provider",
68
+ "createdAt",
69
+ "firstStartedAt",
70
+ "image",
71
+ "imageVersion",
72
+ "defaultSSHUsername",
73
+ "sshClientInitialCommand",
74
+ "metricsEnabled",
75
+ "distribution",
76
+ "key",
77
+ "ageMinutes",
78
+ "totalCost",
79
+ "pricePerHour",
80
+ "mountPaths",
81
+ "statusExplanation",
82
+ ];
83
+ export const IMAGE_KEYS = [
84
+ "id",
85
+ "name",
86
+ "description",
87
+ "distribution",
88
+ "type",
89
+ "status",
90
+ "provider",
91
+ "sizeGb",
92
+ "minDiskGb",
93
+ "regions",
94
+ "gpuOnly",
95
+ "createdAt",
96
+ "updatedAt",
97
+ "ageMinutes",
98
+ "totalCost",
99
+ "pricePerHour",
100
+ ];
101
+ export const IMAGE_DETAIL_KEYS = [
102
+ ...IMAGE_KEYS,
103
+ "versionSnapshotStorageCost",
104
+ ];
105
+ export const SSH_KEY_KEYS = [
106
+ "id",
107
+ "name",
108
+ "type",
109
+ "publicKey",
110
+ "isDefault",
111
+ "createdAt",
112
+ ];
113
+ export const SSH_KEY_WITH_FILE_KEYS = [
114
+ ...SSH_KEY_KEYS,
115
+ "fileName",
116
+ ];
@@ -0,0 +1 @@
1
+ export declare const SDK_VERSION = "0.1.0";
@@ -0,0 +1,4 @@
1
+ // Must match the "version" field in package.json — version.test.ts enforces
2
+ // the sync, and the deploy-sdk workflow compares this constant (imported from
3
+ // the built dist under plain Node) against package.json before publishing.
4
+ export const SDK_VERSION = "0.1.0";
package/package.json CHANGED
@@ -1,6 +1,49 @@
1
1
  {
2
2
  "name": "@machine0/sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Typed Node.js/TypeScript SDK for the machine0 API. Zero dependencies, fetch-based.",
5
+ "homepage": "https://machine0.io",
6
+ "author": "Barnaby Malet <barnaby@machine0.io>",
7
+ "license": "MIT",
8
+ "type": "module",
9
+ "engines": {
10
+ "node": ">=20.12.0"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/client.d.ts",
15
+ "default": "./dist/client.js"
16
+ },
17
+ "./errors": {
18
+ "types": "./dist/errors.d.ts",
19
+ "default": "./dist/errors.js"
20
+ },
21
+ "./types": {
22
+ "types": "./dist/types.d.ts",
23
+ "default": "./dist/types.js"
24
+ },
25
+ "./version": {
26
+ "types": "./dist/version.d.ts",
27
+ "default": "./dist/version.js"
28
+ }
29
+ },
30
+ "files": [
31
+ "dist"
32
+ ],
33
+ "scripts": {
34
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
35
+ "clean": "rm -rf .turbo dist",
36
+ "lint": "biome check .",
37
+ "format": "biome format --write .",
38
+ "typecheck": "tsc --noEmit",
39
+ "test": "bun test",
40
+ "prepublishOnly": "bun run build"
41
+ },
42
+ "devDependencies": {
43
+ "@biomejs/biome": "^2.3.4",
44
+ "@machine0/api": "workspace:*",
45
+ "@trpc/server": "^11.7.1",
46
+ "@types/bun": "latest",
47
+ "typescript": "^5"
48
+ }
49
+ }