unfazed 0.0.1 → 0.2.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,388 @@
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)` | names are unique per owner, case-insensitively |
148
+ | `ensureWorkspace(name)` | get-or-create; **use this in re-runnable scripts** |
149
+ | `listWorkspaces()` | |
150
+ | `deleteWorkspace(id, opts?)` | cascades to deployments by default; also destroys a BOX's data disk |
151
+
152
+ **Files**
153
+ | Method | Notes |
154
+ | --- | --- |
155
+ | `writeFiles(wsId, files, opts?)` | **batched** — many files per request |
156
+ | `writeFile(wsId, path, text)` | |
157
+ | `writeBinaryFile(wsId, path, bytes)` | base64 under the hood |
158
+ | `listFiles(wsId, page?)` | |
159
+ | `readFile(wsId, path, range?)` | |
160
+ | `deleteFile(wsId, path)` | |
161
+
162
+ **Deploy**
163
+ | Method | Notes |
164
+ | --- | --- |
165
+ | `deployService(opts)` | `wait: true` polls to READY |
166
+ | `deployStatic(opts)` | `spaMode` for single-page apps |
167
+ | `deployBox(opts)` | persistent Linux VM; result carries `sshCommand` |
168
+ | `deploy(input, opts?)` | raw `CreateDeploymentInput` escape hatch |
169
+ | `waitForDeployment(id, opts?)` | standalone poller |
170
+ | `getDeployment(id)` / `getDeploymentByAlias(alias)` | |
171
+ | `listDeployments(filter?)` | |
172
+
173
+ **Runtime**
174
+ | Method | Notes |
175
+ | --- | --- |
176
+ | `getLogs(id, { tailBytes })` | stdout + stderr tail |
177
+ | `getStatus(id)` | health, in-flight, restarts, `memSizeMiB` |
178
+ | `getEvents(id, limit?)` | build/boot/probe/cron lifecycle events |
179
+ | `restartService(id)` / `stopService(id)` | |
180
+ | `deleteDeployment(id, opts?)` | `ignoreNotFound` for idempotent cleanup |
181
+
182
+ **Env & secrets**
183
+ | Method | Notes |
184
+ | --- | --- |
185
+ | `setEnv` / `setSecret` / `deleteEnv` / `listEnv` | workspace-wide defaults |
186
+ | `setEnvMany(wsId, vars, { secret? })` | **batched**, and **not atomic** |
187
+ | `setAppEnv` / `setAppSecret` | one alias only |
188
+ | `removeAppEnv` (mask) / `inheritAppEnv` (unmask) | a mask is a persistent row, and costs a slot |
189
+ | `listAppEnv(wsId, alias)` | effective view; secrets redacted |
190
+
191
+ **Releases**
192
+ | Method | Notes |
193
+ | --- | --- |
194
+ | `listReleases(alias)` | newest first; one entry has `aliasHeld: true` |
195
+ | `rollback({ alias, to?, expectedCurrentId?, retries? })` | **whole compare-and-swap in one call** |
196
+
197
+ ## The two things worth knowing
198
+
199
+ ### `writeFiles` batches, because the API is one-file-per-mutation
200
+
201
+ Writing a 12-file app the obvious way costs 12 round trips. `writeFiles` puts
202
+ one **aliased** mutation per file into a single GraphQL document:
203
+
204
+ ```graphql
205
+ mutation SdkWriteFilesBatch($workspaceID: ID!, $p0: String!, $c0: String!, ...) {
206
+ f0: writeWorkspaceTextFile(input: { workspaceID: $workspaceID, path: $p0, text: $c0 }) { file { ... } }
207
+ f1: writeWorkspaceBase64File(input: { workspaceID: $workspaceID, path: $p1, contentBase64: $c1 }) { file { ... } }
208
+ }
209
+ ```
210
+
211
+ Text and binary mix freely in one batch. It splits automatically at 20 files or
212
+ 4 MiB per request (`batchSize`, `maxBatchBytes`).
213
+
214
+ Batches are **not atomic** — GraphQL resolves each field independently. If one
215
+ path fails, the others still commit; the thrown `UnfazedGraphQLError` carries
216
+ `data` with the aliases that succeeded and `errors[].path` naming the ones that
217
+ didn't.
218
+
219
+ ### `rollback` does the compare-and-swap for you
220
+
221
+ The platform requires you to prove you observed the current state: you must send
222
+ the live release's id **and** its opaque `aliasRevision`, and the swap is
223
+ rejected if either moved — including same-id ABA. That's a read-then-write
224
+ protocol, and getting it wrong means racing a concurrent deploy.
225
+
226
+ `rollback` does the whole thing in one call: list releases → find the `aliasHeld`
227
+ entry → pick the target → submit both guard values.
228
+
229
+ ```ts
230
+ await unfazed.rollback({ alias: "my-app" }); // undo the last deploy
231
+ await unfazed.rollback({ alias: "my-app", to: relId }); // a specific release
232
+ ```
233
+
234
+ If the guard is rejected it re-lists and retries once (`retries`). The **target
235
+ stays pinned** across retries while the guard is refreshed — a concurrent deploy
236
+ can never redirect your rollback to a release you didn't choose. Non-READY
237
+ targets and the current holder are refused client-side, before the mutation.
238
+
239
+ > ⚠️ **A bare `rollback({ alias })` is a toggle, not an idempotent step.** It
240
+ > means "restore the most recent READY release that isn't live", which is a
241
+ > *different release each time the alias moves*. Run the same script twice and
242
+ > the second run rolls back the rollback: A → B → A → B, every hop a real alias
243
+ > swap, nothing ever erroring — each call did exactly what it was asked.
244
+
245
+ In anything re-runnable — a retry loop, a CI job, an agent redoing a step — pin
246
+ the outcome instead of asking for "the previous one":
247
+
248
+ ```ts
249
+ // Names the release you want live. A second run refuses instead of flipping.
250
+ await unfazed.rollback({ alias: "my-app", to: relId });
251
+
252
+ // States what you expect to be live NOW. If anything else holds the alias you
253
+ // get UnfazedConflictError before a single mutation goes out — and the CAS
254
+ // retry is disabled, because a moved alias IS the precondition failing.
255
+ await unfazed.rollback({ alias: "my-app", to: relId, expectedCurrentId: liveId });
256
+ ```
257
+
258
+ ## Deploy semantics the SDK can't hide
259
+
260
+ - `createDeployment` returns **`BUILDING`** immediately. `READY` and `FAILED`
261
+ are the only terminal states. `wait: true` exists because of this.
262
+ - `previewUrl` is the immutable by-id URL. It 503s (with `Retry-After`) while
263
+ building and 422s with the build error on failure — use it to verify a build
264
+ before it owns the public alias.
265
+ - Env and secrets are **snapshotted into a release at deploy time**. Changing
266
+ them does not affect a running app until you redeploy that alias. Old releases
267
+ are immutable, and STATIC deployments get no env at all.
268
+ - Release retention keeps only the most recent few per alias (default 5). Older
269
+ releases can't be inspected or restored.
270
+ - **Every deploy call mints a new release. There is no content dedupe** —
271
+ deploying byte-identical files twice gives you two distinct releases. A retry
272
+ loop churns the history and can push a release you still wanted out of
273
+ retention.
274
+ - **`deployBox` without an `alias` creates a NEW box every call.** The server
275
+ generates a fresh random alias, so a script run twice leaves two box records —
276
+ while the data disk is keyed by **workspace**, shared by all of them. Only one
277
+ VM can hold that disk read-write, so the second box won't even start. Always
278
+ pass an explicit `alias` in a re-runnable script.
279
+
280
+ ## Things that are re-runnable, and things that aren't
281
+
282
+ Agents re-run scripts. These are the operations where the second run does not do
283
+ what the first one did:
284
+
285
+ | Instead of | Use | Because |
286
+ | --- | --- | --- |
287
+ | `createWorkspace(name)` | `ensureWorkspace(name)` | names are unique per owner, case-insensitively — a second create is an error |
288
+ | `deployBox({ workspaceId })` | `deployBox({ workspaceId, alias })` | no alias ⇒ a brand-new box each time |
289
+ | `rollback({ alias })` | `rollback({ alias, to })` or `expectedCurrentId` | bare rollback is a toggle |
290
+ | `deleteWorkspace(id)` throwing | `deleteWorkspace(id, { ignoreNotFound: true })` | already-deleted is not a failure |
291
+ | `deleteDeployment(id)` throwing | `deleteDeployment(id, { ignoreNotFound: true })` | same |
292
+
293
+ `ignoreNotFound` matches the server's two exact sentinels — `workspace was not
294
+ found` and `deployment was not found` — and nothing else. A message that merely
295
+ *contains* "not found" still throws, so a real failure can't be mistaken for a
296
+ successful teardown. (Both sentinels also cover "belongs to another tenant": the
297
+ server reports the two identically so ids can't be probed.)
298
+
299
+ ### Deleting a workspace cascades by default
300
+
301
+ `deleteWorkspace` sends `withDeployments: true` unless you say otherwise. It has
302
+ to: the server **refuses** to delete a workspace with any deployment history at
303
+ all — including FAILED-only history — without that flag, so the strict form
304
+ fails on nearly every workspace that was ever deployed.
305
+
306
+ ```ts
307
+ await unfazed.deleteWorkspace(ws.id); // cascade (default)
308
+ await unfazed.deleteWorkspace(ws.id, { withDeployments: false }); // refuse if deployed
309
+ ```
310
+
311
+ ## Env & secrets: three states, not two
312
+
313
+ A name in a given scope is exactly one of **plain**, **secret**, or **masked**,
314
+ and the setters silently move it between them:
315
+
316
+ - `setEnv` deletes a same-named secret; `setSecret` deletes a same-named plain
317
+ var. Same for `setAppEnv` / `setAppSecret`. Last write wins and the *kind*
318
+ flips with it.
319
+ - `setAppEnv` / `setAppSecret` also clear a REMOVE mask — setting a value is how
320
+ you un-block a masked name.
321
+ - `removeAppEnv` writes a **persistent mask row**, even when nothing was
322
+ inherited to mask. It keeps blocking the name if a workspace default appears
323
+ later. `inheritAppEnv` clears it.
324
+ - Masks **cost a slot**. A workspace has 128 env entries total — shared across
325
+ the workspace defaults *and* every app scope combined, not 128 each — and
326
+ masks count like values do.
327
+
328
+ `ok: false` from `deleteEnv`, `deleteFile`, `inheritAppEnv`, or a second
329
+ `removeAppEnv` is **not a failure**. It means "nothing to do — already in the
330
+ state you asked for". Real failures throw. So idempotent cleanup is just
331
+ ignoring the boolean.
332
+
333
+ `setEnvMany` batches like `writeFiles`, and is **not atomic** for the same
334
+ reason: a rejected name leaves the ones before it committed, with the successful
335
+ aliases in the thrown error's `data`.
336
+
337
+ ## Development
338
+
339
+ ```sh
340
+ bun install
341
+ bun run codegen # regenerate typed operations from the vendored schema
342
+ bun run typecheck
343
+ bun test
344
+ bun run build
345
+ bun run check # all of the above, plus a dist smoke test on Bun AND Node
346
+ ```
347
+
348
+ ### Regenerating from the schema
349
+
350
+ `schema/schema.graphqls` is a **vendored copy** of `graph/schema.graphqls` from
351
+ the [unfazed](https://unfazed.dev) repo — the same contract gqlgen serves. It is
352
+ vendored so `bun install && bun run build` works with no access to the platform
353
+ repo, and `schema/SOURCE.json` records the exact unfazed commit it came from.
354
+
355
+ To refresh it against a local unfazed checkout:
356
+
357
+ ```sh
358
+ bun run sync-schema # tries ../../../unfazed, then $UNFAZED_REPO
359
+ UNFAZED_REPO=~/src/unfazed bun run sync-schema
360
+ bun run sync-schema -- --repo ~/src/unfazed
361
+ bun run sync-schema -- --check # verify only; non-zero if stale
362
+
363
+ bun run codegen && bun test # ALWAYS follow a sync with these
364
+ ```
365
+
366
+ `src/generated/` is committed on purpose: consumers of the published package
367
+ never run codegen, and neither does a clean CI checkout. A schema change that
368
+ invalidates an operation shows up as a **compile error**, not a runtime
369
+ surprise.
370
+
371
+ `bun run codegen` also runs `scripts/postcodegen.ts`, which inlines the one
372
+ type-only import graphql-codegen emits (`DocumentTypeDecoration`) so the package
373
+ keeps zero dependencies. `test/generated.test.ts` asserts that invariant.
374
+
375
+ ## Publishing
376
+
377
+ Publishing is a deliberate, attended step — there is intentionally **no**
378
+ `prepublishOnly`/`prepare` script that could fire a publish or a stale build by
379
+ accident. `dist/` is gitignored, so build before publishing:
380
+
381
+ ```sh
382
+ bun run check # typecheck + tests + build + dist smoke
383
+ npm publish # or: bun publish --access public
384
+ ```
385
+
386
+ ## License
387
+
388
+ 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.2.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
+ }