@zksecurity/zkao-cli 0.5.1 → 0.11.0-next.2
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 +1 -0
- package/dist/index.js +377 -115
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ npx @zksecurity/zkao-cli --help
|
|
|
14
14
|
zkao login # browser approval; saves credentials
|
|
15
15
|
zkao repos
|
|
16
16
|
zkao scans launch --repo <repoId> --budget 500 --preset <ref>
|
|
17
|
+
zkao scans launch --repo <repoId> --preset "builtin:Diff Scan" --base main --commit <sha>
|
|
17
18
|
zkao scans wait <scanId> # block until the scan finishes
|
|
18
19
|
zkao scans cancel <scanId> # cancel a running or queued scan
|
|
19
20
|
zkao findings list --scan <scanId>
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,8 @@ import { createRequire } from "module";
|
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import {
|
|
8
8
|
ZkaoApiError,
|
|
9
|
-
ZkaoClient
|
|
9
|
+
ZkaoClient,
|
|
10
|
+
getTokenInfo
|
|
10
11
|
} from "@zksecurity/zkao-sdk";
|
|
11
12
|
|
|
12
13
|
// src/config.ts
|
|
@@ -39,18 +40,93 @@ function readFile() {
|
|
|
39
40
|
);
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
|
-
function
|
|
43
|
-
let
|
|
43
|
+
function readFileForWrite() {
|
|
44
|
+
let file;
|
|
44
45
|
try {
|
|
45
|
-
|
|
46
|
+
file = readFile();
|
|
46
47
|
} catch {
|
|
47
48
|
console.error(`zkao: replacing unparseable config at ${CONFIG_PATH}`);
|
|
48
|
-
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
if (file.projectId && file.token && !file.projects?.[file.projectId]) {
|
|
52
|
+
file.projects = {
|
|
53
|
+
...file.projects,
|
|
54
|
+
[file.projectId]: stripUndefinedProject({
|
|
55
|
+
token: file.token,
|
|
56
|
+
baseUrl: file.baseUrl
|
|
57
|
+
})
|
|
58
|
+
};
|
|
49
59
|
}
|
|
50
|
-
|
|
51
|
-
|
|
60
|
+
return file;
|
|
61
|
+
}
|
|
62
|
+
function writeFile(file) {
|
|
63
|
+
writeSecretFile(CONFIG_PATH, `${JSON.stringify(file, null, 2)}
|
|
52
64
|
`);
|
|
53
|
-
|
|
65
|
+
}
|
|
66
|
+
function activate(file, projectId, saved) {
|
|
67
|
+
file.projectId = projectId;
|
|
68
|
+
file.token = saved.token;
|
|
69
|
+
if (saved.baseUrl === void 0) {
|
|
70
|
+
delete file.baseUrl;
|
|
71
|
+
} else {
|
|
72
|
+
file.baseUrl = saved.baseUrl;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function writeConfig(update) {
|
|
76
|
+
const file = readFileForWrite();
|
|
77
|
+
const projectId = update.projectId ?? file.projectId;
|
|
78
|
+
const saved = update.projectId ? file.projects?.[update.projectId] : void 0;
|
|
79
|
+
if (update.token === void 0 && update.projectId !== void 0 && saved) {
|
|
80
|
+
activate(file, update.projectId, {
|
|
81
|
+
...saved,
|
|
82
|
+
baseUrl: update.baseUrl ?? saved.baseUrl
|
|
83
|
+
});
|
|
84
|
+
} else {
|
|
85
|
+
const switching = update.projectId !== void 0 && update.projectId !== file.projectId;
|
|
86
|
+
Object.assign(file, stripUndefined(update));
|
|
87
|
+
if (switching && update.token === void 0) {
|
|
88
|
+
delete file.token;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (projectId && file.token && file.projectId === projectId) {
|
|
92
|
+
file.projects = {
|
|
93
|
+
...file.projects,
|
|
94
|
+
[projectId]: {
|
|
95
|
+
...file.projects?.[projectId],
|
|
96
|
+
token: file.token,
|
|
97
|
+
baseUrl: file.baseUrl
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
writeFile(file);
|
|
102
|
+
return { token: file.token, projectId: file.projectId, baseUrl: file.baseUrl };
|
|
103
|
+
}
|
|
104
|
+
function saveProjectCredentials(projectId, saved) {
|
|
105
|
+
const file = readFileForWrite();
|
|
106
|
+
file.projects = { ...file.projects, [projectId]: stripUndefinedProject(saved) };
|
|
107
|
+
activate(file, projectId, saved);
|
|
108
|
+
writeFile(file);
|
|
109
|
+
}
|
|
110
|
+
function useProject(projectId) {
|
|
111
|
+
const file = readFileForWrite();
|
|
112
|
+
const saved = file.projects?.[projectId];
|
|
113
|
+
if (!saved) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`No saved credentials for project ${projectId}. Run \`zkao login --project ${projectId}\`.`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
activate(file, projectId, saved);
|
|
119
|
+
writeFile(file);
|
|
120
|
+
return saved;
|
|
121
|
+
}
|
|
122
|
+
function savedProjects() {
|
|
123
|
+
const file = readFile();
|
|
124
|
+
return { activeProjectId: file.projectId, projects: file.projects ?? {} };
|
|
125
|
+
}
|
|
126
|
+
function stripUndefinedProject(saved) {
|
|
127
|
+
return Object.fromEntries(
|
|
128
|
+
Object.entries(saved).filter(([, v]) => v !== void 0)
|
|
129
|
+
);
|
|
54
130
|
}
|
|
55
131
|
function writeSecretFile(path, contents) {
|
|
56
132
|
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
@@ -67,44 +143,84 @@ function stripUndefined(obj) {
|
|
|
67
143
|
}
|
|
68
144
|
function resolveConfig(flags) {
|
|
69
145
|
const file = readFile();
|
|
146
|
+
const projectId = flags.projectId ?? process.env.ZKAO_PROJECT_ID ?? file.projectId;
|
|
147
|
+
const saved = projectId ? file.projects?.[projectId] : void 0;
|
|
148
|
+
const activeMatches = file.projectId === void 0 || file.projectId === projectId;
|
|
70
149
|
return {
|
|
71
|
-
token: flags.token ?? process.env.ZKAO_API_TOKEN ?? file.token,
|
|
72
|
-
projectId
|
|
73
|
-
baseUrl: flags.baseUrl ?? resolveBaseUrlFromEnv() ??
|
|
150
|
+
token: flags.token ?? process.env.ZKAO_API_TOKEN ?? saved?.token ?? (activeMatches ? file.token : void 0),
|
|
151
|
+
projectId,
|
|
152
|
+
baseUrl: flags.baseUrl ?? resolveBaseUrlFromEnv() ?? // The top-level base URL is the active project's; a saved project
|
|
153
|
+
// carries its own (none means production).
|
|
154
|
+
(saved ? saved.baseUrl : file.baseUrl)
|
|
74
155
|
};
|
|
75
156
|
}
|
|
76
157
|
var PENDING_PATH = join(homedir(), ".zkao", "pending-login.json");
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
158
|
+
function isPendingLogin(value) {
|
|
159
|
+
if (typeof value !== "object" || value === null) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
const v = value;
|
|
163
|
+
return typeof v.deviceCode === "string" && typeof v.origin === "string" && typeof v.interval === "number" && typeof v.expiresAtMs === "number";
|
|
80
164
|
}
|
|
81
|
-
function
|
|
165
|
+
function readPendingLogins() {
|
|
82
166
|
let raw;
|
|
83
167
|
try {
|
|
84
168
|
raw = readFileSync(PENDING_PATH, "utf8");
|
|
85
169
|
} catch (err) {
|
|
86
170
|
if (err.code === "ENOENT") {
|
|
87
|
-
return
|
|
171
|
+
return [];
|
|
88
172
|
}
|
|
89
173
|
throw err;
|
|
90
174
|
}
|
|
175
|
+
let parsed;
|
|
91
176
|
try {
|
|
92
|
-
|
|
177
|
+
parsed = JSON.parse(raw);
|
|
93
178
|
} catch {
|
|
94
179
|
console.error(
|
|
95
180
|
`zkao: ignoring unparseable pending login at ${PENDING_PATH}`
|
|
96
181
|
);
|
|
97
|
-
return
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
if (isPendingLogin(parsed)) {
|
|
185
|
+
return [parsed];
|
|
98
186
|
}
|
|
187
|
+
const logins = parsed?.logins;
|
|
188
|
+
return logins && typeof logins === "object" ? Object.values(logins).filter(isPendingLogin) : [];
|
|
99
189
|
}
|
|
100
|
-
function
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
190
|
+
function writePendingLogins(logins) {
|
|
191
|
+
if (logins.length === 0) {
|
|
192
|
+
try {
|
|
193
|
+
unlinkSync(PENDING_PATH);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
if (err.code !== "ENOENT") {
|
|
196
|
+
throw err;
|
|
197
|
+
}
|
|
106
198
|
}
|
|
199
|
+
return;
|
|
107
200
|
}
|
|
201
|
+
const file = {
|
|
202
|
+
logins: Object.fromEntries(logins.map((l) => [l.deviceCode, l]))
|
|
203
|
+
};
|
|
204
|
+
writeSecretFile(PENDING_PATH, `${JSON.stringify(file, null, 2)}
|
|
205
|
+
`);
|
|
206
|
+
}
|
|
207
|
+
function addPendingLogin(pending) {
|
|
208
|
+
const now = Date.now();
|
|
209
|
+
writePendingLogins([
|
|
210
|
+
...readPendingLogins().filter(
|
|
211
|
+
(l) => l.expiresAtMs > now && l.deviceCode !== pending.deviceCode
|
|
212
|
+
),
|
|
213
|
+
pending
|
|
214
|
+
]);
|
|
215
|
+
}
|
|
216
|
+
function removePendingLogins(deviceCodes) {
|
|
217
|
+
const now = Date.now();
|
|
218
|
+
const drop = new Set(deviceCodes);
|
|
219
|
+
writePendingLogins(
|
|
220
|
+
readPendingLogins().filter(
|
|
221
|
+
(l) => l.expiresAtMs > now && !drop.has(l.deviceCode)
|
|
222
|
+
)
|
|
223
|
+
);
|
|
108
224
|
}
|
|
109
225
|
|
|
110
226
|
// src/login.ts
|
|
@@ -157,15 +273,39 @@ Approve only if you started this login.
|
|
|
157
273
|
`
|
|
158
274
|
);
|
|
159
275
|
}
|
|
276
|
+
function describeProject(projectId, names) {
|
|
277
|
+
if (!names.name) {
|
|
278
|
+
return projectId;
|
|
279
|
+
}
|
|
280
|
+
const org = names.organization ? ` of organization "${names.organization}"` : "";
|
|
281
|
+
return `"${names.name}"${org} (${projectId})`;
|
|
282
|
+
}
|
|
160
283
|
function persistCredentials(poll, baseUrl) {
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
284
|
+
const tokens = poll.tokens?.length ? poll.tokens : [poll];
|
|
285
|
+
for (const issued of [...tokens].reverse()) {
|
|
286
|
+
saveProjectCredentials(issued.projectId, {
|
|
287
|
+
token: issued.token,
|
|
288
|
+
baseUrl,
|
|
289
|
+
name: issued.projectName,
|
|
290
|
+
organization: issued.organizationName
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
const describe = (issued) => describeProject(issued.projectId, {
|
|
294
|
+
name: issued.projectName,
|
|
295
|
+
organization: issued.organizationName
|
|
165
296
|
});
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
297
|
+
if (tokens.length === 1) {
|
|
298
|
+
console.log(`Authorized for project ${describe(poll)}.`);
|
|
299
|
+
} else {
|
|
300
|
+
console.log(`Authorized for ${tokens.length} projects:`);
|
|
301
|
+
for (const issued of tokens) {
|
|
302
|
+
console.log(` ${describe(issued)}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
console.log(`Active project: ${describe(poll)}.`);
|
|
306
|
+
console.log(
|
|
307
|
+
"Credentials for other projects are kept. `zkao config show` lists them; `zkao config use <projectId>` switches."
|
|
308
|
+
);
|
|
169
309
|
}
|
|
170
310
|
async function login(flags) {
|
|
171
311
|
const cfg = resolveConfig({
|
|
@@ -175,98 +315,129 @@ async function login(flags) {
|
|
|
175
315
|
});
|
|
176
316
|
const apiBase = cfg.baseUrl ?? DEFAULT_BASE_URL;
|
|
177
317
|
const origin = appOrigin(apiBase);
|
|
318
|
+
const project = flags.project ?? process.env.ZKAO_PROJECT_ID;
|
|
178
319
|
const start = await postJson(`${origin}/api/auth/device`, {
|
|
179
|
-
label: hostname()
|
|
320
|
+
label: hostname(),
|
|
321
|
+
...project ? { project } : {}
|
|
180
322
|
});
|
|
181
323
|
printInstructions(start);
|
|
182
324
|
if (!flags.noBrowser) {
|
|
183
325
|
openBrowser(start.verificationUriComplete);
|
|
184
326
|
}
|
|
185
|
-
|
|
327
|
+
const pending = {
|
|
186
328
|
deviceCode: start.deviceCode,
|
|
329
|
+
userCode: start.userCode,
|
|
187
330
|
origin,
|
|
188
331
|
interval: Math.max(1, start.interval),
|
|
189
332
|
expiresAtMs: Date.now() + start.expiresIn * 1e3,
|
|
190
333
|
baseUrl: cfg.baseUrl
|
|
191
|
-
}
|
|
334
|
+
};
|
|
335
|
+
addPendingLogin(pending);
|
|
192
336
|
if (flags.wait === false) {
|
|
193
337
|
console.log(
|
|
194
338
|
"Once you approve in the browser, run `zkao login --resume` to finish.\n"
|
|
195
339
|
);
|
|
196
340
|
return;
|
|
197
341
|
}
|
|
198
|
-
await pollUntilDone(
|
|
199
|
-
origin,
|
|
200
|
-
deviceCode: start.deviceCode,
|
|
201
|
-
intervalMs: Math.max(1, start.interval) * 1e3,
|
|
202
|
-
deadlineMs: Date.now() + start.expiresIn * 1e3,
|
|
203
|
-
baseUrl: cfg.baseUrl
|
|
204
|
-
});
|
|
342
|
+
await pollUntilDone([pending], pending.expiresAtMs);
|
|
205
343
|
}
|
|
206
344
|
async function resumeLogin(flags) {
|
|
207
|
-
const
|
|
208
|
-
|
|
345
|
+
const all = readPendingLogins();
|
|
346
|
+
const now = Date.now();
|
|
347
|
+
const live = all.filter((l) => l.expiresAtMs > now);
|
|
348
|
+
if (live.length < all.length) {
|
|
349
|
+
removePendingLogins([]);
|
|
350
|
+
}
|
|
351
|
+
if (live.length === 0) {
|
|
209
352
|
throw new Error(
|
|
210
|
-
"No pending login. Run `zkao login --no-wait` to start one first."
|
|
353
|
+
all.length > 0 ? "The login request expired. Run `zkao login` again." : "No pending login. Run `zkao login --no-wait` to start one first."
|
|
211
354
|
);
|
|
212
355
|
}
|
|
213
|
-
if (Date.now() >= pending.expiresAtMs) {
|
|
214
|
-
clearPendingLogin();
|
|
215
|
-
throw new Error("The login request expired. Run `zkao login` again.");
|
|
216
|
-
}
|
|
217
356
|
const waitMs = (flags.timeout ?? 0) * 1e3;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
deviceCode: pending.deviceCode,
|
|
221
|
-
intervalMs: Math.max(1, pending.interval) * 1e3,
|
|
222
|
-
deadlineMs: Math.min(pending.expiresAtMs, Date.now() + waitMs),
|
|
223
|
-
baseUrl: pending.baseUrl,
|
|
224
|
-
reportStillPending: true
|
|
225
|
-
});
|
|
357
|
+
const lastExpiry = Math.max(...live.map((l) => l.expiresAtMs));
|
|
358
|
+
await pollUntilDone(live, Math.min(lastExpiry, Date.now() + waitMs), true);
|
|
226
359
|
}
|
|
227
|
-
|
|
228
|
-
|
|
360
|
+
function label(pending) {
|
|
361
|
+
return pending.userCode ? `code ${pending.userCode}` : "an earlier login";
|
|
362
|
+
}
|
|
363
|
+
async function pollUntilDone(logins, deadlineMs, reportStillPending = false) {
|
|
364
|
+
let open = [...logins];
|
|
365
|
+
let intervalMs = Math.min(...open.map((l) => Math.max(1, l.interval))) * 1e3;
|
|
366
|
+
let lastFailure = "The login request expired. Run `zkao login` again.";
|
|
229
367
|
let polled = false;
|
|
230
|
-
|
|
368
|
+
while (open.length > 0) {
|
|
231
369
|
if (polled) {
|
|
232
|
-
const remaining =
|
|
370
|
+
const remaining = deadlineMs - Date.now();
|
|
233
371
|
if (remaining <= 0) {
|
|
234
372
|
break;
|
|
235
373
|
}
|
|
236
374
|
await sleep(Math.min(intervalMs, remaining));
|
|
237
375
|
}
|
|
238
376
|
polled = true;
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
377
|
+
const resolved = [];
|
|
378
|
+
const dropped = [];
|
|
379
|
+
for (const pending of open) {
|
|
380
|
+
if (Date.now() >= pending.expiresAtMs) {
|
|
381
|
+
resolved.push(pending.deviceCode);
|
|
382
|
+
dropped.push(`${label(pending)} expired`);
|
|
383
|
+
lastFailure = "The login request expired. Run `zkao login` again.";
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
const poll = await postJson(
|
|
387
|
+
`${pending.origin}/api/auth/device/token`,
|
|
388
|
+
{ deviceCode: pending.deviceCode }
|
|
389
|
+
);
|
|
390
|
+
switch (poll.status) {
|
|
391
|
+
case "pending":
|
|
392
|
+
break;
|
|
393
|
+
case "slow_down":
|
|
247
394
|
intervalMs += 2e3;
|
|
395
|
+
break;
|
|
396
|
+
case "denied":
|
|
397
|
+
resolved.push(pending.deviceCode);
|
|
398
|
+
dropped.push(`${label(pending)} was denied`);
|
|
399
|
+
lastFailure = "Authorization was denied in the browser.";
|
|
400
|
+
break;
|
|
401
|
+
case "expired":
|
|
402
|
+
resolved.push(pending.deviceCode);
|
|
403
|
+
dropped.push(`${label(pending)} expired`);
|
|
404
|
+
lastFailure = "The login request expired. Run `zkao login` again.";
|
|
405
|
+
break;
|
|
406
|
+
case "approved": {
|
|
407
|
+
removePendingLogins([...resolved, pending.deviceCode]);
|
|
408
|
+
persistCredentials(poll, pending.baseUrl);
|
|
409
|
+
const rest = open.filter(
|
|
410
|
+
(l) => l !== pending && !resolved.includes(l.deviceCode)
|
|
411
|
+
);
|
|
412
|
+
if (rest.length > 0) {
|
|
413
|
+
console.log(
|
|
414
|
+
`Still pending: ${rest.map(label).join(", ")}. Run \`zkao login --resume\` again to finish ${rest.length > 1 ? "them" : "it"}.`
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
return;
|
|
248
418
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
`Unexpected device status: ${JSON.stringify(_exhaustive)}`
|
|
263
|
-
);
|
|
419
|
+
default: {
|
|
420
|
+
const _exhaustive = poll;
|
|
421
|
+
throw new Error(
|
|
422
|
+
`Unexpected device status: ${JSON.stringify(_exhaustive)}`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (resolved.length > 0) {
|
|
428
|
+
removePendingLogins(resolved);
|
|
429
|
+
open = open.filter((l) => !resolved.includes(l.deviceCode));
|
|
430
|
+
if (open.length > 0) {
|
|
431
|
+
console.error(`zkao: dropped pending login: ${dropped.join(", ")}.`);
|
|
264
432
|
}
|
|
265
433
|
}
|
|
266
434
|
}
|
|
267
|
-
if (
|
|
435
|
+
if (open.length === 0) {
|
|
436
|
+
throw new Error(lastFailure);
|
|
437
|
+
}
|
|
438
|
+
if (reportStillPending) {
|
|
268
439
|
console.log(
|
|
269
|
-
"Still waiting for approval. Approve in the browser, then run `zkao login --resume` again."
|
|
440
|
+
open.length > 1 ? `Still waiting for approval of ${open.length} logins (${open.map(label).join(", ")}). Approve in the browser, then run \`zkao login --resume\` again.` : "Still waiting for approval. Approve in the browser, then run `zkao login --resume` again."
|
|
270
441
|
);
|
|
271
442
|
return;
|
|
272
443
|
}
|
|
@@ -280,6 +451,9 @@ var CACHE_PATH = join2(dirname2(configPath()), "update-check.json");
|
|
|
280
451
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
281
452
|
var REGISTRY_TIMEOUT_MS = 1500;
|
|
282
453
|
var PACKAGE = "@zksecurity/zkao-cli";
|
|
454
|
+
function channelOf(version2) {
|
|
455
|
+
return version2.includes("-") ? "next" : "latest";
|
|
456
|
+
}
|
|
283
457
|
function debug(message, err) {
|
|
284
458
|
if (process.env.ZKAO_DEBUG) {
|
|
285
459
|
console.error(`zkao: ${message}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -288,7 +462,7 @@ function debug(message, err) {
|
|
|
288
462
|
function disabled() {
|
|
289
463
|
return Boolean(process.env.ZKAO_NO_UPDATE_CHECK);
|
|
290
464
|
}
|
|
291
|
-
function readCache() {
|
|
465
|
+
function readCache(channel) {
|
|
292
466
|
let raw;
|
|
293
467
|
try {
|
|
294
468
|
raw = readFileSync2(CACHE_PATH, "utf8");
|
|
@@ -300,7 +474,8 @@ function readCache() {
|
|
|
300
474
|
}
|
|
301
475
|
try {
|
|
302
476
|
const parsed = JSON.parse(raw);
|
|
303
|
-
|
|
477
|
+
const valid = typeof parsed.latest === "string" && typeof parsed.checkedAtMs === "number";
|
|
478
|
+
return valid && (parsed.channel ?? "latest") === channel ? parsed : null;
|
|
304
479
|
} catch (err) {
|
|
305
480
|
debug("ignoring an unparseable update cache", err);
|
|
306
481
|
return null;
|
|
@@ -317,47 +492,53 @@ function writeCache(cache) {
|
|
|
317
492
|
}
|
|
318
493
|
function isNewerVersion(latest, current) {
|
|
319
494
|
const parse = (v) => {
|
|
320
|
-
const match = /^(\d+)\.(\d+)\.(\d+)
|
|
321
|
-
return match ?
|
|
495
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-next\.(\d+))?$/.exec(v.trim());
|
|
496
|
+
return match ? {
|
|
497
|
+
core: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
498
|
+
pre: match[4] === void 0 ? null : Number(match[4])
|
|
499
|
+
} : null;
|
|
322
500
|
};
|
|
323
501
|
const a = parse(latest);
|
|
324
502
|
const b = parse(current);
|
|
325
|
-
if (!(a && b)) {
|
|
503
|
+
if (!(a && b) || a.pre !== null && b.pre === null) {
|
|
326
504
|
return false;
|
|
327
505
|
}
|
|
328
|
-
const [
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
506
|
+
for (const [x, y] of a.core.map((n, i) => [n, b.core[i] ?? 0])) {
|
|
507
|
+
if (x !== y) {
|
|
508
|
+
return x > y;
|
|
509
|
+
}
|
|
332
510
|
}
|
|
333
|
-
if (
|
|
334
|
-
return
|
|
511
|
+
if (a.pre === null || b.pre === null) {
|
|
512
|
+
return a.pre === null && b.pre !== null;
|
|
335
513
|
}
|
|
336
|
-
return
|
|
514
|
+
return a.pre > b.pre;
|
|
337
515
|
}
|
|
338
516
|
function printUpdateNotice(currentVersion) {
|
|
339
517
|
if (disabled()) {
|
|
340
518
|
return;
|
|
341
519
|
}
|
|
342
|
-
const
|
|
520
|
+
const channel = channelOf(currentVersion);
|
|
521
|
+
const cache = readCache(channel);
|
|
343
522
|
if (!(cache && isNewerVersion(cache.latest, currentVersion))) {
|
|
344
523
|
return;
|
|
345
524
|
}
|
|
525
|
+
const spec = channel === "next" ? `${PACKAGE}@next` : PACKAGE;
|
|
346
526
|
console.error(
|
|
347
|
-
`zkao: version ${cache.latest} is available (running ${currentVersion}). Update with \`npm install -g ${
|
|
527
|
+
`zkao: version ${cache.latest} is available (running ${currentVersion}). Update with \`npm install -g ${spec}\`. If you are an agent working from a zkao skill file, update that too: it may describe fewer commands than the API now offers.`
|
|
348
528
|
);
|
|
349
529
|
}
|
|
350
|
-
async function refreshUpdateCache() {
|
|
530
|
+
async function refreshUpdateCache(currentVersion) {
|
|
351
531
|
if (disabled()) {
|
|
352
532
|
return;
|
|
353
533
|
}
|
|
354
|
-
const
|
|
534
|
+
const channel = channelOf(currentVersion);
|
|
535
|
+
const cache = readCache(channel);
|
|
355
536
|
if (cache && Date.now() - cache.checkedAtMs < CHECK_INTERVAL_MS) {
|
|
356
537
|
return;
|
|
357
538
|
}
|
|
358
|
-
const keepPrevious = () => writeCache({ checkedAtMs: Date.now(), latest: cache?.latest ?? "" });
|
|
539
|
+
const keepPrevious = () => writeCache({ checkedAtMs: Date.now(), latest: cache?.latest ?? "", channel });
|
|
359
540
|
try {
|
|
360
|
-
const res = await fetch(`https://registry.npmjs.org/${PACKAGE}
|
|
541
|
+
const res = await fetch(`https://registry.npmjs.org/${PACKAGE}/${channel}`, {
|
|
361
542
|
headers: { accept: "application/json" },
|
|
362
543
|
signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS)
|
|
363
544
|
});
|
|
@@ -372,7 +553,7 @@ async function refreshUpdateCache() {
|
|
|
372
553
|
keepPrevious();
|
|
373
554
|
return;
|
|
374
555
|
}
|
|
375
|
-
writeCache({ checkedAtMs: Date.now(), latest: body.version });
|
|
556
|
+
writeCache({ checkedAtMs: Date.now(), latest: body.version, channel });
|
|
376
557
|
} catch (err) {
|
|
377
558
|
debug("could not reach the npm registry", err);
|
|
378
559
|
keepPrevious();
|
|
@@ -390,11 +571,16 @@ function fail(message) {
|
|
|
390
571
|
console.error(message);
|
|
391
572
|
process.exit(1);
|
|
392
573
|
}
|
|
393
|
-
function
|
|
574
|
+
function globalConfig() {
|
|
394
575
|
const g = program.opts();
|
|
395
|
-
|
|
576
|
+
return resolveConfig({ token: g.token, projectId: g.project, baseUrl: g.baseUrl });
|
|
577
|
+
}
|
|
578
|
+
function client() {
|
|
579
|
+
const cfg = globalConfig();
|
|
396
580
|
if (!cfg.token) {
|
|
397
|
-
fail(
|
|
581
|
+
fail(
|
|
582
|
+
cfg.projectId ? `No saved credentials for project ${cfg.projectId}. Run \`zkao login --project ${cfg.projectId}\`, or pass --token / set ZKAO_API_TOKEN.` : "No API token. Run `zkao login`, pass --token, or set ZKAO_API_TOKEN."
|
|
583
|
+
);
|
|
398
584
|
}
|
|
399
585
|
if (!cfg.projectId) {
|
|
400
586
|
fail("No project id. Pass --project, set ZKAO_PROJECT_ID, or run `zkao config set --project <id>`.");
|
|
@@ -453,8 +639,28 @@ program.command("login").description(
|
|
|
453
639
|
});
|
|
454
640
|
}
|
|
455
641
|
);
|
|
642
|
+
program.command("whoami").description("Show which project and organization the current token belongs to").action(async () => {
|
|
643
|
+
const cfg = globalConfig();
|
|
644
|
+
if (!cfg.token) {
|
|
645
|
+
fail("No API token. Run `zkao login`, pass --token, or set ZKAO_API_TOKEN.");
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
const info = await getTokenInfo({ token: cfg.token, baseUrl: cfg.baseUrl });
|
|
649
|
+
if (cfg.projectId && cfg.projectId !== info.project.id) {
|
|
650
|
+
console.error(
|
|
651
|
+
`zkao: this token belongs to project ${info.project.id}, not the configured ${cfg.projectId}.`
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
out(info);
|
|
655
|
+
} catch (err) {
|
|
656
|
+
if (err instanceof ZkaoApiError) {
|
|
657
|
+
fail(`Error ${err.status} (${err.code}): ${err.message}`);
|
|
658
|
+
}
|
|
659
|
+
throw err;
|
|
660
|
+
}
|
|
661
|
+
});
|
|
456
662
|
var config = program.command("config").description("Manage stored credentials");
|
|
457
|
-
config.command("set").description(`Persist settings to ${configPath()}`).option("--token <token>", "project API token").option("--project <id>", "project id").option("--base-url <url>", "API base URL").action((opts) => {
|
|
663
|
+
config.command("set").description(`Persist settings to ${configPath()}`).option("--token <token>", "project API token").option("--project <id>", "project id").option("--base-url <url>", "API base URL").action(async (opts) => {
|
|
458
664
|
const g = program.opts();
|
|
459
665
|
const update = {
|
|
460
666
|
token: opts.token ?? g.token,
|
|
@@ -464,17 +670,52 @@ config.command("set").description(`Persist settings to ${configPath()}`).option(
|
|
|
464
670
|
if (update.token === void 0 && update.projectId === void 0 && update.baseUrl === void 0) {
|
|
465
671
|
fail("Nothing to save. Pass --token, --project, or --base-url.");
|
|
466
672
|
}
|
|
673
|
+
if (update.token !== void 0 && update.projectId === void 0) {
|
|
674
|
+
const baseUrl = resolveConfig({ baseUrl: update.baseUrl }).baseUrl;
|
|
675
|
+
try {
|
|
676
|
+
const info = await getTokenInfo({ token: update.token, baseUrl });
|
|
677
|
+
const names = {
|
|
678
|
+
name: info.project.name,
|
|
679
|
+
organization: info.project.organization.name
|
|
680
|
+
};
|
|
681
|
+
saveProjectCredentials(info.project.id, { token: update.token, baseUrl, ...names });
|
|
682
|
+
console.log(
|
|
683
|
+
`Saved to ${configPath()}. Active project: ${describeProject(info.project.id, names)}`
|
|
684
|
+
);
|
|
685
|
+
return;
|
|
686
|
+
} catch (err) {
|
|
687
|
+
console.error(
|
|
688
|
+
`zkao: could not look up the token's project (${err instanceof Error ? err.message : String(err)}); saving it without one.`
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
467
692
|
writeConfig(update);
|
|
468
693
|
console.log(`Saved to ${configPath()}`);
|
|
469
694
|
});
|
|
470
695
|
config.command("show").description("Show the resolved settings (token masked)").action(() => {
|
|
471
696
|
const cfg = resolveConfig({});
|
|
697
|
+
const { activeProjectId, projects } = savedProjects();
|
|
472
698
|
out({
|
|
473
699
|
token: cfg.token ? `${cfg.token.slice(0, 14)}\u2026` : null,
|
|
474
700
|
projectId: cfg.projectId ?? null,
|
|
475
|
-
baseUrl: cfg.baseUrl ?? "https://zkao.io/api/v1 (default)"
|
|
701
|
+
baseUrl: cfg.baseUrl ?? "https://zkao.io/api/v1 (default)",
|
|
702
|
+
savedProjects: Object.entries(projects).map(([id, saved]) => ({
|
|
703
|
+
projectId: id,
|
|
704
|
+
name: saved.name ?? null,
|
|
705
|
+
organization: saved.organization ?? null,
|
|
706
|
+
baseUrl: saved.baseUrl ?? "https://zkao.io/api/v1 (default)",
|
|
707
|
+
active: id === activeProjectId
|
|
708
|
+
}))
|
|
476
709
|
});
|
|
477
710
|
});
|
|
711
|
+
config.command("use <projectId>").description("Switch the active project to one with saved credentials").action((projectId) => {
|
|
712
|
+
try {
|
|
713
|
+
const saved = useProject(projectId);
|
|
714
|
+
console.log(`Active project: ${describeProject(projectId, saved)}`);
|
|
715
|
+
} catch (err) {
|
|
716
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
717
|
+
}
|
|
718
|
+
});
|
|
478
719
|
program.command("repos").description("List repositories in the project").action(() => run((c) => c.listRepositories()));
|
|
479
720
|
program.command("repos:wait <repositoryId>").description("Wait until a repository is ready to scan").option("--timeout <ms>", "give up after this long", toInt).action(
|
|
480
721
|
(repositoryId, opts) => run(
|
|
@@ -542,7 +783,14 @@ scans.command("wait <scanId>").description("Poll a scan until it finishes (honor
|
|
|
542
783
|
}).finally(() => process.removeListener("SIGINT", onSigint))
|
|
543
784
|
);
|
|
544
785
|
});
|
|
545
|
-
scans.command("launch").description("Launch a scan").requiredOption("--repo <id>", "repository id").
|
|
786
|
+
scans.command("launch").description("Launch a scan").requiredOption("--repo <id>", "repository id").option(
|
|
787
|
+
"--budget <credits>",
|
|
788
|
+
"max budget, in credits (default: the budget zkao recommends for this scan type on this repository)",
|
|
789
|
+
toInt
|
|
790
|
+
).option("--preset <ref>", "scan preset ref (see `zkao presets`)").option("--branch <name>", "branch to scan").option("--commit <sha>", "specific commit to scan").option(
|
|
791
|
+
"--base <ref>",
|
|
792
|
+
"base commit or ref of a diff scan; the scan audits only the change from it (requires a diff preset)"
|
|
793
|
+
).option("--message <text>", "commit message (metadata)").option(
|
|
546
794
|
"--guidance <file|->",
|
|
547
795
|
"per-scan guidance from a file (or - for stdin); replaces the repo's guidance for this scan"
|
|
548
796
|
).option(
|
|
@@ -558,8 +806,8 @@ scans.command("launch").description("Launch a scan").requiredOption("--repo <id>
|
|
|
558
806
|
presetRef: opts.preset,
|
|
559
807
|
branch: opts.branch ?? null,
|
|
560
808
|
commitHash: opts.commit ?? null,
|
|
809
|
+
baseCommit: opts.base,
|
|
561
810
|
commitMessage: opts.message ?? null,
|
|
562
|
-
optInFlowIds: opts.flow.length > 0 ? opts.flow : void 0,
|
|
563
811
|
auditAreaKeys: opts.area.length > 0 ? opts.area : void 0,
|
|
564
812
|
...opts.guidance !== void 0 ? { guidance: readContentArg(opts.guidance) } : {}
|
|
565
813
|
};
|
|
@@ -594,16 +842,30 @@ findings.command("resolution <findingId> <status>").description("Set resolution
|
|
|
594
842
|
findings.command("publish <findingId>").description("Publish a finding as a public artifact").option("--note <noteId>", "note to pin as the published note").option("--password", "generate an access password").action(
|
|
595
843
|
(findingId, opts) => run((c) => c.publishFinding(findingId, { noteId: opts.note, withPassword: opts.password }))
|
|
596
844
|
);
|
|
597
|
-
var billing = program.command("billing").description("Read the
|
|
598
|
-
billing.command("balance").description("
|
|
845
|
+
var billing = program.command("billing").description("Read the credit balance available to the project and its usage");
|
|
846
|
+
billing.command("balance").description("The organization's shared credit balance, what active scans hold, and what is available").action(() => run((c) => c.getBillingBalance()));
|
|
599
847
|
billing.command("usage").description("Credit ledger movements (defaults to the last 30 days)").option("--from <date>", "ISO date or timestamp to start from").option("--to <date>", "ISO date or timestamp to stop at").option("--limit <n>", "max events to return", toInt).action(
|
|
600
848
|
(opts) => run((c) => c.getBillingUsage(opts))
|
|
601
849
|
);
|
|
602
850
|
billing.command("summary").description("Per-month credits spent and purchased, newest first").option("--months <n>", "how many months to return", toInt).action(
|
|
603
851
|
(opts) => run((c) => c.getBillingSummary({ months: opts.months }))
|
|
604
852
|
);
|
|
853
|
+
var admin = program.command("admin", { hidden: true }).description("Site-admin operations on the project");
|
|
854
|
+
admin.command("manual-audit").description("Record a zkSecurity audit report as a manual audit").requiredOption("--repo <id>", "repository id").requiredOption("--commit <sha>", "commit the audit reviewed").requiredOption(
|
|
855
|
+
"--findings <file|->",
|
|
856
|
+
"findings YAML (report-tool: `just export-findings <slug>`)"
|
|
857
|
+
).option("--report <file|->", "report body markdown (report-tool: `just export-overview <slug>`)").option("--commit-message <text>", "commit message to show with the commit").action(
|
|
858
|
+
(opts) => run(
|
|
859
|
+
(c) => c.createManualAudit({
|
|
860
|
+
repositoryId: opts.repo,
|
|
861
|
+
commitHash: opts.commit,
|
|
862
|
+
findingsYaml: readContentArg(opts.findings),
|
|
863
|
+
reportMarkdown: opts.report === void 0 ? null : readContentArg(opts.report),
|
|
864
|
+
commitMessage: opts.commitMessage ?? null
|
|
865
|
+
})
|
|
866
|
+
)
|
|
867
|
+
);
|
|
605
868
|
program.command("presets").description("List scan presets the project can launch").action(() => run((c) => c.listScanPresets()));
|
|
606
|
-
program.command("flows").description("List opt-in flows available at launch").action(() => run((c) => c.listOptionalFlows()));
|
|
607
869
|
function errorMessage(err) {
|
|
608
870
|
if (err instanceof AggregateError && err.errors.length > 0) {
|
|
609
871
|
return err.errors.map(errorMessage).join("; ");
|
|
@@ -615,7 +877,7 @@ function errorMessage(err) {
|
|
|
615
877
|
return err.cause ? `${message}: ${errorMessage(err.cause)}` : message;
|
|
616
878
|
}
|
|
617
879
|
printUpdateNotice(version);
|
|
618
|
-
program.parseAsync().then(() => refreshUpdateCache()).catch((err) => {
|
|
880
|
+
program.parseAsync().then(() => refreshUpdateCache(version)).catch((err) => {
|
|
619
881
|
console.error(errorMessage(err));
|
|
620
882
|
process.exit(1);
|
|
621
883
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zksecurity/zkao-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0-next.2",
|
|
4
4
|
"description": "Command-line interface for the zkao Project API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "zkSecurity",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"prepublishOnly": "node -e \"if (!(process.env.npm_config_user_agent || '').includes('bun/')) { console.error('Publish with bun publish: npm ships the literal workspace:* dependency and breaks installs.'); process.exit(1); }\""
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@zksecurity/zkao-sdk": "0.
|
|
42
|
+
"@zksecurity/zkao-sdk": "0.11.0-next.2",
|
|
43
43
|
"commander": "^12.1.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|