@folsom/fuse 0.0.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/LICENSE +21 -0
- package/README.md +124 -0
- package/dist/index.d.ts +366 -0
- package/dist/index.js +590 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andrew Nijmeh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# @folsom/fuse
|
|
2
|
+
|
|
3
|
+
**TypeScript SDK for [Fuse](https://github.com/folsomintel/fuse)** — the control
|
|
4
|
+
plane for agents that deploys and drives Firecracker microVMs over a REST API.
|
|
5
|
+
|
|
6
|
+
A typed, zero-dependency, ESM client that mirrors the
|
|
7
|
+
[Go SDK](https://github.com/folsomintel/fuse/tree/main/sdks/go) one-to-one. It uses
|
|
8
|
+
the platform `fetch`/streams, so it runs on Node 18+, Deno, Bun, edge runtimes,
|
|
9
|
+
and the browser.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @folsom/fuse
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires Node **18+** (for built-in `fetch` and web streams) or any runtime with
|
|
18
|
+
a standard `fetch`. The package is **ESM-only**.
|
|
19
|
+
|
|
20
|
+
TypeScript consumers need the web platform types in scope — either `"lib": ["DOM"]`
|
|
21
|
+
(browsers/bundlers) or `@types/node` (Node). Any project that uses `fetch` already
|
|
22
|
+
has one of these.
|
|
23
|
+
|
|
24
|
+
## Quickstart
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { FuseClient, isNotFound } from "@folsom/fuse";
|
|
28
|
+
|
|
29
|
+
const client = new FuseClient({
|
|
30
|
+
baseUrl: "https://fuse.example.com",
|
|
31
|
+
token: process.env.FUSE_TOKEN, // Authorization: Bearer <token>
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Provision a microVM (blocks until running or failed).
|
|
35
|
+
const env = await client.environments.create({
|
|
36
|
+
task_id: "task-1",
|
|
37
|
+
spec: { cpus: 1, ram_mb: 512, storage_gb: 10 },
|
|
38
|
+
});
|
|
39
|
+
console.log(env.id, env.state, env.url);
|
|
40
|
+
|
|
41
|
+
// Tail its lifecycle over SSE — the loop ends after a terminal state.
|
|
42
|
+
for await (const event of await client.environments.events(env.id)) {
|
|
43
|
+
console.log(event.state);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Tear it down.
|
|
47
|
+
await client.environments.drain(env.id);
|
|
48
|
+
await client.environments.destroy(env.id);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Client options
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
new FuseClient({
|
|
55
|
+
baseUrl, // required
|
|
56
|
+
token, // optional bearer token
|
|
57
|
+
fetch, // optional custom fetch
|
|
58
|
+
userAgent, // default: "fuse-ts/<version>" (ignored by browsers)
|
|
59
|
+
requestId: () => "...", // optional X-Request-ID generator (per request)
|
|
60
|
+
timeoutMs: 60_000, // optional default timeout (NOT applied to events())
|
|
61
|
+
headers: { "x-extra": "1" },
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Services
|
|
66
|
+
|
|
67
|
+
| Service | Methods |
|
|
68
|
+
| --------------------- | -------------------------------------------------------------------- |
|
|
69
|
+
| `client.environments` | `list`, `get`, `create`, `drain`, `rotateToken`, `destroy`, `events` |
|
|
70
|
+
| `client.snapshots` | `create`, `list`, `get`, `delete`, `restore` |
|
|
71
|
+
| `client.hosts` | `register`, `list`, `get`, `cordon`, `uncordon`, `deregister` |
|
|
72
|
+
| `client.apiKeys` | `create`, `list`, `revoke` |
|
|
73
|
+
|
|
74
|
+
Every method accepts a trailing `{ signal }` for cancellation, e.g.
|
|
75
|
+
`client.environments.list({}, { signal })`. Request/response field names are
|
|
76
|
+
snake_case, matching the API wire format and the Go SDK.
|
|
77
|
+
|
|
78
|
+
## Errors
|
|
79
|
+
|
|
80
|
+
Non-2xx responses throw a `FuseApiError` with `status`, `code`, `message`,
|
|
81
|
+
`details`, and `requestId`. Use the code helpers instead of comparing strings:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { isNotFound, isConflict, FuseApiError } from "@folsom/fuse";
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
await client.environments.get("missing");
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (isNotFound(err)) {
|
|
90
|
+
// handle 404
|
|
91
|
+
} else if (err instanceof FuseApiError) {
|
|
92
|
+
console.error(err.code, err.status, err.requestId);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Transport, configuration, and decoding failures throw a `FuseError` (with a
|
|
98
|
+
`cause`). Helpers: `isNotFound`, `isConflict`, `isUnauthorized`,
|
|
99
|
+
`isInvalidArgument`, `isUnavailable`, and the `isFuseApiError` type guard.
|
|
100
|
+
|
|
101
|
+
## Events (SSE)
|
|
102
|
+
|
|
103
|
+
`environments.events(id)` returns a promise that rejects on a connect-time error
|
|
104
|
+
(e.g. `not_found`) and otherwise resolves to an `AsyncIterable<Event>`. The
|
|
105
|
+
stream yields the current state first, then each transition, and ends after a
|
|
106
|
+
terminal-state event (`destroyed`/`failed`). There is **no built-in timeout** —
|
|
107
|
+
pass a `signal` to cancel:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
const ac = new AbortController();
|
|
111
|
+
const stream = await client.environments.events(id, { signal: ac.signal });
|
|
112
|
+
for await (const event of stream) {
|
|
113
|
+
if (event.state === "running") ac.abort();
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## API reference
|
|
118
|
+
|
|
119
|
+
The full REST surface is documented in the repository's OpenAPI spec:
|
|
120
|
+
[`api/openapi.yaml`](https://github.com/folsomintel/fuse/blob/main/api/openapi.yaml).
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
MIT © Andrew Nijmeh
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/** A minimal fetch signature, satisfied by the global fetch and most polyfills. */
|
|
2
|
+
type FetchLike = (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
3
|
+
/** Per-call options accepted by every service method. */
|
|
4
|
+
interface CallOptions {
|
|
5
|
+
/** Abort the request (and, for events, the stream). */
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
}
|
|
8
|
+
interface TransportConfig {
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
token?: string;
|
|
11
|
+
fetch?: FetchLike;
|
|
12
|
+
userAgent?: string;
|
|
13
|
+
requestId?: () => string;
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
headers?: Record<string, string>;
|
|
16
|
+
}
|
|
17
|
+
type Query = Record<string, string | undefined>;
|
|
18
|
+
interface RequestSpec {
|
|
19
|
+
query?: Query;
|
|
20
|
+
body?: unknown;
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
}
|
|
23
|
+
declare class Transport {
|
|
24
|
+
private readonly baseUrl;
|
|
25
|
+
private readonly token?;
|
|
26
|
+
private readonly fetchImpl;
|
|
27
|
+
private readonly userAgent?;
|
|
28
|
+
private readonly requestId?;
|
|
29
|
+
private readonly timeoutMs?;
|
|
30
|
+
private readonly defaultHeaders?;
|
|
31
|
+
constructor(cfg: TransportConfig);
|
|
32
|
+
/** Perform a request and decode a JSON response body. */
|
|
33
|
+
json<T>(method: string, path: string, spec?: RequestSpec): Promise<T>;
|
|
34
|
+
/** Perform a request that returns no body (e.g. 204 actions). */
|
|
35
|
+
noContent(method: string, path: string, spec?: RequestSpec): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Open a long-lived stream (SSE). Applies NO timeout — only the caller's
|
|
38
|
+
* signal cancels it — and checks the status eagerly so connect-time errors
|
|
39
|
+
* (404/401/...) surface as a FuseApiError before any iteration.
|
|
40
|
+
*/
|
|
41
|
+
stream(path: string, query: Query | undefined, signal?: AbortSignal): Promise<Response>;
|
|
42
|
+
private execute;
|
|
43
|
+
private buildUrl;
|
|
44
|
+
private buildHeaders;
|
|
45
|
+
private decodeJson;
|
|
46
|
+
private withTimeout;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Spec is the hardware/runtime spec for a microVM. */
|
|
50
|
+
interface Spec {
|
|
51
|
+
cpus?: number;
|
|
52
|
+
ram_mb?: number;
|
|
53
|
+
storage_gb?: number;
|
|
54
|
+
region?: string;
|
|
55
|
+
max_runtime_seconds?: number;
|
|
56
|
+
}
|
|
57
|
+
/** CreateRequest is the body for environments.create. */
|
|
58
|
+
interface CreateRequest {
|
|
59
|
+
task_id: string;
|
|
60
|
+
spec?: Spec;
|
|
61
|
+
manifest_inline?: string;
|
|
62
|
+
secrets?: Record<string, string>;
|
|
63
|
+
startup_script?: string;
|
|
64
|
+
gateway_url?: string;
|
|
65
|
+
gateway_token?: string;
|
|
66
|
+
}
|
|
67
|
+
/** EnvironmentInfo is the server's view of a single microVM. */
|
|
68
|
+
interface EnvironmentInfo {
|
|
69
|
+
id: string;
|
|
70
|
+
state: string;
|
|
71
|
+
task_id: string;
|
|
72
|
+
host_id?: string;
|
|
73
|
+
url: string;
|
|
74
|
+
spec: Spec;
|
|
75
|
+
created_at: string;
|
|
76
|
+
updated_at: string;
|
|
77
|
+
error?: string;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Event is one item from environments.events. It matches the server's SSE
|
|
81
|
+
* wire payload. Stream-level failures are thrown from the async iterator
|
|
82
|
+
* rather than delivered as an in-band event.
|
|
83
|
+
*/
|
|
84
|
+
interface Event {
|
|
85
|
+
id: string;
|
|
86
|
+
/** Event kind. v1 only emits "state". */
|
|
87
|
+
event: string;
|
|
88
|
+
vm_id: string;
|
|
89
|
+
state: string;
|
|
90
|
+
url?: string;
|
|
91
|
+
error?: string;
|
|
92
|
+
updated_at: string;
|
|
93
|
+
}
|
|
94
|
+
/** SnapshotRequest is the optional body for snapshots.create. */
|
|
95
|
+
interface SnapshotRequest {
|
|
96
|
+
comment?: string;
|
|
97
|
+
mode?: string;
|
|
98
|
+
retention_seconds?: number;
|
|
99
|
+
metadata?: Record<string, string>;
|
|
100
|
+
export_ref?: string;
|
|
101
|
+
export_status?: string;
|
|
102
|
+
}
|
|
103
|
+
/** SnapshotExport is an optional exported snapshot artifact. */
|
|
104
|
+
interface SnapshotExport {
|
|
105
|
+
destination: string;
|
|
106
|
+
status?: string;
|
|
107
|
+
requested_at?: string;
|
|
108
|
+
updated_at?: string;
|
|
109
|
+
last_error?: string;
|
|
110
|
+
}
|
|
111
|
+
/** Snapshot is a persisted snapshot record. */
|
|
112
|
+
interface Snapshot {
|
|
113
|
+
id: string;
|
|
114
|
+
vm_id: string;
|
|
115
|
+
task_id?: string;
|
|
116
|
+
tenant_id?: string;
|
|
117
|
+
parent_snapshot_id?: string;
|
|
118
|
+
mode?: string;
|
|
119
|
+
state?: string;
|
|
120
|
+
comment?: string;
|
|
121
|
+
size_bytes?: number;
|
|
122
|
+
created_at: string;
|
|
123
|
+
updated_at?: string;
|
|
124
|
+
retention_until?: string;
|
|
125
|
+
last_error?: string;
|
|
126
|
+
export_ref?: string;
|
|
127
|
+
exports?: SnapshotExport[];
|
|
128
|
+
}
|
|
129
|
+
/** HostCapacity is a host's resource envelope. */
|
|
130
|
+
interface HostCapacity {
|
|
131
|
+
cpus: number;
|
|
132
|
+
ram_mb: number;
|
|
133
|
+
storage_gb: number;
|
|
134
|
+
vm_count: number;
|
|
135
|
+
}
|
|
136
|
+
/** RegisterHostRequest is the body for hosts.register. */
|
|
137
|
+
interface RegisterHostRequest {
|
|
138
|
+
id: string;
|
|
139
|
+
url: string;
|
|
140
|
+
token?: string;
|
|
141
|
+
region?: string;
|
|
142
|
+
capacity?: HostCapacity;
|
|
143
|
+
}
|
|
144
|
+
/** Host is the server's view of a registered host. */
|
|
145
|
+
interface Host {
|
|
146
|
+
id: string;
|
|
147
|
+
url: string;
|
|
148
|
+
region?: string;
|
|
149
|
+
state: string;
|
|
150
|
+
capacity: HostCapacity;
|
|
151
|
+
allocated: HostCapacity;
|
|
152
|
+
last_seen: string;
|
|
153
|
+
created_at: string;
|
|
154
|
+
updated_at: string;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* APIKey is a key's metadata. The raw secret appears only in
|
|
158
|
+
* CreatedAPIKey.key, returned once at creation.
|
|
159
|
+
*/
|
|
160
|
+
interface APIKey {
|
|
161
|
+
id: string;
|
|
162
|
+
label?: string;
|
|
163
|
+
created_at: string;
|
|
164
|
+
last_used_at?: string;
|
|
165
|
+
revoked_at?: string;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* CreatedAPIKey is returned by apiKeys.create. `key` is the raw secret and is
|
|
169
|
+
* unrecoverable after this response.
|
|
170
|
+
*/
|
|
171
|
+
interface CreatedAPIKey extends APIKey {
|
|
172
|
+
key: string;
|
|
173
|
+
}
|
|
174
|
+
/** Lifecycle states for EnvironmentInfo.state and Event.state. */
|
|
175
|
+
declare const State: {
|
|
176
|
+
readonly Provisioning: "provisioning";
|
|
177
|
+
readonly Running: "running";
|
|
178
|
+
readonly Draining: "draining";
|
|
179
|
+
readonly Destroying: "destroying";
|
|
180
|
+
readonly Destroyed: "destroyed";
|
|
181
|
+
readonly Failed: "failed";
|
|
182
|
+
};
|
|
183
|
+
type State = (typeof State)[keyof typeof State];
|
|
184
|
+
/** isTerminalState reports whether state is a terminal lifecycle state. */
|
|
185
|
+
declare function isTerminalState(state: string): boolean;
|
|
186
|
+
|
|
187
|
+
/** Filters for environments.list. */
|
|
188
|
+
interface ListEnvironmentsOptions {
|
|
189
|
+
taskId?: string;
|
|
190
|
+
state?: string;
|
|
191
|
+
hostId?: string;
|
|
192
|
+
}
|
|
193
|
+
/** EnvironmentsService manages microVM lifecycle. */
|
|
194
|
+
declare class EnvironmentsService {
|
|
195
|
+
private readonly t;
|
|
196
|
+
constructor(t: Transport);
|
|
197
|
+
/** List tracked environments, optionally filtered. */
|
|
198
|
+
list(options?: ListEnvironmentsOptions, opts?: CallOptions): Promise<EnvironmentInfo[]>;
|
|
199
|
+
/** Fetch a single environment by VM id. */
|
|
200
|
+
get(vmId: string, opts?: CallOptions): Promise<EnvironmentInfo>;
|
|
201
|
+
/** Provision a new environment. Blocks until running or creation fails. */
|
|
202
|
+
create(body: CreateRequest, opts?: CallOptions): Promise<EnvironmentInfo>;
|
|
203
|
+
/** Gracefully drain an environment; returns the updated environment. */
|
|
204
|
+
drain(vmId: string, opts?: CallOptions): Promise<EnvironmentInfo>;
|
|
205
|
+
/** Rotate the per-VM credentials and push them to the guest. */
|
|
206
|
+
rotateToken(vmId: string, opts?: CallOptions): Promise<void>;
|
|
207
|
+
/** Forcefully destroy an environment. Idempotent. */
|
|
208
|
+
destroy(vmId: string, opts?: CallOptions): Promise<void>;
|
|
209
|
+
/**
|
|
210
|
+
* Open the SSE event stream for an environment. The returned promise rejects
|
|
211
|
+
* with a FuseApiError on a connect-time failure (e.g. not_found); on success
|
|
212
|
+
* it resolves to an AsyncIterable that yields each Event and ends after a
|
|
213
|
+
* terminal-state event. Pass `opts.signal` to cancel; there is no built-in
|
|
214
|
+
* timeout on the stream.
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* for await (const ev of await client.environments.events(id)) {
|
|
218
|
+
* console.log(ev.state);
|
|
219
|
+
* }
|
|
220
|
+
*/
|
|
221
|
+
events(vmId: string, opts?: CallOptions): Promise<AsyncIterable<Event>>;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Filters for snapshots.list. */
|
|
225
|
+
interface ListSnapshotsOptions {
|
|
226
|
+
vmId?: string;
|
|
227
|
+
taskId?: string;
|
|
228
|
+
tenantId?: string;
|
|
229
|
+
state?: string;
|
|
230
|
+
}
|
|
231
|
+
/** SnapshotsService manages microVM snapshots. */
|
|
232
|
+
declare class SnapshotsService {
|
|
233
|
+
private readonly t;
|
|
234
|
+
constructor(t: Transport);
|
|
235
|
+
/** Create a snapshot of a running environment. */
|
|
236
|
+
create(vmId: string, body?: SnapshotRequest, opts?: CallOptions): Promise<Snapshot>;
|
|
237
|
+
/** List snapshots, optionally filtered. */
|
|
238
|
+
list(options?: ListSnapshotsOptions, opts?: CallOptions): Promise<Snapshot[]>;
|
|
239
|
+
/** Fetch a single snapshot by id. */
|
|
240
|
+
get(snapshotId: string, opts?: CallOptions): Promise<Snapshot>;
|
|
241
|
+
/** Delete a snapshot. Idempotent; the snapshot must be a leaf. */
|
|
242
|
+
delete(snapshotId: string, opts?: CallOptions): Promise<void>;
|
|
243
|
+
/** Restore an environment from a snapshot. */
|
|
244
|
+
restore(snapshotId: string, opts?: CallOptions): Promise<void>;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** HostsService manages registered Firecracker hosts. */
|
|
248
|
+
declare class HostsService {
|
|
249
|
+
private readonly t;
|
|
250
|
+
constructor(t: Transport);
|
|
251
|
+
/** Register (or re-register) a host. Idempotent on id. */
|
|
252
|
+
register(body: RegisterHostRequest, opts?: CallOptions): Promise<Host>;
|
|
253
|
+
/** List registered hosts. */
|
|
254
|
+
list(opts?: CallOptions): Promise<Host[]>;
|
|
255
|
+
/** Fetch a single host by id. */
|
|
256
|
+
get(hostId: string, opts?: CallOptions): Promise<Host>;
|
|
257
|
+
/** Mark a host unschedulable; existing VMs keep running. */
|
|
258
|
+
cordon(hostId: string, opts?: CallOptions): Promise<void>;
|
|
259
|
+
/** Return a cordoned host to active scheduling. */
|
|
260
|
+
uncordon(hostId: string, opts?: CallOptions): Promise<void>;
|
|
261
|
+
/** Remove a host. The host must have no running VMs. */
|
|
262
|
+
deregister(hostId: string, opts?: CallOptions): Promise<void>;
|
|
263
|
+
private action;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* ApiKeysService manages revocable API keys. These endpoints require the
|
|
268
|
+
* master token; the server enforces this.
|
|
269
|
+
*/
|
|
270
|
+
declare class ApiKeysService {
|
|
271
|
+
private readonly t;
|
|
272
|
+
constructor(t: Transport);
|
|
273
|
+
/**
|
|
274
|
+
* Issue a new API key. The raw secret is returned once in `key` and is
|
|
275
|
+
* unrecoverable afterward.
|
|
276
|
+
*/
|
|
277
|
+
create(label?: string, opts?: CallOptions): Promise<CreatedAPIKey>;
|
|
278
|
+
/** List API key metadata. */
|
|
279
|
+
list(opts?: CallOptions): Promise<APIKey[]>;
|
|
280
|
+
/** Revoke the API key with the given id. */
|
|
281
|
+
revoke(id: string, opts?: CallOptions): Promise<void>;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Options for constructing a FuseClient. */
|
|
285
|
+
interface FuseClientOptions {
|
|
286
|
+
/** Base URL of the Fuse control plane, e.g. "https://fuse.example.com". Required. */
|
|
287
|
+
baseUrl: string;
|
|
288
|
+
/** Bearer token (master token or an API key). Omit for unauthenticated endpoints. */
|
|
289
|
+
token?: string;
|
|
290
|
+
/** Custom fetch implementation. Defaults to the global fetch (Node 18+). */
|
|
291
|
+
fetch?: FetchLike;
|
|
292
|
+
/** User-Agent header. Defaults to "fuse-ts/<version>". Ignored by browsers. */
|
|
293
|
+
userAgent?: string;
|
|
294
|
+
/** Generator for the X-Request-ID header, called once per request. Empty results are omitted. */
|
|
295
|
+
requestId?: () => string;
|
|
296
|
+
/** Default per-request timeout in milliseconds. Does NOT apply to events() streams. */
|
|
297
|
+
timeoutMs?: number;
|
|
298
|
+
/** Extra default headers merged into every request. */
|
|
299
|
+
headers?: Record<string, string>;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* FuseClient is the entry point for the Fuse API. It groups the resource
|
|
303
|
+
* services that share a single transport.
|
|
304
|
+
*
|
|
305
|
+
* @example
|
|
306
|
+
* const client = new FuseClient({ baseUrl: "https://fuse.example.com", token });
|
|
307
|
+
* const env = await client.environments.create({ task_id: "task-1" });
|
|
308
|
+
*/
|
|
309
|
+
declare class FuseClient {
|
|
310
|
+
readonly environments: EnvironmentsService;
|
|
311
|
+
readonly snapshots: SnapshotsService;
|
|
312
|
+
readonly hosts: HostsService;
|
|
313
|
+
readonly apiKeys: ApiKeysService;
|
|
314
|
+
constructor(options: FuseClientOptions);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
declare const VERSION = "0.0.1";
|
|
318
|
+
|
|
319
|
+
/** Stable, machine-readable error codes returned by the server. */
|
|
320
|
+
declare const ErrorCode: {
|
|
321
|
+
readonly NotFound: "not_found";
|
|
322
|
+
readonly Conflict: "conflict";
|
|
323
|
+
readonly InvalidArgument: "invalid_argument";
|
|
324
|
+
readonly Unavailable: "unavailable";
|
|
325
|
+
readonly Internal: "internal";
|
|
326
|
+
readonly Unauthorized: "unauthorized";
|
|
327
|
+
};
|
|
328
|
+
type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
329
|
+
interface FuseApiErrorInit {
|
|
330
|
+
status: number;
|
|
331
|
+
code?: string;
|
|
332
|
+
message?: string;
|
|
333
|
+
details?: Record<string, string>;
|
|
334
|
+
requestId?: string;
|
|
335
|
+
body?: string;
|
|
336
|
+
}
|
|
337
|
+
/** FuseApiError is a non-2xx response from the server. */
|
|
338
|
+
declare class FuseApiError extends Error {
|
|
339
|
+
readonly status: number;
|
|
340
|
+
readonly code: string;
|
|
341
|
+
readonly details?: Record<string, string>;
|
|
342
|
+
readonly requestId?: string;
|
|
343
|
+
/** Raw response body, when available. */
|
|
344
|
+
readonly body?: string;
|
|
345
|
+
constructor(init: FuseApiErrorInit);
|
|
346
|
+
}
|
|
347
|
+
/** FuseError is a transport, configuration, or decoding failure (not an API error). */
|
|
348
|
+
declare class FuseError extends Error {
|
|
349
|
+
constructor(message: string, options?: {
|
|
350
|
+
cause?: unknown;
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
/** isFuseApiError is a type guard for FuseApiError. */
|
|
354
|
+
declare function isFuseApiError(err: unknown): err is FuseApiError;
|
|
355
|
+
/** isNotFound reports whether err is a not_found api error. */
|
|
356
|
+
declare function isNotFound(err: unknown): boolean;
|
|
357
|
+
/** isConflict reports whether err is a conflict api error. */
|
|
358
|
+
declare function isConflict(err: unknown): boolean;
|
|
359
|
+
/** isUnauthorized reports whether err is an unauthorized api error. */
|
|
360
|
+
declare function isUnauthorized(err: unknown): boolean;
|
|
361
|
+
/** isInvalidArgument reports whether err is an invalid_argument api error. */
|
|
362
|
+
declare function isInvalidArgument(err: unknown): boolean;
|
|
363
|
+
/** isUnavailable reports whether err is an unavailable api error. */
|
|
364
|
+
declare function isUnavailable(err: unknown): boolean;
|
|
365
|
+
|
|
366
|
+
export { type APIKey, ApiKeysService, type CallOptions, type CreateRequest, type CreatedAPIKey, type EnvironmentInfo, EnvironmentsService, ErrorCode, type Event, type FetchLike, FuseApiError, type FuseApiErrorInit, FuseClient, type FuseClientOptions, FuseError, type Host, type HostCapacity, HostsService, type ListEnvironmentsOptions, type ListSnapshotsOptions, type RegisterHostRequest, type Snapshot, type SnapshotExport, type SnapshotRequest, SnapshotsService, type Spec, State, VERSION, isConflict, isFuseApiError, isInvalidArgument, isNotFound, isTerminalState, isUnauthorized, isUnavailable };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var REQUEST_ID_HEADER = "X-Request-ID";
|
|
3
|
+
var ErrorCode = {
|
|
4
|
+
NotFound: "not_found",
|
|
5
|
+
Conflict: "conflict",
|
|
6
|
+
InvalidArgument: "invalid_argument",
|
|
7
|
+
Unavailable: "unavailable",
|
|
8
|
+
Internal: "internal",
|
|
9
|
+
Unauthorized: "unauthorized"
|
|
10
|
+
};
|
|
11
|
+
var STATUS_TEXT = {
|
|
12
|
+
400: "bad request",
|
|
13
|
+
401: "unauthorized",
|
|
14
|
+
403: "forbidden",
|
|
15
|
+
404: "not found",
|
|
16
|
+
409: "conflict",
|
|
17
|
+
500: "internal server error",
|
|
18
|
+
502: "bad gateway",
|
|
19
|
+
503: "service unavailable"
|
|
20
|
+
};
|
|
21
|
+
function statusText(status) {
|
|
22
|
+
return STATUS_TEXT[status] ?? `status ${status}`;
|
|
23
|
+
}
|
|
24
|
+
function formatMessage(init) {
|
|
25
|
+
const parts = [`status=${init.status}`];
|
|
26
|
+
if (init.code) parts.push(`code=${init.code}`);
|
|
27
|
+
if (init.message) parts.push(init.message);
|
|
28
|
+
else parts.push(statusText(init.status));
|
|
29
|
+
if (init.requestId) parts.push(`request_id=${init.requestId}`);
|
|
30
|
+
return "fuse api error: " + parts.join(", ");
|
|
31
|
+
}
|
|
32
|
+
var FuseApiError = class _FuseApiError extends Error {
|
|
33
|
+
status;
|
|
34
|
+
code;
|
|
35
|
+
details;
|
|
36
|
+
requestId;
|
|
37
|
+
/** Raw response body, when available. */
|
|
38
|
+
body;
|
|
39
|
+
constructor(init) {
|
|
40
|
+
super(formatMessage(init));
|
|
41
|
+
this.name = "FuseApiError";
|
|
42
|
+
this.status = init.status;
|
|
43
|
+
this.code = init.code ?? "";
|
|
44
|
+
this.details = init.details;
|
|
45
|
+
this.requestId = init.requestId;
|
|
46
|
+
this.body = init.body;
|
|
47
|
+
Object.setPrototypeOf(this, _FuseApiError.prototype);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var FuseError = class _FuseError extends Error {
|
|
51
|
+
constructor(message, options) {
|
|
52
|
+
super(message, options);
|
|
53
|
+
this.name = "FuseError";
|
|
54
|
+
Object.setPrototypeOf(this, _FuseError.prototype);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
function isFuseApiError(err) {
|
|
58
|
+
return err instanceof FuseApiError;
|
|
59
|
+
}
|
|
60
|
+
function hasCode(err, code) {
|
|
61
|
+
return isFuseApiError(err) && err.code === code;
|
|
62
|
+
}
|
|
63
|
+
function isNotFound(err) {
|
|
64
|
+
return hasCode(err, ErrorCode.NotFound);
|
|
65
|
+
}
|
|
66
|
+
function isConflict(err) {
|
|
67
|
+
return hasCode(err, ErrorCode.Conflict);
|
|
68
|
+
}
|
|
69
|
+
function isUnauthorized(err) {
|
|
70
|
+
return hasCode(err, ErrorCode.Unauthorized);
|
|
71
|
+
}
|
|
72
|
+
function isInvalidArgument(err) {
|
|
73
|
+
return hasCode(err, ErrorCode.InvalidArgument);
|
|
74
|
+
}
|
|
75
|
+
function isUnavailable(err) {
|
|
76
|
+
return hasCode(err, ErrorCode.Unavailable);
|
|
77
|
+
}
|
|
78
|
+
function parseApiError(status, requestId, body) {
|
|
79
|
+
if (body) {
|
|
80
|
+
try {
|
|
81
|
+
const env = JSON.parse(body);
|
|
82
|
+
const e = env?.error;
|
|
83
|
+
if (e && (e.code || e.message)) {
|
|
84
|
+
return new FuseApiError({
|
|
85
|
+
status,
|
|
86
|
+
code: e.code,
|
|
87
|
+
message: e.message,
|
|
88
|
+
details: e.details,
|
|
89
|
+
requestId,
|
|
90
|
+
body
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return new FuseApiError({ status, message: statusText(status), requestId, body });
|
|
97
|
+
}
|
|
98
|
+
async function errorFromResponse(res) {
|
|
99
|
+
const requestId = res.headers.get(REQUEST_ID_HEADER) ?? void 0;
|
|
100
|
+
let body = "";
|
|
101
|
+
try {
|
|
102
|
+
body = await res.text();
|
|
103
|
+
} catch (err) {
|
|
104
|
+
return new FuseApiError({
|
|
105
|
+
status: res.status,
|
|
106
|
+
message: `read error body: ${String(err)}`,
|
|
107
|
+
requestId
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return parseApiError(res.status, requestId, body);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/transport.ts
|
|
114
|
+
var REQUEST_ID_HEADER2 = "X-Request-ID";
|
|
115
|
+
function combineSignals(signals) {
|
|
116
|
+
const active = signals.filter((s) => s != null);
|
|
117
|
+
if (active.length === 0) return { cleanup: () => {
|
|
118
|
+
} };
|
|
119
|
+
if (active.length === 1) return { signal: active[0], cleanup: () => {
|
|
120
|
+
} };
|
|
121
|
+
const controller = new AbortController();
|
|
122
|
+
const onAbort = (event) => {
|
|
123
|
+
const target = event.target;
|
|
124
|
+
controller.abort(target.reason);
|
|
125
|
+
cleanup();
|
|
126
|
+
};
|
|
127
|
+
const cleanup = () => {
|
|
128
|
+
for (const s of active) s.removeEventListener("abort", onAbort);
|
|
129
|
+
};
|
|
130
|
+
for (const s of active) {
|
|
131
|
+
if (s.aborted) {
|
|
132
|
+
controller.abort(s.reason);
|
|
133
|
+
cleanup();
|
|
134
|
+
return { signal: controller.signal, cleanup: () => {
|
|
135
|
+
} };
|
|
136
|
+
}
|
|
137
|
+
s.addEventListener("abort", onAbort);
|
|
138
|
+
}
|
|
139
|
+
return { signal: controller.signal, cleanup };
|
|
140
|
+
}
|
|
141
|
+
var Transport = class {
|
|
142
|
+
baseUrl;
|
|
143
|
+
token;
|
|
144
|
+
fetchImpl;
|
|
145
|
+
userAgent;
|
|
146
|
+
requestId;
|
|
147
|
+
timeoutMs;
|
|
148
|
+
defaultHeaders;
|
|
149
|
+
constructor(cfg) {
|
|
150
|
+
if (!cfg.baseUrl) {
|
|
151
|
+
throw new FuseError("base url is required");
|
|
152
|
+
}
|
|
153
|
+
let parsed;
|
|
154
|
+
try {
|
|
155
|
+
parsed = new URL(cfg.baseUrl);
|
|
156
|
+
} catch {
|
|
157
|
+
throw new FuseError("base url is invalid");
|
|
158
|
+
}
|
|
159
|
+
this.baseUrl = parsed;
|
|
160
|
+
this.token = cfg.token;
|
|
161
|
+
this.userAgent = cfg.userAgent;
|
|
162
|
+
this.requestId = cfg.requestId;
|
|
163
|
+
this.timeoutMs = cfg.timeoutMs;
|
|
164
|
+
this.defaultHeaders = cfg.headers;
|
|
165
|
+
const provided = cfg.fetch;
|
|
166
|
+
if (provided) {
|
|
167
|
+
this.fetchImpl = provided;
|
|
168
|
+
} else if (typeof globalThis.fetch === "function") {
|
|
169
|
+
this.fetchImpl = globalThis.fetch.bind(globalThis);
|
|
170
|
+
} else {
|
|
171
|
+
throw new FuseError(
|
|
172
|
+
"global fetch is not available; pass a fetch implementation via the `fetch` option (Node 18+ required)"
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/** Perform a request and decode a JSON response body. */
|
|
177
|
+
async json(method, path, spec = {}) {
|
|
178
|
+
const { signal: combined, cleanup } = this.withTimeout(spec.signal);
|
|
179
|
+
try {
|
|
180
|
+
const res = await this.execute(method, path, spec, combined);
|
|
181
|
+
if (!res.ok) throw await errorFromResponse(res);
|
|
182
|
+
return await this.decodeJson(res);
|
|
183
|
+
} finally {
|
|
184
|
+
cleanup();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** Perform a request that returns no body (e.g. 204 actions). */
|
|
188
|
+
async noContent(method, path, spec = {}) {
|
|
189
|
+
const { signal: combined, cleanup } = this.withTimeout(spec.signal);
|
|
190
|
+
try {
|
|
191
|
+
const res = await this.execute(method, path, spec, combined);
|
|
192
|
+
if (!res.ok) throw await errorFromResponse(res);
|
|
193
|
+
await res.body?.cancel().catch(() => {
|
|
194
|
+
});
|
|
195
|
+
} finally {
|
|
196
|
+
cleanup();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Open a long-lived stream (SSE). Applies NO timeout — only the caller's
|
|
201
|
+
* signal cancels it — and checks the status eagerly so connect-time errors
|
|
202
|
+
* (404/401/...) surface as a FuseApiError before any iteration.
|
|
203
|
+
*/
|
|
204
|
+
async stream(path, query, signal) {
|
|
205
|
+
const res = await this.execute(
|
|
206
|
+
"GET",
|
|
207
|
+
path,
|
|
208
|
+
{ query, signal },
|
|
209
|
+
signal,
|
|
210
|
+
"text/event-stream"
|
|
211
|
+
);
|
|
212
|
+
if (!res.ok) throw await errorFromResponse(res);
|
|
213
|
+
return res;
|
|
214
|
+
}
|
|
215
|
+
async execute(method, path, spec, signal, accept) {
|
|
216
|
+
const url = this.buildUrl(path, spec.query);
|
|
217
|
+
const hasBody = spec.body !== void 0 && spec.body !== null;
|
|
218
|
+
const headers = this.buildHeaders(hasBody, accept);
|
|
219
|
+
const init = { method, headers };
|
|
220
|
+
if (hasBody) init.body = JSON.stringify(spec.body);
|
|
221
|
+
if (signal) init.signal = signal;
|
|
222
|
+
try {
|
|
223
|
+
return await this.fetchImpl(url, init);
|
|
224
|
+
} catch (err) {
|
|
225
|
+
if (signal?.aborted) throw err;
|
|
226
|
+
throw new FuseError(`request failed: ${method} ${url}`, { cause: err });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
buildUrl(path, query) {
|
|
230
|
+
const p = path.startsWith("/") ? path : "/" + path;
|
|
231
|
+
const u = new URL(p, this.baseUrl);
|
|
232
|
+
if (query) {
|
|
233
|
+
for (const [key, value] of Object.entries(query)) {
|
|
234
|
+
if (value !== void 0 && value !== "") u.searchParams.set(key, value);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return u.toString();
|
|
238
|
+
}
|
|
239
|
+
buildHeaders(hasBody, accept) {
|
|
240
|
+
const headers = { ...this.defaultHeaders ?? {} };
|
|
241
|
+
if (hasBody) headers["Content-Type"] = "application/json";
|
|
242
|
+
if (this.token) headers["Authorization"] = `Bearer ${this.token}`;
|
|
243
|
+
if (this.userAgent) headers["User-Agent"] = this.userAgent;
|
|
244
|
+
if (this.requestId) {
|
|
245
|
+
const id = this.requestId();
|
|
246
|
+
if (id) headers[REQUEST_ID_HEADER2] = id;
|
|
247
|
+
}
|
|
248
|
+
if (accept) headers["Accept"] = accept;
|
|
249
|
+
return headers;
|
|
250
|
+
}
|
|
251
|
+
async decodeJson(res) {
|
|
252
|
+
const text = await res.text();
|
|
253
|
+
if (!text) return void 0;
|
|
254
|
+
try {
|
|
255
|
+
return JSON.parse(text);
|
|
256
|
+
} catch (err) {
|
|
257
|
+
throw new FuseError("decode response body", { cause: err });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
withTimeout(callerSignal) {
|
|
261
|
+
const signals = [callerSignal];
|
|
262
|
+
if (this.timeoutMs && this.timeoutMs > 0) {
|
|
263
|
+
signals.push(AbortSignal.timeout(this.timeoutMs));
|
|
264
|
+
}
|
|
265
|
+
return combineSignals(signals);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// src/validate.ts
|
|
270
|
+
function requireArg(value, name) {
|
|
271
|
+
if (!value) {
|
|
272
|
+
throw new FuseError(`${name} is required`);
|
|
273
|
+
}
|
|
274
|
+
return value;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/types.ts
|
|
278
|
+
var State = {
|
|
279
|
+
Provisioning: "provisioning",
|
|
280
|
+
Running: "running",
|
|
281
|
+
Draining: "draining",
|
|
282
|
+
Destroying: "destroying",
|
|
283
|
+
Destroyed: "destroyed",
|
|
284
|
+
Failed: "failed"
|
|
285
|
+
};
|
|
286
|
+
function isTerminalState(state) {
|
|
287
|
+
return state === State.Destroyed || state === State.Failed;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/events.ts
|
|
291
|
+
var DATA_PREFIX = "data:";
|
|
292
|
+
async function streamEvents(t, vmId, signal) {
|
|
293
|
+
const path = `/v1/environments/${encodeURIComponent(vmId)}/events`;
|
|
294
|
+
const res = await t.stream(path, void 0, signal);
|
|
295
|
+
return parseEventStream(res, signal);
|
|
296
|
+
}
|
|
297
|
+
function parseEventStream(res, signal) {
|
|
298
|
+
return {
|
|
299
|
+
async *[Symbol.asyncIterator]() {
|
|
300
|
+
const body = res.body;
|
|
301
|
+
if (!body) return;
|
|
302
|
+
const reader = body.getReader();
|
|
303
|
+
const decoder = new TextDecoder("utf-8");
|
|
304
|
+
let buffer = "";
|
|
305
|
+
let data = "";
|
|
306
|
+
let hasData = false;
|
|
307
|
+
try {
|
|
308
|
+
while (true) {
|
|
309
|
+
let chunk;
|
|
310
|
+
try {
|
|
311
|
+
chunk = await reader.read();
|
|
312
|
+
} catch (err) {
|
|
313
|
+
if (signal?.aborted) return;
|
|
314
|
+
throw new FuseError("event stream read failed", { cause: err });
|
|
315
|
+
}
|
|
316
|
+
if (chunk.done) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
buffer += decoder.decode(chunk.value, { stream: true });
|
|
320
|
+
let nl;
|
|
321
|
+
while ((nl = buffer.indexOf("\n")) >= 0) {
|
|
322
|
+
let line = buffer.slice(0, nl);
|
|
323
|
+
buffer = buffer.slice(nl + 1);
|
|
324
|
+
if (line.endsWith("\r")) line = line.slice(0, -1);
|
|
325
|
+
if (line === "") {
|
|
326
|
+
if (!hasData) continue;
|
|
327
|
+
const payload = data;
|
|
328
|
+
data = "";
|
|
329
|
+
hasData = false;
|
|
330
|
+
let event;
|
|
331
|
+
try {
|
|
332
|
+
event = JSON.parse(payload);
|
|
333
|
+
} catch (err) {
|
|
334
|
+
throw new FuseError("decode event", { cause: err });
|
|
335
|
+
}
|
|
336
|
+
yield event;
|
|
337
|
+
if (isTerminalState(event.state)) return;
|
|
338
|
+
} else if (line.startsWith(":")) {
|
|
339
|
+
continue;
|
|
340
|
+
} else if (line.startsWith(DATA_PREFIX)) {
|
|
341
|
+
let p = line.slice(DATA_PREFIX.length);
|
|
342
|
+
if (p.startsWith(" ")) p = p.slice(1);
|
|
343
|
+
data += p;
|
|
344
|
+
hasData = true;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
} finally {
|
|
349
|
+
try {
|
|
350
|
+
await reader.cancel();
|
|
351
|
+
} catch {
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/environments.ts
|
|
359
|
+
var EnvironmentsService = class {
|
|
360
|
+
constructor(t) {
|
|
361
|
+
this.t = t;
|
|
362
|
+
}
|
|
363
|
+
t;
|
|
364
|
+
/** List tracked environments, optionally filtered. */
|
|
365
|
+
async list(options = {}, opts = {}) {
|
|
366
|
+
const out = await this.t.json("GET", "/v1/environments", {
|
|
367
|
+
query: { task_id: options.taskId, state: options.state, host_id: options.hostId },
|
|
368
|
+
signal: opts.signal
|
|
369
|
+
});
|
|
370
|
+
return out.environments ?? [];
|
|
371
|
+
}
|
|
372
|
+
/** Fetch a single environment by VM id. */
|
|
373
|
+
async get(vmId, opts = {}) {
|
|
374
|
+
requireArg(vmId, "vm id");
|
|
375
|
+
return this.t.json(
|
|
376
|
+
"GET",
|
|
377
|
+
`/v1/environments/${encodeURIComponent(vmId)}`,
|
|
378
|
+
{ signal: opts.signal }
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
/** Provision a new environment. Blocks until running or creation fails. */
|
|
382
|
+
async create(body, opts = {}) {
|
|
383
|
+
return this.t.json("POST", "/v1/environments", {
|
|
384
|
+
body,
|
|
385
|
+
signal: opts.signal
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
/** Gracefully drain an environment; returns the updated environment. */
|
|
389
|
+
async drain(vmId, opts = {}) {
|
|
390
|
+
requireArg(vmId, "vm id");
|
|
391
|
+
return this.t.json(
|
|
392
|
+
"POST",
|
|
393
|
+
`/v1/environments/${encodeURIComponent(vmId)}`,
|
|
394
|
+
{ query: { action: "drain" }, signal: opts.signal }
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
/** Rotate the per-VM credentials and push them to the guest. */
|
|
398
|
+
async rotateToken(vmId, opts = {}) {
|
|
399
|
+
requireArg(vmId, "vm id");
|
|
400
|
+
await this.t.noContent("POST", `/v1/environments/${encodeURIComponent(vmId)}`, {
|
|
401
|
+
query: { action: "rotate-token" },
|
|
402
|
+
signal: opts.signal
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
/** Forcefully destroy an environment. Idempotent. */
|
|
406
|
+
async destroy(vmId, opts = {}) {
|
|
407
|
+
requireArg(vmId, "vm id");
|
|
408
|
+
await this.t.noContent("DELETE", `/v1/environments/${encodeURIComponent(vmId)}`, {
|
|
409
|
+
signal: opts.signal
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Open the SSE event stream for an environment. The returned promise rejects
|
|
414
|
+
* with a FuseApiError on a connect-time failure (e.g. not_found); on success
|
|
415
|
+
* it resolves to an AsyncIterable that yields each Event and ends after a
|
|
416
|
+
* terminal-state event. Pass `opts.signal` to cancel; there is no built-in
|
|
417
|
+
* timeout on the stream.
|
|
418
|
+
*
|
|
419
|
+
* @example
|
|
420
|
+
* for await (const ev of await client.environments.events(id)) {
|
|
421
|
+
* console.log(ev.state);
|
|
422
|
+
* }
|
|
423
|
+
*/
|
|
424
|
+
async events(vmId, opts = {}) {
|
|
425
|
+
requireArg(vmId, "vm id");
|
|
426
|
+
return streamEvents(this.t, vmId, opts.signal);
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// src/snapshots.ts
|
|
431
|
+
var SnapshotsService = class {
|
|
432
|
+
constructor(t) {
|
|
433
|
+
this.t = t;
|
|
434
|
+
}
|
|
435
|
+
t;
|
|
436
|
+
/** Create a snapshot of a running environment. */
|
|
437
|
+
async create(vmId, body = {}, opts = {}) {
|
|
438
|
+
requireArg(vmId, "vm id");
|
|
439
|
+
return this.t.json(
|
|
440
|
+
"POST",
|
|
441
|
+
`/v1/environments/${encodeURIComponent(vmId)}/snapshots`,
|
|
442
|
+
{ body, signal: opts.signal }
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
/** List snapshots, optionally filtered. */
|
|
446
|
+
async list(options = {}, opts = {}) {
|
|
447
|
+
const out = await this.t.json("GET", "/v1/snapshots", {
|
|
448
|
+
query: {
|
|
449
|
+
vm_id: options.vmId,
|
|
450
|
+
task_id: options.taskId,
|
|
451
|
+
tenant_id: options.tenantId,
|
|
452
|
+
state: options.state
|
|
453
|
+
},
|
|
454
|
+
signal: opts.signal
|
|
455
|
+
});
|
|
456
|
+
return out.snapshots ?? [];
|
|
457
|
+
}
|
|
458
|
+
/** Fetch a single snapshot by id. */
|
|
459
|
+
async get(snapshotId, opts = {}) {
|
|
460
|
+
requireArg(snapshotId, "snapshot id");
|
|
461
|
+
return this.t.json(
|
|
462
|
+
"GET",
|
|
463
|
+
`/v1/snapshots/${encodeURIComponent(snapshotId)}`,
|
|
464
|
+
{ signal: opts.signal }
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
/** Delete a snapshot. Idempotent; the snapshot must be a leaf. */
|
|
468
|
+
async delete(snapshotId, opts = {}) {
|
|
469
|
+
requireArg(snapshotId, "snapshot id");
|
|
470
|
+
await this.t.noContent("DELETE", `/v1/snapshots/${encodeURIComponent(snapshotId)}`, {
|
|
471
|
+
signal: opts.signal
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
/** Restore an environment from a snapshot. */
|
|
475
|
+
async restore(snapshotId, opts = {}) {
|
|
476
|
+
requireArg(snapshotId, "snapshot id");
|
|
477
|
+
await this.t.noContent("POST", `/v1/snapshots/${encodeURIComponent(snapshotId)}`, {
|
|
478
|
+
query: { action: "restore" },
|
|
479
|
+
signal: opts.signal
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
// src/hosts.ts
|
|
485
|
+
var HostsService = class {
|
|
486
|
+
constructor(t) {
|
|
487
|
+
this.t = t;
|
|
488
|
+
}
|
|
489
|
+
t;
|
|
490
|
+
/** Register (or re-register) a host. Idempotent on id. */
|
|
491
|
+
async register(body, opts = {}) {
|
|
492
|
+
return this.t.json("POST", "/v1/hosts", { body, signal: opts.signal });
|
|
493
|
+
}
|
|
494
|
+
/** List registered hosts. */
|
|
495
|
+
async list(opts = {}) {
|
|
496
|
+
const out = await this.t.json("GET", "/v1/hosts", { signal: opts.signal });
|
|
497
|
+
return out.hosts ?? [];
|
|
498
|
+
}
|
|
499
|
+
/** Fetch a single host by id. */
|
|
500
|
+
async get(hostId, opts = {}) {
|
|
501
|
+
requireArg(hostId, "host id");
|
|
502
|
+
return this.t.json("GET", `/v1/hosts/${encodeURIComponent(hostId)}`, {
|
|
503
|
+
signal: opts.signal
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
/** Mark a host unschedulable; existing VMs keep running. */
|
|
507
|
+
async cordon(hostId, opts = {}) {
|
|
508
|
+
await this.action(hostId, "cordon", opts);
|
|
509
|
+
}
|
|
510
|
+
/** Return a cordoned host to active scheduling. */
|
|
511
|
+
async uncordon(hostId, opts = {}) {
|
|
512
|
+
await this.action(hostId, "uncordon", opts);
|
|
513
|
+
}
|
|
514
|
+
/** Remove a host. The host must have no running VMs. */
|
|
515
|
+
async deregister(hostId, opts = {}) {
|
|
516
|
+
requireArg(hostId, "host id");
|
|
517
|
+
await this.t.noContent("DELETE", `/v1/hosts/${encodeURIComponent(hostId)}`, {
|
|
518
|
+
signal: opts.signal
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
async action(hostId, action, opts) {
|
|
522
|
+
requireArg(hostId, "host id");
|
|
523
|
+
await this.t.noContent("POST", `/v1/hosts/${encodeURIComponent(hostId)}`, {
|
|
524
|
+
query: { action },
|
|
525
|
+
signal: opts.signal
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
// src/apiKeys.ts
|
|
531
|
+
var ApiKeysService = class {
|
|
532
|
+
constructor(t) {
|
|
533
|
+
this.t = t;
|
|
534
|
+
}
|
|
535
|
+
t;
|
|
536
|
+
/**
|
|
537
|
+
* Issue a new API key. The raw secret is returned once in `key` and is
|
|
538
|
+
* unrecoverable afterward.
|
|
539
|
+
*/
|
|
540
|
+
async create(label, opts = {}) {
|
|
541
|
+
return this.t.json("POST", "/v1/api-keys", {
|
|
542
|
+
body: label ? { label } : {},
|
|
543
|
+
signal: opts.signal
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
/** List API key metadata. */
|
|
547
|
+
async list(opts = {}) {
|
|
548
|
+
const out = await this.t.json("GET", "/v1/api-keys", {
|
|
549
|
+
signal: opts.signal
|
|
550
|
+
});
|
|
551
|
+
return out.api_keys ?? [];
|
|
552
|
+
}
|
|
553
|
+
/** Revoke the API key with the given id. */
|
|
554
|
+
async revoke(id, opts = {}) {
|
|
555
|
+
requireArg(id, "id");
|
|
556
|
+
await this.t.noContent("DELETE", `/v1/api-keys/${encodeURIComponent(id)}`, {
|
|
557
|
+
signal: opts.signal
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
// src/version.ts
|
|
563
|
+
var VERSION = "0.0.1";
|
|
564
|
+
|
|
565
|
+
// src/client.ts
|
|
566
|
+
var FuseClient = class {
|
|
567
|
+
environments;
|
|
568
|
+
snapshots;
|
|
569
|
+
hosts;
|
|
570
|
+
apiKeys;
|
|
571
|
+
constructor(options) {
|
|
572
|
+
const transport = new Transport({
|
|
573
|
+
baseUrl: options.baseUrl,
|
|
574
|
+
token: options.token,
|
|
575
|
+
fetch: options.fetch,
|
|
576
|
+
userAgent: options.userAgent ?? `fuse-ts/${VERSION}`,
|
|
577
|
+
requestId: options.requestId,
|
|
578
|
+
timeoutMs: options.timeoutMs,
|
|
579
|
+
headers: options.headers
|
|
580
|
+
});
|
|
581
|
+
this.environments = new EnvironmentsService(transport);
|
|
582
|
+
this.snapshots = new SnapshotsService(transport);
|
|
583
|
+
this.hosts = new HostsService(transport);
|
|
584
|
+
this.apiKeys = new ApiKeysService(transport);
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
export { ApiKeysService, EnvironmentsService, ErrorCode, FuseApiError, FuseClient, FuseError, HostsService, SnapshotsService, State, VERSION, isConflict, isFuseApiError, isInvalidArgument, isNotFound, isTerminalState, isUnauthorized, isUnavailable };
|
|
589
|
+
//# sourceMappingURL=index.js.map
|
|
590
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/transport.ts","../src/validate.ts","../src/types.ts","../src/events.ts","../src/environments.ts","../src/snapshots.ts","../src/hosts.ts","../src/apiKeys.ts","../src/version.ts","../src/client.ts"],"names":["REQUEST_ID_HEADER"],"mappings":";AAIA,IAAM,iBAAA,GAAoB,cAAA;AAGnB,IAAM,SAAA,GAAY;AAAA,EACvB,QAAA,EAAU,WAAA;AAAA,EACV,QAAA,EAAU,UAAA;AAAA,EACV,eAAA,EAAiB,kBAAA;AAAA,EACjB,WAAA,EAAa,aAAA;AAAA,EACb,QAAA,EAAU,UAAA;AAAA,EACV,YAAA,EAAc;AAChB;AAIA,IAAM,WAAA,GAAsC;AAAA,EAC1C,GAAA,EAAK,aAAA;AAAA,EACL,GAAA,EAAK,cAAA;AAAA,EACL,GAAA,EAAK,WAAA;AAAA,EACL,GAAA,EAAK,WAAA;AAAA,EACL,GAAA,EAAK,UAAA;AAAA,EACL,GAAA,EAAK,uBAAA;AAAA,EACL,GAAA,EAAK,aAAA;AAAA,EACL,GAAA,EAAK;AACP,CAAA;AAEA,SAAS,WAAW,MAAA,EAAwB;AAC1C,EAAA,OAAO,WAAA,CAAY,MAAM,CAAA,IAAK,CAAA,OAAA,EAAU,MAAM,CAAA,CAAA;AAChD;AAWA,SAAS,cAAc,IAAA,EAAgC;AACrD,EAAA,MAAM,KAAA,GAAkB,CAAC,CAAA,OAAA,EAAU,IAAA,CAAK,MAAM,CAAA,CAAE,CAAA;AAChD,EAAA,IAAI,KAAK,IAAA,EAAM,KAAA,CAAM,KAAK,CAAA,KAAA,EAAQ,IAAA,CAAK,IAAI,CAAA,CAAE,CAAA;AAC7C,EAAA,IAAI,IAAA,CAAK,OAAA,EAAS,KAAA,CAAM,IAAA,CAAK,KAAK,OAAO,CAAA;AAAA,OACpC,KAAA,CAAM,IAAA,CAAK,UAAA,CAAW,IAAA,CAAK,MAAM,CAAC,CAAA;AACvC,EAAA,IAAI,KAAK,SAAA,EAAW,KAAA,CAAM,KAAK,CAAA,WAAA,EAAc,IAAA,CAAK,SAAS,CAAA,CAAE,CAAA;AAC7D,EAAA,OAAO,kBAAA,GAAqB,KAAA,CAAM,IAAA,CAAK,IAAI,CAAA;AAC7C;AAGO,IAAM,YAAA,GAAN,MAAM,aAAA,SAAqB,KAAA,CAAM;AAAA,EAC7B,MAAA;AAAA,EACA,IAAA;AAAA,EACA,OAAA;AAAA,EACA,SAAA;AAAA;AAAA,EAEA,IAAA;AAAA,EAET,YAAY,IAAA,EAAwB;AAClC,IAAA,KAAA,CAAM,aAAA,CAAc,IAAI,CAAC,CAAA;AACzB,IAAA,IAAA,CAAK,IAAA,GAAO,cAAA;AACZ,IAAA,IAAA,CAAK,SAAS,IAAA,CAAK,MAAA;AACnB,IAAA,IAAA,CAAK,IAAA,GAAO,KAAK,IAAA,IAAQ,EAAA;AACzB,IAAA,IAAA,CAAK,UAAU,IAAA,CAAK,OAAA;AACpB,IAAA,IAAA,CAAK,YAAY,IAAA,CAAK,SAAA;AACtB,IAAA,IAAA,CAAK,OAAO,IAAA,CAAK,IAAA;AAEjB,IAAA,MAAA,CAAO,cAAA,CAAe,IAAA,EAAM,aAAA,CAAa,SAAS,CAAA;AAAA,EACpD;AACF;AAGO,IAAM,SAAA,GAAN,MAAM,UAAA,SAAkB,KAAA,CAAM;AAAA,EACnC,WAAA,CAAY,SAAiB,OAAA,EAA+B;AAC1D,IAAA,KAAA,CAAM,SAAS,OAAO,CAAA;AACtB,IAAA,IAAA,CAAK,IAAA,GAAO,WAAA;AACZ,IAAA,MAAA,CAAO,cAAA,CAAe,IAAA,EAAM,UAAA,CAAU,SAAS,CAAA;AAAA,EACjD;AACF;AAGO,SAAS,eAAe,GAAA,EAAmC;AAChE,EAAA,OAAO,GAAA,YAAe,YAAA;AACxB;AAEA,SAAS,OAAA,CAAQ,KAAc,IAAA,EAAuB;AACpD,EAAA,OAAO,cAAA,CAAe,GAAG,CAAA,IAAK,GAAA,CAAI,IAAA,KAAS,IAAA;AAC7C;AAGO,SAAS,WAAW,GAAA,EAAuB;AAChD,EAAA,OAAO,OAAA,CAAQ,GAAA,EAAK,SAAA,CAAU,QAAQ,CAAA;AACxC;AAGO,SAAS,WAAW,GAAA,EAAuB;AAChD,EAAA,OAAO,OAAA,CAAQ,GAAA,EAAK,SAAA,CAAU,QAAQ,CAAA;AACxC;AAGO,SAAS,eAAe,GAAA,EAAuB;AACpD,EAAA,OAAO,OAAA,CAAQ,GAAA,EAAK,SAAA,CAAU,YAAY,CAAA;AAC5C;AAGO,SAAS,kBAAkB,GAAA,EAAuB;AACvD,EAAA,OAAO,OAAA,CAAQ,GAAA,EAAK,SAAA,CAAU,eAAe,CAAA;AAC/C;AAGO,SAAS,cAAc,GAAA,EAAuB;AACnD,EAAA,OAAO,OAAA,CAAQ,GAAA,EAAK,SAAA,CAAU,WAAW,CAAA;AAC3C;AAUA,SAAS,aAAA,CACP,MAAA,EACA,SAAA,EACA,IAAA,EACc;AACd,EAAA,IAAI,IAAA,EAAM;AACR,IAAA,IAAI;AACF,MAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AAC3B,MAAA,MAAM,IAAI,GAAA,EAAK,KAAA;AACf,MAAA,IAAI,CAAA,KAAM,CAAA,CAAE,IAAA,IAAQ,CAAA,CAAE,OAAA,CAAA,EAAU;AAC9B,QAAA,OAAO,IAAI,YAAA,CAAa;AAAA,UACtB,MAAA;AAAA,UACA,MAAM,CAAA,CAAE,IAAA;AAAA,UACR,SAAS,CAAA,CAAE,OAAA;AAAA,UACX,SAAS,CAAA,CAAE,OAAA;AAAA,UACX,SAAA;AAAA,UACA;AAAA,SACD,CAAA;AAAA,MACH;AAAA,IACF,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACF;AACA,EAAA,OAAO,IAAI,YAAA,CAAa,EAAE,MAAA,EAAQ,OAAA,EAAS,WAAW,MAAM,CAAA,EAAG,SAAA,EAAW,IAAA,EAAM,CAAA;AAClF;AAOA,eAAsB,kBAAkB,GAAA,EAAsC;AAC5E,EAAA,MAAM,SAAA,GAAY,GAAA,CAAI,OAAA,CAAQ,GAAA,CAAI,iBAAiB,CAAA,IAAK,MAAA;AACxD,EAAA,IAAI,IAAA,GAAO,EAAA;AACX,EAAA,IAAI;AACF,IAAA,IAAA,GAAO,MAAM,IAAI,IAAA,EAAK;AAAA,EACxB,SAAS,GAAA,EAAK;AACZ,IAAA,OAAO,IAAI,YAAA,CAAa;AAAA,MACtB,QAAQ,GAAA,CAAI,MAAA;AAAA,MACZ,OAAA,EAAS,CAAA,iBAAA,EAAoB,MAAA,CAAO,GAAG,CAAC,CAAA,CAAA;AAAA,MACxC;AAAA,KACD,CAAA;AAAA,EACH;AACA,EAAA,OAAO,aAAA,CAAc,GAAA,CAAI,MAAA,EAAQ,SAAA,EAAW,IAAI,CAAA;AAClD;;;AChKA,IAAMA,kBAAAA,GAAoB,cAAA;AA8B1B,SAAS,eAAe,OAAA,EAGtB;AACA,EAAA,MAAM,SAAS,OAAA,CAAQ,MAAA,CAAO,CAAC,CAAA,KAAwB,KAAK,IAAI,CAAA;AAChE,EAAA,IAAI,OAAO,MAAA,KAAW,CAAA,EAAG,OAAO,EAAE,SAAS,MAAM;AAAA,EAAC,CAAA,EAAE;AACpD,EAAA,IAAI,MAAA,CAAO,MAAA,KAAW,CAAA,EAAG,OAAO,EAAE,QAAQ,MAAA,CAAO,CAAC,CAAA,EAAG,OAAA,EAAS,MAAM;AAAA,EAAC,CAAA,EAAE;AAEvE,EAAA,MAAM,UAAA,GAAa,IAAI,eAAA,EAAgB;AACvC,EAAA,MAAM,OAAA,GAAU,CAAC,KAAA,KAAiB;AAChC,IAAA,MAAM,SAAS,KAAA,CAAM,MAAA;AACrB,IAAA,UAAA,CAAW,KAAA,CAAM,OAAO,MAAM,CAAA;AAC9B,IAAA,OAAA,EAAQ;AAAA,EACV,CAAA;AACA,EAAA,MAAM,UAAU,MAAM;AACpB,IAAA,KAAA,MAAW,CAAA,IAAK,MAAA,EAAQ,CAAA,CAAE,mBAAA,CAAoB,SAAS,OAAO,CAAA;AAAA,EAChE,CAAA;AACA,EAAA,KAAA,MAAW,KAAK,MAAA,EAAQ;AACtB,IAAA,IAAI,EAAE,OAAA,EAAS;AACb,MAAA,UAAA,CAAW,KAAA,CAAM,EAAE,MAAM,CAAA;AACzB,MAAA,OAAA,EAAQ;AACR,MAAA,OAAO,EAAE,MAAA,EAAQ,UAAA,CAAW,MAAA,EAAQ,SAAS,MAAM;AAAA,MAAC,CAAA,EAAE;AAAA,IACxD;AACA,IAAA,CAAA,CAAE,gBAAA,CAAiB,SAAS,OAAO,CAAA;AAAA,EACrC;AACA,EAAA,OAAO,EAAE,MAAA,EAAQ,UAAA,CAAW,MAAA,EAAQ,OAAA,EAAQ;AAC9C;AAEO,IAAM,YAAN,MAAgB;AAAA,EACJ,OAAA;AAAA,EACA,KAAA;AAAA,EACA,SAAA;AAAA,EACA,SAAA;AAAA,EACA,SAAA;AAAA,EACA,SAAA;AAAA,EACA,cAAA;AAAA,EAEjB,YAAY,GAAA,EAAsB;AAChC,IAAA,IAAI,CAAC,IAAI,OAAA,EAAS;AAChB,MAAA,MAAM,IAAI,UAAU,sBAAsB,CAAA;AAAA,IAC5C;AACA,IAAA,IAAI,MAAA;AACJ,IAAA,IAAI;AACF,MAAA,MAAA,GAAS,IAAI,GAAA,CAAI,GAAA,CAAI,OAAO,CAAA;AAAA,IAC9B,CAAA,CAAA,MAAQ;AACN,MAAA,MAAM,IAAI,UAAU,qBAAqB,CAAA;AAAA,IAC3C;AACA,IAAA,IAAA,CAAK,OAAA,GAAU,MAAA;AACf,IAAA,IAAA,CAAK,QAAQ,GAAA,CAAI,KAAA;AACjB,IAAA,IAAA,CAAK,YAAY,GAAA,CAAI,SAAA;AACrB,IAAA,IAAA,CAAK,YAAY,GAAA,CAAI,SAAA;AACrB,IAAA,IAAA,CAAK,YAAY,GAAA,CAAI,SAAA;AACrB,IAAA,IAAA,CAAK,iBAAiB,GAAA,CAAI,OAAA;AAE1B,IAAA,MAAM,WAAW,GAAA,CAAI,KAAA;AACrB,IAAA,IAAI,QAAA,EAAU;AACZ,MAAA,IAAA,CAAK,SAAA,GAAY,QAAA;AAAA,IACnB,CAAA,MAAA,IAAW,OAAO,UAAA,CAAW,KAAA,KAAU,UAAA,EAAY;AACjD,MAAA,IAAA,CAAK,SAAA,GAAY,UAAA,CAAW,KAAA,CAAM,IAAA,CAAK,UAAU,CAAA;AAAA,IACnD,CAAA,MAAO;AACL,MAAA,MAAM,IAAI,SAAA;AAAA,QACR;AAAA,OACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,IAAA,CAAQ,MAAA,EAAgB,IAAA,EAAc,IAAA,GAAoB,EAAC,EAAe;AAC9E,IAAA,MAAM,EAAE,QAAQ,QAAA,EAAU,OAAA,KAAY,IAAA,CAAK,WAAA,CAAY,KAAK,MAAM,CAAA;AAClE,IAAA,IAAI;AACF,MAAA,MAAM,MAAM,MAAM,IAAA,CAAK,QAAQ,MAAA,EAAQ,IAAA,EAAM,MAAM,QAAQ,CAAA;AAC3D,MAAA,IAAI,CAAC,GAAA,CAAI,EAAA,EAAI,MAAM,MAAM,kBAAkB,GAAG,CAAA;AAC9C,MAAA,OAAO,MAAM,IAAA,CAAK,UAAA,CAAc,GAAG,CAAA;AAAA,IACrC,CAAA,SAAE;AACA,MAAA,OAAA,EAAQ;AAAA,IACV;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,SAAA,CAAU,MAAA,EAAgB,IAAA,EAAc,IAAA,GAAoB,EAAC,EAAkB;AACnF,IAAA,MAAM,EAAE,QAAQ,QAAA,EAAU,OAAA,KAAY,IAAA,CAAK,WAAA,CAAY,KAAK,MAAM,CAAA;AAClE,IAAA,IAAI;AACF,MAAA,MAAM,MAAM,MAAM,IAAA,CAAK,QAAQ,MAAA,EAAQ,IAAA,EAAM,MAAM,QAAQ,CAAA;AAC3D,MAAA,IAAI,CAAC,GAAA,CAAI,EAAA,EAAI,MAAM,MAAM,kBAAkB,GAAG,CAAA;AAE9C,MAAA,MAAM,GAAA,CAAI,IAAA,EAAM,MAAA,EAAO,CAAE,MAAM,MAAM;AAAA,MAAC,CAAC,CAAA;AAAA,IACzC,CAAA,SAAE;AACA,MAAA,OAAA,EAAQ;AAAA,IACV;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,MAAA,CACJ,IAAA,EACA,KAAA,EACA,MAAA,EACmB;AACnB,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,OAAA;AAAA,MACrB,KAAA;AAAA,MACA,IAAA;AAAA,MACA,EAAE,OAAO,MAAA,EAAO;AAAA,MAChB,MAAA;AAAA,MACA;AAAA,KACF;AACA,IAAA,IAAI,CAAC,GAAA,CAAI,EAAA,EAAI,MAAM,MAAM,kBAAkB,GAAG,CAAA;AAC9C,IAAA,OAAO,GAAA;AAAA,EACT;AAAA,EAEA,MAAc,OAAA,CACZ,MAAA,EACA,IAAA,EACA,IAAA,EACA,QACA,MAAA,EACmB;AACnB,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,QAAA,CAAS,IAAA,EAAM,KAAK,KAAK,CAAA;AAC1C,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,IAAA,KAAS,MAAA,IAAa,KAAK,IAAA,KAAS,IAAA;AACzD,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,YAAA,CAAa,OAAA,EAAS,MAAM,CAAA;AACjD,IAAA,MAAM,IAAA,GAAoB,EAAE,MAAA,EAAQ,OAAA,EAAQ;AAC5C,IAAA,IAAI,SAAS,IAAA,CAAK,IAAA,GAAO,IAAA,CAAK,SAAA,CAAU,KAAK,IAAI,CAAA;AACjD,IAAA,IAAI,MAAA,OAAa,MAAA,GAAS,MAAA;AAE1B,IAAA,IAAI;AACF,MAAA,OAAO,MAAM,IAAA,CAAK,SAAA,CAAU,GAAA,EAAK,IAAI,CAAA;AAAA,IACvC,SAAS,GAAA,EAAK;AAGZ,MAAA,IAAI,MAAA,EAAQ,SAAS,MAAM,GAAA;AAC3B,MAAA,MAAM,IAAI,SAAA,CAAU,CAAA,gBAAA,EAAmB,MAAM,CAAA,CAAA,EAAI,GAAG,CAAA,CAAA,EAAI,EAAE,KAAA,EAAO,GAAA,EAAK,CAAA;AAAA,IACxE;AAAA,EACF;AAAA,EAEQ,QAAA,CAAS,MAAc,KAAA,EAAuB;AACpD,IAAA,MAAM,IAAI,IAAA,CAAK,UAAA,CAAW,GAAG,CAAA,GAAI,OAAO,GAAA,GAAM,IAAA;AAC9C,IAAA,MAAM,CAAA,GAAI,IAAI,GAAA,CAAI,CAAA,EAAG,KAAK,OAAO,CAAA;AACjC,IAAA,IAAI,KAAA,EAAO;AACT,MAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,KAAK,CAAA,EAAG;AAChD,QAAA,IAAI,KAAA,KAAU,UAAa,KAAA,KAAU,EAAA,IAAM,YAAA,CAAa,GAAA,CAAI,KAAK,KAAK,CAAA;AAAA,MACxE;AAAA,IACF;AACA,IAAA,OAAO,EAAE,QAAA,EAAS;AAAA,EACpB;AAAA,EAEQ,YAAA,CAAa,SAAkB,MAAA,EAAyC;AAC9E,IAAA,MAAM,UAAkC,EAAE,GAAI,IAAA,CAAK,cAAA,IAAkB,EAAC,EAAG;AACzE,IAAA,IAAI,OAAA,EAAS,OAAA,CAAQ,cAAc,CAAA,GAAI,kBAAA;AACvC,IAAA,IAAI,KAAK,KAAA,EAAO,OAAA,CAAQ,eAAe,CAAA,GAAI,CAAA,OAAA,EAAU,KAAK,KAAK,CAAA,CAAA;AAC/D,IAAA,IAAI,IAAA,CAAK,SAAA,EAAW,OAAA,CAAQ,YAAY,IAAI,IAAA,CAAK,SAAA;AACjD,IAAA,IAAI,KAAK,SAAA,EAAW;AAClB,MAAA,MAAM,EAAA,GAAK,KAAK,SAAA,EAAU;AAC1B,MAAA,IAAI,EAAA,EAAI,OAAA,CAAQA,kBAAiB,CAAA,GAAI,EAAA;AAAA,IACvC;AACA,IAAA,IAAI,MAAA,EAAQ,OAAA,CAAQ,QAAQ,CAAA,GAAI,MAAA;AAChC,IAAA,OAAO,OAAA;AAAA,EACT;AAAA,EAEA,MAAc,WAAc,GAAA,EAA2B;AACrD,IAAA,MAAM,IAAA,GAAO,MAAM,GAAA,CAAI,IAAA,EAAK;AAC5B,IAAA,IAAI,CAAC,MAAM,OAAO,MAAA;AAClB,IAAA,IAAI;AACF,MAAA,OAAO,IAAA,CAAK,MAAM,IAAI,CAAA;AAAA,IACxB,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,IAAI,SAAA,CAAU,sBAAA,EAAwB,EAAE,KAAA,EAAO,KAAK,CAAA;AAAA,IAC5D;AAAA,EACF;AAAA,EAEQ,YAAY,YAAA,EAGlB;AACA,IAAA,MAAM,OAAA,GAA0C,CAAC,YAAY,CAAA;AAC7D,IAAA,IAAI,IAAA,CAAK,SAAA,IAAa,IAAA,CAAK,SAAA,GAAY,CAAA,EAAG;AACxC,MAAA,OAAA,CAAQ,IAAA,CAAK,WAAA,CAAY,OAAA,CAAQ,IAAA,CAAK,SAAS,CAAC,CAAA;AAAA,IAClD;AACA,IAAA,OAAO,eAAe,OAAO,CAAA;AAAA,EAC/B;AACF,CAAA;;;ACpNO,SAAS,UAAA,CAAW,OAAkC,IAAA,EAAsB;AACjF,EAAA,IAAI,CAAC,KAAA,EAAO;AACV,IAAA,MAAM,IAAI,SAAA,CAAU,CAAA,EAAG,IAAI,CAAA,YAAA,CAAc,CAAA;AAAA,EAC3C;AACA,EAAA,OAAO,KAAA;AACT;;;ACqIO,IAAM,KAAA,GAAQ;AAAA,EACnB,YAAA,EAAc,cAAA;AAAA,EACd,OAAA,EAAS,SAAA;AAAA,EACT,QAAA,EAAU,UAAA;AAAA,EACV,UAAA,EAAY,YAAA;AAAA,EACZ,SAAA,EAAW,WAAA;AAAA,EACX,MAAA,EAAQ;AACV;AAKO,SAAS,gBAAgB,KAAA,EAAwB;AACtD,EAAA,OAAO,KAAA,KAAU,KAAA,CAAM,SAAA,IAAa,KAAA,KAAU,KAAA,CAAM,MAAA;AACtD;;;ACpJA,IAAM,WAAA,GAAc,OAAA;AAGpB,eAAsB,YAAA,CACpB,CAAA,EACA,IAAA,EACA,MAAA,EAC+B;AAC/B,EAAA,MAAM,IAAA,GAAO,CAAA,iBAAA,EAAoB,kBAAA,CAAmB,IAAI,CAAC,CAAA,OAAA,CAAA;AACzD,EAAA,MAAM,MAAM,MAAM,CAAA,CAAE,MAAA,CAAO,IAAA,EAAM,QAAW,MAAM,CAAA;AAClD,EAAA,OAAO,gBAAA,CAAiB,KAAK,MAAM,CAAA;AACrC;AAEA,SAAS,gBAAA,CAAiB,KAAe,MAAA,EAA4C;AACnF,EAAA,OAAO;AAAA,IACL,QAAQ,MAAA,CAAO,aAAa,CAAA,GAA2B;AACrD,MAAA,MAAM,OAAO,GAAA,CAAI,IAAA;AACjB,MAAA,IAAI,CAAC,IAAA,EAAM;AAEX,MAAA,MAAM,MAAA,GAAS,KAAK,SAAA,EAAU;AAC9B,MAAA,MAAM,OAAA,GAAU,IAAI,WAAA,CAAY,OAAO,CAAA;AACvC,MAAA,IAAI,MAAA,GAAS,EAAA;AACb,MAAA,IAAI,IAAA,GAAO,EAAA;AACX,MAAA,IAAI,OAAA,GAAU,KAAA;AAEd,MAAA,IAAI;AACF,QAAA,OAAO,IAAA,EAAM;AACX,UAAA,IAAI,KAAA;AACJ,UAAA,IAAI;AACF,YAAA,KAAA,GAAQ,MAAM,OAAO,IAAA,EAAK;AAAA,UAC5B,SAAS,GAAA,EAAK;AAEZ,YAAA,IAAI,QAAQ,OAAA,EAAS;AACrB,YAAA,MAAM,IAAI,SAAA,CAAU,0BAAA,EAA4B,EAAE,KAAA,EAAO,KAAK,CAAA;AAAA,UAChE;AACA,UAAA,IAAI,MAAM,IAAA,EAAM;AAEd,YAAA;AAAA,UACF;AAEA,UAAA,MAAA,IAAU,QAAQ,MAAA,CAAO,KAAA,CAAM,OAAO,EAAE,MAAA,EAAQ,MAAM,CAAA;AAEtD,UAAA,IAAI,EAAA;AACJ,UAAA,OAAA,CAAQ,EAAA,GAAK,MAAA,CAAO,OAAA,CAAQ,IAAI,MAAM,CAAA,EAAG;AACvC,YAAA,IAAI,IAAA,GAAO,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA;AAC7B,YAAA,MAAA,GAAS,MAAA,CAAO,KAAA,CAAM,EAAA,GAAK,CAAC,CAAA;AAC5B,YAAA,IAAI,IAAA,CAAK,SAAS,IAAI,CAAA,SAAU,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA,CAAE,CAAA;AAEhD,YAAA,IAAI,SAAS,EAAA,EAAI;AAEf,cAAA,IAAI,CAAC,OAAA,EAAS;AACd,cAAA,MAAM,OAAA,GAAU,IAAA;AAChB,cAAA,IAAA,GAAO,EAAA;AACP,cAAA,OAAA,GAAU,KAAA;AAEV,cAAA,IAAI,KAAA;AACJ,cAAA,IAAI;AACF,gBAAA,KAAA,GAAQ,IAAA,CAAK,MAAM,OAAO,CAAA;AAAA,cAC5B,SAAS,GAAA,EAAK;AACZ,gBAAA,MAAM,IAAI,SAAA,CAAU,cAAA,EAAgB,EAAE,KAAA,EAAO,KAAK,CAAA;AAAA,cACpD;AACA,cAAA,MAAM,KAAA;AACN,cAAA,IAAI,eAAA,CAAgB,KAAA,CAAM,KAAK,CAAA,EAAG;AAAA,YACpC,CAAA,MAAA,IAAW,IAAA,CAAK,UAAA,CAAW,GAAG,CAAA,EAAG;AAE/B,cAAA;AAAA,YACF,CAAA,MAAA,IAAW,IAAA,CAAK,UAAA,CAAW,WAAW,CAAA,EAAG;AAGvC,cAAA,IAAI,CAAA,GAAI,IAAA,CAAK,KAAA,CAAM,WAAA,CAAY,MAAM,CAAA;AACrC,cAAA,IAAI,EAAE,UAAA,CAAW,GAAG,GAAG,CAAA,GAAI,CAAA,CAAE,MAAM,CAAC,CAAA;AACpC,cAAA,IAAA,IAAQ,CAAA;AACR,cAAA,OAAA,GAAU,IAAA;AAAA,YACZ;AAAA,UAEF;AAAA,QACF;AAAA,MACF,CAAA,SAAE;AAEA,QAAA,IAAI;AACF,UAAA,MAAM,OAAO,MAAA,EAAO;AAAA,QACtB,CAAA,CAAA,MAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF;AAAA,GACF;AACF;;;AChFO,IAAM,sBAAN,MAA0B;AAAA,EAC/B,YAA6B,CAAA,EAAc;AAAd,IAAA,IAAA,CAAA,CAAA,GAAA,CAAA;AAAA,EAAe;AAAA,EAAf,CAAA;AAAA;AAAA,EAG7B,MAAM,IAAA,CACJ,OAAA,GAAmC,EAAC,EACpC,IAAA,GAAoB,EAAC,EACO;AAC5B,IAAA,MAAM,MAAM,MAAM,IAAA,CAAK,CAAA,CAAE,IAAA,CAAsB,OAAO,kBAAA,EAAoB;AAAA,MACxE,KAAA,EAAO,EAAE,OAAA,EAAS,OAAA,CAAQ,MAAA,EAAQ,OAAO,OAAA,CAAQ,KAAA,EAAO,OAAA,EAAS,OAAA,CAAQ,MAAA,EAAO;AAAA,MAChF,QAAQ,IAAA,CAAK;AAAA,KACd,CAAA;AACD,IAAA,OAAO,GAAA,CAAI,gBAAgB,EAAC;AAAA,EAC9B;AAAA;AAAA,EAGA,MAAM,GAAA,CAAI,IAAA,EAAc,IAAA,GAAoB,EAAC,EAA6B;AACxE,IAAA,UAAA,CAAW,MAAM,OAAO,CAAA;AACxB,IAAA,OAAO,KAAK,CAAA,CAAE,IAAA;AAAA,MACZ,KAAA;AAAA,MACA,CAAA,iBAAA,EAAoB,kBAAA,CAAmB,IAAI,CAAC,CAAA,CAAA;AAAA,MAC5C,EAAE,MAAA,EAAQ,IAAA,CAAK,MAAA;AAAO,KACxB;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,MAAA,CAAO,IAAA,EAAqB,IAAA,GAAoB,EAAC,EAA6B;AAClF,IAAA,OAAO,IAAA,CAAK,CAAA,CAAE,IAAA,CAAsB,MAAA,EAAQ,kBAAA,EAAoB;AAAA,MAC9D,IAAA;AAAA,MACA,QAAQ,IAAA,CAAK;AAAA,KACd,CAAA;AAAA,EACH;AAAA;AAAA,EAGA,MAAM,KAAA,CAAM,IAAA,EAAc,IAAA,GAAoB,EAAC,EAA6B;AAC1E,IAAA,UAAA,CAAW,MAAM,OAAO,CAAA;AACxB,IAAA,OAAO,KAAK,CAAA,CAAE,IAAA;AAAA,MACZ,MAAA;AAAA,MACA,CAAA,iBAAA,EAAoB,kBAAA,CAAmB,IAAI,CAAC,CAAA,CAAA;AAAA,MAC5C,EAAE,OAAO,EAAE,MAAA,EAAQ,SAAQ,EAAG,MAAA,EAAQ,KAAK,MAAA;AAAO,KACpD;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,WAAA,CAAY,IAAA,EAAc,IAAA,GAAoB,EAAC,EAAkB;AACrE,IAAA,UAAA,CAAW,MAAM,OAAO,CAAA;AACxB,IAAA,MAAM,IAAA,CAAK,EAAE,SAAA,CAAU,MAAA,EAAQ,oBAAoB,kBAAA,CAAmB,IAAI,CAAC,CAAA,CAAA,EAAI;AAAA,MAC7E,KAAA,EAAO,EAAE,MAAA,EAAQ,cAAA,EAAe;AAAA,MAChC,QAAQ,IAAA,CAAK;AAAA,KACd,CAAA;AAAA,EACH;AAAA;AAAA,EAGA,MAAM,OAAA,CAAQ,IAAA,EAAc,IAAA,GAAoB,EAAC,EAAkB;AACjE,IAAA,UAAA,CAAW,MAAM,OAAO,CAAA;AACxB,IAAA,MAAM,IAAA,CAAK,EAAE,SAAA,CAAU,QAAA,EAAU,oBAAoB,kBAAA,CAAmB,IAAI,CAAC,CAAA,CAAA,EAAI;AAAA,MAC/E,QAAQ,IAAA,CAAK;AAAA,KACd,CAAA;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,MAAM,MAAA,CAAO,IAAA,EAAc,IAAA,GAAoB,EAAC,EAAkC;AAChF,IAAA,UAAA,CAAW,MAAM,OAAO,CAAA;AACxB,IAAA,OAAO,YAAA,CAAa,IAAA,CAAK,CAAA,EAAG,IAAA,EAAM,KAAK,MAAM,CAAA;AAAA,EAC/C;AACF;;;AC5EO,IAAM,mBAAN,MAAuB;AAAA,EAC5B,YAA6B,CAAA,EAAc;AAAd,IAAA,IAAA,CAAA,CAAA,GAAA,CAAA;AAAA,EAAe;AAAA,EAAf,CAAA;AAAA;AAAA,EAG7B,MAAM,OACJ,IAAA,EACA,IAAA,GAAwB,EAAC,EACzB,IAAA,GAAoB,EAAC,EACF;AACnB,IAAA,UAAA,CAAW,MAAM,OAAO,CAAA;AACxB,IAAA,OAAO,KAAK,CAAA,CAAE,IAAA;AAAA,MACZ,MAAA;AAAA,MACA,CAAA,iBAAA,EAAoB,kBAAA,CAAmB,IAAI,CAAC,CAAA,UAAA,CAAA;AAAA,MAC5C,EAAE,IAAA,EAAM,MAAA,EAAQ,IAAA,CAAK,MAAA;AAAO,KAC9B;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,IAAA,CACJ,OAAA,GAAgC,EAAC,EACjC,IAAA,GAAoB,EAAC,EACA;AACrB,IAAA,MAAM,MAAM,MAAM,IAAA,CAAK,CAAA,CAAE,IAAA,CAAmB,OAAO,eAAA,EAAiB;AAAA,MAClE,KAAA,EAAO;AAAA,QACL,OAAO,OAAA,CAAQ,IAAA;AAAA,QACf,SAAS,OAAA,CAAQ,MAAA;AAAA,QACjB,WAAW,OAAA,CAAQ,QAAA;AAAA,QACnB,OAAO,OAAA,CAAQ;AAAA,OACjB;AAAA,MACA,QAAQ,IAAA,CAAK;AAAA,KACd,CAAA;AACD,IAAA,OAAO,GAAA,CAAI,aAAa,EAAC;AAAA,EAC3B;AAAA;AAAA,EAGA,MAAM,GAAA,CAAI,UAAA,EAAoB,IAAA,GAAoB,EAAC,EAAsB;AACvE,IAAA,UAAA,CAAW,YAAY,aAAa,CAAA;AACpC,IAAA,OAAO,KAAK,CAAA,CAAE,IAAA;AAAA,MACZ,KAAA;AAAA,MACA,CAAA,cAAA,EAAiB,kBAAA,CAAmB,UAAU,CAAC,CAAA,CAAA;AAAA,MAC/C,EAAE,MAAA,EAAQ,IAAA,CAAK,MAAA;AAAO,KACxB;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,MAAA,CAAO,UAAA,EAAoB,IAAA,GAAoB,EAAC,EAAkB;AACtE,IAAA,UAAA,CAAW,YAAY,aAAa,CAAA;AACpC,IAAA,MAAM,IAAA,CAAK,EAAE,SAAA,CAAU,QAAA,EAAU,iBAAiB,kBAAA,CAAmB,UAAU,CAAC,CAAA,CAAA,EAAI;AAAA,MAClF,QAAQ,IAAA,CAAK;AAAA,KACd,CAAA;AAAA,EACH;AAAA;AAAA,EAGA,MAAM,OAAA,CAAQ,UAAA,EAAoB,IAAA,GAAoB,EAAC,EAAkB;AACvE,IAAA,UAAA,CAAW,YAAY,aAAa,CAAA;AACpC,IAAA,MAAM,IAAA,CAAK,EAAE,SAAA,CAAU,MAAA,EAAQ,iBAAiB,kBAAA,CAAmB,UAAU,CAAC,CAAA,CAAA,EAAI;AAAA,MAChF,KAAA,EAAO,EAAE,MAAA,EAAQ,SAAA,EAAU;AAAA,MAC3B,QAAQ,IAAA,CAAK;AAAA,KACd,CAAA;AAAA,EACH;AACF;;;ACpEO,IAAM,eAAN,MAAmB;AAAA,EACxB,YAA6B,CAAA,EAAc;AAAd,IAAA,IAAA,CAAA,CAAA,GAAA,CAAA;AAAA,EAAe;AAAA,EAAf,CAAA;AAAA;AAAA,EAG7B,MAAM,QAAA,CAAS,IAAA,EAA2B,IAAA,GAAoB,EAAC,EAAkB;AAC/E,IAAA,OAAO,IAAA,CAAK,CAAA,CAAE,IAAA,CAAW,MAAA,EAAQ,WAAA,EAAa,EAAE,IAAA,EAAM,MAAA,EAAQ,IAAA,CAAK,MAAA,EAAQ,CAAA;AAAA,EAC7E;AAAA;AAAA,EAGA,MAAM,IAAA,CAAK,IAAA,GAAoB,EAAC,EAAoB;AAClD,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,CAAA,CAAE,IAAA,CAAe,KAAA,EAAO,WAAA,EAAa,EAAE,MAAA,EAAQ,IAAA,CAAK,MAAA,EAAQ,CAAA;AACnF,IAAA,OAAO,GAAA,CAAI,SAAS,EAAC;AAAA,EACvB;AAAA;AAAA,EAGA,MAAM,GAAA,CAAI,MAAA,EAAgB,IAAA,GAAoB,EAAC,EAAkB;AAC/D,IAAA,UAAA,CAAW,QAAQ,SAAS,CAAA;AAC5B,IAAA,OAAO,IAAA,CAAK,EAAE,IAAA,CAAW,KAAA,EAAO,aAAa,kBAAA,CAAmB,MAAM,CAAC,CAAA,CAAA,EAAI;AAAA,MACzE,QAAQ,IAAA,CAAK;AAAA,KACd,CAAA;AAAA,EACH;AAAA;AAAA,EAGA,MAAM,MAAA,CAAO,MAAA,EAAgB,IAAA,GAAoB,EAAC,EAAkB;AAClE,IAAA,MAAM,IAAA,CAAK,MAAA,CAAO,MAAA,EAAQ,QAAA,EAAU,IAAI,CAAA;AAAA,EAC1C;AAAA;AAAA,EAGA,MAAM,QAAA,CAAS,MAAA,EAAgB,IAAA,GAAoB,EAAC,EAAkB;AACpE,IAAA,MAAM,IAAA,CAAK,MAAA,CAAO,MAAA,EAAQ,UAAA,EAAY,IAAI,CAAA;AAAA,EAC5C;AAAA;AAAA,EAGA,MAAM,UAAA,CAAW,MAAA,EAAgB,IAAA,GAAoB,EAAC,EAAkB;AACtE,IAAA,UAAA,CAAW,QAAQ,SAAS,CAAA;AAC5B,IAAA,MAAM,IAAA,CAAK,EAAE,SAAA,CAAU,QAAA,EAAU,aAAa,kBAAA,CAAmB,MAAM,CAAC,CAAA,CAAA,EAAI;AAAA,MAC1E,QAAQ,IAAA,CAAK;AAAA,KACd,CAAA;AAAA,EACH;AAAA,EAEA,MAAc,MAAA,CAAO,MAAA,EAAgB,MAAA,EAAgB,IAAA,EAAkC;AACrF,IAAA,UAAA,CAAW,QAAQ,SAAS,CAAA;AAC5B,IAAA,MAAM,IAAA,CAAK,EAAE,SAAA,CAAU,MAAA,EAAQ,aAAa,kBAAA,CAAmB,MAAM,CAAC,CAAA,CAAA,EAAI;AAAA,MACxE,KAAA,EAAO,EAAE,MAAA,EAAO;AAAA,MAChB,QAAQ,IAAA,CAAK;AAAA,KACd,CAAA;AAAA,EACH;AACF;;;AC5CO,IAAM,iBAAN,MAAqB;AAAA,EAC1B,YAA6B,CAAA,EAAc;AAAd,IAAA,IAAA,CAAA,CAAA,GAAA,CAAA;AAAA,EAAe;AAAA,EAAf,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAM7B,MAAM,MAAA,CAAO,KAAA,EAAgB,IAAA,GAAoB,EAAC,EAA2B;AAC3E,IAAA,OAAO,IAAA,CAAK,CAAA,CAAE,IAAA,CAAoB,MAAA,EAAQ,cAAA,EAAgB;AAAA,MACxD,IAAA,EAAM,KAAA,GAAQ,EAAE,KAAA,KAAU,EAAC;AAAA,MAC3B,QAAQ,IAAA,CAAK;AAAA,KACd,CAAA;AAAA,EACH;AAAA;AAAA,EAGA,MAAM,IAAA,CAAK,IAAA,GAAoB,EAAC,EAAsB;AACpD,IAAA,MAAM,MAAM,MAAM,IAAA,CAAK,CAAA,CAAE,IAAA,CAAiB,OAAO,cAAA,EAAgB;AAAA,MAC/D,QAAQ,IAAA,CAAK;AAAA,KACd,CAAA;AACD,IAAA,OAAO,GAAA,CAAI,YAAY,EAAC;AAAA,EAC1B;AAAA;AAAA,EAGA,MAAM,MAAA,CAAO,EAAA,EAAY,IAAA,GAAoB,EAAC,EAAkB;AAC9D,IAAA,UAAA,CAAW,IAAI,IAAI,CAAA;AACnB,IAAA,MAAM,IAAA,CAAK,EAAE,SAAA,CAAU,QAAA,EAAU,gBAAgB,kBAAA,CAAmB,EAAE,CAAC,CAAA,CAAA,EAAI;AAAA,MACzE,QAAQ,IAAA,CAAK;AAAA,KACd,CAAA;AAAA,EACH;AACF;;;ACxCO,IAAM,OAAA,GAAU;;;ACgChB,IAAM,aAAN,MAAiB;AAAA,EACb,YAAA;AAAA,EACA,SAAA;AAAA,EACA,KAAA;AAAA,EACA,OAAA;AAAA,EAET,YAAY,OAAA,EAA4B;AACtC,IAAA,MAAM,SAAA,GAAY,IAAI,SAAA,CAAU;AAAA,MAC9B,SAAS,OAAA,CAAQ,OAAA;AAAA,MACjB,OAAO,OAAA,CAAQ,KAAA;AAAA,MACf,OAAO,OAAA,CAAQ,KAAA;AAAA,MACf,SAAA,EAAW,OAAA,CAAQ,SAAA,IAAa,CAAA,QAAA,EAAW,OAAO,CAAA,CAAA;AAAA,MAClD,WAAW,OAAA,CAAQ,SAAA;AAAA,MACnB,WAAW,OAAA,CAAQ,SAAA;AAAA,MACnB,SAAS,OAAA,CAAQ;AAAA,KAClB,CAAA;AACD,IAAA,IAAA,CAAK,YAAA,GAAe,IAAI,mBAAA,CAAoB,SAAS,CAAA;AACrD,IAAA,IAAA,CAAK,SAAA,GAAY,IAAI,gBAAA,CAAiB,SAAS,CAAA;AAC/C,IAAA,IAAA,CAAK,KAAA,GAAQ,IAAI,YAAA,CAAa,SAAS,CAAA;AACvC,IAAA,IAAA,CAAK,OAAA,GAAU,IAAI,cAAA,CAAe,SAAS,CAAA;AAAA,EAC7C;AACF","file":"index.js","sourcesContent":["// Error types for the Fuse SDK, mirroring sdks/go/errors.go: a structured\n// FuseApiError for non-2xx responses plus code-checking helpers, and a\n// FuseError for transport/configuration failures.\n\nconst REQUEST_ID_HEADER = \"X-Request-ID\";\n\n/** Stable, machine-readable error codes returned by the server. */\nexport const ErrorCode = {\n NotFound: \"not_found\",\n Conflict: \"conflict\",\n InvalidArgument: \"invalid_argument\",\n Unavailable: \"unavailable\",\n Internal: \"internal\",\n Unauthorized: \"unauthorized\",\n} as const;\n\nexport type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];\n\nconst STATUS_TEXT: Record<number, string> = {\n 400: \"bad request\",\n 401: \"unauthorized\",\n 403: \"forbidden\",\n 404: \"not found\",\n 409: \"conflict\",\n 500: \"internal server error\",\n 502: \"bad gateway\",\n 503: \"service unavailable\",\n};\n\nfunction statusText(status: number): string {\n return STATUS_TEXT[status] ?? `status ${status}`;\n}\n\nexport interface FuseApiErrorInit {\n status: number;\n code?: string;\n message?: string;\n details?: Record<string, string>;\n requestId?: string;\n body?: string;\n}\n\nfunction formatMessage(init: FuseApiErrorInit): string {\n const parts: string[] = [`status=${init.status}`];\n if (init.code) parts.push(`code=${init.code}`);\n if (init.message) parts.push(init.message);\n else parts.push(statusText(init.status));\n if (init.requestId) parts.push(`request_id=${init.requestId}`);\n return \"fuse api error: \" + parts.join(\", \");\n}\n\n/** FuseApiError is a non-2xx response from the server. */\nexport class FuseApiError extends Error {\n readonly status: number;\n readonly code: string;\n readonly details?: Record<string, string>;\n readonly requestId?: string;\n /** Raw response body, when available. */\n readonly body?: string;\n\n constructor(init: FuseApiErrorInit) {\n super(formatMessage(init));\n this.name = \"FuseApiError\";\n this.status = init.status;\n this.code = init.code ?? \"\";\n this.details = init.details;\n this.requestId = init.requestId;\n this.body = init.body;\n // Restore the prototype chain so `instanceof` works after transpilation.\n Object.setPrototypeOf(this, FuseApiError.prototype);\n }\n}\n\n/** FuseError is a transport, configuration, or decoding failure (not an API error). */\nexport class FuseError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = \"FuseError\";\n Object.setPrototypeOf(this, FuseError.prototype);\n }\n}\n\n/** isFuseApiError is a type guard for FuseApiError. */\nexport function isFuseApiError(err: unknown): err is FuseApiError {\n return err instanceof FuseApiError;\n}\n\nfunction hasCode(err: unknown, code: string): boolean {\n return isFuseApiError(err) && err.code === code;\n}\n\n/** isNotFound reports whether err is a not_found api error. */\nexport function isNotFound(err: unknown): boolean {\n return hasCode(err, ErrorCode.NotFound);\n}\n\n/** isConflict reports whether err is a conflict api error. */\nexport function isConflict(err: unknown): boolean {\n return hasCode(err, ErrorCode.Conflict);\n}\n\n/** isUnauthorized reports whether err is an unauthorized api error. */\nexport function isUnauthorized(err: unknown): boolean {\n return hasCode(err, ErrorCode.Unauthorized);\n}\n\n/** isInvalidArgument reports whether err is an invalid_argument api error. */\nexport function isInvalidArgument(err: unknown): boolean {\n return hasCode(err, ErrorCode.InvalidArgument);\n}\n\n/** isUnavailable reports whether err is an unavailable api error. */\nexport function isUnavailable(err: unknown): boolean {\n return hasCode(err, ErrorCode.Unavailable);\n}\n\ninterface ErrorEnvelope {\n error?: {\n code?: string;\n message?: string;\n details?: Record<string, string>;\n };\n}\n\nfunction parseApiError(\n status: number,\n requestId: string | undefined,\n body: string,\n): FuseApiError {\n if (body) {\n try {\n const env = JSON.parse(body) as ErrorEnvelope;\n const e = env?.error;\n if (e && (e.code || e.message)) {\n return new FuseApiError({\n status,\n code: e.code,\n message: e.message,\n details: e.details,\n requestId,\n body,\n });\n }\n } catch {\n // fall through to the status-text error below\n }\n }\n return new FuseApiError({ status, message: statusText(status), requestId, body });\n}\n\n/**\n * errorFromResponse builds a FuseApiError from a non-2xx Response, parsing the\n * `{\"error\":{code,message,details}}` envelope and reading the X-Request-ID\n * response header for correlation.\n */\nexport async function errorFromResponse(res: Response): Promise<FuseApiError> {\n const requestId = res.headers.get(REQUEST_ID_HEADER) ?? undefined;\n let body = \"\";\n try {\n body = await res.text();\n } catch (err) {\n return new FuseApiError({\n status: res.status,\n message: `read error body: ${String(err)}`,\n requestId,\n });\n }\n return parseApiError(res.status, requestId, body);\n}\n","// The transport layer: builds requests (URL, headers, auth, X-Request-ID,\n// JSON body), applies an optional client-default timeout, executes fetch, and\n// turns non-2xx responses into FuseApiError. Mirrors the transport in\n// sdks/go/environments.go. SSE streams use stream() which deliberately applies\n// no timeout (the Go SDK uses a separate Timeout:0 client for events).\n\nimport { FuseError, errorFromResponse } from \"./errors.js\";\n\nconst REQUEST_ID_HEADER = \"X-Request-ID\";\n\n/** A minimal fetch signature, satisfied by the global fetch and most polyfills. */\nexport type FetchLike = (input: string | URL, init?: RequestInit) => Promise<Response>;\n\n/** Per-call options accepted by every service method. */\nexport interface CallOptions {\n /** Abort the request (and, for events, the stream). */\n signal?: AbortSignal;\n}\n\nexport interface TransportConfig {\n baseUrl: string;\n token?: string;\n fetch?: FetchLike;\n userAgent?: string;\n requestId?: () => string;\n timeoutMs?: number;\n headers?: Record<string, string>;\n}\n\ntype Query = Record<string, string | undefined>;\n\ninterface RequestSpec {\n query?: Query;\n body?: unknown;\n signal?: AbortSignal;\n}\n\n/** Combine zero or more AbortSignals into one, without relying on AbortSignal.any (Node 20+). */\nfunction combineSignals(signals: Array<AbortSignal | undefined>): {\n signal?: AbortSignal;\n cleanup: () => void;\n} {\n const active = signals.filter((s): s is AbortSignal => s != null);\n if (active.length === 0) return { cleanup: () => {} };\n if (active.length === 1) return { signal: active[0], cleanup: () => {} };\n\n const controller = new AbortController();\n const onAbort = (event: Event) => {\n const target = event.target as AbortSignal;\n controller.abort(target.reason);\n cleanup();\n };\n const cleanup = () => {\n for (const s of active) s.removeEventListener(\"abort\", onAbort);\n };\n for (const s of active) {\n if (s.aborted) {\n controller.abort(s.reason);\n cleanup();\n return { signal: controller.signal, cleanup: () => {} };\n }\n s.addEventListener(\"abort\", onAbort);\n }\n return { signal: controller.signal, cleanup };\n}\n\nexport class Transport {\n private readonly baseUrl: URL;\n private readonly token?: string;\n private readonly fetchImpl: FetchLike;\n private readonly userAgent?: string;\n private readonly requestId?: () => string;\n private readonly timeoutMs?: number;\n private readonly defaultHeaders?: Record<string, string>;\n\n constructor(cfg: TransportConfig) {\n if (!cfg.baseUrl) {\n throw new FuseError(\"base url is required\");\n }\n let parsed: URL;\n try {\n parsed = new URL(cfg.baseUrl);\n } catch {\n throw new FuseError(\"base url is invalid\");\n }\n this.baseUrl = parsed;\n this.token = cfg.token;\n this.userAgent = cfg.userAgent;\n this.requestId = cfg.requestId;\n this.timeoutMs = cfg.timeoutMs;\n this.defaultHeaders = cfg.headers;\n\n const provided = cfg.fetch;\n if (provided) {\n this.fetchImpl = provided;\n } else if (typeof globalThis.fetch === \"function\") {\n this.fetchImpl = globalThis.fetch.bind(globalThis) as FetchLike;\n } else {\n throw new FuseError(\n \"global fetch is not available; pass a fetch implementation via the `fetch` option (Node 18+ required)\",\n );\n }\n }\n\n /** Perform a request and decode a JSON response body. */\n async json<T>(method: string, path: string, spec: RequestSpec = {}): Promise<T> {\n const { signal: combined, cleanup } = this.withTimeout(spec.signal);\n try {\n const res = await this.execute(method, path, spec, combined);\n if (!res.ok) throw await errorFromResponse(res);\n return await this.decodeJson<T>(res);\n } finally {\n cleanup();\n }\n }\n\n /** Perform a request that returns no body (e.g. 204 actions). */\n async noContent(method: string, path: string, spec: RequestSpec = {}): Promise<void> {\n const { signal: combined, cleanup } = this.withTimeout(spec.signal);\n try {\n const res = await this.execute(method, path, spec, combined);\n if (!res.ok) throw await errorFromResponse(res);\n // Release the connection even if the server sent a body.\n await res.body?.cancel().catch(() => {});\n } finally {\n cleanup();\n }\n }\n\n /**\n * Open a long-lived stream (SSE). Applies NO timeout — only the caller's\n * signal cancels it — and checks the status eagerly so connect-time errors\n * (404/401/...) surface as a FuseApiError before any iteration.\n */\n async stream(\n path: string,\n query: Query | undefined,\n signal?: AbortSignal,\n ): Promise<Response> {\n const res = await this.execute(\n \"GET\",\n path,\n { query, signal },\n signal,\n \"text/event-stream\",\n );\n if (!res.ok) throw await errorFromResponse(res);\n return res;\n }\n\n private async execute(\n method: string,\n path: string,\n spec: RequestSpec,\n signal: AbortSignal | undefined,\n accept?: string,\n ): Promise<Response> {\n const url = this.buildUrl(path, spec.query);\n const hasBody = spec.body !== undefined && spec.body !== null;\n const headers = this.buildHeaders(hasBody, accept);\n const init: RequestInit = { method, headers };\n if (hasBody) init.body = JSON.stringify(spec.body);\n if (signal) init.signal = signal;\n\n try {\n return await this.fetchImpl(url, init);\n } catch (err) {\n // A caller- or timeout-initiated abort propagates as-is; everything else\n // is wrapped so consumers get a consistent FuseError with a cause.\n if (signal?.aborted) throw err;\n throw new FuseError(`request failed: ${method} ${url}`, { cause: err });\n }\n }\n\n private buildUrl(path: string, query?: Query): string {\n const p = path.startsWith(\"/\") ? path : \"/\" + path;\n const u = new URL(p, this.baseUrl);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== \"\") u.searchParams.set(key, value);\n }\n }\n return u.toString();\n }\n\n private buildHeaders(hasBody: boolean, accept?: string): Record<string, string> {\n const headers: Record<string, string> = { ...(this.defaultHeaders ?? {}) };\n if (hasBody) headers[\"Content-Type\"] = \"application/json\";\n if (this.token) headers[\"Authorization\"] = `Bearer ${this.token}`;\n if (this.userAgent) headers[\"User-Agent\"] = this.userAgent;\n if (this.requestId) {\n const id = this.requestId();\n if (id) headers[REQUEST_ID_HEADER] = id;\n }\n if (accept) headers[\"Accept\"] = accept;\n return headers;\n }\n\n private async decodeJson<T>(res: Response): Promise<T> {\n const text = await res.text();\n if (!text) return undefined as T;\n try {\n return JSON.parse(text) as T;\n } catch (err) {\n throw new FuseError(\"decode response body\", { cause: err });\n }\n }\n\n private withTimeout(callerSignal?: AbortSignal): {\n signal?: AbortSignal;\n cleanup: () => void;\n } {\n const signals: Array<AbortSignal | undefined> = [callerSignal];\n if (this.timeoutMs && this.timeoutMs > 0) {\n signals.push(AbortSignal.timeout(this.timeoutMs));\n }\n return combineSignals(signals);\n }\n}\n","import { FuseError } from \"./errors.js\";\n\n/**\n * requireArg throws a FuseError when a required path argument is missing,\n * mirroring the Go SDK's \"<thing> is required\" guards.\n */\nexport function requireArg(value: string | undefined | null, name: string): string {\n if (!value) {\n throw new FuseError(`${name} is required`);\n }\n return value;\n}\n","// Wire types for the Fuse API. Field names match the JSON sent on the wire\n// (snake_case), so request/response bodies pass through JSON.stringify /\n// JSON.parse untouched — a faithful 1:1 mirror of the Go SDK's struct tags\n// (see sdks/go/types.go). List *options* (see each service) use camelCase\n// keys and are translated to snake_case query params by the service.\n\n/** Spec is the hardware/runtime spec for a microVM. */\nexport interface Spec {\n cpus?: number;\n ram_mb?: number;\n storage_gb?: number;\n region?: string;\n max_runtime_seconds?: number;\n}\n\n/** CreateRequest is the body for environments.create. */\nexport interface CreateRequest {\n task_id: string;\n spec?: Spec;\n manifest_inline?: string;\n secrets?: Record<string, string>;\n startup_script?: string;\n gateway_url?: string;\n gateway_token?: string;\n}\n\n/** EnvironmentInfo is the server's view of a single microVM. */\nexport interface EnvironmentInfo {\n id: string;\n state: string;\n task_id: string;\n host_id?: string;\n url: string;\n spec: Spec;\n created_at: string;\n updated_at: string;\n error?: string;\n}\n\n/**\n * Event is one item from environments.events. It matches the server's SSE\n * wire payload. Stream-level failures are thrown from the async iterator\n * rather than delivered as an in-band event.\n */\nexport interface Event {\n id: string;\n /** Event kind. v1 only emits \"state\". */\n event: string;\n vm_id: string;\n state: string;\n url?: string;\n error?: string;\n updated_at: string;\n}\n\n/** SnapshotRequest is the optional body for snapshots.create. */\nexport interface SnapshotRequest {\n comment?: string;\n mode?: string;\n retention_seconds?: number;\n metadata?: Record<string, string>;\n export_ref?: string;\n export_status?: string;\n}\n\n/** SnapshotExport is an optional exported snapshot artifact. */\nexport interface SnapshotExport {\n destination: string;\n status?: string;\n requested_at?: string;\n updated_at?: string;\n last_error?: string;\n}\n\n/** Snapshot is a persisted snapshot record. */\nexport interface Snapshot {\n id: string;\n vm_id: string;\n task_id?: string;\n tenant_id?: string;\n parent_snapshot_id?: string;\n mode?: string;\n state?: string;\n comment?: string;\n size_bytes?: number;\n created_at: string;\n updated_at?: string;\n retention_until?: string;\n last_error?: string;\n export_ref?: string;\n exports?: SnapshotExport[];\n}\n\n/** HostCapacity is a host's resource envelope. */\nexport interface HostCapacity {\n cpus: number;\n ram_mb: number;\n storage_gb: number;\n vm_count: number;\n}\n\n/** RegisterHostRequest is the body for hosts.register. */\nexport interface RegisterHostRequest {\n id: string;\n url: string;\n token?: string;\n region?: string;\n capacity?: HostCapacity;\n}\n\n/** Host is the server's view of a registered host. */\nexport interface Host {\n id: string;\n url: string;\n region?: string;\n state: string;\n capacity: HostCapacity;\n allocated: HostCapacity;\n last_seen: string;\n created_at: string;\n updated_at: string;\n}\n\n/**\n * APIKey is a key's metadata. The raw secret appears only in\n * CreatedAPIKey.key, returned once at creation.\n */\nexport interface APIKey {\n id: string;\n label?: string;\n created_at: string;\n last_used_at?: string;\n revoked_at?: string;\n}\n\n/**\n * CreatedAPIKey is returned by apiKeys.create. `key` is the raw secret and is\n * unrecoverable after this response.\n */\nexport interface CreatedAPIKey extends APIKey {\n key: string;\n}\n\n/** Lifecycle states for EnvironmentInfo.state and Event.state. */\nexport const State = {\n Provisioning: \"provisioning\",\n Running: \"running\",\n Draining: \"draining\",\n Destroying: \"destroying\",\n Destroyed: \"destroyed\",\n Failed: \"failed\",\n} as const;\n\nexport type State = (typeof State)[keyof typeof State];\n\n/** isTerminalState reports whether state is a terminal lifecycle state. */\nexport function isTerminalState(state: string): boolean {\n return state === State.Destroyed || state === State.Failed;\n}\n","// SSE event streaming, porting the state machine in sdks/go/events.go to an\n// async iterator. The stream is opened (and its status checked) eagerly; the\n// returned AsyncIterable yields each Event and ends after a terminal-state\n// event (destroyed/failed) or on clean EOF. Stream-level failures are thrown\n// from the iterator. Caller abort is treated as normal termination.\n\nimport type { Transport } from \"./transport.js\";\nimport { FuseError } from \"./errors.js\";\nimport { type Event, isTerminalState } from \"./types.js\";\n\nconst DATA_PREFIX = \"data:\";\n\n/** Open the SSE stream for an environment and return an async-iterable of events. */\nexport async function streamEvents(\n t: Transport,\n vmId: string,\n signal?: AbortSignal,\n): Promise<AsyncIterable<Event>> {\n const path = `/v1/environments/${encodeURIComponent(vmId)}/events`;\n const res = await t.stream(path, undefined, signal);\n return parseEventStream(res, signal);\n}\n\nfunction parseEventStream(res: Response, signal?: AbortSignal): AsyncIterable<Event> {\n return {\n async *[Symbol.asyncIterator](): AsyncGenerator<Event> {\n const body = res.body;\n if (!body) return;\n\n const reader = body.getReader();\n const decoder = new TextDecoder(\"utf-8\");\n let buffer = \"\";\n let data = \"\";\n let hasData = false;\n\n try {\n while (true) {\n let chunk;\n try {\n chunk = await reader.read();\n } catch (err) {\n // Caller-initiated abort is a clean end, not an error.\n if (signal?.aborted) return;\n throw new FuseError(\"event stream read failed\", { cause: err });\n }\n if (chunk.done) {\n // Clean EOF with no terminal state: just end (matches Go).\n return;\n }\n\n buffer += decoder.decode(chunk.value, { stream: true });\n\n let nl: number;\n while ((nl = buffer.indexOf(\"\\n\")) >= 0) {\n let line = buffer.slice(0, nl);\n buffer = buffer.slice(nl + 1);\n if (line.endsWith(\"\\r\")) line = line.slice(0, -1); // tolerate CRLF\n\n if (line === \"\") {\n // Blank line terminates the current event.\n if (!hasData) continue;\n const payload = data;\n data = \"\";\n hasData = false;\n\n let event: Event;\n try {\n event = JSON.parse(payload) as Event;\n } catch (err) {\n throw new FuseError(\"decode event\", { cause: err });\n }\n yield event;\n if (isTerminalState(event.state)) return;\n } else if (line.startsWith(\":\")) {\n // Comment / keepalive line — skip.\n continue;\n } else if (line.startsWith(DATA_PREFIX)) {\n // Strip the prefix and a single optional leading space; multiple\n // data: lines concatenate without a separator (matches server).\n let p = line.slice(DATA_PREFIX.length);\n if (p.startsWith(\" \")) p = p.slice(1);\n data += p;\n hasData = true;\n }\n // Other fields (id:, event:, ...) are ignored.\n }\n }\n } finally {\n // Release the underlying socket if the consumer breaks early.\n try {\n await reader.cancel();\n } catch {\n // ignore\n }\n }\n },\n };\n}\n","import type { CallOptions, Transport } from \"./transport.js\";\nimport type { CreateRequest, EnvironmentInfo, Event } from \"./types.js\";\nimport { requireArg } from \"./validate.js\";\nimport { streamEvents } from \"./events.js\";\n\n/** Filters for environments.list. */\nexport interface ListEnvironmentsOptions {\n taskId?: string;\n state?: string;\n hostId?: string;\n}\n\ninterface EnvironmentList {\n environments: EnvironmentInfo[];\n}\n\n/** EnvironmentsService manages microVM lifecycle. */\nexport class EnvironmentsService {\n constructor(private readonly t: Transport) {}\n\n /** List tracked environments, optionally filtered. */\n async list(\n options: ListEnvironmentsOptions = {},\n opts: CallOptions = {},\n ): Promise<EnvironmentInfo[]> {\n const out = await this.t.json<EnvironmentList>(\"GET\", \"/v1/environments\", {\n query: { task_id: options.taskId, state: options.state, host_id: options.hostId },\n signal: opts.signal,\n });\n return out.environments ?? [];\n }\n\n /** Fetch a single environment by VM id. */\n async get(vmId: string, opts: CallOptions = {}): Promise<EnvironmentInfo> {\n requireArg(vmId, \"vm id\");\n return this.t.json<EnvironmentInfo>(\n \"GET\",\n `/v1/environments/${encodeURIComponent(vmId)}`,\n { signal: opts.signal },\n );\n }\n\n /** Provision a new environment. Blocks until running or creation fails. */\n async create(body: CreateRequest, opts: CallOptions = {}): Promise<EnvironmentInfo> {\n return this.t.json<EnvironmentInfo>(\"POST\", \"/v1/environments\", {\n body,\n signal: opts.signal,\n });\n }\n\n /** Gracefully drain an environment; returns the updated environment. */\n async drain(vmId: string, opts: CallOptions = {}): Promise<EnvironmentInfo> {\n requireArg(vmId, \"vm id\");\n return this.t.json<EnvironmentInfo>(\n \"POST\",\n `/v1/environments/${encodeURIComponent(vmId)}`,\n { query: { action: \"drain\" }, signal: opts.signal },\n );\n }\n\n /** Rotate the per-VM credentials and push them to the guest. */\n async rotateToken(vmId: string, opts: CallOptions = {}): Promise<void> {\n requireArg(vmId, \"vm id\");\n await this.t.noContent(\"POST\", `/v1/environments/${encodeURIComponent(vmId)}`, {\n query: { action: \"rotate-token\" },\n signal: opts.signal,\n });\n }\n\n /** Forcefully destroy an environment. Idempotent. */\n async destroy(vmId: string, opts: CallOptions = {}): Promise<void> {\n requireArg(vmId, \"vm id\");\n await this.t.noContent(\"DELETE\", `/v1/environments/${encodeURIComponent(vmId)}`, {\n signal: opts.signal,\n });\n }\n\n /**\n * Open the SSE event stream for an environment. The returned promise rejects\n * with a FuseApiError on a connect-time failure (e.g. not_found); on success\n * it resolves to an AsyncIterable that yields each Event and ends after a\n * terminal-state event. Pass `opts.signal` to cancel; there is no built-in\n * timeout on the stream.\n *\n * @example\n * for await (const ev of await client.environments.events(id)) {\n * console.log(ev.state);\n * }\n */\n async events(vmId: string, opts: CallOptions = {}): Promise<AsyncIterable<Event>> {\n requireArg(vmId, \"vm id\");\n return streamEvents(this.t, vmId, opts.signal);\n }\n}\n","import type { CallOptions, Transport } from \"./transport.js\";\nimport type { Snapshot, SnapshotRequest } from \"./types.js\";\nimport { requireArg } from \"./validate.js\";\n\n/** Filters for snapshots.list. */\nexport interface ListSnapshotsOptions {\n vmId?: string;\n taskId?: string;\n tenantId?: string;\n state?: string;\n}\n\ninterface SnapshotList {\n snapshots: Snapshot[];\n}\n\n/** SnapshotsService manages microVM snapshots. */\nexport class SnapshotsService {\n constructor(private readonly t: Transport) {}\n\n /** Create a snapshot of a running environment. */\n async create(\n vmId: string,\n body: SnapshotRequest = {},\n opts: CallOptions = {},\n ): Promise<Snapshot> {\n requireArg(vmId, \"vm id\");\n return this.t.json<Snapshot>(\n \"POST\",\n `/v1/environments/${encodeURIComponent(vmId)}/snapshots`,\n { body, signal: opts.signal },\n );\n }\n\n /** List snapshots, optionally filtered. */\n async list(\n options: ListSnapshotsOptions = {},\n opts: CallOptions = {},\n ): Promise<Snapshot[]> {\n const out = await this.t.json<SnapshotList>(\"GET\", \"/v1/snapshots\", {\n query: {\n vm_id: options.vmId,\n task_id: options.taskId,\n tenant_id: options.tenantId,\n state: options.state,\n },\n signal: opts.signal,\n });\n return out.snapshots ?? [];\n }\n\n /** Fetch a single snapshot by id. */\n async get(snapshotId: string, opts: CallOptions = {}): Promise<Snapshot> {\n requireArg(snapshotId, \"snapshot id\");\n return this.t.json<Snapshot>(\n \"GET\",\n `/v1/snapshots/${encodeURIComponent(snapshotId)}`,\n { signal: opts.signal },\n );\n }\n\n /** Delete a snapshot. Idempotent; the snapshot must be a leaf. */\n async delete(snapshotId: string, opts: CallOptions = {}): Promise<void> {\n requireArg(snapshotId, \"snapshot id\");\n await this.t.noContent(\"DELETE\", `/v1/snapshots/${encodeURIComponent(snapshotId)}`, {\n signal: opts.signal,\n });\n }\n\n /** Restore an environment from a snapshot. */\n async restore(snapshotId: string, opts: CallOptions = {}): Promise<void> {\n requireArg(snapshotId, \"snapshot id\");\n await this.t.noContent(\"POST\", `/v1/snapshots/${encodeURIComponent(snapshotId)}`, {\n query: { action: \"restore\" },\n signal: opts.signal,\n });\n }\n}\n","import type { CallOptions, Transport } from \"./transport.js\";\nimport type { Host, RegisterHostRequest } from \"./types.js\";\nimport { requireArg } from \"./validate.js\";\n\ninterface HostList {\n hosts: Host[];\n}\n\n/** HostsService manages registered Firecracker hosts. */\nexport class HostsService {\n constructor(private readonly t: Transport) {}\n\n /** Register (or re-register) a host. Idempotent on id. */\n async register(body: RegisterHostRequest, opts: CallOptions = {}): Promise<Host> {\n return this.t.json<Host>(\"POST\", \"/v1/hosts\", { body, signal: opts.signal });\n }\n\n /** List registered hosts. */\n async list(opts: CallOptions = {}): Promise<Host[]> {\n const out = await this.t.json<HostList>(\"GET\", \"/v1/hosts\", { signal: opts.signal });\n return out.hosts ?? [];\n }\n\n /** Fetch a single host by id. */\n async get(hostId: string, opts: CallOptions = {}): Promise<Host> {\n requireArg(hostId, \"host id\");\n return this.t.json<Host>(\"GET\", `/v1/hosts/${encodeURIComponent(hostId)}`, {\n signal: opts.signal,\n });\n }\n\n /** Mark a host unschedulable; existing VMs keep running. */\n async cordon(hostId: string, opts: CallOptions = {}): Promise<void> {\n await this.action(hostId, \"cordon\", opts);\n }\n\n /** Return a cordoned host to active scheduling. */\n async uncordon(hostId: string, opts: CallOptions = {}): Promise<void> {\n await this.action(hostId, \"uncordon\", opts);\n }\n\n /** Remove a host. The host must have no running VMs. */\n async deregister(hostId: string, opts: CallOptions = {}): Promise<void> {\n requireArg(hostId, \"host id\");\n await this.t.noContent(\"DELETE\", `/v1/hosts/${encodeURIComponent(hostId)}`, {\n signal: opts.signal,\n });\n }\n\n private async action(hostId: string, action: string, opts: CallOptions): Promise<void> {\n requireArg(hostId, \"host id\");\n await this.t.noContent(\"POST\", `/v1/hosts/${encodeURIComponent(hostId)}`, {\n query: { action },\n signal: opts.signal,\n });\n }\n}\n","import type { CallOptions, Transport } from \"./transport.js\";\nimport type { APIKey, CreatedAPIKey } from \"./types.js\";\nimport { requireArg } from \"./validate.js\";\n\ninterface APIKeyList {\n api_keys: APIKey[];\n}\n\n/**\n * ApiKeysService manages revocable API keys. These endpoints require the\n * master token; the server enforces this.\n */\nexport class ApiKeysService {\n constructor(private readonly t: Transport) {}\n\n /**\n * Issue a new API key. The raw secret is returned once in `key` and is\n * unrecoverable afterward.\n */\n async create(label?: string, opts: CallOptions = {}): Promise<CreatedAPIKey> {\n return this.t.json<CreatedAPIKey>(\"POST\", \"/v1/api-keys\", {\n body: label ? { label } : {},\n signal: opts.signal,\n });\n }\n\n /** List API key metadata. */\n async list(opts: CallOptions = {}): Promise<APIKey[]> {\n const out = await this.t.json<APIKeyList>(\"GET\", \"/v1/api-keys\", {\n signal: opts.signal,\n });\n return out.api_keys ?? [];\n }\n\n /** Revoke the API key with the given id. */\n async revoke(id: string, opts: CallOptions = {}): Promise<void> {\n requireArg(id, \"id\");\n await this.t.noContent(\"DELETE\", `/v1/api-keys/${encodeURIComponent(id)}`, {\n signal: opts.signal,\n });\n }\n}\n","// generated by scripts/gen-version.mjs. do not edit.\nexport const VERSION = \"0.0.1\";\n","import { Transport, type FetchLike } from \"./transport.js\";\nimport { EnvironmentsService } from \"./environments.js\";\nimport { SnapshotsService } from \"./snapshots.js\";\nimport { HostsService } from \"./hosts.js\";\nimport { ApiKeysService } from \"./apiKeys.js\";\nimport { VERSION } from \"./version.js\";\n\n/** Options for constructing a FuseClient. */\nexport interface FuseClientOptions {\n /** Base URL of the Fuse control plane, e.g. \"https://fuse.example.com\". Required. */\n baseUrl: string;\n /** Bearer token (master token or an API key). Omit for unauthenticated endpoints. */\n token?: string;\n /** Custom fetch implementation. Defaults to the global fetch (Node 18+). */\n fetch?: FetchLike;\n /** User-Agent header. Defaults to \"fuse-ts/<version>\". Ignored by browsers. */\n userAgent?: string;\n /** Generator for the X-Request-ID header, called once per request. Empty results are omitted. */\n requestId?: () => string;\n /** Default per-request timeout in milliseconds. Does NOT apply to events() streams. */\n timeoutMs?: number;\n /** Extra default headers merged into every request. */\n headers?: Record<string, string>;\n}\n\n/**\n * FuseClient is the entry point for the Fuse API. It groups the resource\n * services that share a single transport.\n *\n * @example\n * const client = new FuseClient({ baseUrl: \"https://fuse.example.com\", token });\n * const env = await client.environments.create({ task_id: \"task-1\" });\n */\nexport class FuseClient {\n readonly environments: EnvironmentsService;\n readonly snapshots: SnapshotsService;\n readonly hosts: HostsService;\n readonly apiKeys: ApiKeysService;\n\n constructor(options: FuseClientOptions) {\n const transport = new Transport({\n baseUrl: options.baseUrl,\n token: options.token,\n fetch: options.fetch,\n userAgent: options.userAgent ?? `fuse-ts/${VERSION}`,\n requestId: options.requestId,\n timeoutMs: options.timeoutMs,\n headers: options.headers,\n });\n this.environments = new EnvironmentsService(transport);\n this.snapshots = new SnapshotsService(transport);\n this.hosts = new HostsService(transport);\n this.apiKeys = new ApiKeysService(transport);\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@folsom/fuse",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "TypeScript SDK for the Fuse microVM control plane",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "andrewn6",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/folsomintel/fuse.git",
|
|
31
|
+
"directory": "sdks/typescript"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/folsomintel/fuse/tree/main/sdks/typescript#readme",
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/folsomintel/fuse/issues"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"fuse",
|
|
39
|
+
"microvm",
|
|
40
|
+
"firecracker",
|
|
41
|
+
"sdk",
|
|
42
|
+
"api-client"
|
|
43
|
+
],
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"gen:version": "node scripts/gen-version.mjs",
|
|
49
|
+
"prebuild": "node scripts/gen-version.mjs",
|
|
50
|
+
"pretest": "node scripts/gen-version.mjs",
|
|
51
|
+
"build": "tsup",
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"test": "vitest run",
|
|
54
|
+
"test:watch": "vitest",
|
|
55
|
+
"lint": "prettier --check . && tsc --noEmit",
|
|
56
|
+
"format": "prettier --write .",
|
|
57
|
+
"prepublishOnly": "npm run build"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@types/node": "^20.14.0",
|
|
61
|
+
"prettier": "^3.3.0",
|
|
62
|
+
"tsup": "^8.2.0",
|
|
63
|
+
"typescript": "^5.5.0",
|
|
64
|
+
"vitest": "^2.0.0"
|
|
65
|
+
}
|
|
66
|
+
}
|