@corvio/admin-cli 0.1.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +14 -0
  2. package/dist/cli.js +376 -0
  3. package/dist/core.js +1081 -0
  4. package/package.json +38 -0
package/README.md ADDED
@@ -0,0 +1,14 @@
1
+ # Corvio Platform Administrator CLI
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, supplied by environment or administrator device link.
4
+
5
+ ```bash
6
+ npm install --global @corvio/admin-cli@beta
7
+ corvio-admin auth login
8
+ corvio-admin auth status --json
9
+ corvio-admin catalog --json
10
+ ```
11
+
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).
13
+
14
+ Unknown/duplicate options, malformed bounds, and invalid JSON fail locally. API calls use the same bounded transport/runtime safety policy as the Workspace CLI. `corvio-admin skill download` reads the protected manifest first and verifies the archive SHA-256 before writing the zip.
package/dist/cli.js ADDED
@@ -0,0 +1,376 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createHash } from "node:crypto";
4
+
5
+ import {
6
+ ApiClient,
7
+ CliError,
8
+ EXIT,
9
+ assertKnownOptions,
10
+ assertPositionalCount,
11
+ clearAdminCredential,
12
+ listOption,
13
+ normalizeApiBase,
14
+ numberOption,
15
+ openBrowser,
16
+ option,
17
+ readAdminCredential,
18
+ readTextFile,
19
+ resolveNpmUpdateStatus,
20
+ runMain,
21
+ sleep,
22
+ storeAdminCredential,
23
+ writeBinaryFile,
24
+ writeResult,
25
+ } from "./core.js";
26
+
27
+ const VERSION = "0.1.0-beta.10";
28
+ const DEFAULT_API_BASE = "https://api.corvio.ai/v1";
29
+ const USER_AGENT = `corvio-admin-cli/${VERSION}`;
30
+ const GLOBAL_OPTIONS = Object.freeze(["api-base", "json", "no-input", "help", "version"]);
31
+ const COMMAND_OPTIONS = Object.freeze({
32
+ "auth:login": ["force", "scopes"],
33
+ "auth:status": [],
34
+ "auth:logout": [],
35
+ "capabilities:": [],
36
+ "update:check": [],
37
+ "skill:download": ["output"],
38
+ "catalog:": [],
39
+ "query:": ["file", "purpose", "dataset", "fields", "group-by", "metrics", "filters", "order-by", "limit", "cursor"],
40
+ "data:query": ["file", "purpose", "dataset", "fields", "group-by", "metrics", "filters", "order-by", "limit", "cursor"],
41
+ "workspaces:list": ["purpose", "workspace-id", "status", "limit", "cursor"],
42
+ "users:list": ["purpose", "workspace-id", "status", "limit", "cursor"],
43
+ "worker-jobs:list": ["purpose", "workspace-id", "status", "limit", "cursor"],
44
+ "runs:list": ["purpose", "workspace-id", "status", "limit", "cursor"],
45
+ "runs:content": ["id", "purpose"],
46
+ "usage:events": ["purpose", "workspace-id", "status", "limit", "cursor"],
47
+ "usage:summary": ["purpose", "workspace-id", "status", "limit", "cursor"],
48
+ "receipts:get": ["id"],
49
+ });
50
+
51
+ const HELP = `Corvio Platform Administrator CLI ${VERSION}
52
+
53
+ Usage:
54
+ corvio-admin auth login|status|logout
55
+ corvio-admin capabilities
56
+ corvio-admin skill download [--output <path>]
57
+ corvio-admin catalog
58
+ corvio-admin query|data query --file <request.json>
59
+ corvio-admin workspaces list --purpose <reason>
60
+ corvio-admin users list --purpose <reason>
61
+ corvio-admin runs list|content [id] --purpose <reason>
62
+ corvio-admin worker-jobs list --purpose <reason>
63
+ corvio-admin usage events|summary --purpose <reason>
64
+ corvio-admin receipts get <id>
65
+ corvio-admin update check
66
+
67
+ Authentication:
68
+ CORVIO_ADMIN_API_KEY=cva_... takes precedence over the stored device-link login.
69
+ Run 'corvio-admin auth login' when no environment key is configured.
70
+
71
+ This package contains no Workspace ask, search, docs, files, sync, or share commands.
72
+ `;
73
+
74
+ function resourcePath(value) {
75
+ return encodeURIComponent(String(value));
76
+ }
77
+
78
+ function validateInvocation(options, positionals) {
79
+ const [command, rawAction] = positionals;
80
+ if (!command || command === "help" || command === "version" || options.help || options.version) return;
81
+ const defaultActions = {
82
+ update: "check",
83
+ workspaces: "list",
84
+ users: "list",
85
+ "worker-jobs": "list",
86
+ };
87
+ const action = ["capabilities", "catalog", "query"].includes(command)
88
+ ? ""
89
+ : rawAction || defaultActions[command] || "";
90
+ const schema = COMMAND_OPTIONS[`${command}:${action}`];
91
+ if (!schema) {
92
+ throw new CliError(`Unknown administrator command: ${positionals.join(" ")}`, {
93
+ code: "unknown_command",
94
+ });
95
+ }
96
+ assertKnownOptions(options, [...GLOBAL_OPTIONS, ...schema]);
97
+ const maximum = new Set(["runs:content", "receipts:get"]).has(`${command}:${action}`)
98
+ ? 3
99
+ : action
100
+ ? 2
101
+ : 1;
102
+ assertPositionalCount(positionals, maximum);
103
+ }
104
+
105
+ function parseJsonValue(value, optionName) {
106
+ try {
107
+ return JSON.parse(String(value));
108
+ } catch {
109
+ throw new CliError(`Option --${optionName} must contain valid JSON.`, {
110
+ code: "invalid_json_option",
111
+ });
112
+ }
113
+ }
114
+
115
+ function baseUrl(options, credential = null) {
116
+ return normalizeApiBase(
117
+ options["api-base"]
118
+ || process.env.CORVIO_ADMIN_API_BASE
119
+ || process.env.CORVIO_API_BASE
120
+ || credential?.metadata?.api_base,
121
+ DEFAULT_API_BASE,
122
+ );
123
+ }
124
+
125
+ async function client(options) {
126
+ const credential = await readAdminCredential();
127
+ if (!credential) {
128
+ throw new CliError("Set CORVIO_ADMIN_API_KEY or run `corvio-admin auth login`.", {
129
+ code: "admin_auth_required",
130
+ exitCode: EXIT.AUTH,
131
+ });
132
+ }
133
+ return {
134
+ api: new ApiClient({ baseUrl: baseUrl(options, credential), credential: credential.key, userAgent: USER_AGENT }),
135
+ credential,
136
+ };
137
+ }
138
+
139
+ function listQuery(options) {
140
+ return {
141
+ purpose: option(options, "purpose", { required: true }),
142
+ workspace_id: options["workspace-id"],
143
+ status: options.status,
144
+ limit: numberOption(options, "limit", 100, { maximum: 200 }),
145
+ cursor: options.cursor,
146
+ };
147
+ }
148
+
149
+ async function authLogin(options) {
150
+ const current = await readAdminCredential();
151
+ if (current && !options.force) {
152
+ const api = new ApiClient({ baseUrl: baseUrl(options, current), credential: current.key, userAgent: USER_AGENT });
153
+ const identity = await api.request("/me");
154
+ writeResult({ authenticated: true, source: current.source, identity }, { json: options.json });
155
+ return;
156
+ }
157
+ const requestedScopes = listOption(options, "scopes", [
158
+ "admin_data:read",
159
+ "admin_support:read",
160
+ "admin_costs:read",
161
+ ]);
162
+ if (requestedScopes.includes("admin_content:read") && !options.force) {
163
+ throw new CliError(
164
+ "Requesting admin_content:read requires --force and should be limited to a specific Run investigation.",
165
+ { code: "admin_content_scope_confirmation_required" },
166
+ );
167
+ }
168
+ const api = new ApiClient({ baseUrl: baseUrl(options), userAgent: USER_AGENT });
169
+ const authorization = await api.request("/admin/auth/device-authorizations", {
170
+ method: "POST",
171
+ auth: false,
172
+ body: {
173
+ client_name: "Corvio Platform Administrator CLI",
174
+ cli_version: VERSION,
175
+ scopes: requestedScopes,
176
+ },
177
+ });
178
+ process.stderr.write(`Open ${authorization.verification_uri_complete}\nCode: ${authorization.user_code}\n`);
179
+ if (!options["no-input"]) await openBrowser(authorization.verification_uri_complete);
180
+
181
+ const expiresAt = Date.now() + Number(authorization.expires_in || 600) * 1000;
182
+ let intervalSeconds = Math.max(5, Number(authorization.interval || 5));
183
+ let token;
184
+ while (Date.now() < expiresAt) {
185
+ await sleep(intervalSeconds * 1000);
186
+ try {
187
+ token = await api.request("/admin/auth/device-token", {
188
+ method: "POST",
189
+ auth: false,
190
+ body: { device_code: authorization.device_code },
191
+ });
192
+ break;
193
+ } catch (error) {
194
+ if (!(error instanceof CliError)) throw error;
195
+ if (error.code === "authorization_pending") continue;
196
+ if (error.code === "slow_down") {
197
+ intervalSeconds = Math.min(30, intervalSeconds + 5);
198
+ continue;
199
+ }
200
+ throw error;
201
+ }
202
+ }
203
+ if (!token?.secret || token.audience !== "platform_admin") {
204
+ throw new CliError("Administrator device authorization expired; run `corvio-admin auth login` again.", {
205
+ code: "expired_token",
206
+ exitCode: EXIT.AUTH,
207
+ });
208
+ }
209
+ await storeAdminCredential({ ...token, api_base: baseUrl(options) });
210
+ const authenticatedApi = new ApiClient({ baseUrl: baseUrl(options), credential: token.secret, userAgent: USER_AGENT });
211
+ const identity = await authenticatedApi.request("/me");
212
+ writeResult({
213
+ authenticated: true,
214
+ source: "device_link",
215
+ audience: identity.api_key?.audience,
216
+ scopes: identity.api_key?.scopes || [],
217
+ expires_at: token.expires_at,
218
+ }, { json: options.json });
219
+ }
220
+
221
+ async function authStatus(options) {
222
+ const credential = await readAdminCredential();
223
+ if (!credential) {
224
+ writeResult({ authenticated: false, sources: ["CORVIO_ADMIN_API_KEY", "device_link"] }, { json: options.json });
225
+ return;
226
+ }
227
+ const api = new ApiClient({ baseUrl: baseUrl(options, credential), credential: credential.key, userAgent: USER_AGENT });
228
+ const identity = await api.request("/me");
229
+ if (identity.api_key?.audience !== "platform_admin") {
230
+ throw new CliError("The configured key is not a platform administrator credential.", {
231
+ code: "admin_audience_required",
232
+ exitCode: EXIT.AUTH,
233
+ });
234
+ }
235
+ writeResult({ authenticated: true, source: credential.source, identity }, { json: options.json });
236
+ }
237
+
238
+ async function authLogout(options) {
239
+ const credential = await readAdminCredential();
240
+ if (!credential) {
241
+ writeResult({ authenticated: false, changed: false }, { json: options.json });
242
+ return;
243
+ }
244
+ if (credential.source === "environment") {
245
+ throw new CliError("CORVIO_ADMIN_API_KEY is managed by the parent environment and cannot be removed by the CLI.", {
246
+ code: "environment_credential_managed_externally",
247
+ exitCode: EXIT.AUTH,
248
+ });
249
+ }
250
+ const api = new ApiClient({ baseUrl: baseUrl(options, credential), credential: credential.key, userAgent: USER_AGENT });
251
+ await api.request("/admin/auth/credentials/current:revoke", { method: "POST" });
252
+ await clearAdminCredential();
253
+ writeResult({ authenticated: false, revoked: true }, { json: options.json });
254
+ }
255
+
256
+ async function capabilities(options) {
257
+ const { api, credential } = await client(options);
258
+ const [index, identity] = await Promise.all([api.request("/admin"), api.request("/me")]);
259
+ writeResult({ cli_version: VERSION, auth_source: credential.source, api: index, principal: identity }, { json: options.json });
260
+ }
261
+
262
+ async function updateCheck(options) {
263
+ const { api } = await client(options);
264
+ const index = await api.request("/admin");
265
+ const result = await resolveNpmUpdateStatus({
266
+ currentVersion: VERSION,
267
+ policy: index?.distribution?.admin_cli || {
268
+ package: "@corvio/admin-cli",
269
+ dist_tag: "beta",
270
+ },
271
+ userAgent: USER_AGENT,
272
+ });
273
+ writeResult(result, { json: options.json });
274
+ }
275
+
276
+ async function catalog(options) {
277
+ const { api } = await client(options);
278
+ writeResult(await api.request("/admin/data/catalog"), { json: options.json });
279
+ }
280
+
281
+ async function query(options) {
282
+ let body;
283
+ if (options.file) {
284
+ try {
285
+ body = JSON.parse(await readTextFile(options.file));
286
+ } catch (error) {
287
+ if (error?.code === "ENOENT") throw error;
288
+ throw new CliError("--file must contain a valid JSON request object.", { code: "invalid_json_file" });
289
+ }
290
+ if (!body || Array.isArray(body) || typeof body !== "object") {
291
+ throw new CliError("--file must contain a JSON object.", { code: "invalid_json_file" });
292
+ }
293
+ if (options.purpose) body.purpose = options.purpose;
294
+ } else {
295
+ body = {
296
+ dataset: option(options, "dataset", { required: true }),
297
+ purpose: option(options, "purpose", { required: true }),
298
+ fields: listOption(options, "fields"),
299
+ group_by: listOption(options, "group-by"),
300
+ metrics: options.metrics ? parseJsonValue(options.metrics, "metrics") : [],
301
+ filters: options.filters ? parseJsonValue(options.filters, "filters") : [],
302
+ order_by: options["order-by"] ? parseJsonValue(options["order-by"], "order-by") : [],
303
+ limit: numberOption(options, "limit", 100, { maximum: 200 }),
304
+ cursor: options.cursor || null,
305
+ };
306
+ }
307
+ const { api } = await client(options);
308
+ writeResult(await api.request("/admin/data/query", { method: "POST", body }), { json: options.json });
309
+ }
310
+
311
+ async function listResource(options, resource) {
312
+ const { api } = await client(options);
313
+ writeResult(await api.request(`/admin/${resource}`, { query: listQuery(options) }), { json: options.json });
314
+ }
315
+
316
+ async function downloadSkill(options) {
317
+ const { api } = await client(options);
318
+ const output = String(options.output || "corvio-admin-data.zip");
319
+ const manifest = await api.request("/admin/skill/manifest.json");
320
+ const bytes = await api.requestBytes("/admin/skill/corvio-admin-data.zip");
321
+ const actualHash = createHash("sha256").update(bytes).digest("hex");
322
+ if (!manifest.archive_sha256 || actualHash !== manifest.archive_sha256) {
323
+ throw new CliError("Administrator Skill archive did not match its manifest SHA-256.", {
324
+ code: "admin_skill_integrity_mismatch",
325
+ exitCode: EXIT.REMOTE,
326
+ });
327
+ }
328
+ const path = await writeBinaryFile(output, bytes);
329
+ writeResult({
330
+ downloaded: true,
331
+ output: path,
332
+ install: `npx skills add ${path}`,
333
+ }, { json: options.json });
334
+ }
335
+
336
+ async function main({ options, positionals }) {
337
+ const [command, action, id] = positionals;
338
+ if (options.version || command === "version") return writeResult(VERSION, { json: options.json });
339
+ if (options.help || !command || command === "help") return writeResult(HELP.trimEnd(), { json: false });
340
+ validateInvocation(options, positionals);
341
+ if (command === "auth") {
342
+ if (action === "login") return authLogin(options);
343
+ if (action === "status") return authStatus(options);
344
+ if (action === "logout") return authLogout(options);
345
+ throw new CliError("auth action must be login, status, or logout.");
346
+ }
347
+ if (command === "capabilities") return capabilities(options);
348
+ if (command === "update" && (action || "check") === "check") return updateCheck(options);
349
+ if (command === "skill" && action === "download") return downloadSkill(options);
350
+ if (command === "catalog") return catalog(options);
351
+ if (command === "query") return query(options);
352
+ if (command === "data" && action === "query") return query(options);
353
+ if (command === "workspaces" && (action || "list") === "list") return listResource(options, "workspaces");
354
+ if (command === "users" && (action || "list") === "list") return listResource(options, "users");
355
+ if (command === "worker-jobs" && (action || "list") === "list") return listResource(options, "worker-jobs");
356
+ if (command === "runs" && action === "list") return listResource(options, "runs");
357
+ if (command === "runs" && action === "content") {
358
+ const runId = id || option(options, "id", { required: true });
359
+ const purpose = option(options, "purpose", { required: true });
360
+ const { api } = await client(options);
361
+ return writeResult(await api.request(`/admin/runs/${resourcePath(runId)}/content`, { query: { purpose } }), { json: options.json });
362
+ }
363
+ if (command === "usage" && action === "events") return listResource(options, "usage/events");
364
+ if (command === "usage" && action === "summary") {
365
+ const { api } = await client(options);
366
+ return writeResult(await api.request("/admin/usage/summary", { query: listQuery(options) }), { json: options.json });
367
+ }
368
+ if (command === "receipts" && action === "get") {
369
+ const receiptId = id || option(options, "id", { required: true });
370
+ const { api } = await client(options);
371
+ return writeResult(await api.request(`/admin/data/query-receipts/${resourcePath(receiptId)}`), { json: options.json });
372
+ }
373
+ throw new CliError(`Unknown administrator command: ${positionals.join(" ")}`);
374
+ }
375
+
376
+ await runMain(main);