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.
@@ -0,0 +1,510 @@
1
+ import { UnfazedClient } from "./client.js";
2
+ import { UnfazedConflictError, UnfazedDeployError, UnfazedGraphQLError, UnfazedNotFoundError, looksLikeCASConflict, } from "./errors.js";
3
+ import * as ops from "./operations.js";
4
+ import { pollWithBackoff, resolveWaitOptions } from "./wait.js";
5
+ /**
6
+ * Layer 2 — the agent workflow layer.
7
+ *
8
+ * Wraps the chains an agent actually performs (create → write → deploy → wait
9
+ * → read logs → roll back) so a code-mode script does in one call what would
10
+ * otherwise be many round trips through a model's context. Everything here is
11
+ * built on {@link UnfazedClient}, exposed as `.client` for anything not
12
+ * wrapped.
13
+ *
14
+ * ```ts
15
+ * const unfazed = new Unfazed(); // reads UNFZ_API_TOKEN
16
+ * const ws = await unfazed.createWorkspace("hello");
17
+ * await unfazed.writeFiles(ws.id, { "server.ts": code });
18
+ * const app = await unfazed.deployService({ workspaceId: ws.id, alias: "hello", wait: true });
19
+ * console.log(app.url);
20
+ * ```
21
+ */
22
+ export class Unfazed {
23
+ /** The underlying typed GraphQL client. Use it for un-wrapped operations. */
24
+ client;
25
+ constructor(options = {}) {
26
+ const { client, ...clientOptions } = options;
27
+ this.client = client ?? new UnfazedClient(clientOptions);
28
+ }
29
+ /* ---------------------------------------------------------------- identity */
30
+ /** Who this token authenticates as, plus usage and resource limits. */
31
+ async whoami(options) {
32
+ const data = await this.client.query(ops.WhoamiDocument, {}, options);
33
+ return data.whoami;
34
+ }
35
+ /* -------------------------------------------------------------- workspaces */
36
+ /** Creates a source workspace — the container for files, env and deployments. */
37
+ async createWorkspace(name, options) {
38
+ const data = await this.client.mutate(ops.CreateWorkspaceDocument, { name }, options);
39
+ return data.createWorkspace.workspace;
40
+ }
41
+ /** Workspaces visible to this token. */
42
+ async listWorkspaces(options) {
43
+ const data = await this.client.query(ops.ListWorkspacesDocument, {}, options);
44
+ return data.workspaces;
45
+ }
46
+ /**
47
+ * Deletes a workspace and everything anchored to it — deployments, env, and
48
+ * a BOX's persistent data disk. Not recoverable.
49
+ */
50
+ async deleteWorkspace(workspaceId, options) {
51
+ const data = await this.client.mutate(ops.DeleteWorkspaceDocument, { workspaceID: workspaceId }, options);
52
+ return data.deleteWorkspace.ok;
53
+ }
54
+ /* ------------------------------------------------------------------- files */
55
+ /**
56
+ * Writes many files in as few requests as possible.
57
+ *
58
+ * The API is one-file-per-mutation, so writing a 12-file app naively costs 12
59
+ * round trips. This batches them into a single GraphQL document with one
60
+ * ALIASED root field per file (`f0: writeWorkspaceTextFile(...)`,
61
+ * `f1: ...`), which the server resolves in one request.
62
+ *
63
+ * ```ts
64
+ * await unfazed.writeFiles(ws.id, {
65
+ * "server.ts": serverCode,
66
+ * "unfazed.json": JSON.stringify({ entry: "server.ts" }),
67
+ * "public/logo.png": { bytes: pngBytes },
68
+ * });
69
+ * ```
70
+ *
71
+ * Batches are NOT atomic: GraphQL resolves each aliased field independently,
72
+ * so a partial failure leaves earlier writes committed. The thrown
73
+ * {@link UnfazedGraphQLError} carries `data` with whichever aliases
74
+ * succeeded, and `error.path` identifies the ones that did not.
75
+ */
76
+ async writeFiles(workspaceId, files, options = {}) {
77
+ const entries = Object.entries(files).map(([path, content]) => normalizeFile(path, content));
78
+ if (entries.length === 0)
79
+ return [];
80
+ const batchSize = options.batchSize ?? 20;
81
+ const maxBatchBytes = options.maxBatchBytes ?? 4 * 1024 * 1024;
82
+ const written = [];
83
+ for (const batch of chunkFiles(entries, batchSize, maxBatchBytes)) {
84
+ written.push(...(await this.#writeBatch(workspaceId, batch, options.signal)));
85
+ }
86
+ return written;
87
+ }
88
+ /** Writes one text file. Prefer {@link writeFiles} for more than one. */
89
+ async writeFile(workspaceId, path, text, options) {
90
+ const data = await this.client.mutate(ops.WriteTextFileDocument, { workspaceID: workspaceId, path, text }, options);
91
+ return data.writeWorkspaceTextFile.file;
92
+ }
93
+ /** Writes one binary file. Accepts raw bytes or a base64 string. */
94
+ async writeBinaryFile(workspaceId, path, content, options) {
95
+ const contentBase64 = "base64" in content ? content.base64 : toBase64(content);
96
+ const data = await this.client.mutate(ops.WriteBase64FileDocument, { workspaceID: workspaceId, path, contentBase64 }, options);
97
+ return data.writeWorkspaceBase64File.file;
98
+ }
99
+ /** Lists workspace files (paginated). */
100
+ async listFiles(workspaceId, input = {}, options) {
101
+ const data = await this.client.query(ops.ListFilesDocument, { input: { workspaceID: workspaceId, ...input } }, options);
102
+ return data.workspaceFilesPage;
103
+ }
104
+ /** Reads a text file, optionally a byte range of it. Null when absent. */
105
+ async readFile(workspaceId, path, input = {}, options) {
106
+ const data = await this.client.query(ops.ReadFileDocument, { input: { workspaceID: workspaceId, path, ...input } }, options);
107
+ return data.readWorkspaceFile;
108
+ }
109
+ /** Deletes a workspace file. */
110
+ async deleteFile(workspaceId, path, options) {
111
+ const data = await this.client.mutate(ops.DeleteFileDocument, { workspaceID: workspaceId, path }, options);
112
+ return data.deleteWorkspaceFile.ok;
113
+ }
114
+ /* ----------------------------------------------------------------- deploys */
115
+ /**
116
+ * Deploys the workspace as a SERVICE (an HTTP server).
117
+ *
118
+ * With `wait: true` this resolves only once the build, VM boot and health
119
+ * probe have all passed.
120
+ */
121
+ deployService(options) {
122
+ const { workspaceId, alias, runtime, serviceMode, healthPath, cronSchedule, cronPath } = options;
123
+ return this.#deploy({
124
+ workspaceID: workspaceId,
125
+ kind: "SERVICE",
126
+ ...(alias === undefined ? {} : { alias }),
127
+ ...(runtime === undefined ? {} : { runtime }),
128
+ ...(serviceMode === undefined ? {} : { serviceMode }),
129
+ ...(healthPath === undefined ? {} : { healthPath }),
130
+ ...(cronSchedule === undefined ? {} : { cronSchedule }),
131
+ ...(cronPath === undefined ? {} : { cronPath }),
132
+ }, options);
133
+ }
134
+ /** Deploys the workspace as a STATIC site. */
135
+ deployStatic(options) {
136
+ const { workspaceId, alias, spaMode } = options;
137
+ return this.#deploy({
138
+ workspaceID: workspaceId,
139
+ kind: "STATIC",
140
+ ...(alias === undefined ? {} : { alias }),
141
+ ...(spaMode === undefined ? {} : { spaMode }),
142
+ }, options);
143
+ }
144
+ /**
145
+ * Deploys a BOX: a persistent always-on Linux VM with a data disk that
146
+ * survives stops and redeploys. A dev machine, not an HTTP service. The
147
+ * result's `sshCommand` is how you get in.
148
+ */
149
+ deployBox(options) {
150
+ const { workspaceId, alias, resources } = options;
151
+ return this.#deploy({
152
+ workspaceID: workspaceId,
153
+ kind: "BOX",
154
+ ...(alias === undefined ? {} : { alias }),
155
+ ...(resources === undefined ? {} : { boxResources: resources }),
156
+ }, options);
157
+ }
158
+ /** Escape hatch: deploy with a raw CreateDeploymentInput. */
159
+ deploy(input, options = {}) {
160
+ return this.#deploy(input, options);
161
+ }
162
+ async #deploy(input, options) {
163
+ const requestOptions = options.signal ? { signal: options.signal } : {};
164
+ const data = await this.client.mutate(ops.CreateDeploymentDocument, { input }, requestOptions);
165
+ const deployment = data.createDeployment.deployment;
166
+ if (!options.wait)
167
+ return deployment;
168
+ return this.waitForDeployment(deployment.id, {
169
+ ...options.waitOptions,
170
+ ...(options.signal ? { signal: options.signal } : {}),
171
+ });
172
+ }
173
+ /**
174
+ * Polls a deployment until it is READY, throwing on FAILED or timeout.
175
+ *
176
+ * BUILDING is the only non-terminal status; READY and FAILED are terminal.
177
+ * The poll query selects six fields rather than the full deployment, because
178
+ * this runs every few seconds for potentially minutes — then the READY
179
+ * deployment is re-read in full so callers get the same shape `deployService`
180
+ * returns without `wait`.
181
+ */
182
+ async waitForDeployment(deploymentId, waitOptions = {}) {
183
+ const resolved = resolveWaitOptions(waitOptions);
184
+ const requestOptions = resolved.signal ? { signal: resolved.signal } : {};
185
+ let last = null;
186
+ await pollWithBackoff(async () => {
187
+ const data = await this.client.query(ops.PollDeploymentDocument, { id: deploymentId }, requestOptions);
188
+ const d = data.deployment;
189
+ if (!d) {
190
+ throw new UnfazedNotFoundError(`unfazed: deployment ${deploymentId} not found`);
191
+ }
192
+ last = d;
193
+ if (d.status === "READY")
194
+ return { done: true, value: true };
195
+ if (d.status === "FAILED") {
196
+ throw new UnfazedDeployError("FAILED", deploymentId, d, `unfazed: deployment ${deploymentId} FAILED${d.error ? `: ${d.error}` : ""}`);
197
+ }
198
+ return { done: false };
199
+ }, resolved, () => new UnfazedDeployError("TIMEOUT", deploymentId, last, `unfazed: deployment ${deploymentId} still ${last?.status ?? "BUILDING"} after ${resolved.timeoutMs}ms`));
200
+ const full = await this.getDeployment(deploymentId, requestOptions);
201
+ if (!full)
202
+ throw new UnfazedNotFoundError(`unfazed: deployment ${deploymentId} not found`);
203
+ return full;
204
+ }
205
+ /** Reads one deployment by id. Null when absent. */
206
+ async getDeployment(deploymentId, options) {
207
+ const data = await this.client.query(ops.GetDeploymentDocument, { id: deploymentId }, options);
208
+ return data.deployment;
209
+ }
210
+ /** Reads the deployment currently serving an alias. Null when absent. */
211
+ async getDeploymentByAlias(alias, options) {
212
+ const data = await this.client.query(ops.GetDeploymentByAliasDocument, { alias }, options);
213
+ return data.deploymentByAlias;
214
+ }
215
+ /** Lists deployments (paginated, filterable by kind/alias/workspace). */
216
+ async listDeployments(input = {}, options) {
217
+ const data = await this.client.query(ops.ListDeploymentsDocument, { input }, options);
218
+ return data.deploymentsPage;
219
+ }
220
+ /* ------------------------------------------------------- runtime & logging */
221
+ /** Live VM status: health, in-flight requests, restarts, RAM ceiling. */
222
+ async getStatus(deploymentId, options) {
223
+ const data = await this.client.query(ops.ServiceStatusDocument, { deploymentID: deploymentId }, options);
224
+ return data.serviceStatus;
225
+ }
226
+ /**
227
+ * Tail of the service's stdout/stderr.
228
+ *
229
+ * The point of doing this from a script rather than through tool calls: you
230
+ * can filter locally. `(await unfazed.getLogs(id)).stderr.split("\n")
231
+ * .filter(l => l.includes("ERROR"))` costs a model zero context, where the
232
+ * same tail through an MCP tool spends all of it.
233
+ */
234
+ async getLogs(deploymentId, input = {}, options) {
235
+ const data = await this.client.query(ops.ServiceLogsDocument, { deploymentID: deploymentId, tailBytes: input.tailBytes ?? null }, options);
236
+ return data.serviceLogs;
237
+ }
238
+ /** Deployment lifecycle events — build, boot, probe, cron, restarts. */
239
+ async getEvents(deploymentId, limit, options) {
240
+ const data = await this.client.query(ops.DeploymentEventsDocument, { deploymentID: deploymentId, limit: limit ?? null }, options);
241
+ return data.deploymentEvents;
242
+ }
243
+ /** Restarts a service's VM. */
244
+ async restartService(deploymentId, options) {
245
+ const data = await this.client.mutate(ops.RestartServiceDocument, { deploymentID: deploymentId }, options);
246
+ return data.restartService;
247
+ }
248
+ /** Stops a service's VM. An on-demand service cold-starts again on request. */
249
+ async stopService(deploymentId, options) {
250
+ const data = await this.client.mutate(ops.StopServiceDocument, { deploymentID: deploymentId }, options);
251
+ return data.stopService;
252
+ }
253
+ /** Deletes a deployment. */
254
+ async deleteDeployment(deploymentId, options) {
255
+ const data = await this.client.mutate(ops.DeleteDeploymentDocument, { deploymentID: deploymentId }, options);
256
+ return data.deleteDeployment.ok;
257
+ }
258
+ /* ------------------------------------------------------------ env & secrets
259
+ *
260
+ * Workspace values are SERVICE defaults for every app in the source; app
261
+ * values override them for one alias. Both are SNAPSHOTTED into a release at
262
+ * deploy time — changing either does not affect a running app until you
263
+ * redeploy that alias. Old releases are immutable, and STATIC gets no env.
264
+ */
265
+ /** Sets a workspace-default env var. Redeploy the alias to apply it. */
266
+ async setEnv(workspaceId, name, value, options) {
267
+ const data = await this.client.mutate(ops.SetWorkspaceEnvDocument, { workspaceID: workspaceId, name, value }, options);
268
+ return data.setWorkspaceEnv.env;
269
+ }
270
+ /** Sets a workspace-default secret. The value is never readable again. */
271
+ async setSecret(workspaceId, name, value, options) {
272
+ const data = await this.client.mutate(ops.SetWorkspaceSecretDocument, { workspaceID: workspaceId, name, value }, options);
273
+ return data.setWorkspaceSecret.env;
274
+ }
275
+ /**
276
+ * Sets many workspace env vars in one request, via aliased mutations —
277
+ * the same batching trick as {@link writeFiles}.
278
+ *
279
+ * Pass `{ secret: true }` to route them all to `setWorkspaceSecret`.
280
+ */
281
+ async setEnvMany(workspaceId, vars, options = {}) {
282
+ const entries = Object.entries(vars);
283
+ if (entries.length === 0)
284
+ return [];
285
+ const field = options.secret ? "setWorkspaceSecret" : "setWorkspaceEnv";
286
+ const varDefs = ["$workspaceID: ID!"];
287
+ const fields = [];
288
+ const variables = { workspaceID: workspaceId };
289
+ entries.forEach(([name, value], i) => {
290
+ varDefs.push(`$n${i}: String!`, `$v${i}: String!`);
291
+ variables[`n${i}`] = name;
292
+ variables[`v${i}`] = value;
293
+ fields.push(` e${i}: ${field}(input: { workspaceID: $workspaceID, name: $n${i}, value: $v${i} }) { env { ${ENV_SELECTION} } }`);
294
+ });
295
+ const document = `mutation SdkSetEnvBatch(${varDefs.join(", ")}) {\n${fields.join("\n")}\n}`;
296
+ const data = await this.client.mutate(document, variables, options.signal ? { signal: options.signal } : {});
297
+ return entries.map((_, i) => data[`e${i}`].env);
298
+ }
299
+ /** Deletes a workspace-default env var. */
300
+ async deleteEnv(workspaceId, name, options) {
301
+ const data = await this.client.mutate(ops.DeleteWorkspaceEnvDocument, { workspaceID: workspaceId, name }, options);
302
+ return data.deleteWorkspaceEnv.ok;
303
+ }
304
+ /** Workspace-default env vars. Secret values are redacted. */
305
+ async listEnv(workspaceId, options) {
306
+ const data = await this.client.query(ops.ListWorkspaceEnvDocument, { workspaceID: workspaceId }, options);
307
+ return data.workspaceEnv;
308
+ }
309
+ /** Sets an env var for ONE alias, overriding the workspace default. */
310
+ async setAppEnv(workspaceId, alias, name, value, options) {
311
+ const data = await this.client.mutate(ops.SetAppEnvDocument, { workspaceID: workspaceId, alias, name, value }, options);
312
+ return data.setAppEnv.env;
313
+ }
314
+ /** Sets a secret for ONE alias, overriding the workspace default. */
315
+ async setAppSecret(workspaceId, alias, name, value, options) {
316
+ const data = await this.client.mutate(ops.SetAppSecretDocument, { workspaceID: workspaceId, alias, name, value }, options);
317
+ return data.setAppSecret.env;
318
+ }
319
+ /**
320
+ * REMOVE: blocks this name for the app entirely — the app override, the
321
+ * workspace default, and integration enrichment. Not the same as deleting an
322
+ * override; use {@link inheritAppEnv} for that.
323
+ */
324
+ async removeAppEnv(workspaceId, alias, name, options) {
325
+ const data = await this.client.mutate(ops.RemoveAppEnvDocument, { workspaceID: workspaceId, alias, name }, options);
326
+ return data.removeAppEnv.ok;
327
+ }
328
+ /** INHERIT: clears the app override/mask so workspace and integration values apply again. */
329
+ async inheritAppEnv(workspaceId, alias, name, options) {
330
+ const data = await this.client.mutate(ops.InheritAppEnvDocument, { workspaceID: workspaceId, alias, name }, options);
331
+ return data.inheritAppEnv.ok;
332
+ }
333
+ /** The effective env view for one alias: overrides, inherited and masked. */
334
+ async listAppEnv(workspaceId, alias, options) {
335
+ const data = await this.client.query(ops.ListAppEnvDocument, { workspaceID: workspaceId, alias }, options);
336
+ return data.appEnv;
337
+ }
338
+ /* -------------------------------------------------- releases & rollback */
339
+ /**
340
+ * An alias's terminal release history, newest first.
341
+ *
342
+ * Exactly one entry has `aliasHeld: true` — the live release. FAILED entries
343
+ * appear on purpose (they are rejected promotions, so a kept-live previous
344
+ * release is never mistaken for a silent fallback) and cannot be rolled back
345
+ * to. Retention keeps only the most recent few per alias, default 5.
346
+ */
347
+ async listReleases(alias, options) {
348
+ const data = await this.client.query(ops.ListReleasesDocument, { alias }, options);
349
+ return data.deploymentReleases;
350
+ }
351
+ /**
352
+ * Rolls an alias back to a prior release, handling the whole compare-and-swap
353
+ * protocol in one call.
354
+ *
355
+ * The server requires the caller to prove it observed the current state: you
356
+ * must pass the live release's id AND its opaque `aliasRevision`, and the
357
+ * swap fails if either changed since you listed — including same-id ABA. So
358
+ * this method does the full dance: list releases, find the `aliasHeld` entry,
359
+ * pick the target, and submit both guard values.
360
+ *
361
+ * On a rejected guard it re-lists and retries (default once). The TARGET is
362
+ * pinned across retries while the guard is refreshed, so a concurrent deploy
363
+ * can never redirect the rollback to a different release than the one chosen.
364
+ *
365
+ * ```ts
366
+ * await unfazed.rollback({ alias: "my-app" }); // undo last deploy
367
+ * await unfazed.rollback({ alias: "my-app", to: relId }); // to a specific release
368
+ * ```
369
+ */
370
+ async rollback(options) {
371
+ const { alias, signal } = options;
372
+ const retries = options.retries ?? 1;
373
+ const requestOptions = signal ? { signal } : {};
374
+ let targetId = options.to;
375
+ for (let attempt = 0;; attempt++) {
376
+ const releases = await this.listReleases(alias, requestOptions);
377
+ if (releases.length === 0) {
378
+ throw new UnfazedNotFoundError(`unfazed: alias "${alias}" has no release history`);
379
+ }
380
+ const current = releases.find((r) => r.aliasHeld);
381
+ if (!current) {
382
+ throw new UnfazedNotFoundError(`unfazed: alias "${alias}" has no current release holding the alias`);
383
+ }
384
+ // Pin the target on the first pass so a retry refreshes only the guard.
385
+ if (targetId === undefined) {
386
+ const previous = releases.find((r) => r.id !== current.id && r.status === "READY");
387
+ if (!previous) {
388
+ throw new UnfazedNotFoundError(`unfazed: alias "${alias}" has no prior READY release to roll back to ` +
389
+ `(history: ${releases.map((r) => `${r.id}:${r.status}`).join(", ")})`);
390
+ }
391
+ targetId = previous.id;
392
+ }
393
+ const target = releases.find((r) => r.id === targetId);
394
+ if (!target) {
395
+ throw new UnfazedNotFoundError(`unfazed: release ${targetId} is not in alias "${alias}"'s history ` +
396
+ `(it may have been pruned; retention keeps only the most recent few)`);
397
+ }
398
+ if (target.id === current.id) {
399
+ throw new UnfazedConflictError(`unfazed: release ${targetId} already holds alias "${alias}"`, []);
400
+ }
401
+ if (target.status !== "READY") {
402
+ throw new UnfazedConflictError(`unfazed: release ${targetId} is ${target.status}, only READY releases can be restored`, []);
403
+ }
404
+ try {
405
+ const data = await this.client.mutate(ops.RollbackDocument, {
406
+ deploymentID: target.id,
407
+ expectedCurrentDeploymentID: current.id,
408
+ expectedCurrentAliasRevision: current.aliasRevision,
409
+ }, requestOptions);
410
+ return data.rollbackDeployment;
411
+ }
412
+ catch (e) {
413
+ const retryable = e instanceof UnfazedGraphQLError && looksLikeCASConflict(e.errors) && attempt < retries;
414
+ if (!retryable) {
415
+ if (e instanceof UnfazedGraphQLError && looksLikeCASConflict(e.errors)) {
416
+ throw new UnfazedConflictError(`unfazed: rollback of "${alias}" lost the compare-and-swap after ${attempt + 1} attempt(s) — the alias is being deployed concurrently. ${e.message}`, e.errors);
417
+ }
418
+ throw e;
419
+ }
420
+ // Loop: re-list, rebuild the guard, keep the same target.
421
+ }
422
+ }
423
+ }
424
+ /* -------------------------------------------------------------- internals */
425
+ async #writeBatch(workspaceId, batch, signal) {
426
+ const varDefs = ["$workspaceID: ID!"];
427
+ const fields = [];
428
+ const variables = { workspaceID: workspaceId };
429
+ batch.forEach((file, i) => {
430
+ variables[`p${i}`] = file.path;
431
+ variables[`c${i}`] = file.content;
432
+ varDefs.push(`$p${i}: String!`, `$c${i}: String!`);
433
+ fields.push(file.kind === "text"
434
+ ? ` f${i}: writeWorkspaceTextFile(input: { workspaceID: $workspaceID, path: $p${i}, text: $c${i} }) { file { ${FILE_SELECTION} } }`
435
+ : ` f${i}: writeWorkspaceBase64File(input: { workspaceID: $workspaceID, path: $p${i}, contentBase64: $c${i} }) { file { ${FILE_SELECTION} } }`);
436
+ });
437
+ // Built at runtime rather than codegen'd: the field COUNT varies per call,
438
+ // so no static document can express it. The field shapes are still fixed
439
+ // and typed below, and each individual mutation is codegen-validated by its
440
+ // single-file twin in operations.ts.
441
+ const document = `mutation SdkWriteFilesBatch(${varDefs.join(", ")}) {\n${fields.join("\n")}\n}`;
442
+ const data = await this.client.mutate(document, variables, signal ? { signal } : {});
443
+ return batch.map((_, i) => data[`f${i}`].file);
444
+ }
445
+ }
446
+ const FILE_SELECTION = "path sizeBytes size sha256 updatedAt";
447
+ const ENV_SELECTION = "name value secret alias inherited masked updatedAt";
448
+ function normalizeFile(path, content) {
449
+ if (typeof content === "string")
450
+ return { path, kind: "text", content };
451
+ if ("text" in content)
452
+ return { path, kind: "text", content: content.text };
453
+ if ("base64" in content)
454
+ return { path, kind: "base64", content: content.base64 };
455
+ return { path, kind: "base64", content: toBase64(content.bytes) };
456
+ }
457
+ function toBase64(input) {
458
+ const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
459
+ // Node/Bun have Buffer; browsers and workers do not.
460
+ const maybeBuffer = globalThis
461
+ .Buffer;
462
+ if (maybeBuffer)
463
+ return maybeBuffer.from(bytes).toString("base64");
464
+ let binary = "";
465
+ const CHUNK = 0x8000; // avoid blowing the argument limit on large files
466
+ for (let i = 0; i < bytes.length; i += CHUNK) {
467
+ binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
468
+ }
469
+ return btoa(binary);
470
+ }
471
+ const utf8 = new TextEncoder();
472
+ /**
473
+ * UTF-8 byte length, which is what the wire actually costs.
474
+ *
475
+ * `String.length` counts UTF-16 code units and undercounts by up to 3x on
476
+ * non-ASCII text — a CJK or emoji-heavy payload would sail past a budget it
477
+ * had already blown.
478
+ */
479
+ function utf8Bytes(s) {
480
+ return utf8.encode(s).length;
481
+ }
482
+ /**
483
+ * Splits files into request-sized batches.
484
+ *
485
+ * Closes a batch on whichever limit trips first — count or bytes — but always
486
+ * emits at least one file per batch, so a single file larger than
487
+ * `maxBatchBytes` still gets sent (and fails loudly at the server's limit)
488
+ * rather than silently vanishing into an empty batch.
489
+ */
490
+ function chunkFiles(files, batchSize, maxBatchBytes) {
491
+ const batches = [];
492
+ let current = [];
493
+ let bytes = 0;
494
+ for (const file of files) {
495
+ // Measured once per file, so the encode cost is linear in the payload the
496
+ // request was going to carry anyway.
497
+ const size = utf8Bytes(file.content) + utf8Bytes(file.path);
498
+ if (current.length > 0 && (current.length >= batchSize || bytes + size > maxBatchBytes)) {
499
+ batches.push(current);
500
+ current = [];
501
+ bytes = 0;
502
+ }
503
+ current.push(file);
504
+ bytes += size;
505
+ }
506
+ if (current.length > 0)
507
+ batches.push(current);
508
+ return batches;
509
+ }
510
+ //# sourceMappingURL=unfazed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unfazed.js","sourceRoot":"","sources":["../src/unfazed.ts"],"names":[],"mappings":"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","sourcesContent":["import { UnfazedClient, type ClientOptions, type RequestOptions } from \"./client.js\";\nimport {\n UnfazedConflictError,\n UnfazedDeployError,\n UnfazedGraphQLError,\n UnfazedNotFoundError,\n looksLikeCASConflict,\n} from \"./errors.js\";\nimport * as ops from \"./operations.js\";\nimport type {\n CreateDeploymentInput,\n DeploymentFieldsFragment,\n ListDeploymentsInput,\n ServiceMode,\n ServiceRuntime,\n} from \"./generated/graphql.js\";\nimport { pollWithBackoff, resolveWaitOptions, type WaitOptions } from \"./wait.js\";\n\n/** A deployment as the workflow layer returns it. */\nexport type Deployment = DeploymentFieldsFragment;\n\n/** A file record returned by any write/list operation. */\nexport interface WorkspaceFile {\n path: string;\n sizeBytes: number;\n size: string;\n sha256: string;\n updatedAt: string;\n}\n\n/** One env var in a workspace- or app-scoped view. Secret values are never returned. */\nexport interface EnvVar {\n name: string;\n value: string | null;\n secret: boolean;\n alias: string | null;\n inherited: boolean;\n masked: boolean;\n updatedAt: string;\n}\n\n/**\n * A file's contents for {@link Unfazed.writeFiles}.\n *\n * A bare string is text — the overwhelmingly common case, and what makes\n * `{ \"server.ts\": code }` read well. Binary goes through `bytes` or `base64`,\n * which route to `writeWorkspaceBase64File` instead.\n */\nexport type FileContent =\n | string\n | { text: string }\n | { base64: string }\n | { bytes: Uint8Array | ArrayBuffer };\n\n/** Files keyed by workspace-relative path. */\nexport type FilesMap = Record<string, FileContent>;\n\nexport interface WriteFilesOptions {\n /**\n * Max files per GraphQL request. Default 20.\n *\n * The batch is one document with N aliased root fields; too many at once\n * makes a single oversized request whose failure is all-or-nothing to\n * diagnose. Twenty keeps a typical app to one or two round trips.\n */\n batchSize?: number;\n /**\n * Max total payload bytes per request. Default 4 MiB. Whichever limit trips\n * first closes the batch, so one large file cannot blow the request up.\n */\n maxBatchBytes?: number;\n signal?: AbortSignal;\n}\n\n/** Common options for the deploy helpers. */\nexport interface DeployOptions {\n workspaceId: string;\n alias?: string;\n /**\n * Block until the deployment reaches a terminal status.\n *\n * `createDeployment` returns BUILDING immediately; the build, VM boot and\n * health probe run in the background. With `wait: true` the call resolves\n * only on READY and throws {@link UnfazedDeployError} on FAILED — which is\n * what makes a one-script deploy possible.\n */\n wait?: boolean;\n /** Backoff/timeout knobs used when `wait` is true. */\n waitOptions?: WaitOptions;\n signal?: AbortSignal;\n}\n\nexport interface DeployServiceOptions extends DeployOptions {\n /** Omit to auto-detect: a root go.mod+main.go is GO, otherwise BUN. */\n runtime?: ServiceRuntime;\n /** Omit for ON_DEMAND. Only set ALWAYS_ON to keep the VM warm. */\n serviceMode?: ServiceMode;\n healthPath?: string;\n /** Standard 5-field cron spec. Prefer ON_DEMAND + cron over ALWAYS_ON. */\n cronSchedule?: string;\n /** Path the cron tick POSTs to. Defaults to /cron when cronSchedule is set. */\n cronPath?: string;\n}\n\nexport interface DeployStaticOptions extends DeployOptions {\n /** Serve index.html for missing paths (single-page apps). */\n spaMode?: boolean;\n}\n\nexport interface DeployBoxOptions extends DeployOptions {\n /** VM size. Omit for platform defaults (2048 MiB / 2 vCPU / 8 GiB disk). */\n resources?: { memMiB?: number; vcpus?: number; diskGiB?: number };\n}\n\nexport interface RollbackOptions {\n /** The alias to roll back. */\n alias: string;\n /**\n * Release to restore. Omit to pick the most recent READY release that is not\n * the current holder — i.e. \"undo the last deploy\".\n */\n to?: string;\n /**\n * Re-list-and-retry attempts when the compare-and-swap guard is rejected.\n * Default 1. Each retry refreshes the guard from a fresh read; the TARGET\n * release stays pinned, so a retry can never silently roll back to something\n * other than what was chosen.\n */\n retries?: number;\n signal?: AbortSignal;\n}\n\nexport interface UnfazedOptions extends ClientOptions {\n /** Use an existing core client instead of constructing one. */\n client?: UnfazedClient;\n}\n\n/**\n * Layer 2 — the agent workflow layer.\n *\n * Wraps the chains an agent actually performs (create → write → deploy → wait\n * → read logs → roll back) so a code-mode script does in one call what would\n * otherwise be many round trips through a model's context. Everything here is\n * built on {@link UnfazedClient}, exposed as `.client` for anything not\n * wrapped.\n *\n * ```ts\n * const unfazed = new Unfazed(); // reads UNFZ_API_TOKEN\n * const ws = await unfazed.createWorkspace(\"hello\");\n * await unfazed.writeFiles(ws.id, { \"server.ts\": code });\n * const app = await unfazed.deployService({ workspaceId: ws.id, alias: \"hello\", wait: true });\n * console.log(app.url);\n * ```\n */\nexport class Unfazed {\n /** The underlying typed GraphQL client. Use it for un-wrapped operations. */\n readonly client: UnfazedClient;\n\n constructor(options: UnfazedOptions = {}) {\n const { client, ...clientOptions } = options;\n this.client = client ?? new UnfazedClient(clientOptions);\n }\n\n /* ---------------------------------------------------------------- identity */\n\n /** Who this token authenticates as, plus usage and resource limits. */\n async whoami(options?: RequestOptions) {\n const data = await this.client.query(ops.WhoamiDocument, {}, options);\n return data.whoami;\n }\n\n /* -------------------------------------------------------------- workspaces */\n\n /** Creates a source workspace — the container for files, env and deployments. */\n async createWorkspace(name: string, options?: RequestOptions) {\n const data = await this.client.mutate(ops.CreateWorkspaceDocument, { name }, options);\n return data.createWorkspace.workspace;\n }\n\n /** Workspaces visible to this token. */\n async listWorkspaces(options?: RequestOptions) {\n const data = await this.client.query(ops.ListWorkspacesDocument, {}, options);\n return data.workspaces;\n }\n\n /**\n * Deletes a workspace and everything anchored to it — deployments, env, and\n * a BOX's persistent data disk. Not recoverable.\n */\n async deleteWorkspace(workspaceId: string, options?: RequestOptions): Promise<boolean> {\n const data = await this.client.mutate(\n ops.DeleteWorkspaceDocument,\n { workspaceID: workspaceId },\n options,\n );\n return data.deleteWorkspace.ok;\n }\n\n /* ------------------------------------------------------------------- files */\n\n /**\n * Writes many files in as few requests as possible.\n *\n * The API is one-file-per-mutation, so writing a 12-file app naively costs 12\n * round trips. This batches them into a single GraphQL document with one\n * ALIASED root field per file (`f0: writeWorkspaceTextFile(...)`,\n * `f1: ...`), which the server resolves in one request.\n *\n * ```ts\n * await unfazed.writeFiles(ws.id, {\n * \"server.ts\": serverCode,\n * \"unfazed.json\": JSON.stringify({ entry: \"server.ts\" }),\n * \"public/logo.png\": { bytes: pngBytes },\n * });\n * ```\n *\n * Batches are NOT atomic: GraphQL resolves each aliased field independently,\n * so a partial failure leaves earlier writes committed. The thrown\n * {@link UnfazedGraphQLError} carries `data` with whichever aliases\n * succeeded, and `error.path` identifies the ones that did not.\n */\n async writeFiles(\n workspaceId: string,\n files: FilesMap,\n options: WriteFilesOptions = {},\n ): Promise<WorkspaceFile[]> {\n const entries = Object.entries(files).map(([path, content]) => normalizeFile(path, content));\n if (entries.length === 0) return [];\n\n const batchSize = options.batchSize ?? 20;\n const maxBatchBytes = options.maxBatchBytes ?? 4 * 1024 * 1024;\n\n const written: WorkspaceFile[] = [];\n for (const batch of chunkFiles(entries, batchSize, maxBatchBytes)) {\n written.push(...(await this.#writeBatch(workspaceId, batch, options.signal)));\n }\n return written;\n }\n\n /** Writes one text file. Prefer {@link writeFiles} for more than one. */\n async writeFile(\n workspaceId: string,\n path: string,\n text: string,\n options?: RequestOptions,\n ): Promise<WorkspaceFile> {\n const data = await this.client.mutate(\n ops.WriteTextFileDocument,\n { workspaceID: workspaceId, path, text },\n options,\n );\n return data.writeWorkspaceTextFile.file;\n }\n\n /** Writes one binary file. Accepts raw bytes or a base64 string. */\n async writeBinaryFile(\n workspaceId: string,\n path: string,\n content: Uint8Array | ArrayBuffer | { base64: string },\n options?: RequestOptions,\n ): Promise<WorkspaceFile> {\n const contentBase64 = \"base64\" in content ? content.base64 : toBase64(content);\n const data = await this.client.mutate(\n ops.WriteBase64FileDocument,\n { workspaceID: workspaceId, path, contentBase64 },\n options,\n );\n return data.writeWorkspaceBase64File.file;\n }\n\n /** Lists workspace files (paginated). */\n async listFiles(\n workspaceId: string,\n input: { limit?: number; offset?: number; order?: string } = {},\n options?: RequestOptions,\n ) {\n const data = await this.client.query(\n ops.ListFilesDocument,\n { input: { workspaceID: workspaceId, ...input } },\n options,\n );\n return data.workspaceFilesPage;\n }\n\n /** Reads a text file, optionally a byte range of it. Null when absent. */\n async readFile(\n workspaceId: string,\n path: string,\n input: { offset?: number; limit?: number } = {},\n options?: RequestOptions,\n ) {\n const data = await this.client.query(\n ops.ReadFileDocument,\n { input: { workspaceID: workspaceId, path, ...input } },\n options,\n );\n return data.readWorkspaceFile;\n }\n\n /** Deletes a workspace file. */\n async deleteFile(\n workspaceId: string,\n path: string,\n options?: RequestOptions,\n ): Promise<boolean> {\n const data = await this.client.mutate(\n ops.DeleteFileDocument,\n { workspaceID: workspaceId, path },\n options,\n );\n return data.deleteWorkspaceFile.ok;\n }\n\n /* ----------------------------------------------------------------- deploys */\n\n /**\n * Deploys the workspace as a SERVICE (an HTTP server).\n *\n * With `wait: true` this resolves only once the build, VM boot and health\n * probe have all passed.\n */\n deployService(options: DeployServiceOptions): Promise<Deployment> {\n const { workspaceId, alias, runtime, serviceMode, healthPath, cronSchedule, cronPath } =\n options;\n return this.#deploy(\n {\n workspaceID: workspaceId,\n kind: \"SERVICE\",\n ...(alias === undefined ? {} : { alias }),\n ...(runtime === undefined ? {} : { runtime }),\n ...(serviceMode === undefined ? {} : { serviceMode }),\n ...(healthPath === undefined ? {} : { healthPath }),\n ...(cronSchedule === undefined ? {} : { cronSchedule }),\n ...(cronPath === undefined ? {} : { cronPath }),\n },\n options,\n );\n }\n\n /** Deploys the workspace as a STATIC site. */\n deployStatic(options: DeployStaticOptions): Promise<Deployment> {\n const { workspaceId, alias, spaMode } = options;\n return this.#deploy(\n {\n workspaceID: workspaceId,\n kind: \"STATIC\",\n ...(alias === undefined ? {} : { alias }),\n ...(spaMode === undefined ? {} : { spaMode }),\n },\n options,\n );\n }\n\n /**\n * Deploys a BOX: a persistent always-on Linux VM with a data disk that\n * survives stops and redeploys. A dev machine, not an HTTP service. The\n * result's `sshCommand` is how you get in.\n */\n deployBox(options: DeployBoxOptions): Promise<Deployment> {\n const { workspaceId, alias, resources } = options;\n return this.#deploy(\n {\n workspaceID: workspaceId,\n kind: \"BOX\",\n ...(alias === undefined ? {} : { alias }),\n ...(resources === undefined ? {} : { boxResources: resources }),\n },\n options,\n );\n }\n\n /** Escape hatch: deploy with a raw CreateDeploymentInput. */\n deploy(\n input: CreateDeploymentInput,\n options: Omit<DeployOptions, \"workspaceId\"> = {},\n ): Promise<Deployment> {\n return this.#deploy(input, options);\n }\n\n async #deploy(\n input: CreateDeploymentInput,\n options: Omit<DeployOptions, \"workspaceId\">,\n ): Promise<Deployment> {\n const requestOptions: RequestOptions = options.signal ? { signal: options.signal } : {};\n const data = await this.client.mutate(ops.CreateDeploymentDocument, { input }, requestOptions);\n const deployment = data.createDeployment.deployment;\n if (!options.wait) return deployment;\n return this.waitForDeployment(deployment.id, {\n ...options.waitOptions,\n ...(options.signal ? { signal: options.signal } : {}),\n });\n }\n\n /**\n * Polls a deployment until it is READY, throwing on FAILED or timeout.\n *\n * BUILDING is the only non-terminal status; READY and FAILED are terminal.\n * The poll query selects six fields rather than the full deployment, because\n * this runs every few seconds for potentially minutes — then the READY\n * deployment is re-read in full so callers get the same shape `deployService`\n * returns without `wait`.\n */\n async waitForDeployment(deploymentId: string, waitOptions: WaitOptions = {}): Promise<Deployment> {\n const resolved = resolveWaitOptions(waitOptions);\n const requestOptions: RequestOptions = resolved.signal ? { signal: resolved.signal } : {};\n let last: { id: string; status: string; error: string | null } | null = null;\n\n await pollWithBackoff<true>(\n async () => {\n const data = await this.client.query(\n ops.PollDeploymentDocument,\n { id: deploymentId },\n requestOptions,\n );\n const d = data.deployment;\n if (!d) {\n throw new UnfazedNotFoundError(`unfazed: deployment ${deploymentId} not found`);\n }\n last = d;\n if (d.status === \"READY\") return { done: true, value: true };\n if (d.status === \"FAILED\") {\n throw new UnfazedDeployError(\n \"FAILED\",\n deploymentId,\n d,\n `unfazed: deployment ${deploymentId} FAILED${d.error ? `: ${d.error}` : \"\"}`,\n );\n }\n return { done: false };\n },\n resolved,\n () =>\n new UnfazedDeployError(\n \"TIMEOUT\",\n deploymentId,\n last,\n `unfazed: deployment ${deploymentId} still ${\n (last as { status?: string } | null)?.status ?? \"BUILDING\"\n } after ${resolved.timeoutMs}ms`,\n ),\n );\n\n const full = await this.getDeployment(deploymentId, requestOptions);\n if (!full) throw new UnfazedNotFoundError(`unfazed: deployment ${deploymentId} not found`);\n return full;\n }\n\n /** Reads one deployment by id. Null when absent. */\n async getDeployment(\n deploymentId: string,\n options?: RequestOptions,\n ): Promise<Deployment | null> {\n const data = await this.client.query(ops.GetDeploymentDocument, { id: deploymentId }, options);\n return data.deployment;\n }\n\n /** Reads the deployment currently serving an alias. Null when absent. */\n async getDeploymentByAlias(\n alias: string,\n options?: RequestOptions,\n ): Promise<Deployment | null> {\n const data = await this.client.query(ops.GetDeploymentByAliasDocument, { alias }, options);\n return data.deploymentByAlias;\n }\n\n /** Lists deployments (paginated, filterable by kind/alias/workspace). */\n async listDeployments(input: ListDeploymentsInput = {}, options?: RequestOptions) {\n const data = await this.client.query(ops.ListDeploymentsDocument, { input }, options);\n return data.deploymentsPage;\n }\n\n /* ------------------------------------------------------- runtime & logging */\n\n /** Live VM status: health, in-flight requests, restarts, RAM ceiling. */\n async getStatus(deploymentId: string, options?: RequestOptions) {\n const data = await this.client.query(\n ops.ServiceStatusDocument,\n { deploymentID: deploymentId },\n options,\n );\n return data.serviceStatus;\n }\n\n /**\n * Tail of the service's stdout/stderr.\n *\n * The point of doing this from a script rather than through tool calls: you\n * can filter locally. `(await unfazed.getLogs(id)).stderr.split(\"\\n\")\n * .filter(l => l.includes(\"ERROR\"))` costs a model zero context, where the\n * same tail through an MCP tool spends all of it.\n */\n async getLogs(\n deploymentId: string,\n input: { tailBytes?: number } = {},\n options?: RequestOptions,\n ) {\n const data = await this.client.query(\n ops.ServiceLogsDocument,\n { deploymentID: deploymentId, tailBytes: input.tailBytes ?? null },\n options,\n );\n return data.serviceLogs;\n }\n\n /** Deployment lifecycle events — build, boot, probe, cron, restarts. */\n async getEvents(deploymentId: string, limit?: number, options?: RequestOptions) {\n const data = await this.client.query(\n ops.DeploymentEventsDocument,\n { deploymentID: deploymentId, limit: limit ?? null },\n options,\n );\n return data.deploymentEvents;\n }\n\n /** Restarts a service's VM. */\n async restartService(deploymentId: string, options?: RequestOptions) {\n const data = await this.client.mutate(\n ops.RestartServiceDocument,\n { deploymentID: deploymentId },\n options,\n );\n return data.restartService;\n }\n\n /** Stops a service's VM. An on-demand service cold-starts again on request. */\n async stopService(deploymentId: string, options?: RequestOptions) {\n const data = await this.client.mutate(\n ops.StopServiceDocument,\n { deploymentID: deploymentId },\n options,\n );\n return data.stopService;\n }\n\n /** Deletes a deployment. */\n async deleteDeployment(deploymentId: string, options?: RequestOptions): Promise<boolean> {\n const data = await this.client.mutate(\n ops.DeleteDeploymentDocument,\n { deploymentID: deploymentId },\n options,\n );\n return data.deleteDeployment.ok;\n }\n\n /* ------------------------------------------------------------ env & secrets\n *\n * Workspace values are SERVICE defaults for every app in the source; app\n * values override them for one alias. Both are SNAPSHOTTED into a release at\n * deploy time — changing either does not affect a running app until you\n * redeploy that alias. Old releases are immutable, and STATIC gets no env.\n */\n\n /** Sets a workspace-default env var. Redeploy the alias to apply it. */\n async setEnv(\n workspaceId: string,\n name: string,\n value: string,\n options?: RequestOptions,\n ): Promise<EnvVar> {\n const data = await this.client.mutate(\n ops.SetWorkspaceEnvDocument,\n { workspaceID: workspaceId, name, value },\n options,\n );\n return data.setWorkspaceEnv.env;\n }\n\n /** Sets a workspace-default secret. The value is never readable again. */\n async setSecret(\n workspaceId: string,\n name: string,\n value: string,\n options?: RequestOptions,\n ): Promise<EnvVar> {\n const data = await this.client.mutate(\n ops.SetWorkspaceSecretDocument,\n { workspaceID: workspaceId, name, value },\n options,\n );\n return data.setWorkspaceSecret.env;\n }\n\n /**\n * Sets many workspace env vars in one request, via aliased mutations —\n * the same batching trick as {@link writeFiles}.\n *\n * Pass `{ secret: true }` to route them all to `setWorkspaceSecret`.\n */\n async setEnvMany(\n workspaceId: string,\n vars: Record<string, string>,\n options: { secret?: boolean; signal?: AbortSignal } = {},\n ): Promise<EnvVar[]> {\n const entries = Object.entries(vars);\n if (entries.length === 0) return [];\n\n const field = options.secret ? \"setWorkspaceSecret\" : \"setWorkspaceEnv\";\n const varDefs = [\"$workspaceID: ID!\"];\n const fields: string[] = [];\n const variables: Record<string, string> = { workspaceID: workspaceId };\n\n entries.forEach(([name, value], i) => {\n varDefs.push(`$n${i}: String!`, `$v${i}: String!`);\n variables[`n${i}`] = name;\n variables[`v${i}`] = value;\n fields.push(\n ` e${i}: ${field}(input: { workspaceID: $workspaceID, name: $n${i}, value: $v${i} }) { env { ${ENV_SELECTION} } }`,\n );\n });\n\n const document = `mutation SdkSetEnvBatch(${varDefs.join(\", \")}) {\\n${fields.join(\"\\n\")}\\n}`;\n const data = await this.client.mutate<Record<string, { env: EnvVar }>, typeof variables>(\n document,\n variables,\n options.signal ? { signal: options.signal } : {},\n );\n return entries.map((_, i) => data[`e${i}`]!.env);\n }\n\n /** Deletes a workspace-default env var. */\n async deleteEnv(workspaceId: string, name: string, options?: RequestOptions): Promise<boolean> {\n const data = await this.client.mutate(\n ops.DeleteWorkspaceEnvDocument,\n { workspaceID: workspaceId, name },\n options,\n );\n return data.deleteWorkspaceEnv.ok;\n }\n\n /** Workspace-default env vars. Secret values are redacted. */\n async listEnv(workspaceId: string, options?: RequestOptions): Promise<EnvVar[]> {\n const data = await this.client.query(\n ops.ListWorkspaceEnvDocument,\n { workspaceID: workspaceId },\n options,\n );\n return data.workspaceEnv;\n }\n\n /** Sets an env var for ONE alias, overriding the workspace default. */\n async setAppEnv(\n workspaceId: string,\n alias: string,\n name: string,\n value: string,\n options?: RequestOptions,\n ): Promise<EnvVar> {\n const data = await this.client.mutate(\n ops.SetAppEnvDocument,\n { workspaceID: workspaceId, alias, name, value },\n options,\n );\n return data.setAppEnv.env;\n }\n\n /** Sets a secret for ONE alias, overriding the workspace default. */\n async setAppSecret(\n workspaceId: string,\n alias: string,\n name: string,\n value: string,\n options?: RequestOptions,\n ): Promise<EnvVar> {\n const data = await this.client.mutate(\n ops.SetAppSecretDocument,\n { workspaceID: workspaceId, alias, name, value },\n options,\n );\n return data.setAppSecret.env;\n }\n\n /**\n * REMOVE: blocks this name for the app entirely — the app override, the\n * workspace default, and integration enrichment. Not the same as deleting an\n * override; use {@link inheritAppEnv} for that.\n */\n async removeAppEnv(\n workspaceId: string,\n alias: string,\n name: string,\n options?: RequestOptions,\n ): Promise<boolean> {\n const data = await this.client.mutate(\n ops.RemoveAppEnvDocument,\n { workspaceID: workspaceId, alias, name },\n options,\n );\n return data.removeAppEnv.ok;\n }\n\n /** INHERIT: clears the app override/mask so workspace and integration values apply again. */\n async inheritAppEnv(\n workspaceId: string,\n alias: string,\n name: string,\n options?: RequestOptions,\n ): Promise<boolean> {\n const data = await this.client.mutate(\n ops.InheritAppEnvDocument,\n { workspaceID: workspaceId, alias, name },\n options,\n );\n return data.inheritAppEnv.ok;\n }\n\n /** The effective env view for one alias: overrides, inherited and masked. */\n async listAppEnv(\n workspaceId: string,\n alias: string,\n options?: RequestOptions,\n ): Promise<EnvVar[]> {\n const data = await this.client.query(\n ops.ListAppEnvDocument,\n { workspaceID: workspaceId, alias },\n options,\n );\n return data.appEnv;\n }\n\n /* -------------------------------------------------- releases & rollback */\n\n /**\n * An alias's terminal release history, newest first.\n *\n * Exactly one entry has `aliasHeld: true` — the live release. FAILED entries\n * appear on purpose (they are rejected promotions, so a kept-live previous\n * release is never mistaken for a silent fallback) and cannot be rolled back\n * to. Retention keeps only the most recent few per alias, default 5.\n */\n async listReleases(alias: string, options?: RequestOptions): Promise<Deployment[]> {\n const data = await this.client.query(ops.ListReleasesDocument, { alias }, options);\n return data.deploymentReleases;\n }\n\n /**\n * Rolls an alias back to a prior release, handling the whole compare-and-swap\n * protocol in one call.\n *\n * The server requires the caller to prove it observed the current state: you\n * must pass the live release's id AND its opaque `aliasRevision`, and the\n * swap fails if either changed since you listed — including same-id ABA. So\n * this method does the full dance: list releases, find the `aliasHeld` entry,\n * pick the target, and submit both guard values.\n *\n * On a rejected guard it re-lists and retries (default once). The TARGET is\n * pinned across retries while the guard is refreshed, so a concurrent deploy\n * can never redirect the rollback to a different release than the one chosen.\n *\n * ```ts\n * await unfazed.rollback({ alias: \"my-app\" }); // undo last deploy\n * await unfazed.rollback({ alias: \"my-app\", to: relId }); // to a specific release\n * ```\n */\n async rollback(options: RollbackOptions): Promise<Deployment> {\n const { alias, signal } = options;\n const retries = options.retries ?? 1;\n const requestOptions: RequestOptions = signal ? { signal } : {};\n\n let targetId = options.to;\n\n for (let attempt = 0; ; attempt++) {\n const releases = await this.listReleases(alias, requestOptions);\n if (releases.length === 0) {\n throw new UnfazedNotFoundError(`unfazed: alias \"${alias}\" has no release history`);\n }\n\n const current = releases.find((r) => r.aliasHeld);\n if (!current) {\n throw new UnfazedNotFoundError(\n `unfazed: alias \"${alias}\" has no current release holding the alias`,\n );\n }\n\n // Pin the target on the first pass so a retry refreshes only the guard.\n if (targetId === undefined) {\n const previous = releases.find((r) => r.id !== current.id && r.status === \"READY\");\n if (!previous) {\n throw new UnfazedNotFoundError(\n `unfazed: alias \"${alias}\" has no prior READY release to roll back to ` +\n `(history: ${releases.map((r) => `${r.id}:${r.status}`).join(\", \")})`,\n );\n }\n targetId = previous.id;\n }\n\n const target = releases.find((r) => r.id === targetId);\n if (!target) {\n throw new UnfazedNotFoundError(\n `unfazed: release ${targetId} is not in alias \"${alias}\"'s history ` +\n `(it may have been pruned; retention keeps only the most recent few)`,\n );\n }\n if (target.id === current.id) {\n throw new UnfazedConflictError(\n `unfazed: release ${targetId} already holds alias \"${alias}\"`,\n [],\n );\n }\n if (target.status !== \"READY\") {\n throw new UnfazedConflictError(\n `unfazed: release ${targetId} is ${target.status}, only READY releases can be restored`,\n [],\n );\n }\n\n try {\n const data = await this.client.mutate(\n ops.RollbackDocument,\n {\n deploymentID: target.id,\n expectedCurrentDeploymentID: current.id,\n expectedCurrentAliasRevision: current.aliasRevision,\n },\n requestOptions,\n );\n return data.rollbackDeployment;\n } catch (e) {\n const retryable =\n e instanceof UnfazedGraphQLError && looksLikeCASConflict(e.errors) && attempt < retries;\n if (!retryable) {\n if (e instanceof UnfazedGraphQLError && looksLikeCASConflict(e.errors)) {\n throw new UnfazedConflictError(\n `unfazed: rollback of \"${alias}\" lost the compare-and-swap after ${\n attempt + 1\n } attempt(s) — the alias is being deployed concurrently. ${e.message}`,\n e.errors,\n );\n }\n throw e;\n }\n // Loop: re-list, rebuild the guard, keep the same target.\n }\n }\n }\n\n /* -------------------------------------------------------------- internals */\n\n async #writeBatch(\n workspaceId: string,\n batch: NormalizedFile[],\n signal?: AbortSignal,\n ): Promise<WorkspaceFile[]> {\n const varDefs = [\"$workspaceID: ID!\"];\n const fields: string[] = [];\n const variables: Record<string, string> = { workspaceID: workspaceId };\n\n batch.forEach((file, i) => {\n variables[`p${i}`] = file.path;\n variables[`c${i}`] = file.content;\n varDefs.push(`$p${i}: String!`, `$c${i}: String!`);\n fields.push(\n file.kind === \"text\"\n ? ` f${i}: writeWorkspaceTextFile(input: { workspaceID: $workspaceID, path: $p${i}, text: $c${i} }) { file { ${FILE_SELECTION} } }`\n : ` f${i}: writeWorkspaceBase64File(input: { workspaceID: $workspaceID, path: $p${i}, contentBase64: $c${i} }) { file { ${FILE_SELECTION} } }`,\n );\n });\n\n // Built at runtime rather than codegen'd: the field COUNT varies per call,\n // so no static document can express it. The field shapes are still fixed\n // and typed below, and each individual mutation is codegen-validated by its\n // single-file twin in operations.ts.\n const document = `mutation SdkWriteFilesBatch(${varDefs.join(\", \")}) {\\n${fields.join(\"\\n\")}\\n}`;\n\n const data = await this.client.mutate<\n Record<string, { file: WorkspaceFile }>,\n typeof variables\n >(document, variables, signal ? { signal } : {});\n\n return batch.map((_, i) => data[`f${i}`]!.file);\n }\n}\n\nconst FILE_SELECTION = \"path sizeBytes size sha256 updatedAt\";\nconst ENV_SELECTION = \"name value secret alias inherited masked updatedAt\";\n\ninterface NormalizedFile {\n path: string;\n kind: \"text\" | \"base64\";\n content: string;\n}\n\nfunction normalizeFile(path: string, content: FileContent): NormalizedFile {\n if (typeof content === \"string\") return { path, kind: \"text\", content };\n if (\"text\" in content) return { path, kind: \"text\", content: content.text };\n if (\"base64\" in content) return { path, kind: \"base64\", content: content.base64 };\n return { path, kind: \"base64\", content: toBase64(content.bytes) };\n}\n\nfunction toBase64(input: Uint8Array | ArrayBuffer): string {\n const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);\n // Node/Bun have Buffer; browsers and workers do not.\n const maybeBuffer = (globalThis as { Buffer?: { from(b: Uint8Array): { toString(e: string): string } } })\n .Buffer;\n if (maybeBuffer) return maybeBuffer.from(bytes).toString(\"base64\");\n\n let binary = \"\";\n const CHUNK = 0x8000; // avoid blowing the argument limit on large files\n for (let i = 0; i < bytes.length; i += CHUNK) {\n binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));\n }\n return btoa(binary);\n}\n\nconst utf8 = new TextEncoder();\n\n/**\n * UTF-8 byte length, which is what the wire actually costs.\n *\n * `String.length` counts UTF-16 code units and undercounts by up to 3x on\n * non-ASCII text — a CJK or emoji-heavy payload would sail past a budget it\n * had already blown.\n */\nfunction utf8Bytes(s: string): number {\n return utf8.encode(s).length;\n}\n\n/**\n * Splits files into request-sized batches.\n *\n * Closes a batch on whichever limit trips first — count or bytes — but always\n * emits at least one file per batch, so a single file larger than\n * `maxBatchBytes` still gets sent (and fails loudly at the server's limit)\n * rather than silently vanishing into an empty batch.\n */\nfunction chunkFiles(\n files: NormalizedFile[],\n batchSize: number,\n maxBatchBytes: number,\n): NormalizedFile[][] {\n const batches: NormalizedFile[][] = [];\n let current: NormalizedFile[] = [];\n let bytes = 0;\n\n for (const file of files) {\n // Measured once per file, so the encode cost is linear in the payload the\n // request was going to carry anyway.\n const size = utf8Bytes(file.content) + utf8Bytes(file.path);\n if (current.length > 0 && (current.length >= batchSize || bytes + size > maxBatchBytes)) {\n batches.push(current);\n current = [];\n bytes = 0;\n }\n current.push(file);\n bytes += size;\n }\n if (current.length > 0) batches.push(current);\n return batches;\n}\n"]}