unfazed 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 jilio
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 CHANGED
@@ -1,3 +1,302 @@
1
1
  # unfazed
2
2
 
3
- Placeholder package. This name is reserved and the package is under active development — nothing usable is published yet.
3
+ Official TypeScript SDK for [unfazed](https://unfazed.dev) — deploy and operate
4
+ agent-built apps (static sites, HTTP services, persistent Linux boxes) in
5
+ isolated microVMs.
6
+
7
+ **This is the code-mode companion to the unfazed MCP server.** Same platform,
8
+ same bearer token, different shape. Use the MCP tools when you want the model to
9
+ decide one step at a time. Use this SDK when you'd rather write one script that
10
+ does the whole chain and only reports back what matters.
11
+
12
+ ```
13
+ MCP: create_workspace → write_file → write_file → write_file → deploy
14
+ → list_deployments → list_deployments → list_deployments → get_logs
15
+ (9+ round trips, every result lands in the model's context)
16
+
17
+ SDK: one script. Three requests. You read the four lines you cared about.
18
+ ```
19
+
20
+ - Zero runtime dependencies. Installing `unfazed` installs exactly one package.
21
+ - Strict TypeScript, generated from the platform's own GraphQL schema.
22
+ - Bun-first, runs unchanged on Node >= 20 and Deno.
23
+
24
+ ## Install
25
+
26
+ ```sh
27
+ bun add unfazed # or: npm i unfazed
28
+ ```
29
+
30
+ ## Auth
31
+
32
+ Get a token from <https://unfazed.dev/auth.md> (or `POST
33
+ https://unfazed.dev/agents/register`). The SDK reads `UNFZ_API_TOKEN`:
34
+
35
+ ```sh
36
+ export UNFZ_API_TOKEN="..."
37
+ ```
38
+
39
+ ```ts
40
+ const unfazed = new Unfazed(); // reads UNFZ_API_TOKEN
41
+ const unfazed = new Unfazed({ token }); // or pass it explicitly
42
+ ```
43
+
44
+ Both endpoint and token are configurable. `UNFZ_API_URL` overrides the endpoint
45
+ (default `https://unfazed.dev/query`); `baseUrl` is a convenience for
46
+ self-hosted control planes where only the host differs.
47
+
48
+ ```ts
49
+ new Unfazed({ token, baseUrl: "https://staging.example.com" }); // → /query
50
+ new Unfazed({ token, endpoint: "https://example.com/graphql" }); // exact
51
+ ```
52
+
53
+ ## Code mode: deploy, wait, tail logs, in one script
54
+
55
+ ```ts
56
+ #!/usr/bin/env bun
57
+ import { Unfazed, UnfazedDeployError } from "unfazed";
58
+
59
+ const unfazed = new Unfazed();
60
+
61
+ const ws = await unfazed.createWorkspace("greeter");
62
+
63
+ // One request, not three — batched as aliased mutations.
64
+ await unfazed.writeFiles(ws.id, {
65
+ "server.ts": `
66
+ export default {
67
+ fetch(req: Request) {
68
+ const name = new URL(req.url).searchParams.get("name") ?? "world";
69
+ return Response.json({ hello: name });
70
+ },
71
+ };
72
+ `,
73
+ "unfazed.json": JSON.stringify({ entry: "server.ts" }),
74
+ "README.md": "# greeter",
75
+ });
76
+
77
+ try {
78
+ // Blocks until the build, VM boot and health probe all pass.
79
+ const app = await unfazed.deployService({
80
+ workspaceId: ws.id,
81
+ alias: "greeter",
82
+ wait: true,
83
+ });
84
+ console.log(`live: ${app.url}`);
85
+
86
+ // Verify it for real before trusting the alias.
87
+ const res = await fetch(`${app.url}?name=agent`);
88
+ console.log(res.status, await res.json());
89
+
90
+ // Filter logs LOCALLY. This is the whole point: a 16 KiB tail costs the
91
+ // model nothing, because only the matching lines are ever printed.
92
+ const logs = await unfazed.getLogs(app.id, { tailBytes: 16384 });
93
+ const errors = logs.stdout
94
+ .split("\n")
95
+ .filter((l) => /error|warn/i.test(l))
96
+ .slice(-20);
97
+ console.log(errors.length ? errors.join("\n") : "no errors in tail");
98
+ } catch (e) {
99
+ if (e instanceof UnfazedDeployError && e.reason === "FAILED") {
100
+ // The build finished and is broken — retrying will not help.
101
+ console.error("build failed:", e.message);
102
+ process.exit(1);
103
+ }
104
+ throw e;
105
+ }
106
+ ```
107
+
108
+ ## Two layers
109
+
110
+ ### 1. Typed GraphQL core — `UnfazedClient`
111
+
112
+ A fetch-based transport with full types and nothing else. Use it for any of the
113
+ platform's ~660 schema fields the workflow layer doesn't wrap.
114
+
115
+ ```ts
116
+ import { UnfazedClient, operations } from "unfazed";
117
+
118
+ const client = new UnfazedClient();
119
+
120
+ // Generated documents infer both result and variable types.
121
+ const { whoami } = await client.query(operations.WhoamiDocument, {});
122
+
123
+ // Or your own document, with explicit types.
124
+ const data = await client.query<{ workspaces: { id: string }[] }, never>(
125
+ `query { workspaces { id } }`,
126
+ );
127
+ ```
128
+
129
+ Errors are typed and specific: `UnfazedAuthError`, `UnfazedHTTPError` (carries
130
+ `status`/`body`), `UnfazedGraphQLError` (carries `errors` **and any partial
131
+ `data`**), `UnfazedTimeoutError`, `UnfazedDeployError`, `UnfazedConflictError`,
132
+ `UnfazedNotFoundError`. All extend `UnfazedError`.
133
+
134
+ ### 2. Workflow layer — `Unfazed`
135
+
136
+ The chains an agent actually performs. Reach `client` from it any time:
137
+ `unfazed.client.query(...)`.
138
+
139
+ **Identity**
140
+ | Method | Notes |
141
+ | --- | --- |
142
+ | `whoami()` | actor kind, scopes, usage and resource limits |
143
+
144
+ **Workspaces**
145
+ | Method | Notes |
146
+ | --- | --- |
147
+ | `createWorkspace(name)` | |
148
+ | `listWorkspaces()` | |
149
+ | `deleteWorkspace(id)` | also destroys a BOX's data disk |
150
+
151
+ **Files**
152
+ | Method | Notes |
153
+ | --- | --- |
154
+ | `writeFiles(wsId, files, opts?)` | **batched** — many files per request |
155
+ | `writeFile(wsId, path, text)` | |
156
+ | `writeBinaryFile(wsId, path, bytes)` | base64 under the hood |
157
+ | `listFiles(wsId, page?)` | |
158
+ | `readFile(wsId, path, range?)` | |
159
+ | `deleteFile(wsId, path)` | |
160
+
161
+ **Deploy**
162
+ | Method | Notes |
163
+ | --- | --- |
164
+ | `deployService(opts)` | `wait: true` polls to READY |
165
+ | `deployStatic(opts)` | `spaMode` for single-page apps |
166
+ | `deployBox(opts)` | persistent Linux VM; result carries `sshCommand` |
167
+ | `deploy(input, opts?)` | raw `CreateDeploymentInput` escape hatch |
168
+ | `waitForDeployment(id, opts?)` | standalone poller |
169
+ | `getDeployment(id)` / `getDeploymentByAlias(alias)` | |
170
+ | `listDeployments(filter?)` | |
171
+
172
+ **Runtime**
173
+ | Method | Notes |
174
+ | --- | --- |
175
+ | `getLogs(id, { tailBytes })` | stdout + stderr tail |
176
+ | `getStatus(id)` | health, in-flight, restarts, `memSizeMiB` |
177
+ | `getEvents(id, limit?)` | build/boot/probe/cron lifecycle events |
178
+ | `restartService(id)` / `stopService(id)` | |
179
+ | `deleteDeployment(id)` | |
180
+
181
+ **Env & secrets**
182
+ | Method | Notes |
183
+ | --- | --- |
184
+ | `setEnv` / `setSecret` / `deleteEnv` / `listEnv` | workspace-wide defaults |
185
+ | `setEnvMany(wsId, vars, { secret? })` | **batched** |
186
+ | `setAppEnv` / `setAppSecret` | one alias only |
187
+ | `removeAppEnv` (mask) / `inheritAppEnv` (unmask) | |
188
+ | `listAppEnv(wsId, alias)` | effective view; secrets redacted |
189
+
190
+ **Releases**
191
+ | Method | Notes |
192
+ | --- | --- |
193
+ | `listReleases(alias)` | newest first; one entry has `aliasHeld: true` |
194
+ | `rollback({ alias, to?, retries? })` | **whole compare-and-swap in one call** |
195
+
196
+ ## The two things worth knowing
197
+
198
+ ### `writeFiles` batches, because the API is one-file-per-mutation
199
+
200
+ Writing a 12-file app the obvious way costs 12 round trips. `writeFiles` puts
201
+ one **aliased** mutation per file into a single GraphQL document:
202
+
203
+ ```graphql
204
+ mutation SdkWriteFilesBatch($workspaceID: ID!, $p0: String!, $c0: String!, ...) {
205
+ f0: writeWorkspaceTextFile(input: { workspaceID: $workspaceID, path: $p0, text: $c0 }) { file { ... } }
206
+ f1: writeWorkspaceBase64File(input: { workspaceID: $workspaceID, path: $p1, contentBase64: $c1 }) { file { ... } }
207
+ }
208
+ ```
209
+
210
+ Text and binary mix freely in one batch. It splits automatically at 20 files or
211
+ 4 MiB per request (`batchSize`, `maxBatchBytes`).
212
+
213
+ Batches are **not atomic** — GraphQL resolves each field independently. If one
214
+ path fails, the others still commit; the thrown `UnfazedGraphQLError` carries
215
+ `data` with the aliases that succeeded and `errors[].path` naming the ones that
216
+ didn't.
217
+
218
+ ### `rollback` does the compare-and-swap for you
219
+
220
+ The platform requires you to prove you observed the current state: you must send
221
+ the live release's id **and** its opaque `aliasRevision`, and the swap is
222
+ rejected if either moved — including same-id ABA. That's a read-then-write
223
+ protocol, and getting it wrong means racing a concurrent deploy.
224
+
225
+ `rollback` does the whole thing in one call: list releases → find the `aliasHeld`
226
+ entry → pick the target → submit both guard values.
227
+
228
+ ```ts
229
+ await unfazed.rollback({ alias: "my-app" }); // undo the last deploy
230
+ await unfazed.rollback({ alias: "my-app", to: relId }); // a specific release
231
+ ```
232
+
233
+ If the guard is rejected it re-lists and retries once (`retries`). The **target
234
+ stays pinned** across retries while the guard is refreshed — a concurrent deploy
235
+ can never redirect your rollback to a release you didn't choose. Non-READY
236
+ targets and the current holder are refused client-side, before the mutation.
237
+
238
+ ## Deploy semantics the SDK can't hide
239
+
240
+ - `createDeployment` returns **`BUILDING`** immediately. `READY` and `FAILED`
241
+ are the only terminal states. `wait: true` exists because of this.
242
+ - `previewUrl` is the immutable by-id URL. It 503s (with `Retry-After`) while
243
+ building and 422s with the build error on failure — use it to verify a build
244
+ before it owns the public alias.
245
+ - Env and secrets are **snapshotted into a release at deploy time**. Changing
246
+ them does not affect a running app until you redeploy that alias. Old releases
247
+ are immutable, and STATIC deployments get no env at all.
248
+ - Release retention keeps only the most recent few per alias (default 5). Older
249
+ releases can't be inspected or restored.
250
+
251
+ ## Development
252
+
253
+ ```sh
254
+ bun install
255
+ bun run codegen # regenerate typed operations from the vendored schema
256
+ bun run typecheck
257
+ bun test
258
+ bun run build
259
+ bun run check # all of the above, plus a dist smoke test on Bun AND Node
260
+ ```
261
+
262
+ ### Regenerating from the schema
263
+
264
+ `schema/schema.graphqls` is a **vendored copy** of `graph/schema.graphqls` from
265
+ the [unfazed](https://unfazed.dev) repo — the same contract gqlgen serves. It is
266
+ vendored so `bun install && bun run build` works with no access to the platform
267
+ repo, and `schema/SOURCE.json` records the exact unfazed commit it came from.
268
+
269
+ To refresh it against a local unfazed checkout:
270
+
271
+ ```sh
272
+ bun run sync-schema # tries ../../../unfazed, then $UNFAZED_REPO
273
+ UNFAZED_REPO=~/src/unfazed bun run sync-schema
274
+ bun run sync-schema -- --repo ~/src/unfazed
275
+ bun run sync-schema -- --check # verify only; non-zero if stale
276
+
277
+ bun run codegen && bun test # ALWAYS follow a sync with these
278
+ ```
279
+
280
+ `src/generated/` is committed on purpose: consumers of the published package
281
+ never run codegen, and neither does a clean CI checkout. A schema change that
282
+ invalidates an operation shows up as a **compile error**, not a runtime
283
+ surprise.
284
+
285
+ `bun run codegen` also runs `scripts/postcodegen.ts`, which inlines the one
286
+ type-only import graphql-codegen emits (`DocumentTypeDecoration`) so the package
287
+ keeps zero dependencies. `test/generated.test.ts` asserts that invariant.
288
+
289
+ ## Publishing
290
+
291
+ Publishing is a deliberate, attended step — there is intentionally **no**
292
+ `prepublishOnly`/`prepare` script that could fire a publish or a stale build by
293
+ accident. `dist/` is gitignored, so build before publishing:
294
+
295
+ ```sh
296
+ bun run check # typecheck + tests + build + dist smoke
297
+ npm publish # or: bun publish --access public
298
+ ```
299
+
300
+ ## License
301
+
302
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,134 @@
1
+ /** SDK version, sent as the User-Agent. Keep in sync with package.json. */
2
+ export declare const VERSION = "0.1.0";
3
+ /**
4
+ * Default GraphQL endpoint.
5
+ *
6
+ * Note this is the FULL endpoint, not an origin: the platform's own documented
7
+ * convention (`UNFZ_API_URL="https://unfazed.dev/query"`, and cmd/showcase-deploy
8
+ * in the unfazed repo) treats the /query URL as the configurable unit, so the
9
+ * SDK matches it rather than inventing a base-URL convention.
10
+ */
11
+ export declare const DEFAULT_ENDPOINT = "https://unfazed.dev/query";
12
+ /** Platform origin, used to derive the endpoint when only `baseUrl` is given. */
13
+ export declare const DEFAULT_BASE_URL = "https://unfazed.dev";
14
+ /** Path of the GraphQL endpoint under an origin. */
15
+ export declare const GRAPHQL_PATH = "/query";
16
+ /**
17
+ * Environment variable holding the bearer token.
18
+ *
19
+ * This is the platform's documented name (docs/agent-deployments.md,
20
+ * internal/agentdocs/reference.md). It accepts either an operator API token or
21
+ * an agent access token from POST /agents/register — /query takes both as
22
+ * plain bearers.
23
+ */
24
+ export declare const TOKEN_ENV_VAR = "UNFZ_API_TOKEN";
25
+ /** Environment variable holding the full GraphQL endpoint URL. */
26
+ export declare const ENDPOINT_ENV_VAR = "UNFZ_API_URL";
27
+ /** Minimal structural type of the `fetch` the transport needs. */
28
+ export type FetchLike = (input: string, init: {
29
+ method: string;
30
+ headers: Record<string, string>;
31
+ body: string;
32
+ signal?: AbortSignal;
33
+ }) => Promise<{
34
+ ok: boolean;
35
+ status: number;
36
+ text(): Promise<string>;
37
+ }>;
38
+ export interface ClientOptions {
39
+ /**
40
+ * Bearer token. Falls back to `UNFZ_API_TOKEN`. Get one from
41
+ * https://unfazed.dev/auth.md — the same token the MCP server uses.
42
+ */
43
+ token?: string;
44
+ /**
45
+ * Full GraphQL endpoint URL. Wins over `baseUrl`. Falls back to
46
+ * `UNFZ_API_URL`, then {@link DEFAULT_ENDPOINT}.
47
+ */
48
+ endpoint?: string;
49
+ /**
50
+ * Platform origin; `/query` is appended. Convenience for self-hosted or
51
+ * staging control planes where only the host differs.
52
+ */
53
+ baseUrl?: string;
54
+ /** Extra headers merged into every request. Authorization always wins. */
55
+ headers?: Record<string, string>;
56
+ /**
57
+ * Per-request timeout in ms. Default 60_000. Pass 0 to disable.
58
+ *
59
+ * Bounds the WHOLE request — connect, headers and reading the response body —
60
+ * so a proxy that answers with headers and then stalls still fails on time.
61
+ */
62
+ timeoutMs?: number;
63
+ /** Injection point for tests and instrumented/proxied transports. */
64
+ fetch?: FetchLike;
65
+ /** Overrides the default `unfazed-sdk/<version>` User-Agent. */
66
+ userAgent?: string;
67
+ }
68
+ /**
69
+ * A GraphQL document.
70
+ *
71
+ * Structural rather than `string & {...}` on purpose: graphql-codegen emits
72
+ * `TypedDocumentString`, a String SUBCLASS, which is not assignable to the
73
+ * primitive `string`. Requiring only `toString()` plus the optional phantom
74
+ * `__apiType` lets three things through with full inference:
75
+ *
76
+ * - generated `TypedDocumentString` documents (types inferred),
77
+ * - documents assembled at runtime, like the batched writeFiles mutation,
78
+ * - plain template-literal strings a caller wrote by hand.
79
+ */
80
+ export interface GraphQLDocument<TResult, TVariables> {
81
+ /** Phantom type carrier. Never called; present only to bind the generics. */
82
+ __apiType?: (variables: TVariables) => TResult;
83
+ toString(): string;
84
+ }
85
+ /** Result type carried by a typed document. */
86
+ export type ResultOf<T> = T extends GraphQLDocument<infer R, infer _V> ? R : unknown;
87
+ /** Variables type carried by a typed document. */
88
+ export type VariablesOf<T> = T extends GraphQLDocument<infer _R, infer V> ? V : never;
89
+ /** Per-request knobs. */
90
+ export interface RequestOptions {
91
+ signal?: AbortSignal;
92
+ timeoutMs?: number;
93
+ operationName?: string;
94
+ }
95
+ /**
96
+ * Layer 1 — a typed, dependency-free GraphQL transport over `fetch`.
97
+ *
98
+ * This is the SDK's entire networking surface. It understands auth, timeouts
99
+ * and the GraphQL error envelope, and nothing about deployments. The workflow
100
+ * layer ({@link import("./unfazed.js").Unfazed}) is built strictly on top of
101
+ * it, so you can always drop down here for any operation the workflow layer
102
+ * does not wrap — the schema has ~660 fields and the workflow layer
103
+ * deliberately covers only the agent-facing ones.
104
+ *
105
+ * ```ts
106
+ * const client = new UnfazedClient(); // reads UNFZ_API_TOKEN
107
+ * const data = await client.query<{ whoami: { name: string } }, never>(
108
+ * `query { whoami { name } }`,
109
+ * );
110
+ * ```
111
+ */
112
+ export declare class UnfazedClient {
113
+ #private;
114
+ /** Resolved GraphQL endpoint URL. */
115
+ readonly endpoint: string;
116
+ constructor(options?: ClientOptions);
117
+ /** Runs a query document. Identical wire behaviour to {@link mutate}. */
118
+ query<TResult, TVariables>(document: GraphQLDocument<TResult, TVariables>, variables?: TVariables, options?: RequestOptions): Promise<TResult>;
119
+ /**
120
+ * Runs a mutation document. Distinct from {@link query} for call-site
121
+ * readability only — GraphQL over HTTP POST makes no distinction.
122
+ */
123
+ mutate<TResult, TVariables>(document: GraphQLDocument<TResult, TVariables>, variables?: TVariables, options?: RequestOptions): Promise<TResult>;
124
+ /**
125
+ * The single code path behind {@link query} and {@link mutate}.
126
+ *
127
+ * Throws {@link UnfazedHTTPError} on non-2xx, {@link UnfazedGraphQLError}
128
+ * when the body carries `errors`, {@link UnfazedTimeoutError} on timeout or
129
+ * abort. Resolves to the `data` object on success.
130
+ */
131
+ request<TResult = unknown>(document: string | {
132
+ toString(): string;
133
+ }, variables?: unknown, options?: RequestOptions): Promise<TResult>;
134
+ }