@corvio/admin-cli 0.1.0-beta.2 → 0.1.0-beta.3

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 CHANGED
@@ -1,12 +1,12 @@
1
1
  # Corvio Platform Administrator CLI
2
2
 
3
- Official read-only CLI for bounded Corvio platform investigations. It is separate from the Workspace CLI and accepts only a short-lived `cva_` credential.
3
+ Official read-only CLI for bounded Corvio platform investigations. It is separate from the Workspace CLI and accepts only a short-lived `cva_` credential, supplied by environment or administrator device link.
4
4
 
5
5
  ```bash
6
- npm install --global @corvio/admin-cli
7
- export CORVIO_ADMIN_API_KEY='cva_...'
8
- corvio-admin auth status
6
+ npm install --global @corvio/admin-cli@beta
7
+ corvio-admin auth login
8
+ corvio-admin auth status --json
9
9
  corvio-admin catalog --json
10
10
  ```
11
11
 
12
- Every data query requires a purpose and returns an auditable receipt. This package contains no ordinary Workspace commands. See [the administrator CLI guide](https://corvio.ai/developers/admin-cli).
12
+ For Agents and CI, inject `CORVIO_ADMIN_API_KEY='cva_...'`; it takes precedence over the stored device-link credential. Every data query requires a purpose and returns an auditable receipt. This package contains no ordinary Workspace commands. See [the administrator CLI guide](https://corvio.ai/developers/admin-cli).
package/dist/cli.js CHANGED
@@ -4,25 +4,31 @@ import {
4
4
  ApiClient,
5
5
  CliError,
6
6
  EXIT,
7
+ clearAdminCredential,
7
8
  listOption,
8
9
  normalizeApiBase,
9
10
  numberOption,
11
+ openBrowser,
10
12
  option,
11
13
  readAdminCredential,
12
14
  readTextFile,
13
15
  runMain,
16
+ sleep,
17
+ storeAdminCredential,
18
+ writeBinaryFile,
14
19
  writeResult,
15
20
  } from "./core.js";
16
21
 
17
- const VERSION = "0.1.0-beta.2";
22
+ const VERSION = "0.1.0-beta.3";
18
23
  const DEFAULT_API_BASE = "https://api.corvio.ai/v1";
19
24
  const USER_AGENT = `corvio-admin-cli/${VERSION}`;
20
25
 
21
26
  const HELP = `Corvio Platform Administrator CLI ${VERSION}
22
27
 
23
28
  Usage:
24
- corvio-admin auth status
29
+ corvio-admin auth login|status|logout
25
30
  corvio-admin capabilities
31
+ corvio-admin skill download [--output <path>]
26
32
  corvio-admin catalog
27
33
  corvio-admin query|data query --file <request.json>
28
34
  corvio-admin workspaces list --purpose <reason>
@@ -33,24 +39,34 @@ Usage:
33
39
  corvio-admin receipts get <id>
34
40
 
35
41
  Authentication:
36
- Set CORVIO_ADMIN_API_KEY to a short-lived cva_ key.
42
+ CORVIO_ADMIN_API_KEY=cva_... takes precedence over the stored device-link login.
43
+ Run 'corvio-admin auth login' when no environment key is configured.
37
44
 
38
45
  This package contains no Workspace ask, search, docs, files, sync, or share commands.
39
46
  `;
40
47
 
41
- function baseUrl(options) {
42
- return normalizeApiBase(options["api-base"] || process.env.CORVIO_API_BASE, DEFAULT_API_BASE);
48
+ function baseUrl(options, credential = null) {
49
+ return normalizeApiBase(
50
+ options["api-base"]
51
+ || process.env.CORVIO_ADMIN_API_BASE
52
+ || process.env.CORVIO_API_BASE
53
+ || credential?.metadata?.api_base,
54
+ DEFAULT_API_BASE,
55
+ );
43
56
  }
44
57
 
45
- function client(options) {
46
- const credential = readAdminCredential();
58
+ async function client(options) {
59
+ const credential = await readAdminCredential();
47
60
  if (!credential) {
48
- throw new CliError("Set CORVIO_ADMIN_API_KEY to a short-lived cva_ key.", {
61
+ throw new CliError("Set CORVIO_ADMIN_API_KEY or run `corvio-admin auth login`.", {
49
62
  code: "admin_auth_required",
50
63
  exitCode: EXIT.AUTH,
51
64
  });
52
65
  }
53
- return new ApiClient({ baseUrl: baseUrl(options), credential: credential.key, userAgent: USER_AGENT });
66
+ return {
67
+ api: new ApiClient({ baseUrl: baseUrl(options, credential), credential: credential.key, userAgent: USER_AGENT }),
68
+ credential,
69
+ };
54
70
  }
55
71
 
56
72
  function listQuery(options) {
@@ -63,8 +79,85 @@ function listQuery(options) {
63
79
  };
64
80
  }
65
81
 
82
+ async function authLogin(options) {
83
+ const current = await readAdminCredential();
84
+ if (current && !options.force) {
85
+ const api = new ApiClient({ baseUrl: baseUrl(options, current), credential: current.key, userAgent: USER_AGENT });
86
+ const identity = await api.request("/me");
87
+ writeResult({ authenticated: true, source: current.source, identity }, { json: options.json });
88
+ return;
89
+ }
90
+ const requestedScopes = listOption(options, "scopes", [
91
+ "admin_data:read",
92
+ "admin_support:read",
93
+ "admin_costs:read",
94
+ ]);
95
+ if (requestedScopes.includes("admin_content:read") && !options.force) {
96
+ throw new CliError(
97
+ "Requesting admin_content:read requires --force and should be limited to a specific Run investigation.",
98
+ { code: "admin_content_scope_confirmation_required" },
99
+ );
100
+ }
101
+ const api = new ApiClient({ baseUrl: baseUrl(options), userAgent: USER_AGENT });
102
+ const authorization = await api.request("/admin/auth/device-authorizations", {
103
+ method: "POST",
104
+ auth: false,
105
+ body: {
106
+ client_name: "Corvio Platform Administrator CLI",
107
+ cli_version: VERSION,
108
+ scopes: requestedScopes,
109
+ },
110
+ });
111
+ process.stderr.write(`Open ${authorization.verification_uri_complete}\nCode: ${authorization.user_code}\n`);
112
+ if (!options["no-input"]) await openBrowser(authorization.verification_uri_complete);
113
+
114
+ const expiresAt = Date.now() + Number(authorization.expires_in || 600) * 1000;
115
+ let intervalSeconds = Math.max(5, Number(authorization.interval || 5));
116
+ let token;
117
+ while (Date.now() < expiresAt) {
118
+ await sleep(intervalSeconds * 1000);
119
+ try {
120
+ token = await api.request("/admin/auth/device-token", {
121
+ method: "POST",
122
+ auth: false,
123
+ body: { device_code: authorization.device_code },
124
+ });
125
+ break;
126
+ } catch (error) {
127
+ if (!(error instanceof CliError)) throw error;
128
+ if (error.code === "authorization_pending") continue;
129
+ if (error.code === "slow_down") {
130
+ intervalSeconds = Math.min(30, intervalSeconds + 5);
131
+ continue;
132
+ }
133
+ throw error;
134
+ }
135
+ }
136
+ if (!token?.secret || token.audience !== "platform_admin") {
137
+ throw new CliError("Administrator device authorization expired; run `corvio-admin auth login` again.", {
138
+ code: "expired_token",
139
+ exitCode: EXIT.AUTH,
140
+ });
141
+ }
142
+ await storeAdminCredential({ ...token, api_base: baseUrl(options) });
143
+ const authenticatedApi = new ApiClient({ baseUrl: baseUrl(options), credential: token.secret, userAgent: USER_AGENT });
144
+ const identity = await authenticatedApi.request("/me");
145
+ writeResult({
146
+ authenticated: true,
147
+ source: "device_link",
148
+ audience: identity.api_key?.audience,
149
+ scopes: identity.api_key?.scopes || [],
150
+ expires_at: token.expires_at,
151
+ }, { json: options.json });
152
+ }
153
+
66
154
  async function authStatus(options) {
67
- const api = client(options);
155
+ const credential = await readAdminCredential();
156
+ if (!credential) {
157
+ writeResult({ authenticated: false, sources: ["CORVIO_ADMIN_API_KEY", "device_link"] }, { json: options.json });
158
+ return;
159
+ }
160
+ const api = new ApiClient({ baseUrl: baseUrl(options, credential), credential: credential.key, userAgent: USER_AGENT });
68
161
  const identity = await api.request("/me");
69
162
  if (identity.api_key?.audience !== "platform_admin") {
70
163
  throw new CliError("The configured key is not a platform administrator credential.", {
@@ -72,17 +165,36 @@ async function authStatus(options) {
72
165
  exitCode: EXIT.AUTH,
73
166
  });
74
167
  }
75
- writeResult({ authenticated: true, source: "environment", identity }, { json: options.json });
168
+ writeResult({ authenticated: true, source: credential.source, identity }, { json: options.json });
169
+ }
170
+
171
+ async function authLogout(options) {
172
+ const credential = await readAdminCredential();
173
+ if (!credential) {
174
+ writeResult({ authenticated: false, changed: false }, { json: options.json });
175
+ return;
176
+ }
177
+ if (credential.source === "environment") {
178
+ throw new CliError("CORVIO_ADMIN_API_KEY is managed by the parent environment and cannot be removed by the CLI.", {
179
+ code: "environment_credential_managed_externally",
180
+ exitCode: EXIT.AUTH,
181
+ });
182
+ }
183
+ const api = new ApiClient({ baseUrl: baseUrl(options, credential), credential: credential.key, userAgent: USER_AGENT });
184
+ await api.request("/admin/auth/credentials/current:revoke", { method: "POST" });
185
+ await clearAdminCredential();
186
+ writeResult({ authenticated: false, revoked: true }, { json: options.json });
76
187
  }
77
188
 
78
189
  async function capabilities(options) {
79
- const api = client(options);
190
+ const { api, credential } = await client(options);
80
191
  const [index, identity] = await Promise.all([api.request("/admin"), api.request("/me")]);
81
- writeResult({ cli_version: VERSION, api: index, principal: identity }, { json: options.json });
192
+ writeResult({ cli_version: VERSION, auth_source: credential.source, api: index, principal: identity }, { json: options.json });
82
193
  }
83
194
 
84
195
  async function catalog(options) {
85
- writeResult(await client(options).request("/admin/data/catalog"), { json: options.json });
196
+ const { api } = await client(options);
197
+ writeResult(await api.request("/admin/data/catalog"), { json: options.json });
86
198
  }
87
199
 
88
200
  async function query(options) {
@@ -103,19 +215,39 @@ async function query(options) {
103
215
  cursor: options.cursor || null,
104
216
  };
105
217
  }
106
- writeResult(await client(options).request("/admin/data/query", { method: "POST", body }), { json: options.json });
218
+ const { api } = await client(options);
219
+ writeResult(await api.request("/admin/data/query", { method: "POST", body }), { json: options.json });
107
220
  }
108
221
 
109
222
  async function listResource(options, resource) {
110
- writeResult(await client(options).request(`/admin/${resource}`, { query: listQuery(options) }), { json: options.json });
223
+ const { api } = await client(options);
224
+ writeResult(await api.request(`/admin/${resource}`, { query: listQuery(options) }), { json: options.json });
225
+ }
226
+
227
+ async function downloadSkill(options) {
228
+ const { api } = await client(options);
229
+ const output = String(options.output || "corvio-admin-data.zip");
230
+ const bytes = await api.requestBytes("/admin/skill/corvio-admin-data.zip");
231
+ const path = await writeBinaryFile(output, bytes);
232
+ writeResult({
233
+ downloaded: true,
234
+ output: path,
235
+ install: `npx skills add ${path}`,
236
+ }, { json: options.json });
111
237
  }
112
238
 
113
239
  async function main({ options, positionals }) {
114
240
  const [command, action, id] = positionals;
115
241
  if (options.version || command === "version") return writeResult(VERSION, { json: options.json });
116
242
  if (options.help || !command || command === "help") return writeResult(HELP.trimEnd(), { json: false });
117
- if (command === "auth" && action === "status") return authStatus(options);
243
+ if (command === "auth") {
244
+ if (action === "login") return authLogin(options);
245
+ if (action === "status") return authStatus(options);
246
+ if (action === "logout") return authLogout(options);
247
+ throw new CliError("auth action must be login, status, or logout.");
248
+ }
118
249
  if (command === "capabilities") return capabilities(options);
250
+ if (command === "skill" && action === "download") return downloadSkill(options);
119
251
  if (command === "catalog") return catalog(options);
120
252
  if (command === "query") return query(options);
121
253
  if (command === "data" && action === "query") return query(options);
@@ -126,15 +258,18 @@ async function main({ options, positionals }) {
126
258
  if (command === "runs" && action === "content") {
127
259
  const runId = id || option(options, "id", { required: true });
128
260
  const purpose = option(options, "purpose", { required: true });
129
- return writeResult(await client(options).request(`/admin/runs/${runId}/content`, { query: { purpose } }), { json: options.json });
261
+ const { api } = await client(options);
262
+ return writeResult(await api.request(`/admin/runs/${runId}/content`, { query: { purpose } }), { json: options.json });
130
263
  }
131
264
  if (command === "usage" && action === "events") return listResource(options, "usage/events");
132
265
  if (command === "usage" && action === "summary") {
133
- return writeResult(await client(options).request("/admin/usage/summary", { query: listQuery(options) }), { json: options.json });
266
+ const { api } = await client(options);
267
+ return writeResult(await api.request("/admin/usage/summary", { query: listQuery(options) }), { json: options.json });
134
268
  }
135
269
  if (command === "receipts" && action === "get") {
136
270
  const receiptId = id || option(options, "id", { required: true });
137
- return writeResult(await client(options).request(`/admin/data/query-receipts/${receiptId}`), { json: options.json });
271
+ const { api } = await client(options);
272
+ return writeResult(await api.request(`/admin/data/query-receipts/${receiptId}`), { json: options.json });
138
273
  }
139
274
  throw new CliError(`Unknown administrator command: ${positionals.join(" ")}`);
140
275
  }
package/dist/core.js CHANGED
@@ -163,6 +163,51 @@ export class ApiClient {
163
163
  }
164
164
  return payload;
165
165
  }
166
+
167
+ async requestBytes(path, { query, headers = {}, auth = true } = {}) {
168
+ const url = new URL(`${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`);
169
+ for (const [key, value] of Object.entries(query || {})) {
170
+ if (value !== undefined && value !== null && value !== "") url.searchParams.set(key, String(value));
171
+ }
172
+ const requestHeaders = {
173
+ accept: "application/zip, application/octet-stream",
174
+ "user-agent": this.userAgent,
175
+ ...headers,
176
+ };
177
+ if (auth) {
178
+ if (!this.credential) {
179
+ throw new CliError("Corvio authentication is required.", { code: "auth_required", exitCode: EXIT.AUTH });
180
+ }
181
+ requestHeaders.authorization = `Bearer ${this.credential}`;
182
+ }
183
+ let response;
184
+ try {
185
+ response = await this.fetchImpl(url, { method: "GET", headers: requestHeaders });
186
+ } catch (cause) {
187
+ throw new CliError(`Unable to reach Corvio: ${cause instanceof Error ? cause.message : String(cause)}`, {
188
+ code: "network_error",
189
+ exitCode: EXIT.REMOTE,
190
+ });
191
+ }
192
+ const bytes = new Uint8Array(await response.arrayBuffer());
193
+ if (!response.ok) {
194
+ const text = new TextDecoder().decode(bytes);
195
+ let payload = {};
196
+ try {
197
+ payload = JSON.parse(text);
198
+ } catch {
199
+ payload = { message: text.slice(0, 500) };
200
+ }
201
+ const envelope = payload?.error || payload?.detail || payload || {};
202
+ throw new CliError(String(envelope.message || envelope.code || `Corvio request failed (${response.status}).`), {
203
+ code: String(envelope.code || `http_${response.status}`),
204
+ status: response.status,
205
+ exitCode: exitCodeForStatus(response.status),
206
+ details: envelope.details || null,
207
+ });
208
+ }
209
+ return bytes;
210
+ }
166
211
  }
167
212
 
168
213
  function configRoot(env = process.env) {
@@ -175,6 +220,10 @@ export function workspaceCredentialPath(env = process.env) {
175
220
  return join(configRoot(env), "credentials.json");
176
221
  }
177
222
 
223
+ export function adminCredentialPath(env = process.env) {
224
+ return join(configRoot(env), "admin-credentials.json");
225
+ }
226
+
178
227
  export async function readWorkspaceCredential(env = process.env) {
179
228
  const environmentKey = String(env.CORVIO_API_KEY || "").trim();
180
229
  if (environmentKey) {
@@ -241,16 +290,66 @@ export async function clearWorkspaceCredential(env = process.env) {
241
290
  return path;
242
291
  }
243
292
 
244
- export function readAdminCredential(env = process.env) {
293
+ export async function readAdminCredential(env = process.env) {
245
294
  const key = String(env.CORVIO_ADMIN_API_KEY || "").trim();
246
- if (!key) return null;
247
- if (!key.startsWith("cva_")) {
248
- throw new CliError("CORVIO_ADMIN_API_KEY must be a platform administrator cva_ key.", {
249
- code: "invalid_admin_key",
295
+ if (key) {
296
+ if (!key.startsWith("cva_")) {
297
+ throw new CliError("CORVIO_ADMIN_API_KEY must be a platform administrator cva_ key.", {
298
+ code: "invalid_admin_key",
299
+ exitCode: EXIT.AUTH,
300
+ });
301
+ }
302
+ return { key, source: "environment", metadata: null };
303
+ }
304
+ const path = adminCredentialPath(env);
305
+ let parsed;
306
+ try {
307
+ parsed = JSON.parse(await readFile(path, "utf8"));
308
+ } catch (cause) {
309
+ if (cause?.code === "ENOENT") return null;
310
+ throw new CliError("Stored Corvio administrator credentials are unreadable.", {
311
+ code: "admin_credential_store_invalid",
312
+ exitCode: EXIT.AUTH,
313
+ });
314
+ }
315
+ const storedKey = String(parsed.api_key || "").trim();
316
+ if (!storedKey.startsWith("cva_")) {
317
+ throw new CliError("Stored Corvio administrator credentials are invalid; run `corvio-admin auth login` again.", {
318
+ code: "admin_credential_store_invalid",
250
319
  exitCode: EXIT.AUTH,
251
320
  });
252
321
  }
253
- return { key, source: "environment" };
322
+ return { key: storedKey, source: "device_link", metadata: parsed };
323
+ }
324
+
325
+ export async function storeAdminCredential(payload, env = process.env) {
326
+ const path = adminCredentialPath(env);
327
+ const root = dirname(path);
328
+ await mkdir(root, { recursive: true, mode: 0o700 });
329
+ await chmod(root, 0o700).catch(() => {});
330
+ const document = {
331
+ version: 1,
332
+ api_key: payload.secret,
333
+ key_id: payload.key_id,
334
+ audience: payload.audience,
335
+ api_base: payload.api_base,
336
+ scopes: payload.scopes || [],
337
+ expires_at: payload.expires_at,
338
+ created_at: payload.created_at || new Date().toISOString(),
339
+ source: "device_link",
340
+ };
341
+ const temporary = `${path}.${randomUUID()}.tmp`;
342
+ await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, { mode: 0o600, flag: "wx" });
343
+ await chmod(temporary, 0o600);
344
+ await rename(temporary, path);
345
+ await chmod(path, 0o600);
346
+ return { path };
347
+ }
348
+
349
+ export async function clearAdminCredential(env = process.env) {
350
+ const path = adminCredentialPath(env);
351
+ await rm(path, { force: true });
352
+ return path;
254
353
  }
255
354
 
256
355
  export function writeResult(value, { json = false, stdout = process.stdout } = {}) {
@@ -313,6 +412,13 @@ export async function writeTextFile(path, content) {
313
412
  return target;
314
413
  }
315
414
 
415
+ export async function writeBinaryFile(path, content) {
416
+ const target = resolve(String(path));
417
+ await mkdir(dirname(target), { recursive: true });
418
+ await writeFile(target, content);
419
+ return target;
420
+ }
421
+
316
422
  export function requireConfirmation(options, action) {
317
423
  if (!options.yes) {
318
424
  throw new CliError(`${action} requires --yes.`, { code: "confirmation_required" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@corvio/admin-cli",
3
- "version": "0.1.0-beta.2",
3
+ "version": "0.1.0-beta.3",
4
4
  "description": "Official read-only Corvio platform administrator CLI.",
5
5
  "type": "module",
6
6
  "bin": {