@openbkn/bkn-sdk 0.1.1-alpha.1 → 0.1.1-alpha.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.
- package/dist/{chunk-ADZ23DPF.js → chunk-GNL6Z5VF.js} +687 -474
- package/dist/chunk-GNL6Z5VF.js.map +1 -0
- package/dist/cli.js +432 -368
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +101 -36
- package/dist/index.js +1 -1
- package/package.json +3 -2
- package/dist/chunk-ADZ23DPF.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,34 +1,43 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
DEFAULT_BUSINESS_DOMAIN,
|
|
3
4
|
DEFAULT_LIST_LIMIT,
|
|
4
5
|
DEFAULT_QUERY_LIMIT,
|
|
6
|
+
HttpError,
|
|
5
7
|
InputError,
|
|
6
8
|
activePlatform,
|
|
9
|
+
attachNoAuth,
|
|
7
10
|
attachToken,
|
|
8
|
-
changePassword,
|
|
9
11
|
createClient,
|
|
12
|
+
credentialDeviceLogin,
|
|
10
13
|
currentToken,
|
|
14
|
+
currentTokenFresh,
|
|
15
|
+
decodeJwt,
|
|
11
16
|
deletePlatform,
|
|
17
|
+
deviceLogin,
|
|
12
18
|
exportCreds,
|
|
19
|
+
fetchAuthStatus,
|
|
13
20
|
formatError,
|
|
21
|
+
getUserSafe,
|
|
22
|
+
isHeadless,
|
|
14
23
|
listPlatforms,
|
|
15
24
|
logout,
|
|
25
|
+
openBrowser,
|
|
16
26
|
parseEmbeddingFields,
|
|
17
27
|
parsePkMap,
|
|
18
28
|
rawCall,
|
|
19
29
|
readPlatformConfig,
|
|
20
|
-
renderOrgTree,
|
|
21
30
|
renderReportMarkdown,
|
|
31
|
+
request,
|
|
22
32
|
resolveContext,
|
|
23
33
|
setActivePlatform,
|
|
24
34
|
status,
|
|
25
35
|
switchUser,
|
|
26
36
|
toExitCode,
|
|
27
37
|
use,
|
|
28
|
-
usersOf,
|
|
29
38
|
whoami,
|
|
30
39
|
writePlatformConfig
|
|
31
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-GNL6Z5VF.js";
|
|
32
41
|
|
|
33
42
|
// src/cli.ts
|
|
34
43
|
import { Command as Command16 } from "commander";
|
|
@@ -36,12 +45,12 @@ import { Command as Command16 } from "commander";
|
|
|
36
45
|
// package.json
|
|
37
46
|
var package_default = {
|
|
38
47
|
name: "@openbkn/bkn-sdk",
|
|
39
|
-
version: "0.1.1-alpha.
|
|
48
|
+
version: "0.1.1-alpha.10",
|
|
40
49
|
description: "Unified TypeScript SDK + CLI for the BKN (Business Knowledge Network) platform.",
|
|
41
50
|
type: "module",
|
|
42
51
|
license: "Apache-2.0",
|
|
43
52
|
engines: {
|
|
44
|
-
node: ">=
|
|
53
|
+
node: ">=18"
|
|
45
54
|
},
|
|
46
55
|
bin: {
|
|
47
56
|
openbkn: "./dist/cli.js"
|
|
@@ -76,6 +85,7 @@ var package_default = {
|
|
|
76
85
|
},
|
|
77
86
|
dependencies: {
|
|
78
87
|
"@clack/prompts": "^0.9.1",
|
|
88
|
+
"@openbkn/bkn-sdk": "^0.1.1-alpha.3",
|
|
79
89
|
chalk: "^5.4.1",
|
|
80
90
|
commander: "^13.1.0",
|
|
81
91
|
"csv-parse": "^6.2.1",
|
|
@@ -148,6 +158,19 @@ function installGroupedHelp(root) {
|
|
|
148
158
|
apply(root);
|
|
149
159
|
}
|
|
150
160
|
|
|
161
|
+
// src/utils/org-tree.ts
|
|
162
|
+
function renderOrgTree(nodes, prefix = "") {
|
|
163
|
+
const lines = [];
|
|
164
|
+
nodes.forEach((node, i) => {
|
|
165
|
+
const last = i === nodes.length - 1;
|
|
166
|
+
lines.push(`${prefix}${last ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${node.name} (id: ${node.id})`);
|
|
167
|
+
if (node.children.length) {
|
|
168
|
+
lines.push(renderOrgTree(node.children, `${prefix}${last ? " " : "\u2502 "}`));
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
return lines.join("\n");
|
|
172
|
+
}
|
|
173
|
+
|
|
151
174
|
// src/utils/output.ts
|
|
152
175
|
function printJson(value, opts = {}) {
|
|
153
176
|
if (opts.json || opts.compact) {
|
|
@@ -157,10 +180,11 @@ function printJson(value, opts = {}) {
|
|
|
157
180
|
}
|
|
158
181
|
const rows = toRows(value);
|
|
159
182
|
if (rows) {
|
|
160
|
-
const
|
|
183
|
+
const fullColumns = columnsOf(rows).filter((c) => rows.some((r) => stringifyCell(r[c]) !== ""));
|
|
184
|
+
const columns = opts.full ? fullColumns : selectColumns(rows);
|
|
161
185
|
if (columns.length > 0) {
|
|
162
186
|
printTable(rows, columns);
|
|
163
|
-
const hidden =
|
|
187
|
+
const hidden = fullColumns.length - columns.length;
|
|
164
188
|
if (hidden > 0 && !opts.full) {
|
|
165
189
|
process.stdout.write(`\u2026 ${hidden} more column(s); use --full or --json for everything
|
|
166
190
|
`);
|
|
@@ -168,10 +192,31 @@ function printJson(value, opts = {}) {
|
|
|
168
192
|
return;
|
|
169
193
|
}
|
|
170
194
|
}
|
|
195
|
+
if (isEmptyEnvelope(value)) {
|
|
196
|
+
process.stdout.write("(no results)\n");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
171
199
|
process.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
172
200
|
`);
|
|
173
201
|
}
|
|
174
|
-
|
|
202
|
+
function isEmptyEnvelope(value) {
|
|
203
|
+
if (!value || typeof value !== "object") return false;
|
|
204
|
+
const o = value;
|
|
205
|
+
return ROW_ENVELOPES.some((k) => Array.isArray(o[k]) && o[k].length === 0);
|
|
206
|
+
}
|
|
207
|
+
var ROW_ENVELOPES = [
|
|
208
|
+
"entries",
|
|
209
|
+
"data",
|
|
210
|
+
"cases",
|
|
211
|
+
"reports",
|
|
212
|
+
"results",
|
|
213
|
+
"list",
|
|
214
|
+
"recurringRules",
|
|
215
|
+
"users",
|
|
216
|
+
"roles",
|
|
217
|
+
"departments",
|
|
218
|
+
"members"
|
|
219
|
+
];
|
|
175
220
|
function toRows(value) {
|
|
176
221
|
const isRowArray = (v) => Array.isArray(v) && v.length > 0 && v.every((x) => x !== null && typeof x === "object" && !Array.isArray(x));
|
|
177
222
|
if (isRowArray(value)) return value;
|
|
@@ -255,6 +300,25 @@ function stringifyCell(v) {
|
|
|
255
300
|
return s.length > CELL_MAX ? `${s.slice(0, CELL_MAX - 1)}\u2026` : s;
|
|
256
301
|
}
|
|
257
302
|
|
|
303
|
+
// src/utils/prompt.ts
|
|
304
|
+
import { createInterface } from "readline";
|
|
305
|
+
function promptLine(query, hidden = false) {
|
|
306
|
+
return new Promise((resolve2) => {
|
|
307
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
308
|
+
if (hidden) {
|
|
309
|
+
const mutable = rl;
|
|
310
|
+
mutable._writeToOutput = (s) => {
|
|
311
|
+
if (s.startsWith(query)) process.stdout.write(query);
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
rl.question(query, (answer) => {
|
|
315
|
+
rl.close();
|
|
316
|
+
if (hidden) process.stdout.write("\n");
|
|
317
|
+
resolve2(answer.trim());
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
258
322
|
// src/commands/_shared.ts
|
|
259
323
|
import { readFileSync } from "fs";
|
|
260
324
|
function clientFrom(cmd) {
|
|
@@ -286,350 +350,212 @@ function readBody(opts) {
|
|
|
286
350
|
}
|
|
287
351
|
|
|
288
352
|
// src/commands/auth.ts
|
|
289
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
290
353
|
import { Command } from "commander";
|
|
291
354
|
|
|
292
|
-
// src/
|
|
293
|
-
import {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
2XVy1v2bgSNkGHABgncR7seyIg81JQ3c/Axxd6GsTztjLnlvGAlmT1TphE84mi99
|
|
304
|
-
fUaGD2A1u1qdIuNc+XuisFeNcUW6fct0+x97eS2eEGRr/7qxWmO/P20sFVzXc2bF
|
|
305
|
-
1QIDAQAB
|
|
306
|
-
-----END PUBLIC KEY-----`;
|
|
307
|
-
function normalizeBaseUrl(value) {
|
|
308
|
-
return value.replace(/\/+$/, "");
|
|
309
|
-
}
|
|
310
|
-
function generatePkce() {
|
|
311
|
-
const verifier = randomBytes(48).toString("base64url");
|
|
312
|
-
return { verifier, challenge: createHash("sha256").update(verifier).digest("base64url") };
|
|
313
|
-
}
|
|
314
|
-
function buildAuthorizeUrl(base, clientId, redirectUri, state, codeChallenge, scope = DEFAULT_SCOPE) {
|
|
315
|
-
const params = new URLSearchParams({
|
|
316
|
-
response_type: "code",
|
|
317
|
-
client_id: clientId,
|
|
318
|
-
redirect_uri: redirectUri,
|
|
319
|
-
scope,
|
|
320
|
-
state,
|
|
321
|
-
"x-forwarded-prefix": "",
|
|
322
|
-
lang: "zh-cn",
|
|
323
|
-
product: "adp",
|
|
324
|
-
code_challenge: codeChallenge,
|
|
325
|
-
code_challenge_method: "S256"
|
|
326
|
-
});
|
|
327
|
-
return `${base}/oauth2/auth?${params.toString()}`;
|
|
328
|
-
}
|
|
329
|
-
function mapToken(data) {
|
|
330
|
-
return {
|
|
331
|
-
accessToken: data.access_token,
|
|
332
|
-
refreshToken: data.refresh_token,
|
|
333
|
-
idToken: data.id_token
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
async function registerClient(base, redirectUri, scope = DEFAULT_SCOPE) {
|
|
337
|
-
const res = await fetch(`${base}/oauth2/clients`, {
|
|
338
|
-
method: "POST",
|
|
339
|
-
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
340
|
-
body: JSON.stringify({
|
|
341
|
-
client_name: "openbkn-cli",
|
|
342
|
-
grant_types: ["authorization_code", "implicit", "refresh_token"],
|
|
343
|
-
response_types: ["token id_token", "code", "token"],
|
|
344
|
-
scope,
|
|
345
|
-
redirect_uris: [redirectUri],
|
|
346
|
-
post_logout_redirect_uris: [redirectUri.replace("/callback", "/successful-logout")],
|
|
347
|
-
metadata: { device: { name: "openbkn-cli", client_type: "web", description: "openbkn CLI" } }
|
|
348
|
-
})
|
|
349
|
-
});
|
|
350
|
-
if (!res.ok) {
|
|
351
|
-
throw new Error(
|
|
352
|
-
`Client registration failed (${res.status}): ${await res.text() || res.statusText}`
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
const data = await res.json();
|
|
356
|
-
return { clientId: data.client_id, clientSecret: data.client_secret };
|
|
357
|
-
}
|
|
358
|
-
async function exchangeCode(base, code, redirectUri, client, codeVerifier) {
|
|
359
|
-
const params = {
|
|
360
|
-
grant_type: "authorization_code",
|
|
361
|
-
code,
|
|
362
|
-
redirect_uri: redirectUri,
|
|
363
|
-
code_verifier: codeVerifier
|
|
364
|
-
};
|
|
365
|
-
const headers = {
|
|
366
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
367
|
-
Accept: "application/json"
|
|
368
|
-
};
|
|
369
|
-
if (client.clientSecret) {
|
|
370
|
-
headers.Authorization = `Basic ${Buffer.from(`${client.clientId}:${client.clientSecret}`).toString("base64")}`;
|
|
371
|
-
} else {
|
|
372
|
-
params.client_id = client.clientId;
|
|
373
|
-
}
|
|
374
|
-
const res = await fetch(`${base}/oauth2/token`, {
|
|
355
|
+
// src/api/eacp-crypto.ts
|
|
356
|
+
import {
|
|
357
|
+
constants,
|
|
358
|
+
createPrivateKey,
|
|
359
|
+
createPublicKey,
|
|
360
|
+
publicEncrypt
|
|
361
|
+
} from "crypto";
|
|
362
|
+
|
|
363
|
+
// src/api/admin.ts
|
|
364
|
+
async function changePasswordSafe(ctx, account, oldPassword, newPassword) {
|
|
365
|
+
await request(ctx, "/api/safe/v1/auth/change-password", {
|
|
375
366
|
method: "POST",
|
|
376
|
-
|
|
377
|
-
body: new URLSearchParams(params).toString()
|
|
367
|
+
body: { account, old_password: oldPassword, new_password: newPassword }
|
|
378
368
|
});
|
|
379
|
-
|
|
380
|
-
throw new Error(
|
|
381
|
-
`Token exchange failed (${res.status}): ${await res.text() || res.statusText}`
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
return mapToken(await res.json());
|
|
369
|
+
return { ok: true };
|
|
385
370
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
res.writeHead(404);
|
|
392
|
-
res.end();
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
const code = u.searchParams.get("code");
|
|
396
|
-
const error = u.searchParams.get("error");
|
|
397
|
-
if (error) {
|
|
398
|
-
res.writeHead(400, { "content-type": "text/html" });
|
|
399
|
-
res.end(`<h1>Login failed</h1><p>${error}</p>`);
|
|
400
|
-
server.close(() => reject(new Error(`OAuth error: ${error}`)));
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
if (!code) {
|
|
404
|
-
res.writeHead(400);
|
|
405
|
-
res.end("missing code");
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
409
|
-
res.end("<h1>Login successful</h1><p>You can close this window.</p>");
|
|
410
|
-
resolve2({
|
|
411
|
-
code,
|
|
412
|
-
state: u.searchParams.get("state") ?? void 0,
|
|
413
|
-
close: () => server.close()
|
|
414
|
-
});
|
|
415
|
-
});
|
|
416
|
-
server.on("error", reject);
|
|
417
|
-
server.listen(port, "127.0.0.1");
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
function openBrowser(url) {
|
|
421
|
-
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
371
|
+
|
|
372
|
+
// src/commands/auth.ts
|
|
373
|
+
async function resolveAccount(baseUrl, accessToken, insecure, idToken) {
|
|
374
|
+
const sub = decodeJwt(idToken ?? accessToken)?.sub;
|
|
375
|
+
if (!sub) return void 0;
|
|
422
376
|
try {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
377
|
+
const u = await getUserSafe(
|
|
378
|
+
{ baseUrl, token: accessToken, businessDomain: DEFAULT_BUSINESS_DOMAIN, insecure },
|
|
379
|
+
sub
|
|
380
|
+
);
|
|
381
|
+
return u.account;
|
|
428
382
|
} catch {
|
|
383
|
+
return void 0;
|
|
429
384
|
}
|
|
430
385
|
}
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
const { verifier, challenge } = generatePkce();
|
|
438
|
-
const state = randomBytes(12).toString("hex");
|
|
439
|
-
const authUrl = buildAuthorizeUrl(base, client.clientId, redirectUri, state, challenge, scope);
|
|
440
|
-
const waiter = startCallbackServer(port);
|
|
441
|
-
if (opts.noBrowser) {
|
|
442
|
-
process.stderr.write(`Open this URL to log in:
|
|
443
|
-
${authUrl}
|
|
444
|
-
`);
|
|
445
|
-
} else {
|
|
446
|
-
process.stderr.write(`Opening browser for login\u2026
|
|
447
|
-
If it doesn't open, visit:
|
|
448
|
-
${authUrl}
|
|
449
|
-
`);
|
|
450
|
-
openBrowser(authUrl);
|
|
386
|
+
function renderSessions(items) {
|
|
387
|
+
const byPlatform = /* @__PURE__ */ new Map();
|
|
388
|
+
for (const it of items) {
|
|
389
|
+
const arr = byPlatform.get(it.baseUrl) ?? [];
|
|
390
|
+
arr.push(it);
|
|
391
|
+
byPlatform.set(it.baseUrl, arr);
|
|
451
392
|
}
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
457
|
-
function mergeCookies(existing, res) {
|
|
458
|
-
const setCookies = typeof res.headers.getSetCookie === "function" ? res.headers.getSetCookie() : res.headers.get("set-cookie") ? [res.headers.get("set-cookie")] : [];
|
|
459
|
-
const map = /* @__PURE__ */ new Map();
|
|
460
|
-
const add = (pair) => {
|
|
461
|
-
const eq = pair.indexOf("=");
|
|
462
|
-
if (eq > 0) map.set(pair.slice(0, eq), pair.slice(eq + 1));
|
|
463
|
-
};
|
|
464
|
-
for (const p of existing.split(";").map((s) => s.trim()).filter(Boolean))
|
|
465
|
-
add(p);
|
|
466
|
-
for (const sc of setCookies) add(sc.split(";")[0]?.trim() ?? "");
|
|
467
|
-
return [...map.entries()].map(([k, v]) => `${k}=${v}`).join("; ");
|
|
468
|
-
}
|
|
469
|
-
function parseSigninProps(html) {
|
|
470
|
-
const m = html.match(/<script[^>]*\bid=["']__NEXT_DATA__["'][^>]*>([\s\S]*?)<\/script>/i);
|
|
471
|
-
if (!m?.[1]) throw new Error("Could not find __NEXT_DATA__ on /oauth2/signin.");
|
|
472
|
-
const data = JSON.parse(m[1]);
|
|
473
|
-
const pp = data.props?.pageProps;
|
|
474
|
-
const csrftoken = pp?.csrftoken ?? pp?._csrf;
|
|
475
|
-
if (typeof csrftoken !== "string") throw new Error("Sign-in page did not expose csrftoken.");
|
|
476
|
-
return {
|
|
477
|
-
csrftoken,
|
|
478
|
-
challenge: typeof pp?.challenge === "string" ? pp.challenge : void 0,
|
|
479
|
-
remember: pp?.remember === true || pp?.remember === "true"
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
async function followToCallback(startUrl, jar0, state, redirectUri) {
|
|
483
|
-
let url = startUrl;
|
|
484
|
-
let jar = jar0;
|
|
485
|
-
const cb = new URL(redirectUri);
|
|
486
|
-
for (let hop = 0; hop < 40; hop++) {
|
|
487
|
-
const resp = await fetch(url, {
|
|
488
|
-
headers: { Cookie: jar, Accept: "text/html,*/*;q=0.8" },
|
|
489
|
-
redirect: "manual"
|
|
490
|
-
});
|
|
491
|
-
jar = mergeCookies(jar, resp);
|
|
492
|
-
if (![302, 303, 307, 308].includes(resp.status)) {
|
|
493
|
-
throw new Error(`Unexpected OAuth response (HTTP ${resp.status}).`);
|
|
494
|
-
}
|
|
495
|
-
const loc = resp.headers.get("location");
|
|
496
|
-
if (!loc) throw new Error(`OAuth redirect missing Location (HTTP ${resp.status}).`);
|
|
497
|
-
const next = new URL(loc, url);
|
|
498
|
-
if (next.origin === cb.origin && next.pathname === cb.pathname) {
|
|
499
|
-
const err = next.searchParams.get("error");
|
|
500
|
-
if (err) throw new Error(`Authorization failed: ${err}`);
|
|
501
|
-
const code = next.searchParams.get("code");
|
|
502
|
-
if (next.searchParams.get("state") !== state) throw new Error("OAuth state mismatch.");
|
|
503
|
-
if (!code) throw new Error("Callback missing authorization code.");
|
|
504
|
-
return code;
|
|
505
|
-
}
|
|
506
|
-
url = next.href;
|
|
393
|
+
const lines = [];
|
|
394
|
+
for (const [platform, users] of byPlatform) {
|
|
395
|
+
lines.push(platform);
|
|
396
|
+
for (const u of users) lines.push(` ${u.active ? "*" : " "} ${u.username ?? u.userId}`);
|
|
507
397
|
}
|
|
508
|
-
|
|
398
|
+
return lines.join("\n") || "(no saved sessions)";
|
|
509
399
|
}
|
|
510
|
-
async function passwordLogin(baseUrl, username, password, opts = {}) {
|
|
511
|
-
const base = normalizeBaseUrl(baseUrl);
|
|
512
|
-
const port = opts.port ?? DEFAULT_REDIRECT_PORT;
|
|
513
|
-
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
514
|
-
const scope = opts.scope ?? DEFAULT_SCOPE;
|
|
515
|
-
const client = opts.clientId ? { clientId: opts.clientId } : await registerClient(base, redirectUri, scope);
|
|
516
|
-
const { verifier, challenge } = generatePkce();
|
|
517
|
-
const state = randomBytes(12).toString("hex");
|
|
518
|
-
let jar = "";
|
|
519
|
-
const authResp = await fetch(
|
|
520
|
-
buildAuthorizeUrl(base, client.clientId, redirectUri, state, challenge, scope),
|
|
521
|
-
{ redirect: "manual" }
|
|
522
|
-
);
|
|
523
|
-
jar = mergeCookies(jar, authResp);
|
|
524
|
-
const authLoc = authResp.headers.get("location");
|
|
525
|
-
if (!authLoc) throw new Error(`/oauth2/auth did not redirect (HTTP ${authResp.status}).`);
|
|
526
|
-
const signinUrl = new URL(authLoc, base);
|
|
527
|
-
if (!signinUrl.pathname.includes("signin")) {
|
|
528
|
-
throw new Error(`Expected a sign-in redirect, got: ${authLoc}`);
|
|
529
|
-
}
|
|
530
|
-
const pageResp = await fetch(signinUrl.href, {
|
|
531
|
-
headers: { Cookie: jar, Accept: "text/html,*/*;q=0.8" },
|
|
532
|
-
redirect: "manual"
|
|
533
|
-
});
|
|
534
|
-
jar = mergeCookies(jar, pageResp);
|
|
535
|
-
const props = parseSigninProps(await pageResp.text());
|
|
536
|
-
const loginChallenge = signinUrl.searchParams.get("login_challenge")?.trim() || props.challenge?.trim();
|
|
537
|
-
if (!loginChallenge) throw new Error("Could not resolve the login challenge.");
|
|
538
|
-
const cipher = publicEncrypt(
|
|
539
|
-
{
|
|
540
|
-
key: opts.signinPublicKeyPem ?? STUDIOWEB_LOGIN_PUBLIC_KEY_PEM,
|
|
541
|
-
padding: cryptoConstants.RSA_PKCS1_PADDING
|
|
542
|
-
},
|
|
543
|
-
Buffer.from(password, "utf8")
|
|
544
|
-
).toString("base64");
|
|
545
|
-
const postResp = await fetch(`${base}/oauth2/signin`, {
|
|
546
|
-
method: "POST",
|
|
547
|
-
headers: {
|
|
548
|
-
Cookie: jar,
|
|
549
|
-
"Content-Type": "application/json",
|
|
550
|
-
Accept: "application/json, text/plain, */*",
|
|
551
|
-
Origin: new URL(base).origin,
|
|
552
|
-
Referer: signinUrl.href
|
|
553
|
-
},
|
|
554
|
-
body: JSON.stringify({
|
|
555
|
-
_csrf: props.csrftoken,
|
|
556
|
-
challenge: loginChallenge,
|
|
557
|
-
account: username,
|
|
558
|
-
password: cipher,
|
|
559
|
-
vcode: { id: "", content: "" },
|
|
560
|
-
dualfactorauthinfo: { validcode: { vcode: "" }, OTP: { OTP: "" } },
|
|
561
|
-
remember: props.remember ?? false,
|
|
562
|
-
device: { name: "", description: "", client_type: "console_web", udids: [] }
|
|
563
|
-
}),
|
|
564
|
-
redirect: "manual"
|
|
565
|
-
});
|
|
566
|
-
jar = mergeCookies(jar, postResp);
|
|
567
|
-
let code;
|
|
568
|
-
if ([302, 303, 307].includes(postResp.status)) {
|
|
569
|
-
const loc = postResp.headers.get("location");
|
|
570
|
-
if (!loc) throw new Error("Sign-in response missing Location.");
|
|
571
|
-
code = await followToCallback(new URL(loc, base).href, jar, state, redirectUri);
|
|
572
|
-
} else if (postResp.status === 200) {
|
|
573
|
-
const text = await postResp.text();
|
|
574
|
-
let json = null;
|
|
575
|
-
try {
|
|
576
|
-
json = JSON.parse(text);
|
|
577
|
-
} catch {
|
|
578
|
-
}
|
|
579
|
-
const redir = json && typeof json.redirect === "string" ? json.redirect : "";
|
|
580
|
-
if (!redir) {
|
|
581
|
-
const msg = json && typeof json.message === "string" ? json.message : text.slice(0, 300);
|
|
582
|
-
throw new InputError(`Sign-in failed: ${msg}`);
|
|
583
|
-
}
|
|
584
|
-
code = await followToCallback(new URL(redir, base).href, jar, state, redirectUri);
|
|
585
|
-
} else {
|
|
586
|
-
throw new InputError(
|
|
587
|
-
`Sign-in failed (HTTP ${postResp.status}): ${(await postResp.text()).slice(0, 300)}`
|
|
588
|
-
);
|
|
589
|
-
}
|
|
590
|
-
return exchangeCode(base, code, redirectUri, client, verifier);
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// src/commands/auth.ts
|
|
594
400
|
function registerAuthLeaves(cmd) {
|
|
595
|
-
cmd.command("login <url>").description("Log in to a platform (attach a token, or browser/password OAuth)").option("-u, --username <name>", "username for password signin").option("-p, --password <pwd>", "password for password signin").option("--token <token>", "provide a token directly (CI / headless)").option("--client-id <id>", "use a fixed OAuth2 client id (skip dynamic registration)").option("--client-secret <secret>", "OAuth2 client secret (omit for public/PKCE)").option(
|
|
596
|
-
"--
|
|
597
|
-
"
|
|
598
|
-
|
|
401
|
+
cmd.command("login <url>").description("Log in to a platform (attach a token, or browser/password OAuth)").option("-u, --username <name>", "username for password signin").option("-p, --password <pwd>", "password for password signin").option("--token <token>", "provide a token directly (CI / headless)").option("--client-id <id>", "use a fixed OAuth2 client id (skip dynamic registration)").option("--client-secret <secret>", "OAuth2 client secret (omit for public/PKCE)").option(
|
|
402
|
+
"--port <n>",
|
|
403
|
+
"loopback redirect port for the auth_code flow",
|
|
404
|
+
(v) => Number.parseInt(v, 10)
|
|
405
|
+
).option("--device", "headless device-code login (RFC 8628) \u2014 no callback server, no password").option("--audience <aud>", "device-code token audience", "bkn-safe").option(
|
|
406
|
+
"--timeout <s>",
|
|
407
|
+
"device-login wait before timing out",
|
|
408
|
+
(v) => Number.parseInt(v, 10),
|
|
409
|
+
120
|
|
410
|
+
).option("--no-browser", "(legacy) print the URL instead of opening a browser").option("--product <name>", "(legacy) ISF OAuth product query").option("--signin-public-key-file <path>", "(legacy) RSA public key for ISF /oauth2/signin").option("--no-auth", "register the platform with no authentication (no bkn-safe)").action(async (url, opts, cmd2) => {
|
|
599
411
|
const g = cmd2.optsWithGlobals();
|
|
600
412
|
if (g.insecure) process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
413
|
+
const out = outputOptions(cmd2);
|
|
414
|
+
const report = (r) => {
|
|
415
|
+
if (out.json || out.compact) {
|
|
416
|
+
printJson({ loggedIn: true, ...r }, out);
|
|
417
|
+
} else if (r.noAuth) {
|
|
418
|
+
process.stdout.write(`Registered ${r.baseUrl ?? url} (no authentication)
|
|
419
|
+
`);
|
|
420
|
+
} else {
|
|
421
|
+
process.stdout.write(`Logged in to ${r.baseUrl ?? url} as ${r.username ?? r.userId}
|
|
422
|
+
`);
|
|
423
|
+
}
|
|
424
|
+
};
|
|
601
425
|
const token = opts.token ?? g.token;
|
|
602
426
|
if (token) {
|
|
603
|
-
|
|
604
|
-
printJson({ loggedIn: true, ...r2 }, outputOptions(cmd2));
|
|
427
|
+
report(attachToken(url, token, { insecure: g.insecure }));
|
|
605
428
|
return;
|
|
606
429
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
430
|
+
if (opts.auth === false) {
|
|
431
|
+
report(attachNoAuth(url, { insecure: g.insecure }));
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const authStatus = await fetchAuthStatus(url);
|
|
435
|
+
if (authStatus && !authStatus.enabled) {
|
|
436
|
+
process.stderr.write(
|
|
437
|
+
`Platform auth is disabled (stack: ${authStatus.stack ?? "none"}) \u2014 registering without auth.
|
|
438
|
+
`
|
|
439
|
+
);
|
|
440
|
+
report(attachNoAuth(url, { insecure: g.insecure }));
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
let tokens;
|
|
444
|
+
let account;
|
|
445
|
+
try {
|
|
446
|
+
if (opts.username || opts.password) {
|
|
447
|
+
const username = opts.username ?? await promptLine("Username: ");
|
|
448
|
+
account = username;
|
|
449
|
+
const password = opts.password ?? await promptLine("Password: ", true);
|
|
450
|
+
tokens = await credentialDeviceLogin(url, username, password, {
|
|
451
|
+
clientId: opts.clientId,
|
|
452
|
+
audience: opts.audience,
|
|
453
|
+
timeoutMs: opts.timeout * 1e3
|
|
454
|
+
});
|
|
455
|
+
} else {
|
|
456
|
+
const headless = isHeadless();
|
|
457
|
+
const openInBrowser = !opts.device && opts.browser !== false && !headless;
|
|
458
|
+
tokens = await deviceLogin(url, {
|
|
459
|
+
clientId: opts.clientId,
|
|
460
|
+
audience: opts.audience,
|
|
461
|
+
timeoutMs: opts.timeout * 1e3,
|
|
462
|
+
onPrompt: ({ userCode, verificationUri, verificationUriComplete }) => {
|
|
463
|
+
const target = verificationUriComplete ?? verificationUri;
|
|
464
|
+
process.stderr.write(
|
|
465
|
+
`
|
|
466
|
+
Open this URL to sign in and authorize:
|
|
467
|
+
${target}
|
|
468
|
+
User code: ${userCode}
|
|
469
|
+
`
|
|
470
|
+
);
|
|
471
|
+
if (openInBrowser) openBrowser(target);
|
|
472
|
+
else if (headless && !opts.device && opts.browser !== false)
|
|
473
|
+
process.stderr.write("(headless \u2014 approve on any machine with a browser)\n");
|
|
474
|
+
process.stderr.write("Waiting for authorization\u2026\n");
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
} catch (e) {
|
|
479
|
+
if (e instanceof Error && /Device auth failed \(404\)/.test(e.message)) {
|
|
480
|
+
process.stderr.write("No auth endpoint found \u2014 registering platform without auth.\n");
|
|
481
|
+
report(attachNoAuth(url, { insecure: g.insecure }));
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
throw e;
|
|
485
|
+
}
|
|
486
|
+
if (!account) {
|
|
487
|
+
account = await resolveAccount(
|
|
488
|
+
url,
|
|
489
|
+
tokens.accessToken,
|
|
490
|
+
Boolean(g.insecure),
|
|
491
|
+
tokens.idToken
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
report(
|
|
495
|
+
attachToken(url, tokens.accessToken, {
|
|
496
|
+
refreshToken: tokens.refreshToken,
|
|
497
|
+
idToken: tokens.idToken,
|
|
498
|
+
insecure: g.insecure,
|
|
499
|
+
username: account
|
|
500
|
+
})
|
|
501
|
+
);
|
|
623
502
|
});
|
|
624
503
|
cmd.command("status").description("Show base URL and whether a token is configured").action((_opts, cmd2) => printJson(status(), outputOptions(cmd2)));
|
|
625
|
-
cmd.command("token").description("Print the current access token (keep secret)").action(() => {
|
|
626
|
-
|
|
504
|
+
cmd.command("token").description("Print the current access token, refreshing it if expired (keep secret)").option("--no-refresh", "print the stored token as-is, without refreshing").action(async (opts, cmd2) => {
|
|
505
|
+
const g = cmd2.optsWithGlobals();
|
|
506
|
+
if (g.insecure) process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
507
|
+
const token = opts.refresh === false ? currentToken() : await currentTokenFresh();
|
|
508
|
+
process.stdout.write(`${token}
|
|
509
|
+
`);
|
|
510
|
+
});
|
|
511
|
+
cmd.command("whoami [url]").description("Show current user identity (from the token)").option("--no-lookup", "skip the backend identity fallback (eacp/user/get)").action(async (_url, opts, cmd2) => {
|
|
512
|
+
const g = cmd2.optsWithGlobals();
|
|
513
|
+
if (g.insecure) process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
514
|
+
const me = whoami();
|
|
515
|
+
if (opts.lookup !== false && me.baseUrl && me.sub) {
|
|
516
|
+
try {
|
|
517
|
+
const u = await getUserSafe(
|
|
518
|
+
{
|
|
519
|
+
baseUrl: me.baseUrl,
|
|
520
|
+
token: currentToken(),
|
|
521
|
+
businessDomain: DEFAULT_BUSINESS_DOMAIN,
|
|
522
|
+
insecure: Boolean(g.insecure)
|
|
523
|
+
},
|
|
524
|
+
me.sub
|
|
525
|
+
);
|
|
526
|
+
if (u.account) me.username = u.account;
|
|
527
|
+
if (u.name) me.name = u.name;
|
|
528
|
+
} catch {
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
const out = outputOptions(cmd2);
|
|
532
|
+
if (out.json || out.compact || out.full) {
|
|
533
|
+
printJson(me, out);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const expMs = typeof me.exp === "number" ? me.exp * 1e3 : void 0;
|
|
537
|
+
const expired = expMs !== void 0 && expMs < Date.now();
|
|
538
|
+
const rows = [
|
|
539
|
+
["User", String(me.username ?? me.sub ?? "(unknown)")],
|
|
540
|
+
...me.name && me.name !== me.username ? [["Name", String(me.name)]] : [],
|
|
541
|
+
["ID", String(me.userId ?? me.sub ?? "-")],
|
|
542
|
+
["Platform", String(me.baseUrl ?? "-")],
|
|
543
|
+
...expMs !== void 0 ? [["Expires", `${new Date(expMs).toISOString()}${expired ? " (expired)" : ""}`]] : []
|
|
544
|
+
];
|
|
545
|
+
const pad2 = Math.max(...rows.map(([k]) => k.length));
|
|
546
|
+
process.stdout.write(
|
|
547
|
+
`${rows.map(([k, v]) => `${k.padEnd(pad2)} ${v}`).join("\n")}
|
|
548
|
+
\u2026 use --full or --json for all claims
|
|
549
|
+
`
|
|
550
|
+
);
|
|
551
|
+
});
|
|
552
|
+
cmd.command("list").alias("ls").description("List saved sessions (platform \u2192 users; * = active)").action((_opts, cmd2) => {
|
|
553
|
+
const items = listPlatforms();
|
|
554
|
+
const out = outputOptions(cmd2);
|
|
555
|
+
if (out.json || out.compact) printJson(items, out);
|
|
556
|
+
else process.stdout.write(`${renderSessions(items)}
|
|
627
557
|
`);
|
|
628
558
|
});
|
|
629
|
-
cmd.command("whoami [url]").description("Show current user identity (from the token)").option("--no-lookup", "skip the backend identity fallback (eacp/user/get)").action(
|
|
630
|
-
(_url, _opts, cmd2) => printJson(whoami(), outputOptions(cmd2))
|
|
631
|
-
);
|
|
632
|
-
cmd.command("list").alias("ls").description("List platforms with a saved session").action((_opts, cmd2) => printJson(listPlatforms(), outputOptions(cmd2)));
|
|
633
559
|
cmd.command("use <url>").description("Switch the active platform").action((url, _opts, cmd2) => {
|
|
634
560
|
use(url);
|
|
635
561
|
printJson(status(), outputOptions(cmd2));
|
|
@@ -638,28 +564,56 @@ function registerAuthLeaves(cmd) {
|
|
|
638
564
|
cmd.command("delete <url>").description("Delete saved credentials for a platform").action(
|
|
639
565
|
(url, _opts, cmd2) => printJson({ deleted: deletePlatform(url) }, outputOptions(cmd2))
|
|
640
566
|
);
|
|
641
|
-
cmd.command("switch <url> <user
|
|
642
|
-
|
|
567
|
+
cmd.command("switch <url> <user>").description("Switch the active user for a platform (by username or user id)").action((url, user, _opts, cmd2) => {
|
|
568
|
+
const r = switchUser(url, user);
|
|
569
|
+
const out = outputOptions(cmd2);
|
|
570
|
+
if (out.json || out.compact) printJson(r, out);
|
|
571
|
+
else process.stdout.write(`Switched to ${r.username ?? r.userId} on ${r.baseUrl}
|
|
572
|
+
`);
|
|
643
573
|
});
|
|
644
|
-
cmd.command("users <url>").description("List saved
|
|
645
|
-
|
|
574
|
+
cmd.command("users <url>").description("List saved users for a platform (* = active)").action((url, _opts, cmd2) => {
|
|
575
|
+
const norm = url.replace(/\/+$/, "");
|
|
576
|
+
const items = listPlatforms().filter((i) => i.baseUrl === norm);
|
|
577
|
+
const out = outputOptions(cmd2);
|
|
578
|
+
if (out.json || out.compact) printJson(items, out);
|
|
579
|
+
else process.stdout.write(`${renderSessions(items)}
|
|
580
|
+
`);
|
|
646
581
|
});
|
|
647
582
|
cmd.command("export").description("Export the active session's tokens (for a headless host)").action((_opts, cmd2) => {
|
|
648
583
|
printJson(exportCreds(), outputOptions(cmd2));
|
|
649
584
|
});
|
|
650
|
-
cmd.command("change-password [url]").description("Change your account password (
|
|
585
|
+
cmd.command("change-password [url]").description("Change your account password (bkn-safe self-service; no browser)").option("-a, --account <name>", "account / login name (the login column, e.g. admin)").option("--old-password <pwd>", "current password").option("--new-password <pwd>", "new password").option("--public-key-file <path>", "(legacy) RSA public key for ISF password encryption").action(async (url, opts, cmd2) => {
|
|
651
586
|
const g = cmd2.optsWithGlobals();
|
|
587
|
+
if (g.insecure) process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
652
588
|
const ctx = resolveContext({
|
|
653
|
-
baseUrl: g.baseUrl,
|
|
589
|
+
baseUrl: url ?? g.baseUrl,
|
|
654
590
|
token: g.token,
|
|
655
591
|
user: g.user,
|
|
656
592
|
businessDomain: g.bizDomain,
|
|
657
593
|
insecure: g.insecure
|
|
658
594
|
});
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
)
|
|
595
|
+
const account = opts.account ?? await promptLine("Account: ");
|
|
596
|
+
const oldPassword = opts.oldPassword ?? await promptLine("Current password: ", true);
|
|
597
|
+
let newPassword = opts.newPassword;
|
|
598
|
+
if (!newPassword) {
|
|
599
|
+
newPassword = await promptLine("New password: ", true);
|
|
600
|
+
const confirm = await promptLine("Confirm new password: ", true);
|
|
601
|
+
if (newPassword !== confirm) throw new Error("New passwords do not match.");
|
|
602
|
+
}
|
|
603
|
+
try {
|
|
604
|
+
printJson(
|
|
605
|
+
await changePasswordSafe(ctx, account, oldPassword, newPassword),
|
|
606
|
+
outputOptions(cmd2)
|
|
607
|
+
);
|
|
608
|
+
} catch (e) {
|
|
609
|
+
if (e instanceof HttpError && e.status === 401) {
|
|
610
|
+
throw new InputError("Wrong account or current password.");
|
|
611
|
+
}
|
|
612
|
+
if (e instanceof HttpError && e.status === 400) {
|
|
613
|
+
throw new InputError("New password must differ from the current one.");
|
|
614
|
+
}
|
|
615
|
+
throw e;
|
|
616
|
+
}
|
|
663
617
|
});
|
|
664
618
|
}
|
|
665
619
|
function authCommand() {
|
|
@@ -670,6 +624,7 @@ function authCommand() {
|
|
|
670
624
|
|
|
671
625
|
// src/commands/admin.ts
|
|
672
626
|
var int = (v) => Number.parseInt(v, 10);
|
|
627
|
+
var DEFAULT_RESET_PASSWORD = "openbkn";
|
|
673
628
|
function adminCommand() {
|
|
674
629
|
const admin = new Command2("admin").description(
|
|
675
630
|
"Operator CLI (kweaver-admin): org, user, role, models, audit"
|
|
@@ -802,12 +757,25 @@ function adminCommand() {
|
|
|
802
757
|
user.command("delete <id>").description("Delete a user").action(async (id, _opts, cmd) => {
|
|
803
758
|
printJson(await clientFrom(cmd).admin.userDelete(id), outputOptions(cmd));
|
|
804
759
|
});
|
|
805
|
-
user.command("reset-password [id]").description("Reset a user's password (
|
|
760
|
+
user.command("reset-password [id]").description("Reset a user's password (defaults to the platform initial password)").option("--id <userId>", "explicit user UUID (alt to the positional id)").option("--user <account>", "resolve the user by account / login name").option("--password <s>", "the new password (default: platform initial 'openbkn')").option("--new-password <s>", "the new password (alias of --password)").option("--prompt-password", "type the new password interactively (input hidden)").option("-y, --yes", "skip confirmation").action(async (id, opts, cmd) => {
|
|
806
761
|
const userId = id ?? opts.id ?? opts.user;
|
|
807
|
-
const pwd = opts.password ?? opts.newPassword;
|
|
808
762
|
if (!userId) throw new Error("Provide a user id (positional or --id).");
|
|
809
|
-
|
|
810
|
-
|
|
763
|
+
let explicit = opts.password ?? opts.newPassword;
|
|
764
|
+
if (!explicit && opts.promptPassword) {
|
|
765
|
+
explicit = await promptLine("New password: ", true);
|
|
766
|
+
if (!explicit) throw new InputError("No password entered.");
|
|
767
|
+
}
|
|
768
|
+
const pwd = explicit ?? DEFAULT_RESET_PASSWORD;
|
|
769
|
+
const r = await clientFrom(cmd).admin.userResetPassword(userId, pwd);
|
|
770
|
+
const out = outputOptions(cmd);
|
|
771
|
+
if (out.json || out.compact) printJson(r, out);
|
|
772
|
+
else if (explicit) process.stdout.write(`Password reset for ${userId}.
|
|
773
|
+
`);
|
|
774
|
+
else
|
|
775
|
+
process.stdout.write(
|
|
776
|
+
`Password reset for ${userId} to the initial password '${DEFAULT_RESET_PASSWORD}' (must change on next login).
|
|
777
|
+
`
|
|
778
|
+
);
|
|
811
779
|
});
|
|
812
780
|
const role = admin.command("role").description("Role management");
|
|
813
781
|
role.command("list").description("List roles").option("--keyword <s>", "filter by keyword").option("--limit <n>", "page size", int, 100).option("--offset <n>", "page offset", int, 0).option("--source <s>", "role source filter (business | user)").action(async (opts, cmd) => {
|
|
@@ -840,6 +808,41 @@ function adminCommand() {
|
|
|
840
808
|
outputOptions(cmd)
|
|
841
809
|
);
|
|
842
810
|
});
|
|
811
|
+
role.command("create").description("Create a custom role (bkn-safe; built-in roles are read-only)").requiredOption("--name <name>", "role name").option("--description <text>", "role description").action(async (opts, cmd) => {
|
|
812
|
+
printJson(
|
|
813
|
+
await clientFrom(cmd).admin.roleCreate(opts.name, opts.description),
|
|
814
|
+
outputOptions(cmd)
|
|
815
|
+
);
|
|
816
|
+
});
|
|
817
|
+
role.command("update <role>").description("Update a custom role's name/description (403 on built-in)").option("--name <name>", "new name").option("--description <text>", "new description").action(async (roleId, opts, cmd) => {
|
|
818
|
+
printJson(
|
|
819
|
+
await clientFrom(cmd).admin.roleUpdate(roleId, {
|
|
820
|
+
name: opts.name,
|
|
821
|
+
description: opts.description
|
|
822
|
+
}),
|
|
823
|
+
outputOptions(cmd)
|
|
824
|
+
);
|
|
825
|
+
});
|
|
826
|
+
role.command("delete <role>").description("Delete a custom role (403 on built-in)").option("-y, --yes", "skip confirmation").action(async (roleId, _opts, cmd) => {
|
|
827
|
+
printJson(await clientFrom(cmd).admin.roleDelete(roleId), outputOptions(cmd));
|
|
828
|
+
});
|
|
829
|
+
for (const [verb, grant] of [
|
|
830
|
+
["grant-perm", true],
|
|
831
|
+
["revoke-perm", false]
|
|
832
|
+
]) {
|
|
833
|
+
role.command(`${verb} <role>`).description(`${grant ? "Grant" : "Revoke"} a permission on a custom role (403 on built-in)`).requiredOption("--resource-type <t>", "resource type (e.g. catalog)").option("--resource-id <id>", "resource id ('*' = whole type)", "*").requiredOption("--operations <list>", "comma-separated operations").action(async (roleId, opts, cmd) => {
|
|
834
|
+
printJson(
|
|
835
|
+
await clientFrom(cmd).admin.rolePermission(
|
|
836
|
+
roleId,
|
|
837
|
+
grant,
|
|
838
|
+
opts.resourceType,
|
|
839
|
+
opts.resourceId,
|
|
840
|
+
csv(opts.operations) ?? []
|
|
841
|
+
),
|
|
842
|
+
outputOptions(cmd)
|
|
843
|
+
);
|
|
844
|
+
});
|
|
845
|
+
}
|
|
843
846
|
const modelBody = (opts) => {
|
|
844
847
|
if (opts.body || opts.bodyFile) return readBody(opts);
|
|
845
848
|
const mc = {};
|
|
@@ -1075,7 +1078,7 @@ function agentCommand() {
|
|
|
1075
1078
|
import { Command as Command4 } from "commander";
|
|
1076
1079
|
|
|
1077
1080
|
// src/utils/bkn-validate.ts
|
|
1078
|
-
import { existsSync, readFileSync as
|
|
1081
|
+
import { existsSync, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
|
|
1079
1082
|
import { join, resolve } from "path";
|
|
1080
1083
|
var BKN_OBJECT_NAME_MAX_LENGTH = 40;
|
|
1081
1084
|
function parseFrontmatter(text) {
|
|
@@ -1128,7 +1131,7 @@ function validateBknDirectory(dirPath) {
|
|
|
1128
1131
|
if (!existsSync(networkPath)) {
|
|
1129
1132
|
errors.push("Missing network.bkn at the BKN root.");
|
|
1130
1133
|
} else {
|
|
1131
|
-
const fm = parseFrontmatter(
|
|
1134
|
+
const fm = parseFrontmatter(readFileSync2(networkPath, "utf8"));
|
|
1132
1135
|
if (!fm) errors.push("network.bkn has no frontmatter block.");
|
|
1133
1136
|
else {
|
|
1134
1137
|
if (fm.type !== "knowledge_network")
|
|
@@ -1140,7 +1143,7 @@ function validateBknDirectory(dirPath) {
|
|
|
1140
1143
|
const otIds = /* @__PURE__ */ new Set();
|
|
1141
1144
|
const otFiles = bknFiles(join(dir, "object_types"));
|
|
1142
1145
|
for (const file of otFiles) {
|
|
1143
|
-
const fm = parseFrontmatter(
|
|
1146
|
+
const fm = parseFrontmatter(readFileSync2(file, "utf8"));
|
|
1144
1147
|
const rel = file.slice(dir.length + 1);
|
|
1145
1148
|
if (!fm || fm.type !== "object_type") {
|
|
1146
1149
|
errors.push(`${rel}: not a valid object_type (missing/wrong frontmatter type).`);
|
|
@@ -1160,7 +1163,7 @@ function validateBknDirectory(dirPath) {
|
|
|
1160
1163
|
}
|
|
1161
1164
|
const rtFiles = bknFiles(join(dir, "relation_types"));
|
|
1162
1165
|
for (const file of rtFiles) {
|
|
1163
|
-
const text =
|
|
1166
|
+
const text = readFileSync2(file, "utf8");
|
|
1164
1167
|
const fm = parseFrontmatter(text);
|
|
1165
1168
|
const rel = file.slice(dir.length + 1);
|
|
1166
1169
|
if (!fm || fm.type !== "relation_type") {
|
|
@@ -1590,6 +1593,49 @@ function configCommand() {
|
|
|
1590
1593
|
// src/commands/context.ts
|
|
1591
1594
|
import { Command as Command7 } from "commander";
|
|
1592
1595
|
var int4 = (v) => Number.parseInt(v, 10);
|
|
1596
|
+
var collectArg = (v, prev) => {
|
|
1597
|
+
prev.push(v);
|
|
1598
|
+
return prev;
|
|
1599
|
+
};
|
|
1600
|
+
function buildArgs(opts) {
|
|
1601
|
+
let out = {};
|
|
1602
|
+
if (opts.args) {
|
|
1603
|
+
try {
|
|
1604
|
+
out = JSON.parse(opts.args);
|
|
1605
|
+
} catch {
|
|
1606
|
+
throw new InputError("--args must be valid JSON");
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
for (const pair of opts.arg ?? []) {
|
|
1610
|
+
const idx = pair.indexOf("=");
|
|
1611
|
+
if (idx <= 0) throw new InputError(`--arg must be key=value (got: ${pair})`);
|
|
1612
|
+
const key = pair.slice(0, idx);
|
|
1613
|
+
const raw = pair.slice(idx + 1);
|
|
1614
|
+
try {
|
|
1615
|
+
out[key] = JSON.parse(raw);
|
|
1616
|
+
} catch {
|
|
1617
|
+
out[key] = raw;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
return out;
|
|
1621
|
+
}
|
|
1622
|
+
function printToolList(res, out) {
|
|
1623
|
+
if (out.json || out.compact) {
|
|
1624
|
+
printJson(res, out);
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
const r = res ?? {};
|
|
1628
|
+
const arr = [res, r.tools, r.result?.tools, r.data].find(Array.isArray);
|
|
1629
|
+
if (!arr) {
|
|
1630
|
+
printJson(res, out);
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
const rows = arr.map((t) => ({
|
|
1634
|
+
name: t.name ?? t.tool_name ?? t.key ?? "",
|
|
1635
|
+
description: typeof t.description === "string" ? t.description : ""
|
|
1636
|
+
}));
|
|
1637
|
+
printJson(rows, out);
|
|
1638
|
+
}
|
|
1593
1639
|
function contextCommand() {
|
|
1594
1640
|
const cmd = new Command7("context").description(
|
|
1595
1641
|
"Context loader (MCP) \u2014 schema discovery, instance query, skill recall"
|
|
@@ -1616,17 +1662,35 @@ function contextCommand() {
|
|
|
1616
1662
|
outputOptions(cmd2)
|
|
1617
1663
|
);
|
|
1618
1664
|
});
|
|
1619
|
-
cmd.command("
|
|
1620
|
-
|
|
1665
|
+
cmd.command("info").description("List the deploy's MCP tool catalog (global \u2014 no KN needed)").action(async (_opts, cmd2) => {
|
|
1666
|
+
printToolList(await clientFrom(cmd2).context.info(), outputOptions(cmd2));
|
|
1621
1667
|
});
|
|
1622
|
-
cmd.command("
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1668
|
+
cmd.command("tools <kn-id>").description("List MCP tools advertised for a KN session").action(async (knId, _opts, cmd2) => {
|
|
1669
|
+
printToolList(await clientFrom(cmd2).context.tools(knId), outputOptions(cmd2));
|
|
1670
|
+
});
|
|
1671
|
+
cmd.command("tool-call <kn-id> <name>").description("Call any MCP tool by name \u2014 current or future (use `tools` to discover)").option("--args <json>", "tool arguments as JSON").option(
|
|
1672
|
+
"--arg <key=value>",
|
|
1673
|
+
"one argument (repeatable; value parsed as JSON, else string)",
|
|
1674
|
+
collectArg,
|
|
1675
|
+
[]
|
|
1676
|
+
).action(async (knId, name, opts, cmd2) => {
|
|
1677
|
+
printJson(
|
|
1678
|
+
await clientFrom(cmd2).context.toolCall(knId, name, buildArgs(opts)),
|
|
1679
|
+
outputOptions(cmd2)
|
|
1680
|
+
);
|
|
1681
|
+
});
|
|
1682
|
+
cmd.command("call-method <kn-id> <method>").description(
|
|
1683
|
+
"Call any MCP method by name (e.g. tools/list, resources/read) \u2014 current or future"
|
|
1684
|
+
).option("--args <json>", "method params as JSON").option(
|
|
1685
|
+
"--arg <key=value>",
|
|
1686
|
+
"one param (repeatable; value parsed as JSON, else string)",
|
|
1687
|
+
collectArg,
|
|
1688
|
+
[]
|
|
1689
|
+
).action(async (knId, method, opts, cmd2) => {
|
|
1690
|
+
printJson(
|
|
1691
|
+
await clientFrom(cmd2).context.callMethod(knId, method, buildArgs(opts)),
|
|
1692
|
+
outputOptions(cmd2)
|
|
1693
|
+
);
|
|
1630
1694
|
});
|
|
1631
1695
|
cmd.command("resources <kn-id>").description("List MCP resources").action(async (knId, _opts, cmd2) => {
|
|
1632
1696
|
printJson(await clientFrom(cmd2).context.resources(knId), outputOptions(cmd2));
|
|
@@ -1658,19 +1722,19 @@ function contextCommand() {
|
|
|
1658
1722
|
throw new InputError("--args must be valid JSON");
|
|
1659
1723
|
}
|
|
1660
1724
|
};
|
|
1661
|
-
cmd.command("query-instance-subgraph <kn-id>").description("
|
|
1725
|
+
cmd.command("query-instance-subgraph <kn-id>").description("Query an instance subgraph across relation-type paths").requiredOption("--args <json>", "tool arguments as JSON").action(async (knId, opts, cmd2) => {
|
|
1662
1726
|
printJson(
|
|
1663
1727
|
await clientFrom(cmd2).context.queryInstanceSubgraph(knId, jsonArgs(opts.args)),
|
|
1664
1728
|
outputOptions(cmd2)
|
|
1665
1729
|
);
|
|
1666
1730
|
});
|
|
1667
|
-
cmd.command("get-logic-properties <kn-id>").description("
|
|
1731
|
+
cmd.command("get-logic-properties <kn-id>").description("Compute logic-property values for instances").requiredOption("--args <json>", "tool arguments as JSON").action(async (knId, opts, cmd2) => {
|
|
1668
1732
|
printJson(
|
|
1669
1733
|
await clientFrom(cmd2).context.logicProperties(knId, jsonArgs(opts.args)),
|
|
1670
1734
|
outputOptions(cmd2)
|
|
1671
1735
|
);
|
|
1672
1736
|
});
|
|
1673
|
-
cmd.command("get-action-info <kn-id>").description("
|
|
1737
|
+
cmd.command("get-action-info <kn-id>").description("Fetch action info / dynamic tools for an instance").requiredOption("--args <json>", "tool arguments as JSON").action(async (knId, opts, cmd2) => {
|
|
1674
1738
|
printJson(
|
|
1675
1739
|
await clientFrom(cmd2).context.actionInfo(knId, jsonArgs(opts.args)),
|
|
1676
1740
|
outputOptions(cmd2)
|
|
@@ -1754,7 +1818,7 @@ function dataflowCommand() {
|
|
|
1754
1818
|
}
|
|
1755
1819
|
|
|
1756
1820
|
// src/commands/explore.ts
|
|
1757
|
-
import { createServer
|
|
1821
|
+
import { createServer } from "http";
|
|
1758
1822
|
import { Command as Command9 } from "commander";
|
|
1759
1823
|
var int6 = (v) => Number.parseInt(v, 10);
|
|
1760
1824
|
var ROUTES = {
|
|
@@ -1806,7 +1870,7 @@ function exploreCommand() {
|
|
|
1806
1870
|
);
|
|
1807
1871
|
cmd.option("--port <n>", "port to listen on", int6, 7777).option("--host <h>", "host to bind", "127.0.0.1").action(async (opts, command) => {
|
|
1808
1872
|
const client = clientFrom(command);
|
|
1809
|
-
const server =
|
|
1873
|
+
const server = createServer((reqMsg, res) => {
|
|
1810
1874
|
void handle(client, reqMsg, res);
|
|
1811
1875
|
});
|
|
1812
1876
|
server.listen(opts.port, opts.host, () => {
|
|
@@ -2147,11 +2211,11 @@ function toolCommand() {
|
|
|
2147
2211
|
}
|
|
2148
2212
|
|
|
2149
2213
|
// src/commands/trace.ts
|
|
2150
|
-
import { readFileSync as
|
|
2214
|
+
import { readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
2151
2215
|
import { Command as Command14 } from "commander";
|
|
2152
2216
|
|
|
2153
2217
|
// src/trace-ai/schema-validate.ts
|
|
2154
|
-
import { readFileSync as
|
|
2218
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2155
2219
|
import { extname } from "path";
|
|
2156
2220
|
import yaml from "js-yaml";
|
|
2157
2221
|
import { z } from "zod";
|
|
@@ -2188,7 +2252,7 @@ var DiagnosisRule = z.object({
|
|
|
2188
2252
|
params: z.record(z.string(), z.unknown()).optional()
|
|
2189
2253
|
});
|
|
2190
2254
|
function parseFile(file) {
|
|
2191
|
-
const text =
|
|
2255
|
+
const text = readFileSync3(file, "utf8");
|
|
2192
2256
|
const ext = extname(file).toLowerCase();
|
|
2193
2257
|
if (ext === ".yaml" || ext === ".yml") return yaml.load(text);
|
|
2194
2258
|
return JSON.parse(text);
|
|
@@ -2255,7 +2319,7 @@ function traceCommand() {
|
|
|
2255
2319
|
});
|
|
2256
2320
|
const evalSet = cmd.command("eval-set").description("Build + run trace eval sets");
|
|
2257
2321
|
evalSet.command("build <queries-file>").description("Build eval cases from a queries JSON file").option("--out <file>", "write the cases JSON here (default: stdout)").action(async (queriesFile, opts, cmd2) => {
|
|
2258
|
-
const raw = JSON.parse(
|
|
2322
|
+
const raw = JSON.parse(readFileSync4(queriesFile, "utf8"));
|
|
2259
2323
|
const cases = clientFrom(cmd2).trace.evalSetBuild(raw);
|
|
2260
2324
|
if (opts.out) {
|
|
2261
2325
|
writeFileSync(opts.out, JSON.stringify({ cases }, null, 2));
|
|
@@ -2265,7 +2329,7 @@ function traceCommand() {
|
|
|
2265
2329
|
}
|
|
2266
2330
|
});
|
|
2267
2331
|
evalSet.command("test <cases-file>").description("Run an eval set against an agent (--llm enables semantic_match)").requiredOption("--agent <id>", "agent id to run the queries against").option("--version <v>", "agent version", "v0").option("--llm", "enable semantic_match assertions via the local `claude` CLI").action(async (casesFile, opts, cmd2) => {
|
|
2268
|
-
const raw = JSON.parse(
|
|
2332
|
+
const raw = JSON.parse(readFileSync4(casesFile, "utf8"));
|
|
2269
2333
|
const cases = clientFrom(cmd2).trace.evalSetBuild(raw);
|
|
2270
2334
|
const result = await clientFrom(cmd2).trace.evalSetTest(opts.agent, cases, {
|
|
2271
2335
|
version: opts.version,
|