@corvio/admin-cli 0.1.0-beta.2 → 0.1.0-beta.4
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 +5 -5
- package/dist/cli.js +172 -20
- package/dist/core.js +315 -33
- package/package.json +1 -1
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
|
-
|
|
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,32 @@ 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,
|
|
15
|
+
resolveNpmUpdateStatus,
|
|
13
16
|
runMain,
|
|
17
|
+
sleep,
|
|
18
|
+
storeAdminCredential,
|
|
19
|
+
writeBinaryFile,
|
|
14
20
|
writeResult,
|
|
15
21
|
} from "./core.js";
|
|
16
22
|
|
|
17
|
-
const VERSION = "0.1.0-beta.
|
|
23
|
+
const VERSION = "0.1.0-beta.4";
|
|
18
24
|
const DEFAULT_API_BASE = "https://api.corvio.ai/v1";
|
|
19
25
|
const USER_AGENT = `corvio-admin-cli/${VERSION}`;
|
|
20
26
|
|
|
21
27
|
const HELP = `Corvio Platform Administrator CLI ${VERSION}
|
|
22
28
|
|
|
23
29
|
Usage:
|
|
24
|
-
corvio-admin auth status
|
|
30
|
+
corvio-admin auth login|status|logout
|
|
25
31
|
corvio-admin capabilities
|
|
32
|
+
corvio-admin skill download [--output <path>]
|
|
26
33
|
corvio-admin catalog
|
|
27
34
|
corvio-admin query|data query --file <request.json>
|
|
28
35
|
corvio-admin workspaces list --purpose <reason>
|
|
@@ -31,26 +38,37 @@ Usage:
|
|
|
31
38
|
corvio-admin worker-jobs list --purpose <reason>
|
|
32
39
|
corvio-admin usage events|summary --purpose <reason>
|
|
33
40
|
corvio-admin receipts get <id>
|
|
41
|
+
corvio-admin update check
|
|
34
42
|
|
|
35
43
|
Authentication:
|
|
36
|
-
|
|
44
|
+
CORVIO_ADMIN_API_KEY=cva_... takes precedence over the stored device-link login.
|
|
45
|
+
Run 'corvio-admin auth login' when no environment key is configured.
|
|
37
46
|
|
|
38
47
|
This package contains no Workspace ask, search, docs, files, sync, or share commands.
|
|
39
48
|
`;
|
|
40
49
|
|
|
41
|
-
function baseUrl(options) {
|
|
42
|
-
return normalizeApiBase(
|
|
50
|
+
function baseUrl(options, credential = null) {
|
|
51
|
+
return normalizeApiBase(
|
|
52
|
+
options["api-base"]
|
|
53
|
+
|| process.env.CORVIO_ADMIN_API_BASE
|
|
54
|
+
|| process.env.CORVIO_API_BASE
|
|
55
|
+
|| credential?.metadata?.api_base,
|
|
56
|
+
DEFAULT_API_BASE,
|
|
57
|
+
);
|
|
43
58
|
}
|
|
44
59
|
|
|
45
|
-
function client(options) {
|
|
46
|
-
const credential = readAdminCredential();
|
|
60
|
+
async function client(options) {
|
|
61
|
+
const credential = await readAdminCredential();
|
|
47
62
|
if (!credential) {
|
|
48
|
-
throw new CliError("Set CORVIO_ADMIN_API_KEY
|
|
63
|
+
throw new CliError("Set CORVIO_ADMIN_API_KEY or run `corvio-admin auth login`.", {
|
|
49
64
|
code: "admin_auth_required",
|
|
50
65
|
exitCode: EXIT.AUTH,
|
|
51
66
|
});
|
|
52
67
|
}
|
|
53
|
-
return
|
|
68
|
+
return {
|
|
69
|
+
api: new ApiClient({ baseUrl: baseUrl(options, credential), credential: credential.key, userAgent: USER_AGENT }),
|
|
70
|
+
credential,
|
|
71
|
+
};
|
|
54
72
|
}
|
|
55
73
|
|
|
56
74
|
function listQuery(options) {
|
|
@@ -63,8 +81,85 @@ function listQuery(options) {
|
|
|
63
81
|
};
|
|
64
82
|
}
|
|
65
83
|
|
|
84
|
+
async function authLogin(options) {
|
|
85
|
+
const current = await readAdminCredential();
|
|
86
|
+
if (current && !options.force) {
|
|
87
|
+
const api = new ApiClient({ baseUrl: baseUrl(options, current), credential: current.key, userAgent: USER_AGENT });
|
|
88
|
+
const identity = await api.request("/me");
|
|
89
|
+
writeResult({ authenticated: true, source: current.source, identity }, { json: options.json });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const requestedScopes = listOption(options, "scopes", [
|
|
93
|
+
"admin_data:read",
|
|
94
|
+
"admin_support:read",
|
|
95
|
+
"admin_costs:read",
|
|
96
|
+
]);
|
|
97
|
+
if (requestedScopes.includes("admin_content:read") && !options.force) {
|
|
98
|
+
throw new CliError(
|
|
99
|
+
"Requesting admin_content:read requires --force and should be limited to a specific Run investigation.",
|
|
100
|
+
{ code: "admin_content_scope_confirmation_required" },
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
const api = new ApiClient({ baseUrl: baseUrl(options), userAgent: USER_AGENT });
|
|
104
|
+
const authorization = await api.request("/admin/auth/device-authorizations", {
|
|
105
|
+
method: "POST",
|
|
106
|
+
auth: false,
|
|
107
|
+
body: {
|
|
108
|
+
client_name: "Corvio Platform Administrator CLI",
|
|
109
|
+
cli_version: VERSION,
|
|
110
|
+
scopes: requestedScopes,
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
process.stderr.write(`Open ${authorization.verification_uri_complete}\nCode: ${authorization.user_code}\n`);
|
|
114
|
+
if (!options["no-input"]) await openBrowser(authorization.verification_uri_complete);
|
|
115
|
+
|
|
116
|
+
const expiresAt = Date.now() + Number(authorization.expires_in || 600) * 1000;
|
|
117
|
+
let intervalSeconds = Math.max(5, Number(authorization.interval || 5));
|
|
118
|
+
let token;
|
|
119
|
+
while (Date.now() < expiresAt) {
|
|
120
|
+
await sleep(intervalSeconds * 1000);
|
|
121
|
+
try {
|
|
122
|
+
token = await api.request("/admin/auth/device-token", {
|
|
123
|
+
method: "POST",
|
|
124
|
+
auth: false,
|
|
125
|
+
body: { device_code: authorization.device_code },
|
|
126
|
+
});
|
|
127
|
+
break;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (!(error instanceof CliError)) throw error;
|
|
130
|
+
if (error.code === "authorization_pending") continue;
|
|
131
|
+
if (error.code === "slow_down") {
|
|
132
|
+
intervalSeconds = Math.min(30, intervalSeconds + 5);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (!token?.secret || token.audience !== "platform_admin") {
|
|
139
|
+
throw new CliError("Administrator device authorization expired; run `corvio-admin auth login` again.", {
|
|
140
|
+
code: "expired_token",
|
|
141
|
+
exitCode: EXIT.AUTH,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
await storeAdminCredential({ ...token, api_base: baseUrl(options) });
|
|
145
|
+
const authenticatedApi = new ApiClient({ baseUrl: baseUrl(options), credential: token.secret, userAgent: USER_AGENT });
|
|
146
|
+
const identity = await authenticatedApi.request("/me");
|
|
147
|
+
writeResult({
|
|
148
|
+
authenticated: true,
|
|
149
|
+
source: "device_link",
|
|
150
|
+
audience: identity.api_key?.audience,
|
|
151
|
+
scopes: identity.api_key?.scopes || [],
|
|
152
|
+
expires_at: token.expires_at,
|
|
153
|
+
}, { json: options.json });
|
|
154
|
+
}
|
|
155
|
+
|
|
66
156
|
async function authStatus(options) {
|
|
67
|
-
const
|
|
157
|
+
const credential = await readAdminCredential();
|
|
158
|
+
if (!credential) {
|
|
159
|
+
writeResult({ authenticated: false, sources: ["CORVIO_ADMIN_API_KEY", "device_link"] }, { json: options.json });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const api = new ApiClient({ baseUrl: baseUrl(options, credential), credential: credential.key, userAgent: USER_AGENT });
|
|
68
163
|
const identity = await api.request("/me");
|
|
69
164
|
if (identity.api_key?.audience !== "platform_admin") {
|
|
70
165
|
throw new CliError("The configured key is not a platform administrator credential.", {
|
|
@@ -72,17 +167,50 @@ async function authStatus(options) {
|
|
|
72
167
|
exitCode: EXIT.AUTH,
|
|
73
168
|
});
|
|
74
169
|
}
|
|
75
|
-
writeResult({ authenticated: true, source:
|
|
170
|
+
writeResult({ authenticated: true, source: credential.source, identity }, { json: options.json });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function authLogout(options) {
|
|
174
|
+
const credential = await readAdminCredential();
|
|
175
|
+
if (!credential) {
|
|
176
|
+
writeResult({ authenticated: false, changed: false }, { json: options.json });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (credential.source === "environment") {
|
|
180
|
+
throw new CliError("CORVIO_ADMIN_API_KEY is managed by the parent environment and cannot be removed by the CLI.", {
|
|
181
|
+
code: "environment_credential_managed_externally",
|
|
182
|
+
exitCode: EXIT.AUTH,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
const api = new ApiClient({ baseUrl: baseUrl(options, credential), credential: credential.key, userAgent: USER_AGENT });
|
|
186
|
+
await api.request("/admin/auth/credentials/current:revoke", { method: "POST" });
|
|
187
|
+
await clearAdminCredential();
|
|
188
|
+
writeResult({ authenticated: false, revoked: true }, { json: options.json });
|
|
76
189
|
}
|
|
77
190
|
|
|
78
191
|
async function capabilities(options) {
|
|
79
|
-
const api = client(options);
|
|
192
|
+
const { api, credential } = await client(options);
|
|
80
193
|
const [index, identity] = await Promise.all([api.request("/admin"), api.request("/me")]);
|
|
81
|
-
writeResult({ cli_version: VERSION, api: index, principal: identity }, { json: options.json });
|
|
194
|
+
writeResult({ cli_version: VERSION, auth_source: credential.source, api: index, principal: identity }, { json: options.json });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function updateCheck(options) {
|
|
198
|
+
const { api } = await client(options);
|
|
199
|
+
const index = await api.request("/admin");
|
|
200
|
+
const result = await resolveNpmUpdateStatus({
|
|
201
|
+
currentVersion: VERSION,
|
|
202
|
+
policy: index?.distribution?.admin_cli || {
|
|
203
|
+
package: "@corvio/admin-cli",
|
|
204
|
+
dist_tag: "beta",
|
|
205
|
+
},
|
|
206
|
+
userAgent: USER_AGENT,
|
|
207
|
+
});
|
|
208
|
+
writeResult(result, { json: options.json });
|
|
82
209
|
}
|
|
83
210
|
|
|
84
211
|
async function catalog(options) {
|
|
85
|
-
|
|
212
|
+
const { api } = await client(options);
|
|
213
|
+
writeResult(await api.request("/admin/data/catalog"), { json: options.json });
|
|
86
214
|
}
|
|
87
215
|
|
|
88
216
|
async function query(options) {
|
|
@@ -103,19 +231,40 @@ async function query(options) {
|
|
|
103
231
|
cursor: options.cursor || null,
|
|
104
232
|
};
|
|
105
233
|
}
|
|
106
|
-
|
|
234
|
+
const { api } = await client(options);
|
|
235
|
+
writeResult(await api.request("/admin/data/query", { method: "POST", body }), { json: options.json });
|
|
107
236
|
}
|
|
108
237
|
|
|
109
238
|
async function listResource(options, resource) {
|
|
110
|
-
|
|
239
|
+
const { api } = await client(options);
|
|
240
|
+
writeResult(await api.request(`/admin/${resource}`, { query: listQuery(options) }), { json: options.json });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function downloadSkill(options) {
|
|
244
|
+
const { api } = await client(options);
|
|
245
|
+
const output = String(options.output || "corvio-admin-data.zip");
|
|
246
|
+
const bytes = await api.requestBytes("/admin/skill/corvio-admin-data.zip");
|
|
247
|
+
const path = await writeBinaryFile(output, bytes);
|
|
248
|
+
writeResult({
|
|
249
|
+
downloaded: true,
|
|
250
|
+
output: path,
|
|
251
|
+
install: `npx skills add ${path}`,
|
|
252
|
+
}, { json: options.json });
|
|
111
253
|
}
|
|
112
254
|
|
|
113
255
|
async function main({ options, positionals }) {
|
|
114
256
|
const [command, action, id] = positionals;
|
|
115
257
|
if (options.version || command === "version") return writeResult(VERSION, { json: options.json });
|
|
116
258
|
if (options.help || !command || command === "help") return writeResult(HELP.trimEnd(), { json: false });
|
|
117
|
-
if (command === "auth"
|
|
259
|
+
if (command === "auth") {
|
|
260
|
+
if (action === "login") return authLogin(options);
|
|
261
|
+
if (action === "status") return authStatus(options);
|
|
262
|
+
if (action === "logout") return authLogout(options);
|
|
263
|
+
throw new CliError("auth action must be login, status, or logout.");
|
|
264
|
+
}
|
|
118
265
|
if (command === "capabilities") return capabilities(options);
|
|
266
|
+
if (command === "update" && (action || "check") === "check") return updateCheck(options);
|
|
267
|
+
if (command === "skill" && action === "download") return downloadSkill(options);
|
|
119
268
|
if (command === "catalog") return catalog(options);
|
|
120
269
|
if (command === "query") return query(options);
|
|
121
270
|
if (command === "data" && action === "query") return query(options);
|
|
@@ -126,15 +275,18 @@ async function main({ options, positionals }) {
|
|
|
126
275
|
if (command === "runs" && action === "content") {
|
|
127
276
|
const runId = id || option(options, "id", { required: true });
|
|
128
277
|
const purpose = option(options, "purpose", { required: true });
|
|
129
|
-
|
|
278
|
+
const { api } = await client(options);
|
|
279
|
+
return writeResult(await api.request(`/admin/runs/${runId}/content`, { query: { purpose } }), { json: options.json });
|
|
130
280
|
}
|
|
131
281
|
if (command === "usage" && action === "events") return listResource(options, "usage/events");
|
|
132
282
|
if (command === "usage" && action === "summary") {
|
|
133
|
-
|
|
283
|
+
const { api } = await client(options);
|
|
284
|
+
return writeResult(await api.request("/admin/usage/summary", { query: listQuery(options) }), { json: options.json });
|
|
134
285
|
}
|
|
135
286
|
if (command === "receipts" && action === "get") {
|
|
136
287
|
const receiptId = id || option(options, "id", { required: true });
|
|
137
|
-
|
|
288
|
+
const { api } = await client(options);
|
|
289
|
+
return writeResult(await api.request(`/admin/data/query-receipts/${receiptId}`), { json: options.json });
|
|
138
290
|
}
|
|
139
291
|
throw new CliError(`Unknown administrator command: ${positionals.join(" ")}`);
|
|
140
292
|
}
|
package/dist/core.js
CHANGED
|
@@ -105,10 +105,92 @@ export function normalizeApiBase(value, fallback) {
|
|
|
105
105
|
return raw.endsWith("/v1") ? raw : `${raw}/v1`;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
function parseSemver(value) {
|
|
109
|
+
const match = String(value || "").trim().match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/);
|
|
110
|
+
if (!match) return null;
|
|
111
|
+
return {
|
|
112
|
+
core: match.slice(1, 4).map((item) => Number.parseInt(item, 10)),
|
|
113
|
+
pre: match[4] ? match[4].split(".") : [],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function compareVersions(left, right) {
|
|
118
|
+
const a = parseSemver(left);
|
|
119
|
+
const b = parseSemver(right);
|
|
120
|
+
if (!a || !b) return 0;
|
|
121
|
+
for (let index = 0; index < 3; index += 1) {
|
|
122
|
+
if (a.core[index] !== b.core[index]) return a.core[index] > b.core[index] ? 1 : -1;
|
|
123
|
+
}
|
|
124
|
+
if (a.pre.length === 0 && b.pre.length > 0) return 1;
|
|
125
|
+
if (b.pre.length === 0 && a.pre.length > 0) return -1;
|
|
126
|
+
for (let index = 0; index < Math.max(a.pre.length, b.pre.length); index += 1) {
|
|
127
|
+
if (a.pre[index] === undefined) return -1;
|
|
128
|
+
if (b.pre[index] === undefined) return 1;
|
|
129
|
+
if (a.pre[index] === b.pre[index]) continue;
|
|
130
|
+
const aNumber = /^\d+$/.test(a.pre[index]) ? Number.parseInt(a.pre[index], 10) : null;
|
|
131
|
+
const bNumber = /^\d+$/.test(b.pre[index]) ? Number.parseInt(b.pre[index], 10) : null;
|
|
132
|
+
if (aNumber !== null && bNumber !== null) return aNumber > bNumber ? 1 : -1;
|
|
133
|
+
if (aNumber !== null) return -1;
|
|
134
|
+
if (bNumber !== null) return 1;
|
|
135
|
+
return a.pre[index] > b.pre[index] ? 1 : -1;
|
|
136
|
+
}
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function resolveNpmUpdateStatus({
|
|
141
|
+
currentVersion,
|
|
142
|
+
policy,
|
|
143
|
+
userAgent,
|
|
144
|
+
allowRegistryFailure = false,
|
|
145
|
+
fetchImpl = globalThis.fetch,
|
|
146
|
+
registryBase = process.env.CORVIO_NPM_REGISTRY_BASE || "https://registry.npmjs.org",
|
|
147
|
+
}) {
|
|
148
|
+
const packageName = String(policy?.package || "").trim();
|
|
149
|
+
const distTag = String(policy?.dist_tag || "beta").trim();
|
|
150
|
+
if (!packageName) throw new CliError("The API did not advertise an npm package.", { code: "update_policy_invalid" });
|
|
151
|
+
const encoded = encodeURIComponent(packageName).replace("%40", "@");
|
|
152
|
+
let latestVersion = null;
|
|
153
|
+
let registryError = null;
|
|
154
|
+
try {
|
|
155
|
+
const registry = String(registryBase).replace(/\/+$/, "");
|
|
156
|
+
const response = await fetchImpl(`${registry}/${encoded}`, {
|
|
157
|
+
headers: { accept: "application/json", "user-agent": userAgent },
|
|
158
|
+
signal: AbortSignal.timeout(2500),
|
|
159
|
+
});
|
|
160
|
+
if (!response.ok) throw new Error(`npm registry returned ${response.status}`);
|
|
161
|
+
const metadata = await response.json();
|
|
162
|
+
latestVersion = String(metadata?.["dist-tags"]?.[distTag] || "").trim() || null;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
registryError = error instanceof Error ? error.message : String(error);
|
|
165
|
+
if (!allowRegistryFailure) {
|
|
166
|
+
throw new CliError(`Unable to check npm for updates: ${registryError}`, {
|
|
167
|
+
code: "update_check_failed",
|
|
168
|
+
exitCode: EXIT.REMOTE,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const minimum = String(policy.minimum_supported_version || "").trim() || null;
|
|
173
|
+
return {
|
|
174
|
+
package: packageName,
|
|
175
|
+
channel: String(policy.channel || distTag),
|
|
176
|
+
dist_tag: distTag,
|
|
177
|
+
current_version: currentVersion,
|
|
178
|
+
latest_published_version: latestVersion,
|
|
179
|
+
reviewed_version: String(policy.reviewed_version || "").trim() || null,
|
|
180
|
+
minimum_supported_version: minimum,
|
|
181
|
+
update_available: Boolean(latestVersion && compareVersions(latestVersion, currentVersion) > 0),
|
|
182
|
+
supported: minimum ? compareVersions(currentVersion, minimum) >= 0 : true,
|
|
183
|
+
install_command: String(policy.install_command || `npm install --global ${packageName}@${distTag}`),
|
|
184
|
+
automatic_install: false,
|
|
185
|
+
...(registryError ? { registry_error: registryError } : {}),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
108
189
|
export class ApiClient {
|
|
109
|
-
constructor({ baseUrl, credential = null, userAgent, fetchImpl = globalThis.fetch }) {
|
|
190
|
+
constructor({ baseUrl, credential = null, workspaceId = null, userAgent, fetchImpl = globalThis.fetch }) {
|
|
110
191
|
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
111
192
|
this.credential = credential;
|
|
193
|
+
this.workspaceId = String(workspaceId || "").trim() || null;
|
|
112
194
|
this.userAgent = userAgent;
|
|
113
195
|
this.fetchImpl = fetchImpl;
|
|
114
196
|
}
|
|
@@ -129,6 +211,10 @@ export class ApiClient {
|
|
|
129
211
|
throw new CliError("Corvio authentication is required.", { code: "auth_required", exitCode: EXIT.AUTH });
|
|
130
212
|
}
|
|
131
213
|
requestHeaders.authorization = `Bearer ${this.credential}`;
|
|
214
|
+
if (this.workspaceId) {
|
|
215
|
+
requestHeaders["X-Corvio-Workspace-Id"] = this.workspaceId;
|
|
216
|
+
requestHeaders["X-Corvio-Workspace-Context-Contract"] = "required-v1";
|
|
217
|
+
}
|
|
132
218
|
}
|
|
133
219
|
let response;
|
|
134
220
|
try {
|
|
@@ -163,6 +249,80 @@ export class ApiClient {
|
|
|
163
249
|
}
|
|
164
250
|
return payload;
|
|
165
251
|
}
|
|
252
|
+
|
|
253
|
+
async requestBytes(path, { query, headers = {}, auth = true } = {}) {
|
|
254
|
+
const url = new URL(`${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`);
|
|
255
|
+
for (const [key, value] of Object.entries(query || {})) {
|
|
256
|
+
if (value !== undefined && value !== null && value !== "") url.searchParams.set(key, String(value));
|
|
257
|
+
}
|
|
258
|
+
const requestHeaders = {
|
|
259
|
+
accept: "application/zip, application/octet-stream",
|
|
260
|
+
"user-agent": this.userAgent,
|
|
261
|
+
...headers,
|
|
262
|
+
};
|
|
263
|
+
if (auth) {
|
|
264
|
+
if (!this.credential) {
|
|
265
|
+
throw new CliError("Corvio authentication is required.", { code: "auth_required", exitCode: EXIT.AUTH });
|
|
266
|
+
}
|
|
267
|
+
requestHeaders.authorization = `Bearer ${this.credential}`;
|
|
268
|
+
if (this.workspaceId) {
|
|
269
|
+
requestHeaders["X-Corvio-Workspace-Id"] = this.workspaceId;
|
|
270
|
+
requestHeaders["X-Corvio-Workspace-Context-Contract"] = "required-v1";
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
let response;
|
|
274
|
+
try {
|
|
275
|
+
response = await this.fetchImpl(url, { method: "GET", headers: requestHeaders });
|
|
276
|
+
} catch (cause) {
|
|
277
|
+
throw new CliError(`Unable to reach Corvio: ${cause instanceof Error ? cause.message : String(cause)}`, {
|
|
278
|
+
code: "network_error",
|
|
279
|
+
exitCode: EXIT.REMOTE,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
283
|
+
if (!response.ok) {
|
|
284
|
+
const text = new TextDecoder().decode(bytes);
|
|
285
|
+
let payload = {};
|
|
286
|
+
try {
|
|
287
|
+
payload = JSON.parse(text);
|
|
288
|
+
} catch {
|
|
289
|
+
payload = { message: text.slice(0, 500) };
|
|
290
|
+
}
|
|
291
|
+
const envelope = payload?.error || payload?.detail || payload || {};
|
|
292
|
+
throw new CliError(String(envelope.message || envelope.code || `Corvio request failed (${response.status}).`), {
|
|
293
|
+
code: String(envelope.code || `http_${response.status}`),
|
|
294
|
+
status: response.status,
|
|
295
|
+
exitCode: exitCodeForStatus(response.status),
|
|
296
|
+
details: envelope.details || null,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
return bytes;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async putUrl(url, content, { headers = {} } = {}) {
|
|
303
|
+
let response;
|
|
304
|
+
try {
|
|
305
|
+
response = await this.fetchImpl(String(url), {
|
|
306
|
+
method: "PUT",
|
|
307
|
+
headers,
|
|
308
|
+
body: content,
|
|
309
|
+
});
|
|
310
|
+
} catch (cause) {
|
|
311
|
+
throw new CliError(`Unable to upload the file: ${cause instanceof Error ? cause.message : String(cause)}`, {
|
|
312
|
+
code: "upload_network_error",
|
|
313
|
+
exitCode: EXIT.REMOTE,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
if (!response.ok) {
|
|
317
|
+
const message = (await response.text()).slice(0, 500);
|
|
318
|
+
throw new CliError(message || `File upload failed (${response.status}).`, {
|
|
319
|
+
code: "upload_failed",
|
|
320
|
+
status: response.status,
|
|
321
|
+
exitCode: EXIT.REMOTE,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
return { ok: true, status: response.status };
|
|
325
|
+
}
|
|
166
326
|
}
|
|
167
327
|
|
|
168
328
|
function configRoot(env = process.env) {
|
|
@@ -175,11 +335,19 @@ export function workspaceCredentialPath(env = process.env) {
|
|
|
175
335
|
return join(configRoot(env), "credentials.json");
|
|
176
336
|
}
|
|
177
337
|
|
|
338
|
+
export function workspaceContextPath(env = process.env) {
|
|
339
|
+
return join(configRoot(env), "workspace-context.json");
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function adminCredentialPath(env = process.env) {
|
|
343
|
+
return join(configRoot(env), "admin-credentials.json");
|
|
344
|
+
}
|
|
345
|
+
|
|
178
346
|
export async function readWorkspaceCredential(env = process.env) {
|
|
179
347
|
const environmentKey = String(env.CORVIO_API_KEY || "").trim();
|
|
180
348
|
if (environmentKey) {
|
|
181
|
-
if (!environmentKey.startsWith("cvk_")) {
|
|
182
|
-
throw new CliError("CORVIO_API_KEY must be a Workspace cvk_ key.", {
|
|
349
|
+
if (!environmentKey.startsWith("cvu_") && !environmentKey.startsWith("cvk_")) {
|
|
350
|
+
throw new CliError("CORVIO_API_KEY must be a user cvu_ key or legacy Workspace cvk_ key.", {
|
|
183
351
|
code: "invalid_workspace_key",
|
|
184
352
|
exitCode: EXIT.AUTH,
|
|
185
353
|
});
|
|
@@ -196,14 +364,15 @@ export async function readWorkspaceCredential(env = process.env) {
|
|
|
196
364
|
}
|
|
197
365
|
const profileName = String(parsed.active_profile || "default");
|
|
198
366
|
const profile = parsed.profiles?.[profileName];
|
|
199
|
-
const
|
|
200
|
-
|
|
367
|
+
const metadata = String(parsed.api_key || "").trim() ? parsed : profile;
|
|
368
|
+
const key = String(metadata?.api_key || "").trim();
|
|
369
|
+
if (!key.startsWith("cvu_") && !key.startsWith("cvk_")) {
|
|
201
370
|
throw new CliError("Stored Corvio credentials are invalid; run `corvio auth login` again.", {
|
|
202
371
|
code: "credential_store_invalid",
|
|
203
372
|
exitCode: EXIT.AUTH,
|
|
204
373
|
});
|
|
205
374
|
}
|
|
206
|
-
return { key, source: "device_link", profile: profileName, metadata
|
|
375
|
+
return { key, source: "device_link", profile: profileName, metadata };
|
|
207
376
|
}
|
|
208
377
|
|
|
209
378
|
export async function storeWorkspaceCredential(payload, env = process.env) {
|
|
@@ -211,28 +380,73 @@ export async function storeWorkspaceCredential(payload, env = process.env) {
|
|
|
211
380
|
const root = dirname(path);
|
|
212
381
|
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
213
382
|
await chmod(root, 0o700).catch(() => {});
|
|
214
|
-
const
|
|
383
|
+
const document = {
|
|
384
|
+
version: 2,
|
|
385
|
+
api_key: payload.secret,
|
|
386
|
+
key_id: payload.key_id,
|
|
387
|
+
audience: payload.audience || "user",
|
|
388
|
+
scopes: payload.scopes || [],
|
|
389
|
+
expires_at: payload.expires_at || null,
|
|
390
|
+
created_at: payload.created_at || new Date().toISOString(),
|
|
391
|
+
source: "device_link",
|
|
392
|
+
};
|
|
393
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
394
|
+
await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
395
|
+
await chmod(temporary, 0o600);
|
|
396
|
+
await rename(temporary, path);
|
|
397
|
+
await chmod(path, 0o600);
|
|
398
|
+
return { path, profile: "default" };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export async function readWorkspaceContext(env = process.env) {
|
|
402
|
+
const environmentWorkspaceId = String(env.CORVIO_WORKSPACE_ID || "").trim();
|
|
403
|
+
if (environmentWorkspaceId) {
|
|
404
|
+
return { workspaceId: environmentWorkspaceId, workspaceName: null, source: "environment" };
|
|
405
|
+
}
|
|
406
|
+
const path = workspaceContextPath(env);
|
|
407
|
+
try {
|
|
408
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
409
|
+
const workspaceId = String(parsed.active_workspace_id || "").trim();
|
|
410
|
+
if (!workspaceId) return null;
|
|
411
|
+
return {
|
|
412
|
+
workspaceId,
|
|
413
|
+
workspaceName: String(parsed.active_workspace_name || "").trim() || null,
|
|
414
|
+
source: "stored",
|
|
415
|
+
};
|
|
416
|
+
} catch (cause) {
|
|
417
|
+
if (cause?.code === "ENOENT") return null;
|
|
418
|
+
throw new CliError("Stored Corvio Workspace selection is unreadable.", {
|
|
419
|
+
code: "workspace_context_store_invalid",
|
|
420
|
+
exitCode: EXIT.AUTH,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export async function storeWorkspaceContext({ workspaceId, workspaceName = null }, env = process.env) {
|
|
426
|
+
const normalizedId = String(workspaceId || "").trim();
|
|
427
|
+
if (!normalizedId) throw new CliError("Workspace ID is required.", { code: "workspace_selection_required" });
|
|
428
|
+
const path = workspaceContextPath(env);
|
|
429
|
+
const root = dirname(path);
|
|
430
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
431
|
+
await chmod(root, 0o700).catch(() => {});
|
|
215
432
|
const document = {
|
|
216
433
|
version: 1,
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
api_key: payload.secret,
|
|
221
|
-
key_id: payload.key_id,
|
|
222
|
-
workspace_id: payload.workspace_id,
|
|
223
|
-
workspace_name: payload.workspace_name,
|
|
224
|
-
scopes: payload.scopes || [],
|
|
225
|
-
created_at: payload.created_at || new Date().toISOString(),
|
|
226
|
-
source: "device_link",
|
|
227
|
-
},
|
|
228
|
-
},
|
|
434
|
+
active_workspace_id: normalizedId,
|
|
435
|
+
active_workspace_name: String(workspaceName || "").trim() || null,
|
|
436
|
+
updated_at: new Date().toISOString(),
|
|
229
437
|
};
|
|
230
438
|
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
231
439
|
await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
232
440
|
await chmod(temporary, 0o600);
|
|
233
441
|
await rename(temporary, path);
|
|
234
442
|
await chmod(path, 0o600);
|
|
235
|
-
return { path,
|
|
443
|
+
return { path, workspaceId: normalizedId };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export async function clearWorkspaceContext(env = process.env) {
|
|
447
|
+
const path = workspaceContextPath(env);
|
|
448
|
+
await rm(path, { force: true });
|
|
449
|
+
return path;
|
|
236
450
|
}
|
|
237
451
|
|
|
238
452
|
export async function clearWorkspaceCredential(env = process.env) {
|
|
@@ -241,16 +455,66 @@ export async function clearWorkspaceCredential(env = process.env) {
|
|
|
241
455
|
return path;
|
|
242
456
|
}
|
|
243
457
|
|
|
244
|
-
export function readAdminCredential(env = process.env) {
|
|
458
|
+
export async function readAdminCredential(env = process.env) {
|
|
245
459
|
const key = String(env.CORVIO_ADMIN_API_KEY || "").trim();
|
|
246
|
-
if (
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
460
|
+
if (key) {
|
|
461
|
+
if (!key.startsWith("cva_")) {
|
|
462
|
+
throw new CliError("CORVIO_ADMIN_API_KEY must be a platform administrator cva_ key.", {
|
|
463
|
+
code: "invalid_admin_key",
|
|
464
|
+
exitCode: EXIT.AUTH,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
return { key, source: "environment", metadata: null };
|
|
468
|
+
}
|
|
469
|
+
const path = adminCredentialPath(env);
|
|
470
|
+
let parsed;
|
|
471
|
+
try {
|
|
472
|
+
parsed = JSON.parse(await readFile(path, "utf8"));
|
|
473
|
+
} catch (cause) {
|
|
474
|
+
if (cause?.code === "ENOENT") return null;
|
|
475
|
+
throw new CliError("Stored Corvio administrator credentials are unreadable.", {
|
|
476
|
+
code: "admin_credential_store_invalid",
|
|
250
477
|
exitCode: EXIT.AUTH,
|
|
251
478
|
});
|
|
252
479
|
}
|
|
253
|
-
|
|
480
|
+
const storedKey = String(parsed.api_key || "").trim();
|
|
481
|
+
if (!storedKey.startsWith("cva_")) {
|
|
482
|
+
throw new CliError("Stored Corvio administrator credentials are invalid; run `corvio-admin auth login` again.", {
|
|
483
|
+
code: "admin_credential_store_invalid",
|
|
484
|
+
exitCode: EXIT.AUTH,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
return { key: storedKey, source: "device_link", metadata: parsed };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export async function storeAdminCredential(payload, env = process.env) {
|
|
491
|
+
const path = adminCredentialPath(env);
|
|
492
|
+
const root = dirname(path);
|
|
493
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
494
|
+
await chmod(root, 0o700).catch(() => {});
|
|
495
|
+
const document = {
|
|
496
|
+
version: 1,
|
|
497
|
+
api_key: payload.secret,
|
|
498
|
+
key_id: payload.key_id,
|
|
499
|
+
audience: payload.audience,
|
|
500
|
+
api_base: payload.api_base,
|
|
501
|
+
scopes: payload.scopes || [],
|
|
502
|
+
expires_at: payload.expires_at,
|
|
503
|
+
created_at: payload.created_at || new Date().toISOString(),
|
|
504
|
+
source: "device_link",
|
|
505
|
+
};
|
|
506
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
507
|
+
await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
508
|
+
await chmod(temporary, 0o600);
|
|
509
|
+
await rename(temporary, path);
|
|
510
|
+
await chmod(path, 0o600);
|
|
511
|
+
return { path };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export async function clearAdminCredential(env = process.env) {
|
|
515
|
+
const path = adminCredentialPath(env);
|
|
516
|
+
await rm(path, { force: true });
|
|
517
|
+
return path;
|
|
254
518
|
}
|
|
255
519
|
|
|
256
520
|
export function writeResult(value, { json = false, stdout = process.stdout } = {}) {
|
|
@@ -289,13 +553,24 @@ export async function openBrowser(url) {
|
|
|
289
553
|
const target = String(url);
|
|
290
554
|
const command = platform() === "darwin" ? "open" : platform() === "win32" ? "cmd" : "xdg-open";
|
|
291
555
|
const args = platform() === "win32" ? ["/c", "start", "", target] : [target];
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
556
|
+
return new Promise((resolvePromise) => {
|
|
557
|
+
let settled = false;
|
|
558
|
+
const finish = (opened) => {
|
|
559
|
+
if (settled) return;
|
|
560
|
+
settled = true;
|
|
561
|
+
resolvePromise(opened);
|
|
562
|
+
};
|
|
563
|
+
try {
|
|
564
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
565
|
+
child.once("error", () => finish(false));
|
|
566
|
+
child.once("spawn", () => {
|
|
567
|
+
child.unref();
|
|
568
|
+
finish(true);
|
|
569
|
+
});
|
|
570
|
+
} catch {
|
|
571
|
+
finish(false);
|
|
572
|
+
}
|
|
573
|
+
});
|
|
299
574
|
}
|
|
300
575
|
|
|
301
576
|
export async function sleep(milliseconds) {
|
|
@@ -313,6 +588,13 @@ export async function writeTextFile(path, content) {
|
|
|
313
588
|
return target;
|
|
314
589
|
}
|
|
315
590
|
|
|
591
|
+
export async function writeBinaryFile(path, content) {
|
|
592
|
+
const target = resolve(String(path));
|
|
593
|
+
await mkdir(dirname(target), { recursive: true });
|
|
594
|
+
await writeFile(target, content);
|
|
595
|
+
return target;
|
|
596
|
+
}
|
|
597
|
+
|
|
316
598
|
export function requireConfirmation(options, action) {
|
|
317
599
|
if (!options.yes) {
|
|
318
600
|
throw new CliError(`${action} requires --yes.`, { code: "confirmation_required" });
|