@zocomputer/cli 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -28
- package/dist/argv.js +1 -2
- package/dist/auth.d.ts +14 -1
- package/dist/auth.js +89 -30
- package/dist/commands.js +1 -0
- package/dist/generated/commands.js +19 -79
- package/dist/login.d.ts +30 -5
- package/dist/login.js +81 -0
- package/dist/output.js +4 -2
- package/dist/profiles.d.ts +7 -2
- package/dist/profiles.js +7 -2
- package/dist/rpc.js +1 -1
- package/dist/runner.js +27 -14
- package/dist/update.d.ts +21 -0
- package/dist/update.js +195 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -11,15 +11,14 @@ Once published, macOS 15+ and Linux glibc 2.35+ users on arm64 or x64 can run:
|
|
|
11
11
|
|
|
12
12
|
```sh
|
|
13
13
|
curl -fsSL https://zo.computer/install.sh | sh
|
|
14
|
-
export PATH="$HOME/.local/bin:$PATH"
|
|
15
|
-
zo --version
|
|
16
14
|
```
|
|
17
15
|
|
|
18
|
-
It includes Node, verifies the archive, and keeps versions under
|
|
16
|
+
Then open a new terminal and run `zo --version`. It includes Node, verifies the archive, and keeps versions under
|
|
19
17
|
`${XDG_DATA_HOME:-$HOME/.local/share}/zo/cli/` with a managed `~/.local/bin/zo`
|
|
20
|
-
symlink
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
symlink, and adds `~/.local/bin` to your shell's PATH configuration unless it's
|
|
19
|
+
already on PATH. Run `zo update` to upgrade, or re-run the installer; `--version`,
|
|
20
|
+
`--bin-dir`, and `--no-modify-path` control pinned installs, command placement
|
|
21
|
+
and shell setup. Existing sign-ins are preserved. The [CLI guide](../../src/hostagent/system_api/docs/guides/cli.md)
|
|
23
22
|
covers supported systems, PATH collisions, rollback and removal.
|
|
24
23
|
|
|
25
24
|
Install the latest published version from npm:
|
|
@@ -30,9 +29,9 @@ zo --version
|
|
|
30
29
|
zo login
|
|
31
30
|
```
|
|
32
31
|
|
|
33
|
-
Run the same install command again to upgrade
|
|
34
|
-
|
|
35
|
-
you run a command.
|
|
32
|
+
Run the same install command again to upgrade; `zo update` refuses an npm
|
|
33
|
+
installation and names this command. The CLI installs its matching public SDK
|
|
34
|
+
dependency. It performs no update check when you run a command.
|
|
36
35
|
|
|
37
36
|
To try the CLI without installing it globally:
|
|
38
37
|
|
|
@@ -97,17 +96,27 @@ workspaces, a terminal prompts for a selection; scripts must supply
|
|
|
97
96
|
person separately from the effective subject in the selected workspace.
|
|
98
97
|
`context list`, `context use` and status refresh that view from the server.
|
|
99
98
|
|
|
100
|
-
`login`
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
`
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
99
|
+
`login` signs in as the client ID
|
|
100
|
+
`https://www.zo.computer/oauth/zo-cli-client-metadata.json`, the CLI's OAuth
|
|
101
|
+
client metadata document, so the consent page names it "Zo CLI". By default it
|
|
102
|
+
uses an authorization code with PKCE (S256) and a loopback callback (RFC 8252):
|
|
103
|
+
it listens on `127.0.0.1` at a port the OS assigns, opens your browser at the
|
|
104
|
+
authorization URL, and prints that URL on stderr in case the browser doesn't
|
|
105
|
+
open. The browser returns to `http://127.0.0.1:<port>/callback`; the CLI accepts
|
|
106
|
+
the one callback whose `state` and `iss` match, redeems the code with its
|
|
107
|
+
verifier, and stops listening. It waits 10 minutes; Ctrl-C cancels (exit 130).
|
|
108
|
+
Denial exits 4 with `access_denied`; a timeout exits 3 with `login_timeout`.
|
|
109
|
+
|
|
110
|
+
`login --device` uses the device authorization grant (RFC 8628) instead: it
|
|
111
|
+
prints a URL and a code and polls the token endpoint until you approve on any
|
|
112
|
+
device. Approve only if the page shows the same code. Login does the same
|
|
113
|
+
without `--device` over SSH (`SSH_CONNECTION` or `SSH_TTY` set), with no
|
|
114
|
+
`DISPLAY` or `WAYLAND_DISPLAY` outside macOS, or when the browser fails to open.
|
|
115
|
+
The device request expires after at most 10 minutes, with the same exit codes.
|
|
116
|
+
|
|
117
|
+
A credential saved by an earlier release, which signed in as `zo-public-cli`,
|
|
118
|
+
is refused as `invalid_credential` after an upgrade, `login` included. Run
|
|
119
|
+
`zo logout` for that profile, then `zo login`.
|
|
111
120
|
|
|
112
121
|
After approval, login lists every approved workspace with the identity the CLI
|
|
113
122
|
acts as there (`as you` or `as service account <id>`), even when there is only
|
|
@@ -126,15 +135,16 @@ token. `ZO_SYSTEM_API_ACCESS_TOKEN` supplies an explicit automation token for
|
|
|
126
135
|
the selected profile's fixed origins; the CLI never saves or refreshes it,
|
|
127
136
|
including when the server refuses it.
|
|
128
137
|
|
|
129
|
-
`logout` revokes the saved sign-in and pending replaced sign-ins
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
138
|
+
`logout` revokes the saved sign-in and pending replaced sign-ins, then removes
|
|
139
|
+
them from this computer. If a revocation fails, for a network error, an HTTP
|
|
140
|
+
error or a credential it can't read (such as one saved by an earlier release),
|
|
141
|
+
logout prints a one-line warning with the reason, removes the local credential
|
|
142
|
+
anyway and exits 0; revoke the sign-in in **Authorized apps** if it may still be
|
|
143
|
+
in use. Ctrl-C during logout keeps what is left so a later logout can finish.
|
|
144
|
+
Unset a bearer override before logging in or out of a saved profile.
|
|
133
145
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
Ordinary login and logout preserve the unreadable file so they do not silently
|
|
137
|
-
discard pending revocations.
|
|
146
|
+
Login refuses a malformed credential file, so it never silently discards pending
|
|
147
|
+
revocations; run `logout` for that profile first.
|
|
138
148
|
|
|
139
149
|
## Local storage
|
|
140
150
|
|
package/dist/argv.js
CHANGED
|
@@ -18,8 +18,7 @@ export function parseGlobals(argv) {
|
|
|
18
18
|
const name = arg.slice(2, equal === -1 ? undefined : equal);
|
|
19
19
|
if (!/^[a-z][a-z0-9-]*$/.test(name))
|
|
20
20
|
throw new CliError("Invalid option syntax.");
|
|
21
|
-
const localSwitch = (name === "
|
|
22
|
-
(name === "local-only" && args[0] === "logout") ||
|
|
21
|
+
const localSwitch = (name === "device" && args[0] === "login") ||
|
|
23
22
|
(name === "headers" && args[0] === "auth" && args[1] === "token");
|
|
24
23
|
const flag = switches.has(name) || localSwitch || (name === "version" && !command);
|
|
25
24
|
if (flag && equal !== -1)
|
package/dist/auth.d.ts
CHANGED
|
@@ -35,9 +35,22 @@ export declare class AuthClient {
|
|
|
35
35
|
private cleanup;
|
|
36
36
|
/** RFC 8628 section 3.4 polling until approval, denial or expiry. */
|
|
37
37
|
private poll;
|
|
38
|
+
/**
|
|
39
|
+
* Authorization code with PKCE over a loopback callback when this machine
|
|
40
|
+
* can open a browser; the device flow with --device, without a local
|
|
41
|
+
* browser, or when opening the browser fails.
|
|
42
|
+
*/
|
|
43
|
+
private authorize;
|
|
38
44
|
login(options?: LoginOptions): Promise<{
|
|
39
45
|
cleanupPending: boolean;
|
|
40
46
|
}>;
|
|
41
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Revoke the saved sign-in and pending replaced sign-ins on the server, then remove them from
|
|
49
|
+
* this computer. A revocation that fails, or a credential that can't be read, still removes the
|
|
50
|
+
* local file and returns the reason as a warning; only an interruption keeps it.
|
|
51
|
+
*/
|
|
52
|
+
logout(): Promise<{
|
|
53
|
+
warning?: string;
|
|
54
|
+
}>;
|
|
42
55
|
}
|
|
43
56
|
export {};
|
package/dist/auth.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { CliError, record, throwIfInterrupted } from "./errors.js";
|
|
3
3
|
import { requestJson, RequestFailure } from "./http.js";
|
|
4
|
-
import { browserAvailable, DEVICE_CODE_GRANT, deviceAuthorization, openBrowser, pause, } from "./login.js";
|
|
4
|
+
import { browserAvailable, DEVICE_CODE_GRANT, deviceAuthorization, loopbackAuthorization, openBrowser, pause, } from "./login.js";
|
|
5
5
|
import { environments, profileName } from "./profiles.js";
|
|
6
6
|
import { parseSession } from "./session.js";
|
|
7
7
|
import { privateDirectory, PrivateWriteDurabilityError, readPrivate, removePrivate, withLock, writePrivate, } from "./storage.js";
|
|
8
|
+
// How long login waits for the browser opener to fail before printing the
|
|
9
|
+
// browser URL and waiting on the callback alone.
|
|
10
|
+
const OPENER_GRACE_MS = 1_000;
|
|
8
11
|
// Longer than requestJson's 8-second default, and short enough that a retry
|
|
9
12
|
// after a lost approval lands inside the issuer's 30-second redemption window.
|
|
10
13
|
const DEVICE_POLL_TIMEOUT_MS = 20_000;
|
|
@@ -28,8 +31,24 @@ function tokens(raw, now) {
|
|
|
28
31
|
expiresAt: now + raw.expires_in * 1000,
|
|
29
32
|
};
|
|
30
33
|
}
|
|
34
|
+
/** A revocation the issuer did not confirm, with why: a network error or the HTTP status. */
|
|
35
|
+
class RevocationError extends CliError {
|
|
36
|
+
detail;
|
|
37
|
+
constructor(detail) {
|
|
38
|
+
super(`Revocation failed (${detail}). Local credentials were retained; retry zo logout.`, 5, "revocation_failed");
|
|
39
|
+
this.detail = detail;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Why logout could not revoke, in one line. */
|
|
43
|
+
function revocationProblem(error) {
|
|
44
|
+
if (error instanceof RevocationError)
|
|
45
|
+
return `the server did not confirm it (${error.detail})`;
|
|
46
|
+
if (error instanceof CliError && error.code === "invalid_credential")
|
|
47
|
+
return "the saved credential can't be read, for example one saved by an earlier zo version";
|
|
48
|
+
return error instanceof CliError ? error.code : "unexpected error";
|
|
49
|
+
}
|
|
31
50
|
function invalidCredential() {
|
|
32
|
-
return new CliError("The saved credential cannot be read safely.
|
|
51
|
+
return new CliError("The saved credential cannot be read safely. Run zo logout for this profile, then zo login. If logout can't revoke it, revoke Zo CLI in Authorized apps.", 3, "invalid_credential");
|
|
33
52
|
}
|
|
34
53
|
export class AuthClient {
|
|
35
54
|
profile;
|
|
@@ -202,12 +221,12 @@ export class AuthClient {
|
|
|
202
221
|
}
|
|
203
222
|
catch {
|
|
204
223
|
throwIfInterrupted(signal);
|
|
205
|
-
throw new
|
|
224
|
+
throw new RevocationError("network error");
|
|
206
225
|
}
|
|
207
226
|
await response.body?.cancel();
|
|
208
227
|
throwIfInterrupted(signal);
|
|
209
228
|
if (!response.ok)
|
|
210
|
-
throw new
|
|
229
|
+
throw new RevocationError(`HTTP ${response.status}`);
|
|
211
230
|
}
|
|
212
231
|
async cleanup(credential, signal = this.signal) {
|
|
213
232
|
while (credential.pendingRevocations.length) {
|
|
@@ -275,6 +294,50 @@ export class AuthClient {
|
|
|
275
294
|
}
|
|
276
295
|
}
|
|
277
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* Authorization code with PKCE over a loopback callback when this machine
|
|
299
|
+
* can open a browser; the device flow with --device, without a local
|
|
300
|
+
* browser, or when opening the browser fails.
|
|
301
|
+
*/
|
|
302
|
+
async authorize(options, signal) {
|
|
303
|
+
if (!options.device &&
|
|
304
|
+
browserAvailable(this.env, options.platform ?? process.platform)) {
|
|
305
|
+
const loopback = await loopbackAuthorization(this.binding.issuer, this.binding.audience, this.binding.clientId, { signal, timeoutMs: options.callbackTimeoutMs });
|
|
306
|
+
// The opener can outlive the sign-in (xdg-open may become the browser
|
|
307
|
+
// itself), so it is never awaited past a short grace: a failure inside
|
|
308
|
+
// it falls back to the device flow; after it, the printed URL stands.
|
|
309
|
+
// The callback, the timeout and Ctrl-C end the wait on their own.
|
|
310
|
+
let grace;
|
|
311
|
+
const outcome = await Promise.race([
|
|
312
|
+
(options.openBrowser ?? openBrowser)(loopback.url).then(() => "opened", () => "failed"),
|
|
313
|
+
new Promise((resolve) => {
|
|
314
|
+
grace = setTimeout(() => resolve("pending"), OPENER_GRACE_MS);
|
|
315
|
+
}),
|
|
316
|
+
loopback.code.then(() => "pending", () => "pending"),
|
|
317
|
+
]);
|
|
318
|
+
clearTimeout(grace);
|
|
319
|
+
if (outcome !== "failed") {
|
|
320
|
+
options.announce?.({ flow: "browser", url: loopback.url });
|
|
321
|
+
const code = await loopback.code;
|
|
322
|
+
throwIfInterrupted(signal);
|
|
323
|
+
return this.tokenRequest({
|
|
324
|
+
grant_type: "authorization_code",
|
|
325
|
+
code,
|
|
326
|
+
redirect_uri: loopback.redirectUri,
|
|
327
|
+
code_verifier: loopback.codeVerifier,
|
|
328
|
+
}, signal);
|
|
329
|
+
}
|
|
330
|
+
await loopback.close();
|
|
331
|
+
throwIfInterrupted(signal);
|
|
332
|
+
}
|
|
333
|
+
const device = await deviceAuthorization(this.binding.issuer, this.binding.audience, this.binding.clientId, this.fetcher, signal);
|
|
334
|
+
options.announce?.({
|
|
335
|
+
flow: "device",
|
|
336
|
+
url: device.url,
|
|
337
|
+
userCode: device.userCode,
|
|
338
|
+
});
|
|
339
|
+
return this.poll(device, signal);
|
|
340
|
+
}
|
|
278
341
|
async login(options = {}) {
|
|
279
342
|
const signal = this.signal && options.signal
|
|
280
343
|
? AbortSignal.any([this.signal, options.signal])
|
|
@@ -285,18 +348,7 @@ export class AuthClient {
|
|
|
285
348
|
return this.locked(async () => {
|
|
286
349
|
const previous = await this.read();
|
|
287
350
|
throwIfInterrupted(signal);
|
|
288
|
-
const
|
|
289
|
-
const browser = !options.noBrowser &&
|
|
290
|
-
browserAvailable(this.env, options.platform ?? process.platform);
|
|
291
|
-
options.announce?.({
|
|
292
|
-
url: device.url,
|
|
293
|
-
userCode: device.userCode,
|
|
294
|
-
browser,
|
|
295
|
-
});
|
|
296
|
-
// The printed URL is the fallback when the opener fails.
|
|
297
|
-
if (browser)
|
|
298
|
-
void (options.openBrowser ?? openBrowser)(device.url).catch(() => { });
|
|
299
|
-
const active = await this.poll(device, signal);
|
|
351
|
+
const active = await this.authorize(options, signal);
|
|
300
352
|
const credential = {
|
|
301
353
|
version: 1,
|
|
302
354
|
binding: this.binding,
|
|
@@ -335,28 +387,35 @@ export class AuthClient {
|
|
|
335
387
|
return { cleanupPending: false };
|
|
336
388
|
}, signal);
|
|
337
389
|
}
|
|
338
|
-
|
|
390
|
+
/**
|
|
391
|
+
* Revoke the saved sign-in and pending replaced sign-ins on the server, then remove them from
|
|
392
|
+
* this computer. A revocation that fails, or a credential that can't be read, still removes the
|
|
393
|
+
* local file and returns the reason as a warning; only an interruption keeps it.
|
|
394
|
+
*/
|
|
395
|
+
async logout() {
|
|
339
396
|
throwIfInterrupted(this.signal);
|
|
340
397
|
if (this.env.ZO_SYSTEM_API_ACCESS_TOKEN !== undefined)
|
|
341
398
|
throw new CliError("Unset ZO_SYSTEM_API_ACCESS_TOKEN before logging out a saved profile.");
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
399
|
+
return this.locked(async () => {
|
|
400
|
+
let warning;
|
|
401
|
+
try {
|
|
402
|
+
const credential = await this.read();
|
|
345
403
|
throwIfInterrupted(this.signal);
|
|
346
|
-
|
|
404
|
+
if (credential?.active) {
|
|
405
|
+
await this.revoke(credential.active.refreshToken);
|
|
406
|
+
delete credential.active;
|
|
407
|
+
await writePrivate(this.path, credential);
|
|
408
|
+
}
|
|
409
|
+
if (credential)
|
|
410
|
+
await this.cleanup(credential);
|
|
347
411
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
return;
|
|
352
|
-
if (credential.active) {
|
|
353
|
-
await this.revoke(credential.active.refreshToken);
|
|
354
|
-
delete credential.active;
|
|
355
|
-
await writePrivate(this.path, credential);
|
|
412
|
+
catch (error) {
|
|
413
|
+
throwIfInterrupted(this.signal);
|
|
414
|
+
warning = `Could not revoke the saved sign-in on the server: ${revocationProblem(error)}. It was removed from this computer; revoke Zo CLI in Authorized apps if it may still be in use.`;
|
|
356
415
|
}
|
|
357
|
-
await this.cleanup(credential);
|
|
358
416
|
await removePrivate(this.path);
|
|
359
417
|
throwIfInterrupted(this.signal);
|
|
418
|
+
return warning === undefined ? {} : { warning };
|
|
360
419
|
});
|
|
361
420
|
}
|
|
362
421
|
}
|
package/dist/commands.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Generated by scripts/generate-commands.mjs. Do not edit.
|
|
2
2
|
export const commandManifest = {
|
|
3
|
-
schemaDigest: "sha256:
|
|
3
|
+
schemaDigest: "sha256:f3723f93d9d519ebe2517e16cb3db50c99b38e60b9705187006ada3eaeb4e391",
|
|
4
4
|
commands: [
|
|
5
5
|
{
|
|
6
6
|
action: "zo.activity.list",
|
|
@@ -507,7 +507,7 @@ export const commandManifest = {
|
|
|
507
507
|
{
|
|
508
508
|
version: 1,
|
|
509
509
|
description: "Validate a payload against an active event definition and publish the occurrence durably under a caller-scoped idempotency key.",
|
|
510
|
-
inputJson: '{"additionalProperties":false,"description":"The payload and
|
|
510
|
+
inputJson: '{"additionalProperties":false,"description":"The payload and an optional caller-scoped idempotency key (1 to 255 printable ASCII\\ncharacters). Every envelope attribute (``id``, ``source``, ``type``, ``time``,\\nthe workspace, the definition and the causal fields) is derived by the server\\nand rejected as input.","properties":{"event_id":{"title":"Event Id","type":"string"},"idempotency_key":{"anyOf":[{"maxLength":255,"minLength":1,"pattern":"^[!-~]{1,255}$","type":"string"},{"type":"null"}],"default":null,"title":"Idempotency Key"},"data":{"additionalProperties":{"x-zo-json-value":true},"title":"Data","type":"object"},"subject":{"anyOf":[{"maxLength":200,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Subject"}},"required":["event_id","data"],"title":"EventDispatchInput","type":"object"}',
|
|
511
511
|
resultKind: "inline",
|
|
512
512
|
},
|
|
513
513
|
],
|
|
@@ -968,6 +968,19 @@ export const commandManifest = {
|
|
|
968
968
|
},
|
|
969
969
|
],
|
|
970
970
|
},
|
|
971
|
+
{
|
|
972
|
+
action: "zo.model.list",
|
|
973
|
+
path: ["model", "list"],
|
|
974
|
+
versions: [
|
|
975
|
+
{
|
|
976
|
+
version: 1,
|
|
977
|
+
description: "List the models this workspace can call, each with its ID, capabilities, accepted file types, reasoning levels, limits and current prices.",
|
|
978
|
+
inputJson: '{"additionalProperties":false,"properties":{"workspace_id":{"maxLength":200,"minLength":1,"title":"Workspace Id","type":"string"}},"required":["workspace_id"],"title":"ModelListInput","type":"object"}',
|
|
979
|
+
queryJson: '{"type":"object","additionalProperties":false,"properties":{"limit":{"type":"integer","minimum":1,"default":100,"description":"Rows per page, at most 200; a larger value is reduced to it."},"cursor":{"type":"string","minLength":1,"maxLength":5504,"description":"The previous page\'s next_cursor, sent with the same sort, filter and search."},"sort":{"type":"string","enum":["id.asc"],"default":"id.asc"},"filter":{"additionalProperties":false,"properties":{"capability":{"anyOf":[{"enum":["text","structured_output","embedding","rerank","image","speech","transcription","video"],"type":"string"},{"type":"null"}],"default":null,"description":"Only models with this capability.","title":"Capability"}},"type":"object","required":[],"default":{}}},"required":[]}',
|
|
980
|
+
resultKind: "inline",
|
|
981
|
+
},
|
|
982
|
+
],
|
|
983
|
+
},
|
|
971
984
|
{
|
|
972
985
|
action: "zo.oauth_app.create",
|
|
973
986
|
path: ["oauth-app", "create"],
|
|
@@ -1457,79 +1470,6 @@ export const commandManifest = {
|
|
|
1457
1470
|
},
|
|
1458
1471
|
],
|
|
1459
1472
|
},
|
|
1460
|
-
{
|
|
1461
|
-
action: "zo.vault.create",
|
|
1462
|
-
path: ["vault", "create"],
|
|
1463
|
-
versions: [
|
|
1464
|
-
{
|
|
1465
|
-
version: 1,
|
|
1466
|
-
description: "Store a pasted credential, such as an API key, with the hosts it may be sent to. The value is never returned.",
|
|
1467
|
-
inputJson: '{"additionalProperties":false,"properties":{"workspace_id":{"maxLength":200,"minLength":1,"title":"Workspace Id","type":"string"},"display_name":{"maxLength":256,"minLength":1,"title":"Display Name","type":"string"},"description":{"default":"","maxLength":2000,"title":"Description","type":"string"},"value":{"description":"Secret: the credential. Stored sealed and never returned.","maxLength":16384,"minLength":1,"title":"Value","type":"string"},"environment_variable":{"anyOf":[{"pattern":"^[A-Z_][A-Z0-9_]{0,127}$","type":"string"},{"type":"null"}],"default":null,"description":"The environment variable name a machine gets the value under, such as `OPENAI_API_KEY`.","title":"Environment Variable"},"injections":{"default":[],"items":{"$ref":"#/$defs/VaultInjection"},"maxItems":10,"title":"Injections","type":"array"},"visibility":{"default":"workspace","description":"`workspace` (default): every member can use it. `protected`: only you, and whoever you share it with; workspace-wide grants don\'t reach it. Fixed at creation.","enum":["workspace","protected"],"title":"Visibility","type":"string"}},"required":["workspace_id","display_name","value"],"title":"VaultCreateInput","type":"object","$defs":{"BasicFormat":{"additionalProperties":false,"properties":{"type":{"const":"basic","description":"`Basic` with base64 of `<username>:<value>`.","title":"Type","type":"string"},"username":{"pattern":"^[^:]{1,256}$","title":"Username","type":"string"}},"required":["type","username"],"title":"BasicFormat","type":"object"},"BearerFormat":{"additionalProperties":false,"properties":{"type":{"const":"bearer","description":"`Bearer <value>`.","title":"Type","type":"string"}},"required":["type"],"title":"BearerFormat","type":"object"},"HeaderLocation":{"additionalProperties":false,"properties":{"type":{"const":"header","title":"Type","type":"string"},"name":{"description":"The header to set, such as `Authorization` or `X-API-Key`.","pattern":"^[A-Za-z0-9!#$%&\'*+.^_`|~-]{1,128}$","title":"Name","type":"string"}},"required":["type","name"],"title":"HeaderLocation","type":"object"},"RawFormat":{"additionalProperties":false,"properties":{"type":{"const":"raw","description":"The value as it is.","title":"Type","type":"string"}},"required":["type"],"title":"RawFormat","type":"object"},"VaultInjection":{"additionalProperties":false,"description":"Where and how the value is sent on requests to ``hosts``. A host appears\\nin at most one entry of a vault\'s list.","properties":{"hosts":{"items":{"description":"An exact lowercase DNS name, such as `api.openai.com`: no wildcard, port or path.","maxLength":253,"pattern":"^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$","type":"string"},"maxItems":20,"minItems":1,"title":"Hosts","type":"array"},"location":{"$ref":"#/$defs/HeaderLocation"},"format":{"discriminator":{"mapping":{"basic":"#/$defs/BasicFormat","bearer":"#/$defs/BearerFormat","raw":"#/$defs/RawFormat"},"propertyName":"type"},"oneOf":[{"$ref":"#/$defs/BearerFormat"},{"$ref":"#/$defs/BasicFormat"},{"$ref":"#/$defs/RawFormat"}],"title":"Format"}},"required":["hosts","location","format"],"title":"VaultInjection","type":"object"}}}',
|
|
1468
|
-
resultKind: "inline",
|
|
1469
|
-
},
|
|
1470
|
-
],
|
|
1471
|
-
},
|
|
1472
|
-
{
|
|
1473
|
-
action: "zo.vault.delete",
|
|
1474
|
-
path: ["vault", "delete"],
|
|
1475
|
-
versions: [
|
|
1476
|
-
{
|
|
1477
|
-
version: 1,
|
|
1478
|
-
description: "Delete a vault: its value is destroyed and every grant on it is revoked.",
|
|
1479
|
-
inputJson: '{"additionalProperties":false,"properties":{"vault_id":{"maxLength":200,"minLength":1,"title":"Vault Id","type":"string"},"expected_revision":{"exclusiveMinimum":0,"maximum":9007199254740991,"title":"Expected Revision","type":"integer"}},"required":["vault_id","expected_revision"],"title":"VaultTargetInput","type":"object"}',
|
|
1480
|
-
resultKind: "inline",
|
|
1481
|
-
},
|
|
1482
|
-
],
|
|
1483
|
-
},
|
|
1484
|
-
{
|
|
1485
|
-
action: "zo.vault.get",
|
|
1486
|
-
path: ["vault", "get"],
|
|
1487
|
-
versions: [
|
|
1488
|
-
{
|
|
1489
|
-
version: 1,
|
|
1490
|
-
description: "Read a vault's settings. The value is never returned.",
|
|
1491
|
-
inputJson: '{"additionalProperties":false,"properties":{"vault_id":{"maxLength":200,"minLength":1,"title":"Vault Id","type":"string"}},"required":["vault_id"],"title":"VaultGetInput","type":"object"}',
|
|
1492
|
-
resultKind: "inline",
|
|
1493
|
-
},
|
|
1494
|
-
],
|
|
1495
|
-
},
|
|
1496
|
-
{
|
|
1497
|
-
action: "zo.vault.list",
|
|
1498
|
-
path: ["vault", "list"],
|
|
1499
|
-
versions: [
|
|
1500
|
-
{
|
|
1501
|
-
version: 1,
|
|
1502
|
-
description: "List the vaults the caller can read in a workspace.",
|
|
1503
|
-
inputJson: '{"additionalProperties":false,"properties":{"workspace_id":{"maxLength":200,"minLength":1,"title":"Workspace Id","type":"string"}},"required":["workspace_id"],"title":"VaultListInput","type":"object"}',
|
|
1504
|
-
queryJson: '{"type":"object","additionalProperties":false,"properties":{"limit":{"type":"integer","minimum":1,"default":50,"description":"Rows per page, at most 100; a larger value is reduced to it."},"cursor":{"type":"string","minLength":1,"maxLength":5504,"description":"The previous page\'s next_cursor, sent with the same sort, filter and search."},"sort":{"type":"string","enum":["id.asc","created_at.desc","created_at.asc","display_name.asc"],"default":"id.asc"}},"required":[]}',
|
|
1505
|
-
resultKind: "inline",
|
|
1506
|
-
},
|
|
1507
|
-
],
|
|
1508
|
-
},
|
|
1509
|
-
{
|
|
1510
|
-
action: "zo.vault.rotate",
|
|
1511
|
-
path: ["vault", "rotate"],
|
|
1512
|
-
versions: [
|
|
1513
|
-
{
|
|
1514
|
-
version: 1,
|
|
1515
|
-
description: "Replace a vault's value. The previous value is destroyed.",
|
|
1516
|
-
inputJson: '{"additionalProperties":false,"properties":{"vault_id":{"maxLength":200,"minLength":1,"title":"Vault Id","type":"string"},"expected_revision":{"exclusiveMinimum":0,"maximum":9007199254740991,"title":"Expected Revision","type":"integer"},"value":{"description":"Secret: the credential. Stored sealed and never returned.","maxLength":16384,"minLength":1,"title":"Value","type":"string"}},"required":["vault_id","expected_revision","value"],"title":"VaultRotateInput","type":"object"}',
|
|
1517
|
-
resultKind: "inline",
|
|
1518
|
-
},
|
|
1519
|
-
],
|
|
1520
|
-
},
|
|
1521
|
-
{
|
|
1522
|
-
action: "zo.vault.update",
|
|
1523
|
-
path: ["vault", "update"],
|
|
1524
|
-
versions: [
|
|
1525
|
-
{
|
|
1526
|
-
version: 1,
|
|
1527
|
-
description: "Change a vault's name, description, environment variable or injections against its current revision.",
|
|
1528
|
-
inputJson: '{"additionalProperties":false,"description":"Omitted fields keep their value; ``injections`` replaces the whole list.","properties":{"vault_id":{"maxLength":200,"minLength":1,"title":"Vault Id","type":"string"},"expected_revision":{"exclusiveMinimum":0,"maximum":9007199254740991,"title":"Expected Revision","type":"integer"},"display_name":{"anyOf":[{"maxLength":256,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Display Name"},"description":{"anyOf":[{"maxLength":2000,"type":"string"},{"type":"null"}],"default":null,"description":"`\\"\\"` clears the description.","title":"Description"},"environment_variable":{"anyOf":[{"pattern":"^([A-Z_][A-Z0-9_]{0,127})?$","type":"string"},{"type":"null"}],"default":null,"description":"`\\"\\"` clears the variable.","title":"Environment Variable"},"injections":{"anyOf":[{"items":{"$ref":"#/$defs/VaultInjection"},"maxItems":10,"type":"array"},{"type":"null"}],"default":null,"title":"Injections"}},"required":["vault_id","expected_revision"],"title":"VaultUpdateInput","type":"object","$defs":{"BasicFormat":{"additionalProperties":false,"properties":{"type":{"const":"basic","description":"`Basic` with base64 of `<username>:<value>`.","title":"Type","type":"string"},"username":{"pattern":"^[^:]{1,256}$","title":"Username","type":"string"}},"required":["type","username"],"title":"BasicFormat","type":"object"},"BearerFormat":{"additionalProperties":false,"properties":{"type":{"const":"bearer","description":"`Bearer <value>`.","title":"Type","type":"string"}},"required":["type"],"title":"BearerFormat","type":"object"},"HeaderLocation":{"additionalProperties":false,"properties":{"type":{"const":"header","title":"Type","type":"string"},"name":{"description":"The header to set, such as `Authorization` or `X-API-Key`.","pattern":"^[A-Za-z0-9!#$%&\'*+.^_`|~-]{1,128}$","title":"Name","type":"string"}},"required":["type","name"],"title":"HeaderLocation","type":"object"},"RawFormat":{"additionalProperties":false,"properties":{"type":{"const":"raw","description":"The value as it is.","title":"Type","type":"string"}},"required":["type"],"title":"RawFormat","type":"object"},"VaultInjection":{"additionalProperties":false,"description":"Where and how the value is sent on requests to ``hosts``. A host appears\\nin at most one entry of a vault\'s list.","properties":{"hosts":{"items":{"description":"An exact lowercase DNS name, such as `api.openai.com`: no wildcard, port or path.","maxLength":253,"pattern":"^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$","type":"string"},"maxItems":20,"minItems":1,"title":"Hosts","type":"array"},"location":{"$ref":"#/$defs/HeaderLocation"},"format":{"discriminator":{"mapping":{"basic":"#/$defs/BasicFormat","bearer":"#/$defs/BearerFormat","raw":"#/$defs/RawFormat"},"propertyName":"type"},"oneOf":[{"$ref":"#/$defs/BearerFormat"},{"$ref":"#/$defs/BasicFormat"},{"$ref":"#/$defs/RawFormat"}],"title":"Format"}},"required":["hosts","location","format"],"title":"VaultInjection","type":"object"}}}',
|
|
1529
|
-
resultKind: "inline",
|
|
1530
|
-
},
|
|
1531
|
-
],
|
|
1532
|
-
},
|
|
1533
1473
|
{
|
|
1534
1474
|
action: "zo.web.fetch",
|
|
1535
1475
|
path: ["web", "fetch"],
|
|
@@ -1561,7 +1501,7 @@ export const commandManifest = {
|
|
|
1561
1501
|
{
|
|
1562
1502
|
version: 1,
|
|
1563
1503
|
description: "Create a Website address in a workspace. Describe what this Website is for in the user's terms.",
|
|
1564
|
-
inputJson: '{"additionalProperties":false,"properties":{"tags":{"default":[],"items":{"description":"A tag ID or name; unknown names are created, unknown IDs are refused.","maxLength":256,"minLength":1,"type":"string"},"maxItems":100,"title":"Tags","type":"array"},"workspace_id":{"maxLength":200,"minLength":1,"title":"Workspace Id","type":"string"},"slug":{"pattern":"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$","title":"Slug","type":"string"},"display_name":{"maxLength":200,"minLength":1,"title":"Display Name","type":"string"},"description":{"default":"","description":"Describe what this Website is for in the user\'s terms.","maxLength":2000,"title":"Description","type":"string"},"visibility":{"default":"workspace","enum":["workspace","protected"],"title":"Visibility","type":"string"}},"required":["workspace_id","slug","display_name"],"title":"WebsiteCreateInput","type":"object"}',
|
|
1504
|
+
inputJson: '{"additionalProperties":false,"properties":{"tags":{"default":[],"items":{"description":"A tag ID or name; unknown names are created, unknown IDs are refused.","maxLength":256,"minLength":1,"type":"string"},"maxItems":100,"title":"Tags","type":"array"},"workspace_id":{"maxLength":200,"minLength":1,"title":"Workspace Id","type":"string"},"slug":{"pattern":"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$","title":"Slug","type":"string"},"display_name":{"maxLength":200,"minLength":1,"title":"Display Name","type":"string"},"description":{"default":"","description":"Describe what this Website is for in the user\'s terms.","maxLength":2000,"title":"Description","type":"string"},"visibility":{"default":"workspace","enum":["workspace","protected"],"title":"Visibility","type":"string"},"access":{"default":"public","enum":["public","authenticated"],"title":"Access","type":"string"}},"required":["workspace_id","slug","display_name"],"title":"WebsiteCreateInput","type":"object"}',
|
|
1565
1505
|
resultKind: "inline",
|
|
1566
1506
|
},
|
|
1567
1507
|
],
|
|
@@ -1610,7 +1550,7 @@ export const commandManifest = {
|
|
|
1610
1550
|
{
|
|
1611
1551
|
version: 1,
|
|
1612
1552
|
description: "Update a Website display name or custom hostname.",
|
|
1613
|
-
inputJson: '{"additionalProperties":false,"properties":{"website_id":{"maxLength":200,"minLength":1,"title":"Website Id","type":"string"},"expected_revision":{"exclusiveMinimum":0,"maximum":9007199254740991,"title":"Expected Revision","type":"integer"},"display_name":{"anyOf":[{"maxLength":200,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Display Name"},"description":{"anyOf":[{"maxLength":2000,"type":"string"},{"type":"null"}],"default":null,"description":"Describe what this Website is for in the user\'s terms.","title":"Description"},"custom_hostname":{"anyOf":[{"$ref":"#/$defs/CustomHostnameChange"},{"type":"null"}],"default":null}},"required":["website_id","expected_revision"],"title":"WebsiteUpdateInput","type":"object","$defs":{"CustomHostnameChange":{"additionalProperties":false,"properties":{"hostname":{"anyOf":[{"maxLength":253,"minLength":4,"type":"string"},{"type":"null"}],"title":"Hostname"}},"required":["hostname"],"title":"CustomHostnameChange","type":"object"}}}',
|
|
1553
|
+
inputJson: '{"additionalProperties":false,"properties":{"website_id":{"maxLength":200,"minLength":1,"title":"Website Id","type":"string"},"expected_revision":{"exclusiveMinimum":0,"maximum":9007199254740991,"title":"Expected Revision","type":"integer"},"display_name":{"anyOf":[{"maxLength":200,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Display Name"},"description":{"anyOf":[{"maxLength":2000,"type":"string"},{"type":"null"}],"default":null,"description":"Describe what this Website is for in the user\'s terms.","title":"Description"},"custom_hostname":{"anyOf":[{"$ref":"#/$defs/CustomHostnameChange"},{"type":"null"}],"default":null},"access":{"anyOf":[{"enum":["public","authenticated"],"type":"string"},{"type":"null"}],"default":null,"title":"Access"}},"required":["website_id","expected_revision"],"title":"WebsiteUpdateInput","type":"object","$defs":{"CustomHostnameChange":{"additionalProperties":false,"properties":{"hostname":{"anyOf":[{"maxLength":253,"minLength":4,"type":"string"},{"type":"null"}],"title":"Hostname"}},"required":["hostname"],"title":"CustomHostnameChange","type":"object"}}}',
|
|
1614
1554
|
resultKind: "inline",
|
|
1615
1555
|
},
|
|
1616
1556
|
],
|
|
@@ -1622,7 +1562,7 @@ export const commandManifest = {
|
|
|
1622
1562
|
{
|
|
1623
1563
|
version: 1,
|
|
1624
1564
|
description: "Create an unpublished Website route.",
|
|
1625
|
-
inputJson: '{"additionalProperties":false,"properties":{"website_id":{"maxLength":200,"minLength":1,"title":"Website Id","type":"string"},"expected_revision":{"exclusiveMinimum":0,"maximum":9007199254740991,"title":"Expected Revision","type":"integer"},"path":{"maxLength":1024,"minLength":1,"title":"Path","type":"string"},"target":{"discriminator":{"mapping":{"function":"#/$defs/FunctionRouteConfigInput","kb":"#/$defs/KbRouteConfigInput","machine":"#/$defs/MachineRouteConfigInput"},"propertyName":"kind"},"oneOf":[{"$ref":"#/$defs/FunctionRouteConfigInput"},{"$ref":"#/$defs/MachineRouteConfigInput"},{"$ref":"#/$defs/KbRouteConfigInput"}],"title":"Target"},"acknowledge_bound_accounts":{"default":[],"items":{"maxLength":200,"minLength":1,"type":"string"},"maxItems":8,"title":"Acknowledge Bound Accounts","type":"array"}},"required":["website_id","expected_revision","path","target"],"title":"RouteCreateInput","type":"object","$defs":{"FunctionInputMapping":{"additionalProperties":false,"properties":{"handler":{"default":"http","maxLength":200,"minLength":1,"title":"Handler","type":"string"},"strip_prefix":{"default":false,"title":"Strip Prefix","type":"boolean"}},"title":"FunctionInputMapping","type":"object","required":[]},"FunctionRouteConfigInput":{"additionalProperties":false,"properties":{"kind":{"const":"function","title":"Kind","type":"string"},"function_id":{"maxLength":200,"minLength":1,"title":"Function Id","type":"string"},"deployment_id":{"maxLength":200,"minLength":1,"title":"Deployment Id","type":"string"},"input_mapping":{"$ref":"#/$defs/FunctionInputMapping"}},"required":["kind","function_id","deployment_id"],"title":"FunctionRouteConfigInput","type":"object"},"KbRouteConfigInput":{"additionalProperties":false,"properties":{"kind":{"const":"kb","title":"Kind","type":"string"},"kb_path":{"maxLength":4096,"minLength":1,"title":"Kb Path","type":"string"},"spa_fallback":{"default":false,"title":"Spa Fallback","type":"boolean"},"published_as_of":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Published As Of"}},"required":["kind","kb_path"],"title":"KbRouteConfigInput","type":"object"},"MachineRouteConfigInput":{"additionalProperties":false,"properties":{"kind":{"const":"machine","title":"Kind","type":"string"},"machine_id":{"maxLength":200,"minLength":1,"title":"Machine Id","type":"string"},"port":{"maximum":65535,"minimum":1,"title":"Port","type":"integer"},"strip_prefix":{"default":false,"title":"Strip Prefix","type":"boolean"}},"required":["kind","machine_id","port"],"title":"MachineRouteConfigInput","type":"object"}}}',
|
|
1565
|
+
inputJson: '{"additionalProperties":false,"properties":{"website_id":{"maxLength":200,"minLength":1,"title":"Website Id","type":"string"},"expected_revision":{"exclusiveMinimum":0,"maximum":9007199254740991,"title":"Expected Revision","type":"integer"},"path":{"maxLength":1024,"minLength":1,"title":"Path","type":"string"},"target":{"discriminator":{"mapping":{"function":"#/$defs/FunctionRouteConfigInput","kb":"#/$defs/KbRouteConfigInput","machine":"#/$defs/MachineRouteConfigInput"},"propertyName":"kind"},"oneOf":[{"$ref":"#/$defs/FunctionRouteConfigInput"},{"$ref":"#/$defs/MachineRouteConfigInput"},{"$ref":"#/$defs/KbRouteConfigInput"}],"title":"Target"},"acknowledge_bound_accounts":{"default":[],"items":{"maxLength":200,"minLength":1,"type":"string"},"maxItems":8,"title":"Acknowledge Bound Accounts","type":"array"},"access":{"default":"authenticated","enum":["public","authenticated"],"title":"Access","type":"string"}},"required":["website_id","expected_revision","path","target"],"title":"RouteCreateInput","type":"object","$defs":{"FunctionInputMapping":{"additionalProperties":false,"properties":{"handler":{"default":"http","maxLength":200,"minLength":1,"title":"Handler","type":"string"},"strip_prefix":{"default":false,"title":"Strip Prefix","type":"boolean"}},"title":"FunctionInputMapping","type":"object","required":[]},"FunctionRouteConfigInput":{"additionalProperties":false,"properties":{"kind":{"const":"function","title":"Kind","type":"string"},"function_id":{"maxLength":200,"minLength":1,"title":"Function Id","type":"string"},"deployment_id":{"maxLength":200,"minLength":1,"title":"Deployment Id","type":"string"},"input_mapping":{"$ref":"#/$defs/FunctionInputMapping"}},"required":["kind","function_id","deployment_id"],"title":"FunctionRouteConfigInput","type":"object"},"KbRouteConfigInput":{"additionalProperties":false,"properties":{"kind":{"const":"kb","title":"Kind","type":"string"},"kb_path":{"maxLength":4096,"minLength":1,"title":"Kb Path","type":"string"},"spa_fallback":{"default":false,"title":"Spa Fallback","type":"boolean"},"published_as_of":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Published As Of"}},"required":["kind","kb_path"],"title":"KbRouteConfigInput","type":"object"},"MachineRouteConfigInput":{"additionalProperties":false,"properties":{"kind":{"const":"machine","title":"Kind","type":"string"},"machine_id":{"maxLength":200,"minLength":1,"title":"Machine Id","type":"string"},"port":{"maximum":65535,"minimum":1,"title":"Port","type":"integer"},"strip_prefix":{"default":false,"title":"Strip Prefix","type":"boolean"}},"required":["kind","machine_id","port"],"title":"MachineRouteConfigInput","type":"object"}}}',
|
|
1626
1566
|
resultKind: "inline",
|
|
1627
1567
|
},
|
|
1628
1568
|
],
|
|
@@ -1695,7 +1635,7 @@ export const commandManifest = {
|
|
|
1695
1635
|
{
|
|
1696
1636
|
version: 1,
|
|
1697
1637
|
description: "Update a Website route and recheck its published source.",
|
|
1698
|
-
inputJson: '{"additionalProperties":false,"properties":{"website_id":{"maxLength":200,"minLength":1,"title":"Website Id","type":"string"},"route_id":{"maxLength":200,"minLength":1,"title":"Route Id","type":"string"},"expected_revision":{"exclusiveMinimum":0,"maximum":9007199254740991,"title":"Expected Revision","type":"integer"},"path":{"anyOf":[{"maxLength":1024,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Path"},"target":{"anyOf":[{"discriminator":{"mapping":{"function":"#/$defs/FunctionRouteConfigInput","kb":"#/$defs/KbRouteConfigInput","machine":"#/$defs/MachineRouteConfigInput"},"propertyName":"kind"},"oneOf":[{"$ref":"#/$defs/FunctionRouteConfigInput"},{"$ref":"#/$defs/MachineRouteConfigInput"},{"$ref":"#/$defs/KbRouteConfigInput"}]},{"type":"null"}],"default":null,"title":"Target"},"acknowledge_bound_accounts":{"default":[],"items":{"maxLength":200,"minLength":1,"type":"string"},"maxItems":8,"title":"Acknowledge Bound Accounts","type":"array"}},"required":["website_id","route_id","expected_revision"],"title":"RouteUpdateInput","type":"object","$defs":{"FunctionInputMapping":{"additionalProperties":false,"properties":{"handler":{"default":"http","maxLength":200,"minLength":1,"title":"Handler","type":"string"},"strip_prefix":{"default":false,"title":"Strip Prefix","type":"boolean"}},"title":"FunctionInputMapping","type":"object","required":[]},"FunctionRouteConfigInput":{"additionalProperties":false,"properties":{"kind":{"const":"function","title":"Kind","type":"string"},"function_id":{"maxLength":200,"minLength":1,"title":"Function Id","type":"string"},"deployment_id":{"maxLength":200,"minLength":1,"title":"Deployment Id","type":"string"},"input_mapping":{"$ref":"#/$defs/FunctionInputMapping"}},"required":["kind","function_id","deployment_id"],"title":"FunctionRouteConfigInput","type":"object"},"KbRouteConfigInput":{"additionalProperties":false,"properties":{"kind":{"const":"kb","title":"Kind","type":"string"},"kb_path":{"maxLength":4096,"minLength":1,"title":"Kb Path","type":"string"},"spa_fallback":{"default":false,"title":"Spa Fallback","type":"boolean"},"published_as_of":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Published As Of"}},"required":["kind","kb_path"],"title":"KbRouteConfigInput","type":"object"},"MachineRouteConfigInput":{"additionalProperties":false,"properties":{"kind":{"const":"machine","title":"Kind","type":"string"},"machine_id":{"maxLength":200,"minLength":1,"title":"Machine Id","type":"string"},"port":{"maximum":65535,"minimum":1,"title":"Port","type":"integer"},"strip_prefix":{"default":false,"title":"Strip Prefix","type":"boolean"}},"required":["kind","machine_id","port"],"title":"MachineRouteConfigInput","type":"object"}}}',
|
|
1638
|
+
inputJson: '{"additionalProperties":false,"properties":{"website_id":{"maxLength":200,"minLength":1,"title":"Website Id","type":"string"},"route_id":{"maxLength":200,"minLength":1,"title":"Route Id","type":"string"},"expected_revision":{"exclusiveMinimum":0,"maximum":9007199254740991,"title":"Expected Revision","type":"integer"},"path":{"anyOf":[{"maxLength":1024,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Path"},"target":{"anyOf":[{"discriminator":{"mapping":{"function":"#/$defs/FunctionRouteConfigInput","kb":"#/$defs/KbRouteConfigInput","machine":"#/$defs/MachineRouteConfigInput"},"propertyName":"kind"},"oneOf":[{"$ref":"#/$defs/FunctionRouteConfigInput"},{"$ref":"#/$defs/MachineRouteConfigInput"},{"$ref":"#/$defs/KbRouteConfigInput"}]},{"type":"null"}],"default":null,"title":"Target"},"acknowledge_bound_accounts":{"default":[],"items":{"maxLength":200,"minLength":1,"type":"string"},"maxItems":8,"title":"Acknowledge Bound Accounts","type":"array"},"access":{"anyOf":[{"enum":["public","authenticated"],"type":"string"},{"type":"null"}],"default":null,"title":"Access"}},"required":["website_id","route_id","expected_revision"],"title":"RouteUpdateInput","type":"object","$defs":{"FunctionInputMapping":{"additionalProperties":false,"properties":{"handler":{"default":"http","maxLength":200,"minLength":1,"title":"Handler","type":"string"},"strip_prefix":{"default":false,"title":"Strip Prefix","type":"boolean"}},"title":"FunctionInputMapping","type":"object","required":[]},"FunctionRouteConfigInput":{"additionalProperties":false,"properties":{"kind":{"const":"function","title":"Kind","type":"string"},"function_id":{"maxLength":200,"minLength":1,"title":"Function Id","type":"string"},"deployment_id":{"maxLength":200,"minLength":1,"title":"Deployment Id","type":"string"},"input_mapping":{"$ref":"#/$defs/FunctionInputMapping"}},"required":["kind","function_id","deployment_id"],"title":"FunctionRouteConfigInput","type":"object"},"KbRouteConfigInput":{"additionalProperties":false,"properties":{"kind":{"const":"kb","title":"Kind","type":"string"},"kb_path":{"maxLength":4096,"minLength":1,"title":"Kb Path","type":"string"},"spa_fallback":{"default":false,"title":"Spa Fallback","type":"boolean"},"published_as_of":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Published As Of"}},"required":["kind","kb_path"],"title":"KbRouteConfigInput","type":"object"},"MachineRouteConfigInput":{"additionalProperties":false,"properties":{"kind":{"const":"machine","title":"Kind","type":"string"},"machine_id":{"maxLength":200,"minLength":1,"title":"Machine Id","type":"string"},"port":{"maximum":65535,"minimum":1,"title":"Port","type":"integer"},"strip_prefix":{"default":false,"title":"Strip Prefix","type":"boolean"}},"required":["kind","machine_id","port"],"title":"MachineRouteConfigInput","type":"object"}}}',
|
|
1699
1639
|
resultKind: "inline",
|
|
1700
1640
|
},
|
|
1701
1641
|
],
|
package/dist/login.d.ts
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
export declare const DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
2
|
-
|
|
2
|
+
/** The browser opened at `url` on this machine, or a device flow to approve anywhere. */
|
|
3
|
+
export type LoginPrompt = {
|
|
4
|
+
flow: "browser";
|
|
5
|
+
url: string;
|
|
6
|
+
} | {
|
|
7
|
+
flow: "device";
|
|
3
8
|
url: string;
|
|
4
9
|
userCode: string;
|
|
5
|
-
|
|
6
|
-
browser: boolean;
|
|
7
|
-
}
|
|
10
|
+
};
|
|
8
11
|
export interface LoginOptions {
|
|
9
|
-
|
|
12
|
+
/** Use the device flow even when this machine can open a browser. */
|
|
13
|
+
device?: boolean;
|
|
10
14
|
signal?: AbortSignal;
|
|
11
15
|
platform?: NodeJS.Platform;
|
|
12
16
|
announce?: (prompt: LoginPrompt) => void;
|
|
13
17
|
openBrowser?: (url: string) => Promise<void>;
|
|
18
|
+
/** How long the loopback listener waits for the browser's callback. */
|
|
19
|
+
callbackTimeoutMs?: number;
|
|
14
20
|
}
|
|
15
21
|
export interface DeviceAuthorization {
|
|
16
22
|
deviceCode: string;
|
|
@@ -25,6 +31,25 @@ export declare function openBrowser(url: string): Promise<void>;
|
|
|
25
31
|
* nowhere. xdg-open needs a display on every system other than macOS.
|
|
26
32
|
*/
|
|
27
33
|
export declare function browserAvailable(env: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
|
|
34
|
+
export declare const CALLBACK_TIMEOUT_MS = 600000;
|
|
35
|
+
export interface LoopbackAuthorization {
|
|
36
|
+
/** The authorization URL to open in the browser. */
|
|
37
|
+
url: string;
|
|
38
|
+
redirectUri: string;
|
|
39
|
+
codeVerifier: string;
|
|
40
|
+
/** Resolves with the authorization code from the one callback whose state matches. */
|
|
41
|
+
code: Promise<string>;
|
|
42
|
+
close: () => Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* RFC 8252 loopback redirect with PKCE (RFC 7636): listen on 127.0.0.1 at an
|
|
46
|
+
* OS-assigned port until one callback carries the matching state, the timeout
|
|
47
|
+
* passes, or the signal aborts. The listener closes in every case.
|
|
48
|
+
*/
|
|
49
|
+
export declare function loopbackAuthorization(issuer: string, audience: string, clientId: string, options?: {
|
|
50
|
+
signal?: AbortSignal;
|
|
51
|
+
timeoutMs?: number;
|
|
52
|
+
}): Promise<LoopbackAuthorization>;
|
|
28
53
|
export declare function pause(ms: number, signal?: AbortSignal | null): Promise<void>;
|
|
29
54
|
export declare function parseDeviceAuthorization(raw: unknown, issuer: string): DeviceAuthorization;
|
|
30
55
|
export declare function deviceAuthorization(issuer: string, audience: string, clientId: string, fetcher: typeof fetch, signal?: AbortSignal): Promise<DeviceAuthorization>;
|
package/dist/login.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import { createServer } from "node:http";
|
|
2
4
|
import { CliError, record, throwIfInterrupted } from "./errors.js";
|
|
3
5
|
import { requestJson, RequestFailure } from "./http.js";
|
|
4
6
|
export const DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
@@ -19,6 +21,85 @@ export function browserAvailable(env, platform = process.platform) {
|
|
|
19
21
|
return false;
|
|
20
22
|
return platform === "darwin" || !!env.DISPLAY || !!env.WAYLAND_DISPLAY;
|
|
21
23
|
}
|
|
24
|
+
export const CALLBACK_TIMEOUT_MS = 600_000;
|
|
25
|
+
const base64url = (bytes) => bytes.toString("base64url");
|
|
26
|
+
const page = (heading, detail) => `<!doctype html><meta charset="utf-8"><title>Zo CLI</title><body style="font-family:system-ui,sans-serif;max-width:32rem;margin:4rem auto;padding:0 1rem"><h1 style="font-size:1.25rem">${heading}</h1><p>${detail}</p></body>`;
|
|
27
|
+
/**
|
|
28
|
+
* Sends the page, then runs `then` once it is flushed: settling the login
|
|
29
|
+
* closes every connection, which would otherwise cut the page off.
|
|
30
|
+
*/
|
|
31
|
+
function answer(response, status, html, then) {
|
|
32
|
+
response.writeHead(status, {
|
|
33
|
+
"content-type": "text/html; charset=utf-8",
|
|
34
|
+
"cache-control": "no-store",
|
|
35
|
+
connection: "close",
|
|
36
|
+
});
|
|
37
|
+
response.end(html, then);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* RFC 8252 loopback redirect with PKCE (RFC 7636): listen on 127.0.0.1 at an
|
|
41
|
+
* OS-assigned port until one callback carries the matching state, the timeout
|
|
42
|
+
* passes, or the signal aborts. The listener closes in every case.
|
|
43
|
+
*/
|
|
44
|
+
export async function loopbackAuthorization(issuer, audience, clientId, options = {}) {
|
|
45
|
+
throwIfInterrupted(options.signal);
|
|
46
|
+
const codeVerifier = base64url(randomBytes(32));
|
|
47
|
+
const state = base64url(randomBytes(32));
|
|
48
|
+
const challenge = base64url(createHash("sha256").update(codeVerifier).digest());
|
|
49
|
+
const result = Promise.withResolvers();
|
|
50
|
+
const server = createServer((request, response) => {
|
|
51
|
+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
52
|
+
if (request.method !== "GET" || url.pathname !== "/callback")
|
|
53
|
+
return answer(response, 404);
|
|
54
|
+
const params = url.searchParams;
|
|
55
|
+
if (params.get("state") !== state)
|
|
56
|
+
return answer(response, 400, page("This sign-in link doesn't match", "Return to your terminal and run zo login again if it is still waiting."));
|
|
57
|
+
// RFC 9207: a response from another issuer is a mix-up, never a code to redeem.
|
|
58
|
+
if (params.get("iss") !== issuer) {
|
|
59
|
+
return answer(response, 400, page("Sign-in failed", "Return to your terminal and try again."), () => result.reject(new CliError("The sign-in service returned an invalid response.", 5, "invalid_response")));
|
|
60
|
+
}
|
|
61
|
+
const code = params.get("code");
|
|
62
|
+
if (params.get("error") === "access_denied") {
|
|
63
|
+
return answer(response, 200, page("Sign-in denied", "You can close this tab."), () => result.reject(new CliError("Authorization was denied. Run zo login to try again.", 4, "access_denied")));
|
|
64
|
+
}
|
|
65
|
+
if (params.has("error") || !code)
|
|
66
|
+
return answer(response, 400, page("Sign-in failed", "Return to your terminal and try again."), () => result.reject(new CliError("The sign-in service returned an invalid response.", 5, "invalid_response")));
|
|
67
|
+
answer(response, 200, page("Signed in to the Zo CLI", "You can close this tab and return to your terminal."), () => result.resolve(code));
|
|
68
|
+
});
|
|
69
|
+
await new Promise((resolve, reject) => {
|
|
70
|
+
server.once("error", reject);
|
|
71
|
+
server.listen(0, "127.0.0.1", () => {
|
|
72
|
+
server.off("error", reject);
|
|
73
|
+
resolve();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
const { port } = server.address();
|
|
77
|
+
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
78
|
+
const timer = setTimeout(() => result.reject(new CliError("Login timed out. Run zo login to try again.", 3, "login_timeout")), options.timeoutMs ?? CALLBACK_TIMEOUT_MS);
|
|
79
|
+
const abort = () => result.reject(new CliError("Interrupted.", 130, "interrupted"));
|
|
80
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
81
|
+
let closed;
|
|
82
|
+
const close = () => (closed ??= new Promise((resolve) => {
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
options.signal?.removeEventListener("abort", abort);
|
|
85
|
+
server.close(() => resolve());
|
|
86
|
+
server.closeAllConnections();
|
|
87
|
+
}));
|
|
88
|
+
// Stop listening after the one accepted callback, a failure or the timeout.
|
|
89
|
+
const code = result.promise.finally(close);
|
|
90
|
+
code.catch(() => { });
|
|
91
|
+
const url = new URL(`${issuer}/zo-system-api/oauth/authorize`);
|
|
92
|
+
url.search = new URLSearchParams({
|
|
93
|
+
response_type: "code",
|
|
94
|
+
client_id: clientId,
|
|
95
|
+
redirect_uri: redirectUri,
|
|
96
|
+
code_challenge_method: "S256",
|
|
97
|
+
code_challenge: challenge,
|
|
98
|
+
state,
|
|
99
|
+
resource: audience,
|
|
100
|
+
}).toString();
|
|
101
|
+
return { url: url.href, redirectUri, codeVerifier, code, close };
|
|
102
|
+
}
|
|
22
103
|
export function pause(ms, signal) {
|
|
23
104
|
throwIfInterrupted(signal);
|
|
24
105
|
return new Promise((resolve, reject) => {
|
package/dist/output.js
CHANGED
|
@@ -55,7 +55,7 @@ export function writeRpcResult(result, options = {}) {
|
|
|
55
55
|
}
|
|
56
56
|
const detailFields = {
|
|
57
57
|
invalid_envelope: ["reason"],
|
|
58
|
-
conflict: ["reason", "pointer"],
|
|
58
|
+
conflict: ["reason", "pointer", "route_ids", "total"],
|
|
59
59
|
quota_exceeded: ["reason"],
|
|
60
60
|
invalid_input: ["violations"],
|
|
61
61
|
invalid_query: ["violations"],
|
|
@@ -120,7 +120,9 @@ export function safeDetails(code, details) {
|
|
|
120
120
|
return integer === undefined ? [] : [integer];
|
|
121
121
|
});
|
|
122
122
|
}
|
|
123
|
-
else if (field === "function_ids" ||
|
|
123
|
+
else if (field === "function_ids" ||
|
|
124
|
+
field === "subscription_ids" ||
|
|
125
|
+
field === "route_ids")
|
|
124
126
|
projected[field] = value.filter((item) => boundedString(item));
|
|
125
127
|
}
|
|
126
128
|
}
|
package/dist/profiles.d.ts
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Zo CLI's OAuth client metadata document, served from www as its own
|
|
3
|
+
* client ID for both environments; its redirects are the loopback callbacks.
|
|
4
|
+
*/
|
|
5
|
+
export declare const CLIENT_ID = "https://www.zo.computer/oauth/zo-cli-client-metadata.json";
|
|
1
6
|
export declare const environments: {
|
|
2
7
|
readonly production: {
|
|
3
8
|
readonly issuer: "https://auth.zo.computer";
|
|
4
9
|
readonly api: "https://api.zo.computer";
|
|
5
10
|
readonly audience: "https://api.zo.computer/zo-system-api";
|
|
6
|
-
readonly clientId: "zo-
|
|
11
|
+
readonly clientId: "https://www.zo.computer/oauth/zo-cli-client-metadata.json";
|
|
7
12
|
};
|
|
8
13
|
readonly staging: {
|
|
9
14
|
readonly issuer: "https://auth.zostaging.computer";
|
|
10
15
|
readonly api: "https://api.zostaging.computer";
|
|
11
16
|
readonly audience: "https://api.zostaging.computer/zo-system-api";
|
|
12
|
-
readonly clientId: "zo-
|
|
17
|
+
readonly clientId: "https://www.zo.computer/oauth/zo-cli-client-metadata.json";
|
|
13
18
|
};
|
|
14
19
|
};
|
|
15
20
|
export type Environment = keyof typeof environments;
|
package/dist/profiles.js
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { CliError, record, throwIfInterrupted } from "./errors.js";
|
|
3
3
|
import { configDirectory, privateDirectory, readPrivate, withLock, writePrivate, } from "./storage.js";
|
|
4
|
+
/**
|
|
5
|
+
* The Zo CLI's OAuth client metadata document, served from www as its own
|
|
6
|
+
* client ID for both environments; its redirects are the loopback callbacks.
|
|
7
|
+
*/
|
|
8
|
+
export const CLIENT_ID = "https://www.zo.computer/oauth/zo-cli-client-metadata.json";
|
|
4
9
|
export const environments = {
|
|
5
10
|
production: {
|
|
6
11
|
issuer: "https://auth.zo.computer",
|
|
7
12
|
api: "https://api.zo.computer",
|
|
8
13
|
audience: "https://api.zo.computer/zo-system-api",
|
|
9
|
-
clientId:
|
|
14
|
+
clientId: CLIENT_ID,
|
|
10
15
|
},
|
|
11
16
|
staging: {
|
|
12
17
|
issuer: "https://auth.zostaging.computer",
|
|
13
18
|
api: "https://api.zostaging.computer",
|
|
14
19
|
audience: "https://api.zostaging.computer/zo-system-api",
|
|
15
|
-
clientId:
|
|
20
|
+
clientId: CLIENT_ID,
|
|
16
21
|
},
|
|
17
22
|
};
|
|
18
23
|
export function profileName(name) {
|
package/dist/rpc.js
CHANGED
|
@@ -154,7 +154,7 @@ export async function runRpc(context, options, loadedCatalog) {
|
|
|
154
154
|
});
|
|
155
155
|
if (options.preview)
|
|
156
156
|
return { kind: "preview", workspaceId: context.workspaceId, requestJson };
|
|
157
|
-
//
|
|
157
|
+
// A call may run for 210 seconds; leave time to receive its result.
|
|
158
158
|
const timeout = AbortSignal.timeout(240_000);
|
|
159
159
|
const signal = context.signal
|
|
160
160
|
? AbortSignal.any([context.signal, timeout])
|
package/dist/runner.js
CHANGED
|
@@ -18,6 +18,7 @@ Usage: zo <command> [--profile <name>] [--workspace <id>] [--json]
|
|
|
18
18
|
rpc <action> Invoke any callable action from JSON
|
|
19
19
|
<resource> <action> Invoke an installed built-in command
|
|
20
20
|
completion <shell> Print bash, zsh or fish completion (offline)
|
|
21
|
+
update Install the latest standalone release
|
|
21
22
|
--version Show the installed CLI version
|
|
22
23
|
|
|
23
24
|
Run zo <command> --help for options; for example, zo machine list --help.
|
|
@@ -25,8 +26,8 @@ Use --profile to keep separate sign-ins. New profiles use production.
|
|
|
25
26
|
ZO_SYSTEM_API_ACCESS_TOKEN overrides saved credentials; ZO_WORKSPACE_ID selects a workspace.
|
|
26
27
|
`;
|
|
27
28
|
const localHelp = {
|
|
28
|
-
login: "Usage: zo login [--profile <name>] [--workspace <id>] [--
|
|
29
|
-
logout: "Usage: zo logout [--profile <name>]
|
|
29
|
+
login: "Usage: zo login [--profile <name>] [--workspace <id>] [--device]\nOpen your browser to sign in and wait for approval.\nNew profiles use production. Browser consent approves workspaces and identities.\n--device prints a URL and code to approve on any device; login does the same when this machine can't open a browser.\nInternal testing: --environment staging explicitly binds a separate profile to staging.\n",
|
|
30
|
+
logout: "Usage: zo logout [--profile <name>]\nRevoke the saved sign-in and pending replaced sign-ins, then remove them from this computer. If revocation fails, logout says why and removes them anyway.\n",
|
|
30
31
|
auth: "Usage: zo auth status | token [--headers]\nStatus shows the current identity; token explicitly prints an access token.\n",
|
|
31
32
|
"auth status": "Usage: zo auth status [--profile <name>] [--workspace <id>] [--json]\nShow the approving person, sign-in expiry and effective identity in the selected workspace.\n",
|
|
32
33
|
"auth token": "Usage: zo auth token [--profile <name>] [--headers]\nExplicitly print a short-lived access token; --headers prints Authorization JSON.\n",
|
|
@@ -39,6 +40,7 @@ const localHelp = {
|
|
|
39
40
|
"actions describe": "Usage: zo actions describe <action> [--version <integer>] [--catalog scoped|all] [--json]\nShow live input, output and query schemas. Omit --version to inspect every published version.\n",
|
|
40
41
|
rpc: "Usage: zo rpc <action> [--input <json> | --input-file <path|->] [--version <integer>]\n --query <json> Declared list-query options; one page per call\n --idempotency-key <key> Envelope key where the action supports it\n --preview Print the request without invoking\n --envelope Print the complete success envelope\n --json Print exact result JSON\n --profile <name> --workspace <id>\nThe live catalog supplies the default version. RPC preserves supplied inputs. Invocations are never retried.\n",
|
|
41
42
|
completion: "Usage: zo completion bash|zsh|fish\nPrint completion from installed metadata without credentials or network requests.\n",
|
|
43
|
+
update: "Usage: zo update [--json]\nDownload the latest standalone release, verify it and switch the zo command to it. Sign-ins are kept.\nDoes nothing when this version is already the latest. An npm installation upgrades with npm install --global @zocomputer/cli@latest.\n",
|
|
42
44
|
};
|
|
43
45
|
// Printed after login so a first-time caller, person or agent, knows the next
|
|
44
46
|
// command and that one sign-in serves the CLI, HTTP and MCP alike.
|
|
@@ -106,7 +108,7 @@ export async function run(argv = process.argv.slice(2), runtime = {}) {
|
|
|
106
108
|
stdout.write((await import("./commands.js")).completionScript(args[1]));
|
|
107
109
|
return;
|
|
108
110
|
}
|
|
109
|
-
const local = ["login", "logout", "auth", "context"].includes(args[0] ?? "");
|
|
111
|
+
const local = ["login", "logout", "auth", "context", "update"].includes(args[0] ?? "");
|
|
110
112
|
if (options.envelope && local)
|
|
111
113
|
throw new CliError("--envelope is only supported for action invocation.");
|
|
112
114
|
const controller = new AbortController();
|
|
@@ -123,8 +125,20 @@ export async function run(argv = process.argv.slice(2), runtime = {}) {
|
|
|
123
125
|
throwIfInterrupted(signal);
|
|
124
126
|
writeJson(value, output);
|
|
125
127
|
};
|
|
128
|
+
if (args[0] === "update") {
|
|
129
|
+
commandOptions(args.slice(1), []);
|
|
130
|
+
const { update } = await import("./update.js");
|
|
131
|
+
writeLocalJson(await update({
|
|
132
|
+
version: runtime.version,
|
|
133
|
+
env,
|
|
134
|
+
fetch: runtime.fetch,
|
|
135
|
+
signal,
|
|
136
|
+
stderr,
|
|
137
|
+
}));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
126
140
|
if (args[0] === "login") {
|
|
127
|
-
const parsed = commandOptions(args.slice(1), ["environment"], ["
|
|
141
|
+
const parsed = commandOptions(args.slice(1), ["environment"], ["device"]);
|
|
128
142
|
const requested = parsed.values.get("environment");
|
|
129
143
|
if (requested !== undefined &&
|
|
130
144
|
requested !== "production" &&
|
|
@@ -137,11 +151,11 @@ export async function run(argv = process.argv.slice(2), runtime = {}) {
|
|
|
137
151
|
signal,
|
|
138
152
|
});
|
|
139
153
|
const login = await auth.login({
|
|
140
|
-
|
|
154
|
+
device: parsed.flags.has("device"),
|
|
141
155
|
signal,
|
|
142
|
-
announce: (
|
|
143
|
-
?
|
|
144
|
-
:
|
|
156
|
+
announce: (prompt) => stderr.write(prompt.flow === "browser"
|
|
157
|
+
? `Opening your browser to sign in. If it doesn't open, visit this URL on this computer:\n\n ${prompt.url}\n\nWaiting for approval… (Ctrl-C to cancel)\n`
|
|
158
|
+
: `To sign in, visit this URL on any device:\n\n ${prompt.url}\n\nCode: ${prompt.userCode}. Approve only if your browser shows the same code.\nWaiting for approval… (Ctrl-C to cancel)\n`),
|
|
145
159
|
});
|
|
146
160
|
stderr.write(`\nApproved in the browser. Sign-in saved for profile ${profile.name}.\n`);
|
|
147
161
|
if (login.cleanupPending)
|
|
@@ -207,13 +221,12 @@ export async function run(argv = process.argv.slice(2), runtime = {}) {
|
|
|
207
221
|
signal,
|
|
208
222
|
});
|
|
209
223
|
if (args[0] === "logout") {
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
await auth.logout(localOnly);
|
|
224
|
+
commandOptions(args.slice(1), [], []);
|
|
225
|
+
const { warning } = await auth.logout();
|
|
213
226
|
throwIfInterrupted(signal);
|
|
214
|
-
stderr.write(
|
|
215
|
-
? "
|
|
216
|
-
:
|
|
227
|
+
stderr.write(warning === undefined
|
|
228
|
+
? "Signed out; saved sign-ins revoked.\n"
|
|
229
|
+
: `Warning: ${warning}\nSigned out of this computer.\n`);
|
|
217
230
|
return;
|
|
218
231
|
}
|
|
219
232
|
if (args[0] === "auth" && args[1] === "token") {
|
package/dist/update.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FetchLike } from "@zocomputer/system-api";
|
|
2
|
+
export interface UpdateOptions {
|
|
3
|
+
/** The standalone executable's version. npm installations have none. */
|
|
4
|
+
version?: string;
|
|
5
|
+
execPath?: string;
|
|
6
|
+
env?: NodeJS.ProcessEnv;
|
|
7
|
+
fetch?: FetchLike;
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
stderr?: {
|
|
10
|
+
write(text: string): unknown;
|
|
11
|
+
};
|
|
12
|
+
/** Runs the release installer and resolves with its exit status. */
|
|
13
|
+
runInstaller?: (script: Buffer, args: string[]) => Promise<number>;
|
|
14
|
+
}
|
|
15
|
+
export interface UpdateResult {
|
|
16
|
+
/** The version the zo command runs after this update. */
|
|
17
|
+
version: string;
|
|
18
|
+
latest_version: string;
|
|
19
|
+
updated: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare function update(options?: UpdateOptions): Promise<UpdateResult>;
|
package/dist/update.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { readFile, readlink, realpath } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { delimiter, dirname, isAbsolute, join } from "node:path";
|
|
6
|
+
import { CliError, record, safeText, throwIfInterrupted } from "./errors.js";
|
|
7
|
+
const releases = "https://github.com/zocomputer/zo-cli/releases";
|
|
8
|
+
const installCommand = "curl -fsSL https://zo.computer/install.sh | sh";
|
|
9
|
+
const versionPattern = /^(\d+)\.(\d+)\.(\d+)(-[A-Za-z0-9][A-Za-z0-9.-]*)?$/;
|
|
10
|
+
// The installer links <data home>/zo/cli/releases/<version>/<target>/zo.
|
|
11
|
+
const managedPath = /^(\/.*\/zo\/cli)\/releases\/[^/]+\/[^/]+\/zo$/;
|
|
12
|
+
function unreachable() {
|
|
13
|
+
return new CliError("Could not download the latest Zo CLI release. Check your connection and try again.", 5, "transport_error");
|
|
14
|
+
}
|
|
15
|
+
/** Follows redirects by hand so a release can never be fetched over plain HTTP. */
|
|
16
|
+
async function download(url, fetcher, signal) {
|
|
17
|
+
const timeout = AbortSignal.timeout(60_000);
|
|
18
|
+
const combined = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
19
|
+
try {
|
|
20
|
+
let target = url;
|
|
21
|
+
for (let hop = 0; hop < 5; hop++) {
|
|
22
|
+
const response = await fetcher(target, {
|
|
23
|
+
redirect: "manual",
|
|
24
|
+
signal: combined,
|
|
25
|
+
});
|
|
26
|
+
const location = response.headers.get("location");
|
|
27
|
+
if (response.status >= 300 && response.status < 400 && location) {
|
|
28
|
+
await response.body?.cancel();
|
|
29
|
+
target = new URL(location, target).href;
|
|
30
|
+
if (!target.startsWith("https://"))
|
|
31
|
+
break;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (!response.ok || !response.body)
|
|
35
|
+
break;
|
|
36
|
+
// The manifest and installer are a few kilobytes; refuse anything else.
|
|
37
|
+
const reader = response.body.getReader();
|
|
38
|
+
const chunks = [];
|
|
39
|
+
for (let size = 0;;) {
|
|
40
|
+
const { value, done } = await reader.read();
|
|
41
|
+
if (done)
|
|
42
|
+
return Buffer.concat(chunks);
|
|
43
|
+
size += value.length;
|
|
44
|
+
if (size > 1_048_576) {
|
|
45
|
+
await reader.cancel().catch(() => { });
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
chunks.push(value);
|
|
49
|
+
}
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throwIfInterrupted(signal);
|
|
55
|
+
}
|
|
56
|
+
throw unreachable();
|
|
57
|
+
}
|
|
58
|
+
function isNewer(candidate, current) {
|
|
59
|
+
const next = versionPattern.exec(candidate);
|
|
60
|
+
const now = versionPattern.exec(current);
|
|
61
|
+
if (!next || !now)
|
|
62
|
+
return false;
|
|
63
|
+
for (const index of [1, 2, 3]) {
|
|
64
|
+
const difference = Number(next[index]) - Number(now[index]);
|
|
65
|
+
if (difference !== 0)
|
|
66
|
+
return difference > 0;
|
|
67
|
+
}
|
|
68
|
+
// 0.4.0 follows 0.4.0-rc.1; the latest release is never a prerelease.
|
|
69
|
+
return now[4] !== undefined && next[4] === undefined;
|
|
70
|
+
}
|
|
71
|
+
/** Finds the installer's link on PATH, else in the installer's default directory. */
|
|
72
|
+
async function managedCommand(executable, env) {
|
|
73
|
+
const directories = (env.PATH ?? "").split(delimiter);
|
|
74
|
+
directories.push(join(env.HOME ?? homedir(), ".local/bin"));
|
|
75
|
+
let shadowedBy;
|
|
76
|
+
for (const directory of directories) {
|
|
77
|
+
if (!isAbsolute(directory))
|
|
78
|
+
continue;
|
|
79
|
+
const command = join(directory, "zo");
|
|
80
|
+
const resolved = await realpath(command).catch(() => undefined);
|
|
81
|
+
if (resolved === undefined)
|
|
82
|
+
continue;
|
|
83
|
+
if (resolved !== executable) {
|
|
84
|
+
shadowedBy ??= command;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
// The installer replaces only a link it wrote, so an alias that points at
|
|
88
|
+
// that link is passed over for the link itself.
|
|
89
|
+
const root = managedPath.exec(await readlink(command).catch(() => ""))?.[1];
|
|
90
|
+
if (root)
|
|
91
|
+
return { directory, root, shadowedBy };
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
function shell(env, signal, stderr) {
|
|
96
|
+
return (script, args) => new Promise((resolve, reject) => {
|
|
97
|
+
// The installer reads itself from stdin, as it does under `curl | sh`.
|
|
98
|
+
// It gets its own process group so an interrupt also stops its download;
|
|
99
|
+
// a shell alone would wait for curl before running its trap.
|
|
100
|
+
const child = spawn("/bin/sh", ["-s", "--", ...args], {
|
|
101
|
+
env,
|
|
102
|
+
detached: true,
|
|
103
|
+
stdio: ["pipe", "ignore", "pipe"],
|
|
104
|
+
});
|
|
105
|
+
const stop = () => {
|
|
106
|
+
try {
|
|
107
|
+
if (child.pid !== undefined)
|
|
108
|
+
process.kill(-child.pid, "SIGTERM");
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
/* The installer already exited. */
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
115
|
+
if (signal?.aborted)
|
|
116
|
+
stop();
|
|
117
|
+
child.on("error", reject);
|
|
118
|
+
child.on("close", (code) => {
|
|
119
|
+
signal?.removeEventListener("abort", stop);
|
|
120
|
+
resolve(code ?? 1);
|
|
121
|
+
});
|
|
122
|
+
child.stderr.on("data", (chunk) => stderr.write(String(chunk)));
|
|
123
|
+
// A shell that exits early closes the pipe; its status reports why.
|
|
124
|
+
child.stdin.on("error", () => { });
|
|
125
|
+
child.stdin.end(script);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
export async function update(options = {}) {
|
|
129
|
+
const env = options.env ?? process.env;
|
|
130
|
+
const stderr = options.stderr ?? process.stderr;
|
|
131
|
+
const current = options.version;
|
|
132
|
+
if (current === undefined)
|
|
133
|
+
throw new CliError("zo update replaces the standalone executable. This zo was installed with npm; run npm install --global @zocomputer/cli@latest instead.", 2, "unsupported_installation");
|
|
134
|
+
const unmanaged = (reason) => new CliError(`${reason} Install the latest release with: ${installCommand}`, 2, "unsupported_installation");
|
|
135
|
+
const executable = await realpath(options.execPath ?? process.execPath);
|
|
136
|
+
const command = await managedCommand(executable, env);
|
|
137
|
+
if (managedPath.test(executable) && !command)
|
|
138
|
+
throw unmanaged("No zo command on your PATH links to this executable.");
|
|
139
|
+
const marker = command
|
|
140
|
+
? await readFile(join(command.root, ".installer-v1"), "utf8").catch(() => "")
|
|
141
|
+
: "";
|
|
142
|
+
if (!command || marker.trim() !== "zo-cli-installer-v1")
|
|
143
|
+
throw unmanaged("This zo was not installed by the standalone installer, so it cannot update itself.");
|
|
144
|
+
const fetcher = options.fetch ?? fetch;
|
|
145
|
+
let manifest;
|
|
146
|
+
try {
|
|
147
|
+
manifest = JSON.parse((await download(`${releases}/latest/download/release.json`, fetcher, options.signal)).toString("utf8"));
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (error instanceof CliError)
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
const invalid = new CliError("The latest Zo CLI release could not be verified. Nothing was changed.", 5, "invalid_response");
|
|
154
|
+
if (!record(manifest) ||
|
|
155
|
+
manifest.schema_version !== 1 ||
|
|
156
|
+
typeof manifest.cli_version !== "string" ||
|
|
157
|
+
!versionPattern.test(manifest.cli_version) ||
|
|
158
|
+
typeof manifest.installer_sha256 !== "string")
|
|
159
|
+
throw invalid;
|
|
160
|
+
const latest = manifest.cli_version;
|
|
161
|
+
if (!isNewer(latest, current)) {
|
|
162
|
+
stderr.write(latest === current
|
|
163
|
+
? `zo ${current} is the latest version.\n`
|
|
164
|
+
: `zo ${current} is newer than the latest release, ${latest}. Nothing to update; to install ${latest}, run: ${installCommand}\n`);
|
|
165
|
+
return { version: current, latest_version: latest, updated: false };
|
|
166
|
+
}
|
|
167
|
+
// Each release publishes the installer pinned to its own version.
|
|
168
|
+
const installer = await download(`${releases}/download/v${latest}/install.sh`, fetcher, options.signal);
|
|
169
|
+
if (createHash("sha256").update(installer).digest("hex") !==
|
|
170
|
+
manifest.installer_sha256)
|
|
171
|
+
throw invalid;
|
|
172
|
+
stderr.write(`Updating zo ${current} to ${latest}…\n`);
|
|
173
|
+
// The installer derives its root from XDG_DATA_HOME, which may have changed
|
|
174
|
+
// since installation; the link records the root it must keep using.
|
|
175
|
+
const run = options.runInstaller ??
|
|
176
|
+
shell({ ...env, XDG_DATA_HOME: dirname(dirname(command.root)) }, options.signal, stderr);
|
|
177
|
+
let status;
|
|
178
|
+
try {
|
|
179
|
+
status = await run(installer, [
|
|
180
|
+
"--bin-dir",
|
|
181
|
+
command.directory,
|
|
182
|
+
"--no-modify-path",
|
|
183
|
+
]);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
status = 1;
|
|
187
|
+
}
|
|
188
|
+
throwIfInterrupted(options.signal);
|
|
189
|
+
if (status !== 0)
|
|
190
|
+
throw new CliError(`The update did not finish. zo ${current} is still installed.`, 5, "update_failed");
|
|
191
|
+
stderr.write(`Updated zo to ${latest}. Sign-ins are unchanged.\n`);
|
|
192
|
+
if (command.shadowedBy)
|
|
193
|
+
stderr.write(`Another zo at ${safeText(command.shadowedBy)} comes first on your PATH and was not updated.\n`);
|
|
194
|
+
return { version: latest, latest_version: latest, updated: true };
|
|
195
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zocomputer/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Public Zo command line client",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"node": "^22.23.4 || ^24.22.1 || ^26.13.0"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@zocomputer/system-api": "0.
|
|
17
|
+
"@zocomputer/system-api": "0.3.0"
|
|
18
18
|
},
|
|
19
19
|
"publishConfig": {
|
|
20
20
|
"access": "public",
|