@machine0/sdk 0.0.0-stage → 0.1.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/README.md +117 -2
- package/dist/client.d.ts +76 -0
- package/dist/client.js +132 -0
- package/dist/errors.d.ts +33 -0
- package/dist/errors.js +64 -0
- package/dist/types.d.ts +198 -0
- package/dist/types.js +119 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- package/package.json +47 -4
package/README.md
CHANGED
|
@@ -1,3 +1,118 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @machine0/sdk
|
|
2
2
|
|
|
3
|
-
|
|
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
|
package/dist/client.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
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" | "ORDERED";
|
|
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
|
+
* Standing-order deadline (`machine0 order`): set while the machine is an
|
|
40
|
+
* ORDERED (or expired) order, cleared once the order fills. Null for
|
|
41
|
+
* machines created directly.
|
|
42
|
+
*/
|
|
43
|
+
orderExpiresAt: string | null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Machine as returned by `machines.create`. The create path responds from
|
|
47
|
+
* the freshly-inserted row before the image join exists, so `image` and
|
|
48
|
+
* `distribution` are absent — fetch the machine (`machines.get`) if you need
|
|
49
|
+
* them immediately after creation.
|
|
50
|
+
*/
|
|
51
|
+
export type MachineCreated = Omit<Machine, "image" | "distribution">;
|
|
52
|
+
/** Machine as returned by `machines.get`, `machines.getByName` and `machines.update`. */
|
|
53
|
+
export interface MachineDetail {
|
|
54
|
+
id: string;
|
|
55
|
+
name: string;
|
|
56
|
+
url: string;
|
|
57
|
+
status: Open<MachineStatus>;
|
|
58
|
+
ip: string | null;
|
|
59
|
+
size: Open<MachineSize>;
|
|
60
|
+
vcpu: number | null;
|
|
61
|
+
ram: number | null;
|
|
62
|
+
disk: number | null;
|
|
63
|
+
region: Open<MachineRegion>;
|
|
64
|
+
provider: string;
|
|
65
|
+
createdAt: string;
|
|
66
|
+
firstStartedAt: string | null;
|
|
67
|
+
image: string | null;
|
|
68
|
+
imageVersion: number | null;
|
|
69
|
+
defaultSSHUsername: string | null;
|
|
70
|
+
sshClientInitialCommand: string | null;
|
|
71
|
+
metricsEnabled: boolean;
|
|
72
|
+
distribution: Open<Distribution>;
|
|
73
|
+
/** SSH key the machine was created with (null if since destroyed). */
|
|
74
|
+
key: {
|
|
75
|
+
id: string;
|
|
76
|
+
name: string;
|
|
77
|
+
type: Open<SshKeyType>;
|
|
78
|
+
fileName: string;
|
|
79
|
+
publicKey: string;
|
|
80
|
+
} | null;
|
|
81
|
+
ageMinutes: number | null;
|
|
82
|
+
totalCost: number | null;
|
|
83
|
+
pricePerHour: number | null;
|
|
84
|
+
mountPaths: string[];
|
|
85
|
+
statusExplanation: string | null;
|
|
86
|
+
/** See {@link Machine.orderExpiresAt}. */
|
|
87
|
+
orderExpiresAt: string | null;
|
|
88
|
+
}
|
|
89
|
+
export interface DiskMount {
|
|
90
|
+
diskName: string;
|
|
91
|
+
path: string;
|
|
92
|
+
}
|
|
93
|
+
export interface MachineCreateInput {
|
|
94
|
+
name: string;
|
|
95
|
+
size: MachineSize;
|
|
96
|
+
/** `"us"` is a legacy alias for `"us-east"` accepted by the API. */
|
|
97
|
+
region: MachineRegion | "us";
|
|
98
|
+
imageName: string;
|
|
99
|
+
sshKeyId?: string;
|
|
100
|
+
imageVersion?: number;
|
|
101
|
+
profileName?: string;
|
|
102
|
+
mounts?: DiskMount[];
|
|
103
|
+
metricsEnabled?: boolean;
|
|
104
|
+
}
|
|
105
|
+
export interface MachineUpdateInput {
|
|
106
|
+
id: string;
|
|
107
|
+
name?: string;
|
|
108
|
+
defaultSSHUsername?: string;
|
|
109
|
+
sshClientInitialCommand?: string;
|
|
110
|
+
metricsEnabled?: boolean;
|
|
111
|
+
}
|
|
112
|
+
/** Returned by machine lifecycle mutations (start/stop/reboot/suspend/destroy). */
|
|
113
|
+
export interface ActionResult {
|
|
114
|
+
success: boolean;
|
|
115
|
+
}
|
|
116
|
+
export interface MachineMetricsPoint {
|
|
117
|
+
/** Bucket timestamp (ISO-8601). */
|
|
118
|
+
t: string;
|
|
119
|
+
cpuPct: number | null;
|
|
120
|
+
memPct: number | null;
|
|
121
|
+
rxBps: number | null;
|
|
122
|
+
txBps: number | null;
|
|
123
|
+
load1: number | null;
|
|
124
|
+
load5: number | null;
|
|
125
|
+
load15: number | null;
|
|
126
|
+
diskPct: number | null;
|
|
127
|
+
diskReadBps: number | null;
|
|
128
|
+
diskWriteBps: number | null;
|
|
129
|
+
gpuPct: number | null;
|
|
130
|
+
}
|
|
131
|
+
export interface MachineMetrics {
|
|
132
|
+
points: MachineMetricsPoint[];
|
|
133
|
+
}
|
|
134
|
+
/** Image as returned by `images.list`. */
|
|
135
|
+
export interface Image {
|
|
136
|
+
id: string;
|
|
137
|
+
name: string;
|
|
138
|
+
description: string | null;
|
|
139
|
+
distribution: Open<Distribution>;
|
|
140
|
+
type: Open<ImageType>;
|
|
141
|
+
status: Open<ImageStatus>;
|
|
142
|
+
provider: string;
|
|
143
|
+
sizeGb: number | null;
|
|
144
|
+
minDiskGb: number | null;
|
|
145
|
+
regions: string[];
|
|
146
|
+
gpuOnly: boolean;
|
|
147
|
+
createdAt: string;
|
|
148
|
+
updatedAt: string;
|
|
149
|
+
ageMinutes: number;
|
|
150
|
+
totalCost: number | null;
|
|
151
|
+
pricePerHour: number | null;
|
|
152
|
+
}
|
|
153
|
+
/** Image as returned by `images.get`. */
|
|
154
|
+
export interface ImageDetail extends Image {
|
|
155
|
+
versionSnapshotStorageCost: number | null;
|
|
156
|
+
}
|
|
157
|
+
/** SSH key as returned by `sshKeys.list`. */
|
|
158
|
+
export interface SshKey {
|
|
159
|
+
id: string;
|
|
160
|
+
name: string;
|
|
161
|
+
type: Open<SshKeyType>;
|
|
162
|
+
publicKey: string;
|
|
163
|
+
isDefault: boolean;
|
|
164
|
+
createdAt: string;
|
|
165
|
+
}
|
|
166
|
+
/** SSH key as returned by `sshKeys.get` and `sshKeys.update` (adds the key's file name). */
|
|
167
|
+
export interface SshKeyWithFile extends SshKey {
|
|
168
|
+
fileName: string;
|
|
169
|
+
}
|
|
170
|
+
export type SshKeyCreateInput = {
|
|
171
|
+
type: "PUBLIC";
|
|
172
|
+
name: string;
|
|
173
|
+
fileName: string;
|
|
174
|
+
publicKey: string;
|
|
175
|
+
isDefault?: boolean;
|
|
176
|
+
} | {
|
|
177
|
+
type: "MANAGED";
|
|
178
|
+
name: string;
|
|
179
|
+
isDefault?: boolean;
|
|
180
|
+
};
|
|
181
|
+
/** Result of `sshKeys.create` for PUBLIC keys. */
|
|
182
|
+
export interface SshKeyCreated extends SshKeyWithFile {
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Result of `sshKeys.create` for MANAGED keys: includes the plaintext
|
|
186
|
+
* private key — store it. This SDK never requests the private key again
|
|
187
|
+
* (the platform can re-issue managed keys via the CLI's `keys get --download`).
|
|
188
|
+
*/
|
|
189
|
+
export interface SshKeyCreatedManaged extends SshKeyWithFile {
|
|
190
|
+
privateKey: string;
|
|
191
|
+
}
|
|
192
|
+
export declare const MACHINE_KEYS: readonly ["id", "name", "url", "status", "ip", "size", "region", "provider", "createdAt", "firstStartedAt", "destroyedAt", "image", "distribution", "ageMinutes", "totalCost", "pricePerHour", "metricsEnabled", "orderExpiresAt"];
|
|
193
|
+
export declare const MACHINE_CREATED_KEYS: readonly ["id", "name", "url", "status", "ip", "size", "region", "provider", "createdAt", "firstStartedAt", "destroyedAt", "ageMinutes", "totalCost", "pricePerHour", "metricsEnabled", "orderExpiresAt"];
|
|
194
|
+
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", "orderExpiresAt"];
|
|
195
|
+
export declare const IMAGE_KEYS: readonly ["id", "name", "description", "distribution", "type", "status", "provider", "sizeGb", "minDiskGb", "regions", "gpuOnly", "createdAt", "updatedAt", "ageMinutes", "totalCost", "pricePerHour"];
|
|
196
|
+
export declare const IMAGE_DETAIL_KEYS: readonly ["id", "name", "description", "distribution", "type", "status", "provider", "sizeGb", "minDiskGb", "regions", "gpuOnly", "createdAt", "updatedAt", "ageMinutes", "totalCost", "pricePerHour", "versionSnapshotStorageCost"];
|
|
197
|
+
export declare const SSH_KEY_KEYS: readonly ["id", "name", "type", "publicKey", "isDefault", "createdAt"];
|
|
198
|
+
export declare const SSH_KEY_WITH_FILE_KEYS: readonly ["id", "name", "type", "publicKey", "isDefault", "createdAt", "fileName"];
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
"orderExpiresAt",
|
|
39
|
+
];
|
|
40
|
+
export const MACHINE_CREATED_KEYS = [
|
|
41
|
+
"id",
|
|
42
|
+
"name",
|
|
43
|
+
"url",
|
|
44
|
+
"status",
|
|
45
|
+
"ip",
|
|
46
|
+
"size",
|
|
47
|
+
"region",
|
|
48
|
+
"provider",
|
|
49
|
+
"createdAt",
|
|
50
|
+
"firstStartedAt",
|
|
51
|
+
"destroyedAt",
|
|
52
|
+
"ageMinutes",
|
|
53
|
+
"totalCost",
|
|
54
|
+
"pricePerHour",
|
|
55
|
+
"metricsEnabled",
|
|
56
|
+
"orderExpiresAt",
|
|
57
|
+
];
|
|
58
|
+
export const MACHINE_DETAIL_KEYS = [
|
|
59
|
+
"id",
|
|
60
|
+
"name",
|
|
61
|
+
"url",
|
|
62
|
+
"status",
|
|
63
|
+
"ip",
|
|
64
|
+
"size",
|
|
65
|
+
"vcpu",
|
|
66
|
+
"ram",
|
|
67
|
+
"disk",
|
|
68
|
+
"region",
|
|
69
|
+
"provider",
|
|
70
|
+
"createdAt",
|
|
71
|
+
"firstStartedAt",
|
|
72
|
+
"image",
|
|
73
|
+
"imageVersion",
|
|
74
|
+
"defaultSSHUsername",
|
|
75
|
+
"sshClientInitialCommand",
|
|
76
|
+
"metricsEnabled",
|
|
77
|
+
"distribution",
|
|
78
|
+
"key",
|
|
79
|
+
"ageMinutes",
|
|
80
|
+
"totalCost",
|
|
81
|
+
"pricePerHour",
|
|
82
|
+
"mountPaths",
|
|
83
|
+
"statusExplanation",
|
|
84
|
+
"orderExpiresAt",
|
|
85
|
+
];
|
|
86
|
+
export const IMAGE_KEYS = [
|
|
87
|
+
"id",
|
|
88
|
+
"name",
|
|
89
|
+
"description",
|
|
90
|
+
"distribution",
|
|
91
|
+
"type",
|
|
92
|
+
"status",
|
|
93
|
+
"provider",
|
|
94
|
+
"sizeGb",
|
|
95
|
+
"minDiskGb",
|
|
96
|
+
"regions",
|
|
97
|
+
"gpuOnly",
|
|
98
|
+
"createdAt",
|
|
99
|
+
"updatedAt",
|
|
100
|
+
"ageMinutes",
|
|
101
|
+
"totalCost",
|
|
102
|
+
"pricePerHour",
|
|
103
|
+
];
|
|
104
|
+
export const IMAGE_DETAIL_KEYS = [
|
|
105
|
+
...IMAGE_KEYS,
|
|
106
|
+
"versionSnapshotStorageCost",
|
|
107
|
+
];
|
|
108
|
+
export const SSH_KEY_KEYS = [
|
|
109
|
+
"id",
|
|
110
|
+
"name",
|
|
111
|
+
"type",
|
|
112
|
+
"publicKey",
|
|
113
|
+
"isDefault",
|
|
114
|
+
"createdAt",
|
|
115
|
+
];
|
|
116
|
+
export const SSH_KEY_WITH_FILE_KEYS = [
|
|
117
|
+
...SSH_KEY_KEYS,
|
|
118
|
+
"fileName",
|
|
119
|
+
];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SDK_VERSION = "0.1.1";
|
package/dist/version.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@machine0/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
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
|
+
}
|