unfazed 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -29
- package/dist/cli/login.d.ts +129 -0
- package/dist/cli/login.js +596 -0
- package/dist/cli/login.js.map +1 -0
- package/dist/cli/workflow.d.ts +31 -0
- package/dist/cli/workflow.js +157 -0
- package/dist/cli/workflow.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +173 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +26 -5
- package/dist/client.js +93 -48
- package/dist/client.js.map +1 -1
- package/dist/credentials.d.ts +123 -0
- package/dist/credentials.js +317 -0
- package/dist/credentials.js.map +1 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.js +11 -2
- package/dist/index.js.map +1 -1
- package/dist/remote/app.js +2 -1
- package/dist/remote/app.js.map +1 -1
- package/dist/remote/world.d.ts +2 -2
- package/dist/remote/world.js +32 -16
- package/dist/remote/world.js.map +1 -1
- package/dist/unfazed.d.ts +1 -1
- package/dist/unfazed.js +1 -1
- package/dist/unfazed.js.map +1 -1
- package/package.json +18 -6
|
@@ -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/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* ```ts
|
|
9
9
|
* import { Unfazed } from "unfazed";
|
|
10
10
|
*
|
|
11
|
-
* const unfazed = new Unfazed();
|
|
11
|
+
* const unfazed = new Unfazed(); // UNFZ_API_TOKEN, else your `npx unfazed login`
|
|
12
12
|
* const ws = await unfazed.createWorkspace("hello");
|
|
13
13
|
* await unfazed.writeFiles(ws.id, {
|
|
14
14
|
* "server.ts": `export default { fetch: () => new Response("hi") }`,
|
|
@@ -29,6 +29,13 @@ export type { ClientOptions, FetchLike, GraphQLDocument, RequestOptions, ResultO
|
|
|
29
29
|
export { DEPLOYMENT_NOT_FOUND, UnfazedAuthError, UnfazedConfigError, UnfazedConflictError, UnfazedDeployError, UnfazedError, UnfazedGraphQLError, UnfazedHTTPError, UnfazedNotFoundError, UnfazedRunError, UnfazedTimeoutError, WORKSPACE_NOT_FOUND, isDuplicateWorkspaceName, isNotFoundSentinel, } from "./errors.js";
|
|
30
30
|
export type { GraphQLResponseError } from "./errors.js";
|
|
31
31
|
export type { WaitOptions } from "./wait.js";
|
|
32
|
+
/**
|
|
33
|
+
* The stored login of `npx unfazed login`, which {@link UnfazedClient} uses when
|
|
34
|
+
* no token is passed and `UNFZ_API_TOKEN` is unset. The helpers that read,
|
|
35
|
+
* write and refresh it are in `unfazed/credentials`.
|
|
36
|
+
*/
|
|
37
|
+
export { CREDENTIALS_FILE, LOGIN_COMMAND, credentialsPath } from "./credentials.js";
|
|
38
|
+
export type { StoredCredentials } from "./credentials.js";
|
|
32
39
|
export { UnfazedInspectError } from "./errors.js";
|
|
33
40
|
export { APP_ENV_VAR } from "./remote/app.js";
|
|
34
41
|
export type { RemoteWorld, RemoteWorldOptions } from "./remote/world.js";
|
|
@@ -37,10 +44,13 @@ export type { RemoteWorld, RemoteWorldOptions } from "./remote/world.js";
|
|
|
37
44
|
* platform's workflow inspection API, for one app.
|
|
38
45
|
*
|
|
39
46
|
* ```sh
|
|
40
|
-
*
|
|
47
|
+
* npx unfazed login # once: signs you in, in the browser
|
|
41
48
|
* UNFZ_APP=birthday-card npx workflow inspect runs --backend unfazed
|
|
42
49
|
* ```
|
|
43
50
|
*
|
|
51
|
+
* The token is `UNFZ_API_TOKEN` when set, else the stored login of
|
|
52
|
+
* `npx unfazed login`, refreshed as it expires.
|
|
53
|
+
*
|
|
44
54
|
* The CLI (and its `--web` UI) resolve `unfazed` from the project and call
|
|
45
55
|
* this with no arguments. The app is `UNFZ_APP`, else the `alias` (or the
|
|
46
56
|
* single `apps[]` entry) of `unfazed.json` in the current directory. Reads
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* ```ts
|
|
9
9
|
* import { Unfazed } from "unfazed";
|
|
10
10
|
*
|
|
11
|
-
* const unfazed = new Unfazed();
|
|
11
|
+
* const unfazed = new Unfazed(); // UNFZ_API_TOKEN, else your `npx unfazed login`
|
|
12
12
|
* const ws = await unfazed.createWorkspace("hello");
|
|
13
13
|
* await unfazed.writeFiles(ws.id, {
|
|
14
14
|
* "server.ts": `export default { fetch: () => new Response("hi") }`,
|
|
@@ -24,6 +24,12 @@
|
|
|
24
24
|
export { Unfazed } from "./unfazed.js";
|
|
25
25
|
export { UnfazedClient, DEFAULT_BASE_URL, DEFAULT_ENDPOINT, ENDPOINT_ENV_VAR, GRAPHQL_PATH, TOKEN_ENV_VAR, VERSION, } from "./client.js";
|
|
26
26
|
export { DEPLOYMENT_NOT_FOUND, UnfazedAuthError, UnfazedConfigError, UnfazedConflictError, UnfazedDeployError, UnfazedError, UnfazedGraphQLError, UnfazedHTTPError, UnfazedNotFoundError, UnfazedRunError, UnfazedTimeoutError, WORKSPACE_NOT_FOUND, isDuplicateWorkspaceName, isNotFoundSentinel, } from "./errors.js";
|
|
27
|
+
/**
|
|
28
|
+
* The stored login of `npx unfazed login`, which {@link UnfazedClient} uses when
|
|
29
|
+
* no token is passed and `UNFZ_API_TOKEN` is unset. The helpers that read,
|
|
30
|
+
* write and refresh it are in `unfazed/credentials`.
|
|
31
|
+
*/
|
|
32
|
+
export { CREDENTIALS_FILE, LOGIN_COMMAND, credentialsPath } from "./credentials.js";
|
|
27
33
|
export { UnfazedInspectError } from "./errors.js";
|
|
28
34
|
export { APP_ENV_VAR } from "./remote/app.js";
|
|
29
35
|
/**
|
|
@@ -31,10 +37,13 @@ export { APP_ENV_VAR } from "./remote/app.js";
|
|
|
31
37
|
* platform's workflow inspection API, for one app.
|
|
32
38
|
*
|
|
33
39
|
* ```sh
|
|
34
|
-
*
|
|
40
|
+
* npx unfazed login # once: signs you in, in the browser
|
|
35
41
|
* UNFZ_APP=birthday-card npx workflow inspect runs --backend unfazed
|
|
36
42
|
* ```
|
|
37
43
|
*
|
|
44
|
+
* The token is `UNFZ_API_TOKEN` when set, else the stored login of
|
|
45
|
+
* `npx unfazed login`, refreshed as it expires.
|
|
46
|
+
*
|
|
38
47
|
* The CLI (and its `--web` UI) resolve `unfazed` from the project and call
|
|
39
48
|
* this with no arguments. The app is `UNFZ_APP`, else the `alias` (or the
|
|
40
49
|
* single `apps[]` entry) of `unfazed.json` in the current directory. Reads
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAuBvC,OAAO,EACL,aAAa,EACb,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,aAAa,EACb,OAAO,GACR,MAAM,aAAa,CAAC;AAUrB,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,kBAAkB,EAClB,YAAY,EACZ,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,EACpB,eAAe,EACf,mBAAmB,EACnB,mBAAmB,EACnB,wBAAwB,EACxB,kBAAkB,GACnB,MAAM,aAAa,CAAC;AAKrB,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAG9C
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAuBvC,OAAO,EACL,aAAa,EACb,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,aAAa,EACb,OAAO,GACR,MAAM,aAAa,CAAC;AAUrB,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,kBAAkB,EAClB,YAAY,EACZ,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,EACpB,eAAe,EACf,mBAAmB,EACnB,mBAAmB,EACnB,wBAAwB,EACxB,kBAAkB,GACnB,MAAM,aAAa,CAAC;AAKrB;;;;GAIG;AACH,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGpF,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAG9C;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAA4B;IAC5D,MAAM,EAAE,iBAAiB,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAC;IAChE,OAAO,iBAAiB,CAAC,OAAO,CAAC,CAAC;AACpC,CAAC;AAED;;;GAGG;AACH,OAAO,KAAK,UAAU,MAAM,iBAAiB,CAAC","sourcesContent":["/**\n * unfazed — official TypeScript SDK.\n *\n * The code-mode companion to the unfazed MCP server: write one script that\n * deploys, waits, reads logs and filters them locally, instead of chaining a\n * dozen tool calls through a model's context.\n *\n * ```ts\n * import { Unfazed } from \"unfazed\";\n *\n * const unfazed = new Unfazed(); // UNFZ_API_TOKEN, else your `npx unfazed login`\n * const ws = await unfazed.createWorkspace(\"hello\");\n * await unfazed.writeFiles(ws.id, {\n * \"server.ts\": `export default { fetch: () => new Response(\"hi\") }`,\n * });\n * const app = await unfazed.deployService({ workspaceId: ws.id, alias: \"hello\", wait: true });\n * console.log(app.url);\n * ```\n *\n * Two layers, both public:\n * - {@link Unfazed} — the workflow layer (deploy + wait, batch writes, one-call rollback).\n * - {@link UnfazedClient} — the typed GraphQL transport, for anything not wrapped.\n */\n\nimport type { RemoteWorld, RemoteWorldOptions } from \"./remote/world.js\";\n\nexport { Unfazed } from \"./unfazed.js\";\nexport type {\n CreateRunOptions,\n FollowUpRunOptions,\n DeleteDeploymentOptions,\n DeleteWorkspaceOptions,\n Deployment,\n DeployBoxOptions,\n DeployOptions,\n DeployServiceOptions,\n DeployStaticOptions,\n EnvVar,\n FileContent,\n FilesMap,\n ListRunsFilter,\n RollbackOptions,\n Run,\n UnfazedOptions,\n Workspace,\n WorkspaceFile,\n WriteFilesOptions,\n} from \"./unfazed.js\";\n\nexport {\n UnfazedClient,\n DEFAULT_BASE_URL,\n DEFAULT_ENDPOINT,\n ENDPOINT_ENV_VAR,\n GRAPHQL_PATH,\n TOKEN_ENV_VAR,\n VERSION,\n} from \"./client.js\";\nexport type {\n ClientOptions,\n FetchLike,\n GraphQLDocument,\n RequestOptions,\n ResultOf,\n VariablesOf,\n} from \"./client.js\";\n\nexport {\n DEPLOYMENT_NOT_FOUND,\n UnfazedAuthError,\n UnfazedConfigError,\n UnfazedConflictError,\n UnfazedDeployError,\n UnfazedError,\n UnfazedGraphQLError,\n UnfazedHTTPError,\n UnfazedNotFoundError,\n UnfazedRunError,\n UnfazedTimeoutError,\n WORKSPACE_NOT_FOUND,\n isDuplicateWorkspaceName,\n isNotFoundSentinel,\n} from \"./errors.js\";\nexport type { GraphQLResponseError } from \"./errors.js\";\n\nexport type { WaitOptions } from \"./wait.js\";\n\n/**\n * The stored login of `npx unfazed login`, which {@link UnfazedClient} uses when\n * no token is passed and `UNFZ_API_TOKEN` is unset. The helpers that read,\n * write and refresh it are in `unfazed/credentials`.\n */\nexport { CREDENTIALS_FILE, LOGIN_COMMAND, credentialsPath } from \"./credentials.js\";\nexport type { StoredCredentials } from \"./credentials.js\";\n\nexport { UnfazedInspectError } from \"./errors.js\";\nexport { APP_ENV_VAR } from \"./remote/app.js\";\nexport type { RemoteWorld, RemoteWorldOptions } from \"./remote/world.js\";\n\n/**\n * The `unfazed` backend of Workflow SDK's CLI: a read-mostly World over the\n * platform's workflow inspection API, for one app.\n *\n * ```sh\n * npx unfazed login # once: signs you in, in the browser\n * UNFZ_APP=birthday-card npx workflow inspect runs --backend unfazed\n * ```\n *\n * The token is `UNFZ_API_TOKEN` when set, else the stored login of\n * `npx unfazed login`, refreshed as it expires.\n *\n * The CLI (and its `--web` UI) resolve `unfazed` from the project and call\n * this with no arguments. The app is `UNFZ_APP`, else the `alias` (or the\n * single `apps[]` entry) of `unfazed.json` in the current directory. Reads\n * and `workflow cancel` work; every other write throws, since an app's own\n * World is `unfazed/workflow`. The module behind it loads on first call, so\n * `import \"unfazed\"` stays as light as before.\n */\nexport async function createWorld(options?: RemoteWorldOptions): Promise<RemoteWorld> {\n const { createRemoteWorld } = await import(\"./remote/world.js\");\n return createRemoteWorld(options);\n}\n\n/**\n * Every operation document the workflow layer uses, for callers that want the\n * typed documents but not the wrapper methods.\n */\nexport * as operations from \"./operations.js\";\n\n/**\n * Schema types generated from the platform's `graph/schema.graphqls`: enums\n * (`DeploymentStatus`, `ServiceMode`, ...), input objects\n * (`CreateDeploymentInput`, ...) and every operation's result type.\n */\nexport type * from \"./generated/graphql.js\";\n"]}
|
package/dist/remote/app.js
CHANGED
|
@@ -20,7 +20,8 @@ export function resolveAlias(options) {
|
|
|
20
20
|
const text = readText(path);
|
|
21
21
|
const hint = `Set ${APP_ENV_VAR} to the app's alias (the <alias> of https://<alias>.unfz.to), e.g.\n` +
|
|
22
22
|
` ${APP_ENV_VAR}=my-app npx workflow inspect runs --backend unfazed\n` +
|
|
23
|
-
`or run it from the app's directory with "alias" in its ${MANIFEST_FILE}
|
|
23
|
+
`or run it from the app's directory with "alias" in its ${MANIFEST_FILE}. ` +
|
|
24
|
+
`(\`npx unfazed workflow …\` takes --app my-app.)`;
|
|
24
25
|
if (text === undefined)
|
|
25
26
|
throw new UnfazedConfigError(`unfazed: which app? ${APP_ENV_VAR} is not set and there is no ${path}. ${hint}`);
|
|
26
27
|
let manifest;
|
package/dist/remote/app.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.js","sourceRoot":"","sources":["../../src/remote/app.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAElD,6EAA6E;AAC7E,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC;AAEtC,wEAAwE;AACxE,MAAM,CAAC,MAAM,aAAa,GAAG,cAAc,CAAC;AAS5C;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,OAAkD;IAC7E,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,CAAC;IAC3E,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAE9B,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC;IAClD,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC5B,MAAM,IAAI,GACR,OAAO,WAAW,sEAAsE;QACxF,KAAK,WAAW,uDAAuD;QACvE,0DAA0D,aAAa,
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../../src/remote/app.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Which app the `unfazed` CLI backend reads: `UNFZ_APP`, else the alias the\n * current directory's `unfazed.json` names.\n */\nimport { UnfazedConfigError } from \"../errors.js\";\n\n/** Environment variable naming the app (its alias) the CLI backend reads. */\nexport const APP_ENV_VAR = \"UNFZ_APP\";\n\n/** The project file the alias is read from when `UNFZ_APP` is unset. */\nexport const MANIFEST_FILE = \"unfazed.json\";\n\ntype Env = Record<string, string | undefined>;\n\ninterface Manifest {\n alias?: unknown;\n apps?: { alias?: unknown; rootDir?: unknown }[];\n}\n\n/**\n * The app alias: `alias` when given, else `UNFZ_APP`, else `unfazed.json` in\n * `cwd`: its `alias`, or the alias of its only `apps[]` entry. Throws\n * {@link UnfazedConfigError} saying what to set when none of them names one.\n */\nexport function resolveAlias(options: { alias?: string; env: Env; cwd: string }): string {\n const explicit = options.alias?.trim() || options.env[APP_ENV_VAR]?.trim();\n if (explicit) return explicit;\n\n const path = joinPath(options.cwd, MANIFEST_FILE);\n const text = readText(path);\n const hint =\n `Set ${APP_ENV_VAR} to the app's alias (the <alias> of https://<alias>.unfz.to), e.g.\\n` +\n ` ${APP_ENV_VAR}=my-app npx workflow inspect runs --backend unfazed\\n` +\n `or run it from the app's directory with \"alias\" in its ${MANIFEST_FILE}. ` +\n `(\\`npx unfazed workflow …\\` takes --app my-app.)`;\n if (text === undefined) throw new UnfazedConfigError(`unfazed: which app? ${APP_ENV_VAR} is not set and there is no ${path}. ${hint}`);\n\n let manifest: Manifest;\n try {\n manifest = JSON.parse(text) as Manifest;\n } catch (error) {\n throw new UnfazedConfigError(`unfazed: ${path} is not valid JSON (${(error as Error).message}). ${hint}`);\n }\n if (typeof manifest.alias === \"string\" && manifest.alias.trim()) return manifest.alias.trim();\n const aliases = (Array.isArray(manifest.apps) ? manifest.apps : [])\n .map((app) => (typeof app?.alias === \"string\" ? app.alias.trim() : \"\"))\n .filter((alias) => alias !== \"\");\n if (aliases.length === 1) return aliases[0]!;\n if (aliases.length > 1) {\n throw new UnfazedConfigError(\n `unfazed: ${path} deploys ${aliases.length} apps (${aliases.join(\", \")}). ` +\n `Set ${APP_ENV_VAR} to the one to inspect, e.g. ${APP_ENV_VAR}=${aliases[0]}.`,\n );\n }\n throw new UnfazedConfigError(`unfazed: which app? ${path} names no \"alias\" and no apps[]. ${hint}`);\n}\n\n/** The current directory, or `\".\"` where there is no `process`. */\nexport function currentDirectory(): string {\n const proc = (globalThis as { process?: { cwd?: () => string } }).process;\n return proc?.cwd?.() ?? \".\";\n}\n\n/**\n * A file's text, or `undefined` when it does not exist. `node:fs` comes from\n * `process.getBuiltinModule` (Node >= 20.16, Bun, Deno), never from an\n * import: the root entry stays free of Node imports, so a browser bundle of\n * `unfazed` does not trip over them.\n */\nfunction readText(path: string): string | undefined {\n const proc = (globalThis as { process?: { getBuiltinModule?: (id: string) => unknown } }).process;\n const fs = proc?.getBuiltinModule?.(\"node:fs\") as\n | { readFileSync(path: string, encoding: \"utf8\"): string }\n | undefined;\n if (!fs) {\n throw new UnfazedConfigError(\n `unfazed: cannot read ${path} on this runtime (it needs process.getBuiltinModule: Node >= 20.16, Bun or Deno). ` +\n `Set ${APP_ENV_VAR} to the app's alias.`,\n );\n }\n try {\n return fs.readFileSync(path, \"utf8\");\n } catch (error) {\n if ((error as { code?: string }).code === \"ENOENT\") return undefined;\n throw new UnfazedConfigError(`unfazed: could not read ${path}: ${(error as Error).message}`);\n }\n}\n\nfunction joinPath(dir: string, file: string): string {\n return dir.endsWith(\"/\") || dir.endsWith(\"\\\\\") ? dir + file : `${dir}/${file}`;\n}\n"]}
|
package/dist/remote/world.d.ts
CHANGED
|
@@ -2,11 +2,11 @@ import { type FetchLike } from "../client.js";
|
|
|
2
2
|
export interface RemoteWorldOptions {
|
|
3
3
|
/** The app's alias. Default: `UNFZ_APP`, else the `alias` (or single `apps[]` entry) of `unfazed.json` in `cwd`. */
|
|
4
4
|
alias?: string;
|
|
5
|
-
/** Bearer token. Default: `UNFZ_API_TOKEN
|
|
5
|
+
/** Bearer token. Default: `UNFZ_API_TOKEN`, else the stored login of `npx unfazed login`. */
|
|
6
6
|
token?: string;
|
|
7
7
|
/** Full GraphQL endpoint. Default: `UNFZ_API_URL`, else https://unfazed.dev/query. */
|
|
8
8
|
endpoint?: string;
|
|
9
|
-
/** Where the defaults above are read from. Default: `process.env`. */
|
|
9
|
+
/** Where the defaults above (and the stored login's location) are read from. Default: `process.env`. */
|
|
10
10
|
env?: Record<string, string | undefined>;
|
|
11
11
|
/** Directory whose `unfazed.json` names the app. Default: the current directory. */
|
|
12
12
|
cwd?: string;
|
package/dist/remote/world.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ENDPOINT_ENV_VAR, TOKEN_ENV_VAR, UnfazedClient, VERSION } from "../client.js";
|
|
2
|
+
import { LOGIN_COMMAND } from "../credentials.js";
|
|
2
3
|
import { UnfazedAuthError, UnfazedGraphQLError, UnfazedHTTPError, UnfazedInspectError } from "../errors.js";
|
|
3
4
|
import { APP_ENV_VAR, currentDirectory, resolveAlias } from "./app.js";
|
|
4
5
|
import * as docs from "./documents.js";
|
|
@@ -28,24 +29,37 @@ const REDACTED = "[redacted]";
|
|
|
28
29
|
*/
|
|
29
30
|
export function createRemoteWorld(options = {}) {
|
|
30
31
|
const env = options.env ?? globalThis.process?.env ?? {};
|
|
31
|
-
const
|
|
32
|
-
|
|
32
|
+
const endpointEnv = env[ENDPOINT_ENV_VAR];
|
|
33
|
+
let client;
|
|
34
|
+
try {
|
|
35
|
+
// The token is `token`, else UNFZ_API_TOKEN, else the stored login of
|
|
36
|
+
// `npx unfazed login` (refreshed as it expires), all read from `env`.
|
|
37
|
+
client = new UnfazedClient({
|
|
38
|
+
...(options.token ? { token: options.token } : {}),
|
|
39
|
+
env,
|
|
40
|
+
endpoint: options.endpoint ?? (endpointEnv ? normalizeEndpoint(endpointEnv) : undefined),
|
|
41
|
+
timeoutMs: options.timeoutMs ?? 60_000,
|
|
42
|
+
userAgent: `unfazed-sdk/${VERSION} (workflow-cli-backend)`,
|
|
43
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (!(error instanceof UnfazedAuthError))
|
|
48
|
+
throw error;
|
|
33
49
|
const inApp = env["UNFZ_STATE_URL"]
|
|
34
50
|
? ' Inside an unfazed app the World is unfazed/workflow (its Nitro module, or setWorld(createWorld()) from "unfazed/workflow"); ' +
|
|
35
51
|
"WORKFLOW_TARGET_WORLD=unfazed selects this read-only CLI backend instead."
|
|
36
52
|
: "";
|
|
37
|
-
|
|
38
|
-
|
|
53
|
+
// A login for another control plane says which command fixes it; keep that.
|
|
54
|
+
const message = error.message.includes("the stored login is for")
|
|
55
|
+
? error.message
|
|
56
|
+
: `unfazed: not signed in. Run \`${LOGIN_COMMAND}\` to inspect your apps' workflow runs with your own account, ` +
|
|
57
|
+
`or set ${TOKEN_ENV_VAR} (a token with the unfz:read scope; unfz:deploy too for cancel and wake; ` +
|
|
58
|
+
`an agent gets one at https://unfazed.dev/auth.md).`;
|
|
59
|
+
throw new UnfazedAuthError(message + inApp, { cause: error });
|
|
39
60
|
}
|
|
40
61
|
const alias = resolveAlias({ alias: options.alias, env, cwd: options.cwd ?? currentDirectory() });
|
|
41
|
-
const
|
|
42
|
-
const client = new UnfazedClient({
|
|
43
|
-
token,
|
|
44
|
-
endpoint: options.endpoint ?? (endpointEnv ? normalizeEndpoint(endpointEnv) : undefined),
|
|
45
|
-
timeoutMs: options.timeoutMs ?? 60_000,
|
|
46
|
-
userAgent: `unfazed-sdk/${VERSION} (workflow-cli-backend)`,
|
|
47
|
-
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
48
|
-
});
|
|
62
|
+
const authSource = client.authSource;
|
|
49
63
|
const pollMaxMs = Math.max(1, options.streamPollMs ?? 5_000);
|
|
50
64
|
const pollMinMs = Math.min(STREAM_POLL_MIN_MS, pollMaxMs);
|
|
51
65
|
let closed = false;
|
|
@@ -66,7 +80,7 @@ export function createRemoteWorld(options = {}) {
|
|
|
66
80
|
return await client.request(document, variables);
|
|
67
81
|
}
|
|
68
82
|
catch (error) {
|
|
69
|
-
throw translate(error, alias, runId);
|
|
83
|
+
throw translate(error, alias, runId, authSource);
|
|
70
84
|
}
|
|
71
85
|
}
|
|
72
86
|
// ---- Run pages: one relay serves runs.get, hooks, streams.list ----
|
|
@@ -541,9 +555,11 @@ function syntheticEvent(runId, data) {
|
|
|
541
555
|
* missing run is `WorkflowRunNotFoundError` (404), a refused token says how
|
|
542
556
|
* to get one, a token without the deploy scope says what needs it.
|
|
543
557
|
*/
|
|
544
|
-
function translate(error, alias, runId) {
|
|
545
|
-
const refused = (status, cause) => new UnfazedInspectError(
|
|
546
|
-
|
|
558
|
+
function translate(error, alias, runId, authSource) {
|
|
559
|
+
const refused = (status, cause) => new UnfazedInspectError(authSource === "login"
|
|
560
|
+
? `unfazed: the platform refused the stored login (HTTP ${status}). Run \`${LOGIN_COMMAND}\` again.`
|
|
561
|
+
: `unfazed: the platform refused the API token (HTTP ${status}). Check ${TOKEN_ENV_VAR}; ` +
|
|
562
|
+
`sign in with \`${LOGIN_COMMAND}\` instead, or get an agent token at https://unfazed.dev/auth.md.`, { status, cause });
|
|
547
563
|
if (error instanceof UnfazedHTTPError) {
|
|
548
564
|
if (error.status === 401 || error.status === 403)
|
|
549
565
|
return refused(error.status, error);
|