unfazed 0.6.0 → 0.8.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +299 -78
  3. package/REFERENCE.md +284 -0
  4. package/dist/cli/login.d.ts +129 -0
  5. package/dist/cli/login.js +596 -0
  6. package/dist/cli/login.js.map +1 -0
  7. package/dist/cli/workflow.d.ts +31 -0
  8. package/dist/cli/workflow.js +157 -0
  9. package/dist/cli/workflow.js.map +1 -0
  10. package/dist/cli.d.ts +2 -0
  11. package/dist/cli.js +173 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/client.d.ts +26 -5
  14. package/dist/client.js +93 -48
  15. package/dist/client.js.map +1 -1
  16. package/dist/credentials.d.ts +123 -0
  17. package/dist/credentials.js +317 -0
  18. package/dist/credentials.js.map +1 -0
  19. package/dist/errors.d.ts +64 -0
  20. package/dist/errors.js +95 -0
  21. package/dist/errors.js.map +1 -1
  22. package/dist/generated/gql.d.ts +78 -2
  23. package/dist/generated/gql.js +21 -2
  24. package/dist/generated/gql.js.map +1 -1
  25. package/dist/generated/graphql.d.ts +505 -6
  26. package/dist/generated/graphql.js +439 -2
  27. package/dist/generated/graphql.js.map +1 -1
  28. package/dist/index.d.ts +25 -6
  29. package/dist/index.js +23 -5
  30. package/dist/index.js.map +1 -1
  31. package/dist/operations.d.ts +116 -5
  32. package/dist/operations.js +379 -7
  33. package/dist/operations.js.map +1 -1
  34. package/dist/reference.d.ts +18 -0
  35. package/dist/reference.generated.d.ts +2 -0
  36. package/dist/reference.generated.js +5 -0
  37. package/dist/reference.generated.js.map +1 -0
  38. package/dist/reference.js +38 -0
  39. package/dist/reference.js.map +1 -0
  40. package/dist/remote/app.js +2 -1
  41. package/dist/remote/app.js.map +1 -1
  42. package/dist/remote/world.d.ts +2 -2
  43. package/dist/remote/world.js +32 -16
  44. package/dist/remote/world.js.map +1 -1
  45. package/dist/unfazed.d.ts +545 -68
  46. package/dist/unfazed.js +688 -64
  47. package/dist/unfazed.js.map +1 -1
  48. package/dist/wait.d.ts +7 -0
  49. package/dist/wait.js.map +1 -1
  50. package/dist/workflow/nitro.js +5 -3
  51. package/dist/workflow/nitro.js.map +1 -1
  52. package/package.json +22 -6
  53. package/schema/SOURCE.json +4 -4
  54. package/schema/schema.graphqls +1628 -30
@@ -0,0 +1,317 @@
1
+ /**
2
+ * The stored login of `npx unfazed login`.
3
+ *
4
+ * `unfazed login` signs a person in through the platform's OAuth server in the
5
+ * browser and writes their own access and refresh token to
6
+ * `${XDG_CONFIG_HOME:-~/.config}/unfazed/credentials.json` (mode 0600).
7
+ * {@link import("./client.js").UnfazedClient} falls back to that file when no
8
+ * token is passed and `UNFZ_API_TOKEN` is unset, and refreshes the access
9
+ * token (an hour long on unfazed) before it expires, writing the rotated pair
10
+ * back for the next process.
11
+ *
12
+ * `node:fs` comes from `process.getBuiltinModule` (Node >= 20.16, Bun, Deno),
13
+ * never from an import, so the root entry stays free of Node imports and a
14
+ * browser bundle of `unfazed` simply has no stored login.
15
+ */
16
+ import { UnfazedAuthError, UnfazedConfigError } from "./errors.js";
17
+ /** The command that writes the stored login. Error messages point at it. */
18
+ export const LOGIN_COMMAND = "npx unfazed login";
19
+ /** The stored login's file name, under {@link credentialsDir}. */
20
+ export const CREDENTIALS_FILE = "credentials.json";
21
+ /** The access token is refreshed once it has less than this left. */
22
+ export const REFRESH_MARGIN_MS = 60_000;
23
+ /** A refresh that has not answered by then fails rather than hanging the caller. */
24
+ const REFRESH_TIMEOUT_MS = 30_000;
25
+ function nodeFS() {
26
+ const proc = globalThis.process;
27
+ return proc?.getBuiltinModule?.("node:fs");
28
+ }
29
+ /**
30
+ * The home directory from the environment only (`HOME`, else `USERPROFILE`),
31
+ * so a caller passing its own `env` gets exactly the files it names.
32
+ */
33
+ function homeDir(env) {
34
+ return env["HOME"]?.trim() || env["USERPROFILE"]?.trim() || undefined;
35
+ }
36
+ /** `${XDG_CONFIG_HOME:-~/.config}/unfazed`, or undefined where there is no home. */
37
+ export function credentialsDir(env = processEnv()) {
38
+ const xdg = env["XDG_CONFIG_HOME"]?.trim();
39
+ if (xdg)
40
+ return joinPath(xdg, "unfazed");
41
+ const home = homeDir(env);
42
+ return home ? joinPath(joinPath(home, ".config"), "unfazed") : undefined;
43
+ }
44
+ /** Where the stored login lives, or undefined where there is no home directory. */
45
+ export function credentialsPath(env = processEnv()) {
46
+ const dir = credentialsDir(env);
47
+ return dir ? joinPath(dir, CREDENTIALS_FILE) : undefined;
48
+ }
49
+ /**
50
+ * Reads the stored login. `undefined` when there is none (or no filesystem on
51
+ * this runtime); throws {@link UnfazedConfigError} when the file is there but
52
+ * unusable, saying to log in again.
53
+ */
54
+ export function readCredentials(path) {
55
+ const fs = nodeFS();
56
+ if (!fs)
57
+ return undefined;
58
+ let text;
59
+ try {
60
+ text = fs.readFileSync(path, "utf8");
61
+ }
62
+ catch (error) {
63
+ const code = error.code;
64
+ if (code === "ENOENT" || code === "ENOTDIR")
65
+ return undefined;
66
+ throw new UnfazedConfigError(`unfazed: could not read ${path} (${code ?? "error"}). Run \`${LOGIN_COMMAND}\` again.`);
67
+ }
68
+ let parsed;
69
+ try {
70
+ parsed = JSON.parse(text);
71
+ }
72
+ catch {
73
+ throw new UnfazedConfigError(`unfazed: ${path} is not valid JSON. Run \`${LOGIN_COMMAND}\` again.`);
74
+ }
75
+ if (parsed === null ||
76
+ typeof parsed !== "object" ||
77
+ parsed.version !== 1 ||
78
+ typeof parsed.access_token !== "string" ||
79
+ !parsed.access_token ||
80
+ typeof parsed.endpoint !== "string" ||
81
+ typeof parsed.token_endpoint !== "string" ||
82
+ typeof parsed.client_id !== "string") {
83
+ throw new UnfazedConfigError(`unfazed: ${path} is not a login this version understands. Run \`${LOGIN_COMMAND}\` again.`);
84
+ }
85
+ return parsed;
86
+ }
87
+ /**
88
+ * Writes the stored login atomically with mode 0600 (its directory 0700): a
89
+ * temporary file created exclusively with that mode, then renamed over the
90
+ * old one, so a reader never sees half a file and the tokens are never
91
+ * readable by anyone else, not even for a moment.
92
+ */
93
+ export function writeCredentials(path, credentials) {
94
+ const fs = nodeFS();
95
+ if (!fs)
96
+ throw new UnfazedConfigError("unfazed: this runtime has no filesystem to store a login on.");
97
+ const dir = parentDir(path);
98
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
99
+ const temp = `${path}.${randomSuffix()}.tmp`;
100
+ try {
101
+ fs.writeFileSync(temp, JSON.stringify(credentials, null, 2) + "\n", { mode: 0o600, flag: "wx" });
102
+ // The mode passed at creation is narrowed by the umask, never widened;
103
+ // chmod pins it exactly on systems with an unusual umask.
104
+ fs.chmodSync(temp, 0o600);
105
+ fs.renameSync(temp, path);
106
+ }
107
+ catch (error) {
108
+ try {
109
+ fs.unlinkSync(temp);
110
+ }
111
+ catch {
112
+ // already renamed or never created
113
+ }
114
+ throw new UnfazedConfigError(`unfazed: could not write ${path} (${error.code ?? "error"}).`);
115
+ }
116
+ }
117
+ /** Deletes the stored login. Returns whether there was one. */
118
+ export function deleteCredentials(path) {
119
+ const fs = nodeFS();
120
+ if (!fs)
121
+ return false;
122
+ try {
123
+ fs.unlinkSync(path);
124
+ return true;
125
+ }
126
+ catch (error) {
127
+ if (error.code === "ENOENT")
128
+ return false;
129
+ throw new UnfazedConfigError(`unfazed: could not delete ${path} (${error.code ?? "error"}).`);
130
+ }
131
+ }
132
+ /** Whether the access token is expired or within {@link REFRESH_MARGIN_MS} of it. */
133
+ export function needsRefresh(credentials, now = Date.now()) {
134
+ if (!credentials.expires_at)
135
+ return false;
136
+ const expires = Date.parse(credentials.expires_at);
137
+ return Number.isFinite(expires) && expires - REFRESH_MARGIN_MS <= now;
138
+ }
139
+ /** Turns a token response into the stored fields it replaces. */
140
+ export function tokenFields(response, previous, now = Date.now()) {
141
+ const fields = {
142
+ access_token: response.access_token,
143
+ };
144
+ // A server that does not rotate refresh tokens answers without one: keep the old.
145
+ const refresh = response.refresh_token || previous?.refresh_token;
146
+ if (refresh)
147
+ fields.refresh_token = refresh;
148
+ if (typeof response.expires_in === "number" && response.expires_in > 0) {
149
+ fields.expires_at = new Date(now + response.expires_in * 1000).toISOString();
150
+ }
151
+ const scope = response.scope ?? previous?.scope;
152
+ if (scope)
153
+ fields.scope = scope;
154
+ return fields;
155
+ }
156
+ /**
157
+ * Posts a form to an OAuth endpoint and parses its JSON answer. On an OAuth
158
+ * error the thrown message carries only `error` and `error_description` —
159
+ * never the request, which holds the secrets.
160
+ */
161
+ export async function postTokenForm(fetch, url, form, userAgent, signal) {
162
+ const response = await fetch(url, {
163
+ method: "POST",
164
+ headers: {
165
+ "Content-Type": "application/x-www-form-urlencoded",
166
+ Accept: "application/json",
167
+ "User-Agent": userAgent,
168
+ },
169
+ body: new URLSearchParams(form).toString(),
170
+ ...(signal ? { signal } : {}),
171
+ });
172
+ const text = await response.text();
173
+ let body = {};
174
+ try {
175
+ const parsed = JSON.parse(text);
176
+ if (parsed && typeof parsed === "object")
177
+ body = parsed;
178
+ }
179
+ catch {
180
+ // not JSON; the status says what happened
181
+ }
182
+ return { status: response.status, body };
183
+ }
184
+ /** `error: description` from an OAuth error body, or the HTTP status. */
185
+ export function oauthErrorText(status, body) {
186
+ const error = typeof body["error"] === "string" ? body["error"] : "";
187
+ const description = typeof body["error_description"] === "string" ? body["error_description"] : "";
188
+ if (error && description)
189
+ return `${error}: ${description}`;
190
+ return error || description || `HTTP ${status}`;
191
+ }
192
+ /**
193
+ * Exchanges the stored refresh token for a new pair. Throws
194
+ * {@link UnfazedAuthError} saying to log in again when the server refuses it.
195
+ */
196
+ export async function refreshCredentials(credentials, fetch, userAgent, signal) {
197
+ if (!credentials.refresh_token) {
198
+ throw new UnfazedAuthError(`unfazed: the stored login for ${credentials.issuer} has expired. Run \`${LOGIN_COMMAND}\` again.`);
199
+ }
200
+ const { status, body } = await postTokenForm(fetch, credentials.token_endpoint, { grant_type: "refresh_token", refresh_token: credentials.refresh_token, client_id: credentials.client_id }, userAgent, signal);
201
+ if (status < 200 || status >= 300 || typeof body["access_token"] !== "string" || !body["access_token"]) {
202
+ if (status === 400 || status === 401) {
203
+ throw new UnfazedAuthError(`unfazed: the stored login for ${credentials.issuer} has expired or was signed out ` +
204
+ `(${oauthErrorText(status, body)}). Run \`${LOGIN_COMMAND}\` again.`);
205
+ }
206
+ throw new UnfazedAuthError(`unfazed: could not refresh the stored login at ${credentials.token_endpoint} (${oauthErrorText(status, body)}).`);
207
+ }
208
+ return {
209
+ ...credentials,
210
+ ...tokenFields(body, credentials),
211
+ updated_at: new Date().toISOString(),
212
+ };
213
+ }
214
+ /**
215
+ * The stored login as a token source for one client: hands out the access
216
+ * token, refreshing it before it expires and when the platform refuses it.
217
+ *
218
+ * Several processes may share the file (the Workflow CLI's web UI, a script,
219
+ * `npx unfazed whoami`): before refreshing, the file is read again and a token some
220
+ * other process already refreshed is adopted instead. Two that refresh at the
221
+ * same moment both end up with valid pairs: unfazed keeps a rotated refresh
222
+ * token exchangeable for a short grace window for exactly that race.
223
+ */
224
+ export class StoredLogin {
225
+ path;
226
+ #credentials;
227
+ #refreshing;
228
+ #fetch;
229
+ #userAgent;
230
+ constructor(path, credentials, fetch, userAgent) {
231
+ this.path = path;
232
+ this.#credentials = credentials;
233
+ this.#fetch = fetch;
234
+ this.#userAgent = userAgent;
235
+ }
236
+ /** The login's GraphQL endpoint. */
237
+ get endpoint() {
238
+ return this.#credentials.endpoint;
239
+ }
240
+ /** The current credentials (for display; the caller must not print tokens). */
241
+ get credentials() {
242
+ return this.#credentials;
243
+ }
244
+ /** An access token that is not about to expire. */
245
+ async token() {
246
+ if (this.#refreshing)
247
+ return this.#refreshing;
248
+ if (!needsRefresh(this.#credentials))
249
+ return this.#credentials.access_token;
250
+ return this.refresh(this.#credentials.access_token);
251
+ }
252
+ /**
253
+ * A token other than `stale`: one another process already wrote, else a
254
+ * freshly refreshed one. Concurrent callers share one refresh.
255
+ */
256
+ refresh(stale) {
257
+ if (!this.#refreshing) {
258
+ this.#refreshing = this.#refresh(stale).finally(() => {
259
+ this.#refreshing = undefined;
260
+ });
261
+ }
262
+ return this.#refreshing;
263
+ }
264
+ async #refresh(stale) {
265
+ const onDisk = readCredentials(this.path);
266
+ if (!onDisk || !sameEndpoint(onDisk.endpoint, this.#credentials.endpoint)) {
267
+ throw new UnfazedAuthError(`unfazed: signed out of ${this.#credentials.issuer}. Run \`${LOGIN_COMMAND}\` to sign in again.`);
268
+ }
269
+ if (onDisk.access_token !== stale && !needsRefresh(onDisk)) {
270
+ this.#credentials = onDisk;
271
+ return onDisk.access_token;
272
+ }
273
+ const refreshed = await refreshCredentials(onDisk, this.#fetch, this.#userAgent, timeoutSignal(REFRESH_TIMEOUT_MS));
274
+ writeCredentials(this.path, refreshed);
275
+ this.#credentials = refreshed;
276
+ return refreshed.access_token;
277
+ }
278
+ }
279
+ /**
280
+ * Opens the stored login for `endpoint` (undefined: whichever endpoint the
281
+ * login is for). Returns undefined when there is no login, or when it is for
282
+ * another endpoint — a token is never sent anywhere but where it was issued.
283
+ */
284
+ export function openStoredLogin(options) {
285
+ const path = credentialsPath(options.env);
286
+ if (!path)
287
+ return {};
288
+ const credentials = readCredentials(path);
289
+ if (!credentials)
290
+ return {};
291
+ if (options.endpoint && !sameEndpoint(options.endpoint, credentials.endpoint))
292
+ return { other: credentials };
293
+ return { login: new StoredLogin(path, credentials, options.fetch, options.userAgent) };
294
+ }
295
+ export function sameEndpoint(a, b) {
296
+ return a.replace(/\/+$/, "") === b.replace(/\/+$/, "");
297
+ }
298
+ function timeoutSignal(ms) {
299
+ const timeout = AbortSignal.timeout;
300
+ return typeof timeout === "function" ? timeout.call(AbortSignal, ms) : undefined;
301
+ }
302
+ function processEnv() {
303
+ return globalThis.process?.env ?? {};
304
+ }
305
+ function joinPath(dir, name) {
306
+ return dir.endsWith("/") || dir.endsWith("\\") ? dir + name : `${dir}/${name}`;
307
+ }
308
+ function parentDir(path) {
309
+ const i = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
310
+ return i > 0 ? path.slice(0, i) : ".";
311
+ }
312
+ function randomSuffix() {
313
+ const bytes = new Uint8Array(6);
314
+ globalThis.crypto.getRandomValues(bytes);
315
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
316
+ }
317
+ //# sourceMappingURL=credentials.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The stored login of `npx unfazed login`.\n *\n * `unfazed login` signs a person in through the platform's OAuth server in the\n * browser and writes their own access and refresh token to\n * `${XDG_CONFIG_HOME:-~/.config}/unfazed/credentials.json` (mode 0600).\n * {@link import(\"./client.js\").UnfazedClient} falls back to that file when no\n * token is passed and `UNFZ_API_TOKEN` is unset, and refreshes the access\n * token (an hour long on unfazed) before it expires, writing the rotated pair\n * back for the next process.\n *\n * `node:fs` comes from `process.getBuiltinModule` (Node >= 20.16, Bun, Deno),\n * never from an import, so the root entry stays free of Node imports and a\n * browser bundle of `unfazed` simply has no stored login.\n */\nimport { UnfazedAuthError, UnfazedConfigError } from \"./errors.js\";\nimport type { FetchLike } from \"./client.js\";\n\n/** The command that writes the stored login. Error messages point at it. */\nexport const LOGIN_COMMAND = \"npx unfazed login\";\n\n/** The stored login's file name, under {@link credentialsDir}. */\nexport const CREDENTIALS_FILE = \"credentials.json\";\n\n/** The access token is refreshed once it has less than this left. */\nexport const REFRESH_MARGIN_MS = 60_000;\n\n/** A refresh that has not answered by then fails rather than hanging the caller. */\nconst REFRESH_TIMEOUT_MS = 30_000;\n\n/** What `unfazed login` writes. Field names follow OAuth's own. */\nexport interface StoredCredentials {\n version: 1;\n /** The GraphQL endpoint the login is for, e.g. `https://unfazed.dev/query`. */\n endpoint: string;\n /** The OAuth issuer that signed the person in, e.g. `https://unfazed.dev`. */\n issuer: string;\n token_endpoint: string;\n /** RFC 7009 revocation endpoint, when the server advertises one. */\n revocation_endpoint?: string;\n /** The OAuth client `unfazed login` registered (a public client: no secret). */\n client_id: string;\n access_token: string;\n refresh_token?: string;\n /** When the access token expires (ISO 8601). Unset: never refreshed proactively. */\n expires_at?: string;\n scope?: string;\n /** Who signed in, from GraphQL `whoami` at login. Display only. */\n account?: { kind?: string; user_id?: string; name?: string };\n created_at?: string;\n updated_at?: string;\n}\n\ntype Env = Record<string, string | undefined>;\n\ninterface FS {\n readFileSync(path: string, encoding: \"utf8\"): string;\n writeFileSync(path: string, data: string, options: { mode: number; flag: string }): void;\n renameSync(from: string, to: string): void;\n mkdirSync(path: string, options: { recursive: true; mode: number }): unknown;\n chmodSync(path: string, mode: number): void;\n unlinkSync(path: string): void;\n}\n\nfunction nodeFS(): FS | undefined {\n const proc = (globalThis as { process?: { getBuiltinModule?: (id: string) => unknown } }).process;\n return proc?.getBuiltinModule?.(\"node:fs\") as FS | undefined;\n}\n\n/**\n * The home directory from the environment only (`HOME`, else `USERPROFILE`),\n * so a caller passing its own `env` gets exactly the files it names.\n */\nfunction homeDir(env: Env): string | undefined {\n return env[\"HOME\"]?.trim() || env[\"USERPROFILE\"]?.trim() || undefined;\n}\n\n/** `${XDG_CONFIG_HOME:-~/.config}/unfazed`, or undefined where there is no home. */\nexport function credentialsDir(env: Env = processEnv()): string | undefined {\n const xdg = env[\"XDG_CONFIG_HOME\"]?.trim();\n if (xdg) return joinPath(xdg, \"unfazed\");\n const home = homeDir(env);\n return home ? joinPath(joinPath(home, \".config\"), \"unfazed\") : undefined;\n}\n\n/** Where the stored login lives, or undefined where there is no home directory. */\nexport function credentialsPath(env: Env = processEnv()): string | undefined {\n const dir = credentialsDir(env);\n return dir ? joinPath(dir, CREDENTIALS_FILE) : undefined;\n}\n\n/**\n * Reads the stored login. `undefined` when there is none (or no filesystem on\n * this runtime); throws {@link UnfazedConfigError} when the file is there but\n * unusable, saying to log in again.\n */\nexport function readCredentials(path: string): StoredCredentials | undefined {\n const fs = nodeFS();\n if (!fs) return undefined;\n let text: string;\n try {\n text = fs.readFileSync(path, \"utf8\");\n } catch (error) {\n const code = (error as { code?: string }).code;\n if (code === \"ENOENT\" || code === \"ENOTDIR\") return undefined;\n throw new UnfazedConfigError(`unfazed: could not read ${path} (${code ?? \"error\"}). Run \\`${LOGIN_COMMAND}\\` again.`);\n }\n let parsed: Partial<StoredCredentials>;\n try {\n parsed = JSON.parse(text) as Partial<StoredCredentials>;\n } catch {\n throw new UnfazedConfigError(`unfazed: ${path} is not valid JSON. Run \\`${LOGIN_COMMAND}\\` again.`);\n }\n if (\n parsed === null ||\n typeof parsed !== \"object\" ||\n parsed.version !== 1 ||\n typeof parsed.access_token !== \"string\" ||\n !parsed.access_token ||\n typeof parsed.endpoint !== \"string\" ||\n typeof parsed.token_endpoint !== \"string\" ||\n typeof parsed.client_id !== \"string\"\n ) {\n throw new UnfazedConfigError(`unfazed: ${path} is not a login this version understands. Run \\`${LOGIN_COMMAND}\\` again.`);\n }\n return parsed as StoredCredentials;\n}\n\n/**\n * Writes the stored login atomically with mode 0600 (its directory 0700): a\n * temporary file created exclusively with that mode, then renamed over the\n * old one, so a reader never sees half a file and the tokens are never\n * readable by anyone else, not even for a moment.\n */\nexport function writeCredentials(path: string, credentials: StoredCredentials): void {\n const fs = nodeFS();\n if (!fs) throw new UnfazedConfigError(\"unfazed: this runtime has no filesystem to store a login on.\");\n const dir = parentDir(path);\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n const temp = `${path}.${randomSuffix()}.tmp`;\n try {\n fs.writeFileSync(temp, JSON.stringify(credentials, null, 2) + \"\\n\", { mode: 0o600, flag: \"wx\" });\n // The mode passed at creation is narrowed by the umask, never widened;\n // chmod pins it exactly on systems with an unusual umask.\n fs.chmodSync(temp, 0o600);\n fs.renameSync(temp, path);\n } catch (error) {\n try {\n fs.unlinkSync(temp);\n } catch {\n // already renamed or never created\n }\n throw new UnfazedConfigError(`unfazed: could not write ${path} (${(error as { code?: string }).code ?? \"error\"}).`);\n }\n}\n\n/** Deletes the stored login. Returns whether there was one. */\nexport function deleteCredentials(path: string): boolean {\n const fs = nodeFS();\n if (!fs) return false;\n try {\n fs.unlinkSync(path);\n return true;\n } catch (error) {\n if ((error as { code?: string }).code === \"ENOENT\") return false;\n throw new UnfazedConfigError(`unfazed: could not delete ${path} (${(error as { code?: string }).code ?? \"error\"}).`);\n }\n}\n\n/** Whether the access token is expired or within {@link REFRESH_MARGIN_MS} of it. */\nexport function needsRefresh(credentials: StoredCredentials, now = Date.now()): boolean {\n if (!credentials.expires_at) return false;\n const expires = Date.parse(credentials.expires_at);\n return Number.isFinite(expires) && expires - REFRESH_MARGIN_MS <= now;\n}\n\n/** The token endpoint's answer to a code or refresh grant. */\nexport interface TokenResponse {\n access_token: string;\n refresh_token?: string;\n token_type?: string;\n expires_in?: number;\n scope?: string;\n}\n\n/** Turns a token response into the stored fields it replaces. */\nexport function tokenFields(\n response: TokenResponse,\n previous?: Pick<StoredCredentials, \"refresh_token\" | \"scope\">,\n now = Date.now(),\n): Pick<StoredCredentials, \"access_token\" | \"refresh_token\" | \"expires_at\" | \"scope\"> {\n const fields: Pick<StoredCredentials, \"access_token\" | \"refresh_token\" | \"expires_at\" | \"scope\"> = {\n access_token: response.access_token,\n };\n // A server that does not rotate refresh tokens answers without one: keep the old.\n const refresh = response.refresh_token || previous?.refresh_token;\n if (refresh) fields.refresh_token = refresh;\n if (typeof response.expires_in === \"number\" && response.expires_in > 0) {\n fields.expires_at = new Date(now + response.expires_in * 1000).toISOString();\n }\n const scope = response.scope ?? previous?.scope;\n if (scope) fields.scope = scope;\n return fields;\n}\n\n/**\n * Posts a form to an OAuth endpoint and parses its JSON answer. On an OAuth\n * error the thrown message carries only `error` and `error_description` —\n * never the request, which holds the secrets.\n */\nexport async function postTokenForm(\n fetch: FetchLike,\n url: string,\n form: Record<string, string>,\n userAgent: string,\n signal?: AbortSignal,\n): Promise<{ status: number; body: Record<string, unknown> }> {\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n Accept: \"application/json\",\n \"User-Agent\": userAgent,\n },\n body: new URLSearchParams(form).toString(),\n ...(signal ? { signal } : {}),\n });\n const text = await response.text();\n let body: Record<string, unknown> = {};\n try {\n const parsed = JSON.parse(text) as unknown;\n if (parsed && typeof parsed === \"object\") body = parsed as Record<string, unknown>;\n } catch {\n // not JSON; the status says what happened\n }\n return { status: response.status, body };\n}\n\n/** `error: description` from an OAuth error body, or the HTTP status. */\nexport function oauthErrorText(status: number, body: Record<string, unknown>): string {\n const error = typeof body[\"error\"] === \"string\" ? body[\"error\"] : \"\";\n const description = typeof body[\"error_description\"] === \"string\" ? body[\"error_description\"] : \"\";\n if (error && description) return `${error}: ${description}`;\n return error || description || `HTTP ${status}`;\n}\n\n/**\n * Exchanges the stored refresh token for a new pair. Throws\n * {@link UnfazedAuthError} saying to log in again when the server refuses it.\n */\nexport async function refreshCredentials(\n credentials: StoredCredentials,\n fetch: FetchLike,\n userAgent: string,\n signal?: AbortSignal,\n): Promise<StoredCredentials> {\n if (!credentials.refresh_token) {\n throw new UnfazedAuthError(`unfazed: the stored login for ${credentials.issuer} has expired. Run \\`${LOGIN_COMMAND}\\` again.`);\n }\n const { status, body } = await postTokenForm(\n fetch,\n credentials.token_endpoint,\n { grant_type: \"refresh_token\", refresh_token: credentials.refresh_token, client_id: credentials.client_id },\n userAgent,\n signal,\n );\n if (status < 200 || status >= 300 || typeof body[\"access_token\"] !== \"string\" || !body[\"access_token\"]) {\n if (status === 400 || status === 401) {\n throw new UnfazedAuthError(\n `unfazed: the stored login for ${credentials.issuer} has expired or was signed out ` +\n `(${oauthErrorText(status, body)}). Run \\`${LOGIN_COMMAND}\\` again.`,\n );\n }\n throw new UnfazedAuthError(\n `unfazed: could not refresh the stored login at ${credentials.token_endpoint} (${oauthErrorText(status, body)}).`,\n );\n }\n return {\n ...credentials,\n ...tokenFields(body as unknown as TokenResponse, credentials),\n updated_at: new Date().toISOString(),\n };\n}\n\n/**\n * The stored login as a token source for one client: hands out the access\n * token, refreshing it before it expires and when the platform refuses it.\n *\n * Several processes may share the file (the Workflow CLI's web UI, a script,\n * `npx unfazed whoami`): before refreshing, the file is read again and a token some\n * other process already refreshed is adopted instead. Two that refresh at the\n * same moment both end up with valid pairs: unfazed keeps a rotated refresh\n * token exchangeable for a short grace window for exactly that race.\n */\nexport class StoredLogin {\n readonly path: string;\n #credentials: StoredCredentials;\n #refreshing: Promise<string> | undefined;\n readonly #fetch: FetchLike;\n readonly #userAgent: string;\n\n constructor(path: string, credentials: StoredCredentials, fetch: FetchLike, userAgent: string) {\n this.path = path;\n this.#credentials = credentials;\n this.#fetch = fetch;\n this.#userAgent = userAgent;\n }\n\n /** The login's GraphQL endpoint. */\n get endpoint(): string {\n return this.#credentials.endpoint;\n }\n\n /** The current credentials (for display; the caller must not print tokens). */\n get credentials(): StoredCredentials {\n return this.#credentials;\n }\n\n /** An access token that is not about to expire. */\n async token(): Promise<string> {\n if (this.#refreshing) return this.#refreshing;\n if (!needsRefresh(this.#credentials)) return this.#credentials.access_token;\n return this.refresh(this.#credentials.access_token);\n }\n\n /**\n * A token other than `stale`: one another process already wrote, else a\n * freshly refreshed one. Concurrent callers share one refresh.\n */\n refresh(stale: string): Promise<string> {\n if (!this.#refreshing) {\n this.#refreshing = this.#refresh(stale).finally(() => {\n this.#refreshing = undefined;\n });\n }\n return this.#refreshing;\n }\n\n async #refresh(stale: string): Promise<string> {\n const onDisk = readCredentials(this.path);\n if (!onDisk || !sameEndpoint(onDisk.endpoint, this.#credentials.endpoint)) {\n throw new UnfazedAuthError(`unfazed: signed out of ${this.#credentials.issuer}. Run \\`${LOGIN_COMMAND}\\` to sign in again.`);\n }\n if (onDisk.access_token !== stale && !needsRefresh(onDisk)) {\n this.#credentials = onDisk;\n return onDisk.access_token;\n }\n const refreshed = await refreshCredentials(onDisk, this.#fetch, this.#userAgent, timeoutSignal(REFRESH_TIMEOUT_MS));\n writeCredentials(this.path, refreshed);\n this.#credentials = refreshed;\n return refreshed.access_token;\n }\n}\n\n/**\n * Opens the stored login for `endpoint` (undefined: whichever endpoint the\n * login is for). Returns undefined when there is no login, or when it is for\n * another endpoint — a token is never sent anywhere but where it was issued.\n */\nexport function openStoredLogin(options: {\n env: Env;\n endpoint?: string;\n fetch: FetchLike;\n userAgent: string;\n}): { login?: StoredLogin; other?: StoredCredentials } {\n const path = credentialsPath(options.env);\n if (!path) return {};\n const credentials = readCredentials(path);\n if (!credentials) return {};\n if (options.endpoint && !sameEndpoint(options.endpoint, credentials.endpoint)) return { other: credentials };\n return { login: new StoredLogin(path, credentials, options.fetch, options.userAgent) };\n}\n\nexport function sameEndpoint(a: string, b: string): boolean {\n return a.replace(/\\/+$/, \"\") === b.replace(/\\/+$/, \"\");\n}\n\nfunction timeoutSignal(ms: number): AbortSignal | undefined {\n const timeout = (AbortSignal as { timeout?: (ms: number) => AbortSignal }).timeout;\n return typeof timeout === \"function\" ? timeout.call(AbortSignal, ms) : undefined;\n}\n\nfunction processEnv(): Env {\n return (globalThis as { process?: { env?: Env } }).process?.env ?? {};\n}\n\nfunction joinPath(dir: string, name: string): string {\n return dir.endsWith(\"/\") || dir.endsWith(\"\\\\\") ? dir + name : `${dir}/${name}`;\n}\n\nfunction parentDir(path: string): string {\n const i = Math.max(path.lastIndexOf(\"/\"), path.lastIndexOf(\"\\\\\"));\n return i > 0 ? path.slice(0, i) : \".\";\n}\n\nfunction randomSuffix(): string {\n const bytes = new Uint8Array(6);\n globalThis.crypto.getRandomValues(bytes);\n return Array.from(bytes, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n"]}
package/dist/errors.d.ts CHANGED
@@ -45,9 +45,66 @@ export declare class UnfazedGraphQLError extends UnfazedError {
45
45
  readonly data: unknown;
46
46
  constructor(errors: GraphQLResponseError[], data: unknown);
47
47
  }
48
+ /**
49
+ * The server refused a delete that would take apps offline without the call
50
+ * naming exactly those apps, and deleted nothing.
51
+ *
52
+ * `deleteWorkspace` with `withDeployments` takes down every app the workspace
53
+ * serves, so its `confirmAliases` must equal the workspace's live apps — no
54
+ * more, no fewer. A missing or extra alias, or an app that went live after the
55
+ * check, is refused. The message is the server's, verbatim: it lists each live
56
+ * app with its custom domains and says how to name them. Look the apps up
57
+ * deliberately with `unfazed.liveApps(workspaceId)`, decide, and delete again,
58
+ * or delete one app with `unfazed.deleteApp`.
59
+ *
60
+ * Still an {@link UnfazedGraphQLError}, so existing `catch` sites keep matching.
61
+ */
62
+ export declare class UnfazedUnconfirmedAppsError extends UnfazedGraphQLError {
63
+ /** The workspace whose delete was refused. */
64
+ readonly workspaceId: string;
65
+ /** The aliases the refused call named. */
66
+ readonly confirmAliases: readonly string[];
67
+ constructor(workspaceId: string, confirmAliases: readonly string[], errors: GraphQLResponseError[], data: unknown);
68
+ }
69
+ /** Did the server refuse a delete because the named apps are not exactly the live ones? */
70
+ export declare function isUnconfirmedAppsRefusal(error: unknown): error is UnfazedGraphQLError;
48
71
  /** A request exceeded `timeoutMs`, or the caller's AbortSignal fired. */
49
72
  export declare class UnfazedTimeoutError extends UnfazedError {
50
73
  }
74
+ /**
75
+ * `writeFiles` wrote some files and not others.
76
+ *
77
+ * Writes are not transactional, exactly as MCP `write_workspace_files`: the
78
+ * files listed in `result.files` with `ok: true` ARE written (and, on an app
79
+ * with platform git, committed), the others are not. `result` is the whole
80
+ * answer, `failed` names the paths that did not land and why. Retry only
81
+ * those paths. Pass `allowPartial: true` to get the result instead of this
82
+ * error.
83
+ */
84
+ export declare class UnfazedWriteError extends UnfazedError {
85
+ /** What the call wrote: every file's outcome and the commit. */
86
+ readonly result: unknown;
87
+ /** The paths that were not written, with the server's reason. */
88
+ readonly failed: {
89
+ path: string;
90
+ error: string;
91
+ }[];
92
+ constructor(result: unknown, failed: {
93
+ path: string;
94
+ error: string;
95
+ }[]);
96
+ }
97
+ /**
98
+ * Did the platform refuse a long wait because the caller (or the server) has
99
+ * too many in progress? Nothing was waited on; polling is the way on.
100
+ */
101
+ export declare function isTooManyWaits(error: unknown): boolean;
102
+ /**
103
+ * Did the server refuse the document because it does not know `field` (a
104
+ * platform older than this SDK)? The SDK uses it to fall back to the older
105
+ * way of doing the same thing, never to hide a real error.
106
+ */
107
+ export declare function isUnsupportedField(error: unknown, field: string): boolean;
51
108
  /**
52
109
  * Waiting on a deploy ended badly.
53
110
  *
@@ -143,6 +200,13 @@ export declare const DEPLOYMENT_NOT_FOUND = "deployment was not found";
143
200
  * anything else is a genuine error and must keep throwing.
144
201
  */
145
202
  export declare function isNotFoundSentinel(error: unknown, sentinel: string): boolean;
203
+ /**
204
+ * Does this error say that exactly `alias` is not an app of the workspace?
205
+ *
206
+ * The workspace name in the message varies, so this matches the shape, then
207
+ * the alias itself: a refusal about a different alias still throws.
208
+ */
209
+ export declare function isAppNotFound(error: unknown, alias: string): boolean;
146
210
  /** Did `createWorkspace` fail because the name is already taken by this owner? */
147
211
  export declare function isDuplicateWorkspaceName(error: unknown): boolean;
148
212
  /**
package/dist/errors.js CHANGED
@@ -49,9 +49,88 @@ export class UnfazedGraphQLError extends UnfazedError {
49
49
  this.data = data;
50
50
  }
51
51
  }
52
+ /**
53
+ * The server refused a delete that would take apps offline without the call
54
+ * naming exactly those apps, and deleted nothing.
55
+ *
56
+ * `deleteWorkspace` with `withDeployments` takes down every app the workspace
57
+ * serves, so its `confirmAliases` must equal the workspace's live apps — no
58
+ * more, no fewer. A missing or extra alias, or an app that went live after the
59
+ * check, is refused. The message is the server's, verbatim: it lists each live
60
+ * app with its custom domains and says how to name them. Look the apps up
61
+ * deliberately with `unfazed.liveApps(workspaceId)`, decide, and delete again,
62
+ * or delete one app with `unfazed.deleteApp`.
63
+ *
64
+ * Still an {@link UnfazedGraphQLError}, so existing `catch` sites keep matching.
65
+ */
66
+ export class UnfazedUnconfirmedAppsError extends UnfazedGraphQLError {
67
+ /** The workspace whose delete was refused. */
68
+ workspaceId;
69
+ /** The aliases the refused call named. */
70
+ confirmAliases;
71
+ constructor(workspaceId, confirmAliases, errors, data) {
72
+ super(errors, data);
73
+ this.workspaceId = workspaceId;
74
+ this.confirmAliases = confirmAliases;
75
+ }
76
+ }
77
+ /**
78
+ * How the server opens every refusal of a delete whose named apps do not match
79
+ * the live ones (usecases.UnconfirmedAppsError, and the race where an app went
80
+ * live after the check). Anchored at the start, so a message that merely
81
+ * mentions the phrase is not mistaken for one.
82
+ */
83
+ const NOTHING_WAS_DELETED = /^nothing was deleted:/i;
84
+ /** Did the server refuse a delete because the named apps are not exactly the live ones? */
85
+ export function isUnconfirmedAppsRefusal(error) {
86
+ return (error instanceof UnfazedGraphQLError &&
87
+ error.errors.some((e) => NOTHING_WAS_DELETED.test(e.message.trim())));
88
+ }
52
89
  /** A request exceeded `timeoutMs`, or the caller's AbortSignal fired. */
53
90
  export class UnfazedTimeoutError extends UnfazedError {
54
91
  }
92
+ /**
93
+ * `writeFiles` wrote some files and not others.
94
+ *
95
+ * Writes are not transactional, exactly as MCP `write_workspace_files`: the
96
+ * files listed in `result.files` with `ok: true` ARE written (and, on an app
97
+ * with platform git, committed), the others are not. `result` is the whole
98
+ * answer, `failed` names the paths that did not land and why. Retry only
99
+ * those paths. Pass `allowPartial: true` to get the result instead of this
100
+ * error.
101
+ */
102
+ export class UnfazedWriteError extends UnfazedError {
103
+ /** What the call wrote: every file's outcome and the commit. */
104
+ result;
105
+ /** The paths that were not written, with the server's reason. */
106
+ failed;
107
+ constructor(result, failed) {
108
+ super(`unfazed: ${failed.length} file(s) were not written (the others were): ` +
109
+ failed.map((f) => `${f.path}: ${f.error}`).join("; "));
110
+ this.result = result;
111
+ this.failed = failed;
112
+ }
113
+ }
114
+ /**
115
+ * Did the platform refuse a long wait because the caller (or the server) has
116
+ * too many in progress? Nothing was waited on; polling is the way on.
117
+ */
118
+ export function isTooManyWaits(error) {
119
+ return (error instanceof UnfazedGraphQLError &&
120
+ error.errors.some((e) => /waits in progress|long wait per operation/i.test(e.message)));
121
+ }
122
+ /**
123
+ * Did the server refuse the document because it does not know `field` (a
124
+ * platform older than this SDK)? The SDK uses it to fall back to the older
125
+ * way of doing the same thing, never to hide a real error.
126
+ */
127
+ export function isUnsupportedField(error, field) {
128
+ if (!(error instanceof UnfazedGraphQLError))
129
+ return false;
130
+ const quoted = `"${field}"`;
131
+ return error.errors.some((e) => e.message.includes(quoted) &&
132
+ /Cannot query field|is not defined by type|Unknown argument|Unknown type|not defined on/i.test(e.message));
133
+ }
55
134
  /**
56
135
  * Waiting on a deploy ended badly.
57
136
  *
@@ -163,6 +242,22 @@ export function isNotFoundSentinel(error, sentinel) {
163
242
  return (error instanceof UnfazedGraphQLError &&
164
243
  error.errors.some((e) => e.message.trim() === sentinel));
165
244
  }
245
+ /**
246
+ * The server's "no such app here" refusal of `deleteApp`:
247
+ * `app "<alias>" was not found in workspace "<workspace name>"`. Anchored at
248
+ * both ends, like the exact sentinels above.
249
+ */
250
+ const APP_NOT_FOUND = /^app "(.+)" was not found in workspace ".*"$/;
251
+ /**
252
+ * Does this error say that exactly `alias` is not an app of the workspace?
253
+ *
254
+ * The workspace name in the message varies, so this matches the shape, then
255
+ * the alias itself: a refusal about a different alias still throws.
256
+ */
257
+ export function isAppNotFound(error, alias) {
258
+ return (error instanceof UnfazedGraphQLError &&
259
+ error.errors.some((e) => APP_NOT_FOUND.exec(e.message.trim())?.[1] === alias.trim()));
260
+ }
166
261
  /**
167
262
  * Message shape of the server's per-owner duplicate workspace name error:
168
263
  * `a workspace named "x" already exists; reuse it (id <uuid>) or pick a
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Error taxonomy.\n *\n * Everything the SDK throws is an {@link UnfazedError}, so a code-mode script\n * can `catch (e) { if (e instanceof UnfazedError) ... }` and still\n * discriminate on the subclass to decide whether to retry.\n */\n\n/** A single entry from a GraphQL response's `errors` array. */\nexport interface GraphQLResponseError {\n message: string;\n path?: (string | number)[];\n extensions?: Record<string, unknown>;\n}\n\n/** Base class for everything this SDK throws. */\nexport class UnfazedError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = new.target.name;\n }\n}\n\n/** No token was passed and no token environment variable was set. */\nexport class UnfazedAuthError extends UnfazedError {}\n\n/** The runtime has no global `fetch` and none was injected. */\nexport class UnfazedConfigError extends UnfazedError {}\n\n/**\n * The endpoint answered with a non-2xx status. `status` is the HTTP status and\n * `body` the response text, which is where the platform reports non-GraphQL\n * failures such as an expired or wrong-scope bearer token.\n */\nexport class UnfazedHTTPError extends UnfazedError {\n readonly status: number;\n readonly body: string;\n\n constructor(status: number, body: string, url: string) {\n super(`unfazed: HTTP ${status} from ${url}${body ? `: ${truncate(body)}` : \"\"}`);\n this.status = status;\n this.body = body;\n }\n}\n\n/**\n * The endpoint answered 200 with a GraphQL `errors` array.\n *\n * Partial `data` is preserved deliberately: the batched writeFiles document\n * resolves each aliased field independently, so one path can fail while the\n * others commit, and the caller needs to see which landed.\n */\nexport class UnfazedGraphQLError extends UnfazedError {\n readonly errors: GraphQLResponseError[];\n readonly data: unknown;\n\n constructor(errors: GraphQLResponseError[], data: unknown) {\n const summary = errors.map((e) => e.message).join(\"; \") || \"unknown GraphQL error\";\n super(`unfazed: ${summary}`);\n this.errors = errors;\n this.data = data;\n }\n}\n\n/** A request exceeded `timeoutMs`, or the caller's AbortSignal fired. */\nexport class UnfazedTimeoutError extends UnfazedError {}\n\n/**\n * Waiting on a deploy ended badly.\n *\n * `reason` separates the two outcomes because they need different handling:\n * FAILED means the build finished and is broken (read `deployment.error` and\n * fix the source — retrying will not help), TIMEOUT means it is still BUILDING\n * and polling longer may still succeed.\n */\nexport class UnfazedDeployError extends UnfazedError {\n readonly reason: \"FAILED\" | \"TIMEOUT\";\n readonly deploymentId: string;\n /** Last deployment snapshot observed before giving up. */\n readonly deployment: unknown;\n\n constructor(\n reason: \"FAILED\" | \"TIMEOUT\",\n deploymentId: string,\n deployment: unknown,\n message: string,\n ) {\n super(message);\n this.reason = reason;\n this.deploymentId = deploymentId;\n this.deployment = deployment;\n }\n}\n\n/**\n * Waiting on a run timed out.\n *\n * Unlike {@link UnfazedDeployError}, a run's FAILED status is never thrown\n * here: {@link import(\"./unfazed.js\").Unfazed.waitForRun} resolves on\n * SUCCEEDED, FAILED and CANCELLED alike, because a run that executed and\n * exited non-zero is a completed answer — read `run.error`/`run.exitCode` —\n * not a broken build the way a FAILED deployment is. TIMEOUT is the only\n * outcome still worth an exception: the run is, as far as the last poll\n * showed, still QUEUED or RUNNING, and waiting longer may yet succeed.\n */\nexport class UnfazedRunError extends UnfazedError {\n readonly reason: \"TIMEOUT\";\n readonly runId: string;\n /** Last run snapshot observed before giving up. */\n readonly run: unknown;\n\n constructor(reason: \"TIMEOUT\", runId: string, run: unknown, message: string) {\n super(message);\n this.reason = reason;\n this.runId = runId;\n this.run = run;\n }\n}\n\n/**\n * A rollback's compare-and-swap guard was rejected: the alias moved between\n * reading the current release and swapping it.\n *\n * Recovery is always \"re-list releases and rebuild the guard\" — never \"reuse\n * the old tuple\", because the server checks the holder id, epoch AND opaque\n * revision to close same-id ABA. `Unfazed.rollback` does that re-list for you\n * once by default.\n */\nexport class UnfazedConflictError extends UnfazedError {\n readonly errors: GraphQLResponseError[];\n\n constructor(message: string, errors: GraphQLResponseError[]) {\n super(message);\n this.errors = errors;\n }\n}\n\n/** The requested alias, deployment or release does not exist (or was pruned). */\nexport class UnfazedNotFoundError extends UnfazedError {}\n\n/**\n * What the `unfazed` CLI backend's World (`createWorld()`) throws.\n *\n * Its `name` is the Workflow SDK error a World would throw in its place\n * (`WorkflowWorldError`, `WorkflowRunNotFoundError`, `HookNotFoundError`),\n * because Workflow SDK's CLI and web UI pick their message by `name`,\n * `status` and `code`: a missing run reads as a 404 there, an argument the\n * backend cannot serve (`code: \"INVALID_ARGUMENT\"`) prints as given.\n * `instanceof UnfazedError` still holds.\n */\nexport class UnfazedInspectError extends UnfazedError {\n /** HTTP-like status: 404 for a missing run, step, event, hook or app; 401/403 for a refused token. */\n readonly status: number | undefined;\n /** `INVALID_ARGUMENT`, `READ_ONLY`, or undefined. */\n readonly code: string | undefined;\n /** The run, for `WorkflowRunNotFoundError`. */\n readonly runId: string | undefined;\n\n constructor(\n message: string,\n options: {\n name?: \"WorkflowWorldError\" | \"WorkflowRunNotFoundError\" | \"HookNotFoundError\";\n status?: number;\n code?: string;\n runId?: string;\n cause?: unknown;\n } = {},\n ) {\n super(message, options.cause === undefined ? undefined : { cause: options.cause });\n this.name = options.name ?? \"WorkflowWorldError\";\n this.status = options.status;\n this.code = options.code;\n this.runId = options.runId;\n }\n}\n\n/**\n * The server's exact \"no such workspace\" sentinel.\n *\n * A missing workspace and a workspace belonging to another tenant return the\n * SAME message on purpose, so the API cannot be used to probe for someone\n * else's ids. Treat it as \"not yours to see\", not strictly as \"absent\".\n */\nexport const WORKSPACE_NOT_FOUND = \"workspace was not found\";\n\n/** The server's exact \"no such deployment\" sentinel. See {@link WORKSPACE_NOT_FOUND}. */\nexport const DEPLOYMENT_NOT_FOUND = \"deployment was not found\";\n\n/**\n * Does this error carry EXACTLY the given not-found sentinel?\n *\n * Exact equality, deliberately. The tempting version — \"does any message\n * contain 'not found'\" — also swallows `release not found in history`,\n * `integration not found` and every future message that happens to use the\n * phrase, turning real failures into a silent `false` from a delete. These two\n * strings are stable authz sentinels with test coverage on the server side;\n * anything else is a genuine error and must keep throwing.\n */\nexport function isNotFoundSentinel(error: unknown, sentinel: string): boolean {\n return (\n error instanceof UnfazedGraphQLError &&\n error.errors.some((e) => e.message.trim() === sentinel)\n );\n}\n\n/**\n * Message shape of the server's per-owner duplicate workspace name error:\n * `a workspace named \"x\" already exists; reuse it (id <uuid>) or pick a\n * different name`.\n *\n * Used only to turn a lost `ensureWorkspace` race into a re-read. Requiring\n * both \"workspace\" and \"already exists\" in one clause keeps it off unrelated\n * conflicts (a duplicate ALIAS, say), and a false negative is safe: the\n * caller just gets the server's error verbatim.\n */\nconst DUPLICATE_WORKSPACE_NAME = /\\bworkspace\\b[^.;]{0,80}\\balready exists\\b/i;\n\n/** Did `createWorkspace` fail because the name is already taken by this owner? */\nexport function isDuplicateWorkspaceName(error: unknown): boolean {\n return (\n error instanceof UnfazedGraphQLError &&\n error.errors.some((e) => DUPLICATE_WORKSPACE_NAME.test(e.message))\n );\n}\n\nfunction truncate(s: string, max = 500): string {\n return s.length <= max ? s : `${s.slice(0, max)}…`;\n}\n\n/**\n * Message shapes that mean \"the alias moved under you\".\n *\n * The first two are the platform's ACTUAL strings. A rejected rollback guard\n * returns `store.ErrAliasMoved` verbatim — every guard point in the rollback\n * and adopt paths returns that one sentinel unwrapped — and the staged-service\n * rollback wraps the same guard with its own prefix. The rest are defensive:\n * the guard ARGUMENTS named back at you, or the alias-revision concept in any\n * spelling, or `alias` sitting in the same clause as a conflict verb, so a\n * rewording on the server side does not silently disable the retry.\n *\n * Deliberately NOT matched: a bare \"conflict\", \"changed\" or \"stale\" anywhere in\n * any message. Those fire on errors that have nothing to do with the alias\n * compare-and-swap, and a false positive is not free — it spends a pointless\n * re-list AND rewraps an unrelated failure as {@link UnfazedConflictError},\n * whose message would then blame concurrent deploys for, say, a permission\n * problem.\n */\nconst CAS_PATTERNS: RegExp[] = [\n /alias was repointed by another change/i,\n /became stale before alias change/i,\n /expected[\\s_-]*current/i,\n /alias[\\s_-]*revision/i,\n /\\balias\\b[^.;]{0,80}\\b(?:moved|repointed|changed|conflicts?|stale|mismatch)\\b/i,\n /\\b(?:moved|repointed|changed|conflicts?|stale|mismatch)\\b[^.;]{0,80}\\balias\\b/i,\n];\n\n/**\n * Heuristic: does this GraphQL error set look like a rejected compare-and-swap?\n *\n * The server carries no machine-readable code in `extensions` — its error\n * presenter returns gqlgen's default untouched — so this has to match message\n * text. It is used ONLY to decide whether to re-list and retry once: a false\n * negative costs a surfaced error the caller can retry by hand, a false\n * positive costs an extra read and a misleading error class. Neither can\n * promote the wrong release, because the retry rebuilds the guard from a fresh\n * read and the server re-validates it.\n */\nexport function looksLikeCASConflict(errors: GraphQLResponseError[]): boolean {\n return errors.some((e) => CAS_PATTERNS.some((pattern) => pattern.test(e.message)));\n}\n"]}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Error taxonomy.\n *\n * Everything the SDK throws is an {@link UnfazedError}, so a code-mode script\n * can `catch (e) { if (e instanceof UnfazedError) ... }` and still\n * discriminate on the subclass to decide whether to retry.\n */\n\n/** A single entry from a GraphQL response's `errors` array. */\nexport interface GraphQLResponseError {\n message: string;\n path?: (string | number)[];\n extensions?: Record<string, unknown>;\n}\n\n/** Base class for everything this SDK throws. */\nexport class UnfazedError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = new.target.name;\n }\n}\n\n/** No token was passed and no token environment variable was set. */\nexport class UnfazedAuthError extends UnfazedError {}\n\n/** The runtime has no global `fetch` and none was injected. */\nexport class UnfazedConfigError extends UnfazedError {}\n\n/**\n * The endpoint answered with a non-2xx status. `status` is the HTTP status and\n * `body` the response text, which is where the platform reports non-GraphQL\n * failures such as an expired or wrong-scope bearer token.\n */\nexport class UnfazedHTTPError extends UnfazedError {\n readonly status: number;\n readonly body: string;\n\n constructor(status: number, body: string, url: string) {\n super(`unfazed: HTTP ${status} from ${url}${body ? `: ${truncate(body)}` : \"\"}`);\n this.status = status;\n this.body = body;\n }\n}\n\n/**\n * The endpoint answered 200 with a GraphQL `errors` array.\n *\n * Partial `data` is preserved deliberately: the batched writeFiles document\n * resolves each aliased field independently, so one path can fail while the\n * others commit, and the caller needs to see which landed.\n */\nexport class UnfazedGraphQLError extends UnfazedError {\n readonly errors: GraphQLResponseError[];\n readonly data: unknown;\n\n constructor(errors: GraphQLResponseError[], data: unknown) {\n const summary = errors.map((e) => e.message).join(\"; \") || \"unknown GraphQL error\";\n super(`unfazed: ${summary}`);\n this.errors = errors;\n this.data = data;\n }\n}\n\n/**\n * The server refused a delete that would take apps offline without the call\n * naming exactly those apps, and deleted nothing.\n *\n * `deleteWorkspace` with `withDeployments` takes down every app the workspace\n * serves, so its `confirmAliases` must equal the workspace's live apps — no\n * more, no fewer. A missing or extra alias, or an app that went live after the\n * check, is refused. The message is the server's, verbatim: it lists each live\n * app with its custom domains and says how to name them. Look the apps up\n * deliberately with `unfazed.liveApps(workspaceId)`, decide, and delete again,\n * or delete one app with `unfazed.deleteApp`.\n *\n * Still an {@link UnfazedGraphQLError}, so existing `catch` sites keep matching.\n */\nexport class UnfazedUnconfirmedAppsError extends UnfazedGraphQLError {\n /** The workspace whose delete was refused. */\n readonly workspaceId: string;\n /** The aliases the refused call named. */\n readonly confirmAliases: readonly string[];\n\n constructor(\n workspaceId: string,\n confirmAliases: readonly string[],\n errors: GraphQLResponseError[],\n data: unknown,\n ) {\n super(errors, data);\n this.workspaceId = workspaceId;\n this.confirmAliases = confirmAliases;\n }\n}\n\n/**\n * How the server opens every refusal of a delete whose named apps do not match\n * the live ones (usecases.UnconfirmedAppsError, and the race where an app went\n * live after the check). Anchored at the start, so a message that merely\n * mentions the phrase is not mistaken for one.\n */\nconst NOTHING_WAS_DELETED = /^nothing was deleted:/i;\n\n/** Did the server refuse a delete because the named apps are not exactly the live ones? */\nexport function isUnconfirmedAppsRefusal(error: unknown): error is UnfazedGraphQLError {\n return (\n error instanceof UnfazedGraphQLError &&\n error.errors.some((e) => NOTHING_WAS_DELETED.test(e.message.trim()))\n );\n}\n\n/** A request exceeded `timeoutMs`, or the caller's AbortSignal fired. */\nexport class UnfazedTimeoutError extends UnfazedError {}\n\n/**\n * `writeFiles` wrote some files and not others.\n *\n * Writes are not transactional, exactly as MCP `write_workspace_files`: the\n * files listed in `result.files` with `ok: true` ARE written (and, on an app\n * with platform git, committed), the others are not. `result` is the whole\n * answer, `failed` names the paths that did not land and why. Retry only\n * those paths. Pass `allowPartial: true` to get the result instead of this\n * error.\n */\nexport class UnfazedWriteError extends UnfazedError {\n /** What the call wrote: every file's outcome and the commit. */\n readonly result: unknown;\n /** The paths that were not written, with the server's reason. */\n readonly failed: { path: string; error: string }[];\n\n constructor(result: unknown, failed: { path: string; error: string }[]) {\n super(\n `unfazed: ${failed.length} file(s) were not written (the others were): ` +\n failed.map((f) => `${f.path}: ${f.error}`).join(\"; \"),\n );\n this.result = result;\n this.failed = failed;\n }\n}\n\n/**\n * Did the platform refuse a long wait because the caller (or the server) has\n * too many in progress? Nothing was waited on; polling is the way on.\n */\nexport function isTooManyWaits(error: unknown): boolean {\n return (\n error instanceof UnfazedGraphQLError &&\n error.errors.some((e) => /waits in progress|long wait per operation/i.test(e.message))\n );\n}\n\n/**\n * Did the server refuse the document because it does not know `field` (a\n * platform older than this SDK)? The SDK uses it to fall back to the older\n * way of doing the same thing, never to hide a real error.\n */\nexport function isUnsupportedField(error: unknown, field: string): boolean {\n if (!(error instanceof UnfazedGraphQLError)) return false;\n const quoted = `\"${field}\"`;\n return error.errors.some(\n (e) =>\n e.message.includes(quoted) &&\n /Cannot query field|is not defined by type|Unknown argument|Unknown type|not defined on/i.test(e.message),\n );\n}\n\n/**\n * Waiting on a deploy ended badly.\n *\n * `reason` separates the two outcomes because they need different handling:\n * FAILED means the build finished and is broken (read `deployment.error` and\n * fix the source — retrying will not help), TIMEOUT means it is still BUILDING\n * and polling longer may still succeed.\n */\nexport class UnfazedDeployError extends UnfazedError {\n readonly reason: \"FAILED\" | \"TIMEOUT\";\n readonly deploymentId: string;\n /** Last deployment snapshot observed before giving up. */\n readonly deployment: unknown;\n\n constructor(\n reason: \"FAILED\" | \"TIMEOUT\",\n deploymentId: string,\n deployment: unknown,\n message: string,\n ) {\n super(message);\n this.reason = reason;\n this.deploymentId = deploymentId;\n this.deployment = deployment;\n }\n}\n\n/**\n * Waiting on a run timed out.\n *\n * Unlike {@link UnfazedDeployError}, a run's FAILED status is never thrown\n * here: {@link import(\"./unfazed.js\").Unfazed.waitForRun} resolves on\n * SUCCEEDED, FAILED and CANCELLED alike, because a run that executed and\n * exited non-zero is a completed answer — read `run.error`/`run.exitCode` —\n * not a broken build the way a FAILED deployment is. TIMEOUT is the only\n * outcome still worth an exception: the run is, as far as the last poll\n * showed, still QUEUED or RUNNING, and waiting longer may yet succeed.\n */\nexport class UnfazedRunError extends UnfazedError {\n readonly reason: \"TIMEOUT\";\n readonly runId: string;\n /** Last run snapshot observed before giving up. */\n readonly run: unknown;\n\n constructor(reason: \"TIMEOUT\", runId: string, run: unknown, message: string) {\n super(message);\n this.reason = reason;\n this.runId = runId;\n this.run = run;\n }\n}\n\n/**\n * A rollback's compare-and-swap guard was rejected: the alias moved between\n * reading the current release and swapping it.\n *\n * Recovery is always \"re-list releases and rebuild the guard\" — never \"reuse\n * the old tuple\", because the server checks the holder id, epoch AND opaque\n * revision to close same-id ABA. `Unfazed.rollback` does that re-list for you\n * once by default.\n */\nexport class UnfazedConflictError extends UnfazedError {\n readonly errors: GraphQLResponseError[];\n\n constructor(message: string, errors: GraphQLResponseError[]) {\n super(message);\n this.errors = errors;\n }\n}\n\n/** The requested alias, deployment or release does not exist (or was pruned). */\nexport class UnfazedNotFoundError extends UnfazedError {}\n\n/**\n * What the `unfazed` CLI backend's World (`createWorld()`) throws.\n *\n * Its `name` is the Workflow SDK error a World would throw in its place\n * (`WorkflowWorldError`, `WorkflowRunNotFoundError`, `HookNotFoundError`),\n * because Workflow SDK's CLI and web UI pick their message by `name`,\n * `status` and `code`: a missing run reads as a 404 there, an argument the\n * backend cannot serve (`code: \"INVALID_ARGUMENT\"`) prints as given.\n * `instanceof UnfazedError` still holds.\n */\nexport class UnfazedInspectError extends UnfazedError {\n /** HTTP-like status: 404 for a missing run, step, event, hook or app; 401/403 for a refused token. */\n readonly status: number | undefined;\n /** `INVALID_ARGUMENT`, `READ_ONLY`, or undefined. */\n readonly code: string | undefined;\n /** The run, for `WorkflowRunNotFoundError`. */\n readonly runId: string | undefined;\n\n constructor(\n message: string,\n options: {\n name?: \"WorkflowWorldError\" | \"WorkflowRunNotFoundError\" | \"HookNotFoundError\";\n status?: number;\n code?: string;\n runId?: string;\n cause?: unknown;\n } = {},\n ) {\n super(message, options.cause === undefined ? undefined : { cause: options.cause });\n this.name = options.name ?? \"WorkflowWorldError\";\n this.status = options.status;\n this.code = options.code;\n this.runId = options.runId;\n }\n}\n\n/**\n * The server's exact \"no such workspace\" sentinel.\n *\n * A missing workspace and a workspace belonging to another tenant return the\n * SAME message on purpose, so the API cannot be used to probe for someone\n * else's ids. Treat it as \"not yours to see\", not strictly as \"absent\".\n */\nexport const WORKSPACE_NOT_FOUND = \"workspace was not found\";\n\n/** The server's exact \"no such deployment\" sentinel. See {@link WORKSPACE_NOT_FOUND}. */\nexport const DEPLOYMENT_NOT_FOUND = \"deployment was not found\";\n\n/**\n * Does this error carry EXACTLY the given not-found sentinel?\n *\n * Exact equality, deliberately. The tempting version — \"does any message\n * contain 'not found'\" — also swallows `release not found in history`,\n * `integration not found` and every future message that happens to use the\n * phrase, turning real failures into a silent `false` from a delete. These two\n * strings are stable authz sentinels with test coverage on the server side;\n * anything else is a genuine error and must keep throwing.\n */\nexport function isNotFoundSentinel(error: unknown, sentinel: string): boolean {\n return (\n error instanceof UnfazedGraphQLError &&\n error.errors.some((e) => e.message.trim() === sentinel)\n );\n}\n\n/**\n * The server's \"no such app here\" refusal of `deleteApp`:\n * `app \"<alias>\" was not found in workspace \"<workspace name>\"`. Anchored at\n * both ends, like the exact sentinels above.\n */\nconst APP_NOT_FOUND = /^app \"(.+)\" was not found in workspace \".*\"$/;\n\n/**\n * Does this error say that exactly `alias` is not an app of the workspace?\n *\n * The workspace name in the message varies, so this matches the shape, then\n * the alias itself: a refusal about a different alias still throws.\n */\nexport function isAppNotFound(error: unknown, alias: string): boolean {\n return (\n error instanceof UnfazedGraphQLError &&\n error.errors.some((e) => APP_NOT_FOUND.exec(e.message.trim())?.[1] === alias.trim())\n );\n}\n\n/**\n * Message shape of the server's per-owner duplicate workspace name error:\n * `a workspace named \"x\" already exists; reuse it (id <uuid>) or pick a\n * different name`.\n *\n * Used only to turn a lost `ensureWorkspace` race into a re-read. Requiring\n * both \"workspace\" and \"already exists\" in one clause keeps it off unrelated\n * conflicts (a duplicate ALIAS, say), and a false negative is safe: the\n * caller just gets the server's error verbatim.\n */\nconst DUPLICATE_WORKSPACE_NAME = /\\bworkspace\\b[^.;]{0,80}\\balready exists\\b/i;\n\n/** Did `createWorkspace` fail because the name is already taken by this owner? */\nexport function isDuplicateWorkspaceName(error: unknown): boolean {\n return (\n error instanceof UnfazedGraphQLError &&\n error.errors.some((e) => DUPLICATE_WORKSPACE_NAME.test(e.message))\n );\n}\n\nfunction truncate(s: string, max = 500): string {\n return s.length <= max ? s : `${s.slice(0, max)}…`;\n}\n\n/**\n * Message shapes that mean \"the alias moved under you\".\n *\n * The first two are the platform's ACTUAL strings. A rejected rollback guard\n * returns `store.ErrAliasMoved` verbatim — every guard point in the rollback\n * and adopt paths returns that one sentinel unwrapped — and the staged-service\n * rollback wraps the same guard with its own prefix. The rest are defensive:\n * the guard ARGUMENTS named back at you, or the alias-revision concept in any\n * spelling, or `alias` sitting in the same clause as a conflict verb, so a\n * rewording on the server side does not silently disable the retry.\n *\n * Deliberately NOT matched: a bare \"conflict\", \"changed\" or \"stale\" anywhere in\n * any message. Those fire on errors that have nothing to do with the alias\n * compare-and-swap, and a false positive is not free — it spends a pointless\n * re-list AND rewraps an unrelated failure as {@link UnfazedConflictError},\n * whose message would then blame concurrent deploys for, say, a permission\n * problem.\n */\nconst CAS_PATTERNS: RegExp[] = [\n /alias was repointed by another change/i,\n /became stale before alias change/i,\n /expected[\\s_-]*current/i,\n /alias[\\s_-]*revision/i,\n /\\balias\\b[^.;]{0,80}\\b(?:moved|repointed|changed|conflicts?|stale|mismatch)\\b/i,\n /\\b(?:moved|repointed|changed|conflicts?|stale|mismatch)\\b[^.;]{0,80}\\balias\\b/i,\n];\n\n/**\n * Heuristic: does this GraphQL error set look like a rejected compare-and-swap?\n *\n * The server carries no machine-readable code in `extensions` — its error\n * presenter returns gqlgen's default untouched — so this has to match message\n * text. It is used ONLY to decide whether to re-list and retry once: a false\n * negative costs a surfaced error the caller can retry by hand, a false\n * positive costs an extra read and a misleading error class. Neither can\n * promote the wrong release, because the retry rebuilds the guard from a fresh\n * read and the server re-validates it.\n */\nexport function looksLikeCASConflict(errors: GraphQLResponseError[]): boolean {\n return errors.some((e) => CAS_PATTERNS.some((pattern) => pattern.test(e.message)));\n}\n"]}