@openbkn/bkn-sdk 0.1.1-alpha.1 → 0.1.1-alpha.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/dist/cli.js CHANGED
@@ -1,34 +1,39 @@
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,
7
9
  attachToken,
8
- changePassword,
9
10
  createClient,
11
+ credentialDeviceLogin,
10
12
  currentToken,
13
+ decodeJwt,
11
14
  deletePlatform,
15
+ deviceLogin,
12
16
  exportCreds,
13
17
  formatError,
18
+ getUserSafe,
14
19
  listPlatforms,
15
20
  logout,
21
+ openBrowser,
16
22
  parseEmbeddingFields,
17
23
  parsePkMap,
18
24
  rawCall,
19
25
  readPlatformConfig,
20
- renderOrgTree,
21
26
  renderReportMarkdown,
27
+ request,
22
28
  resolveContext,
23
29
  setActivePlatform,
24
30
  status,
25
31
  switchUser,
26
32
  toExitCode,
27
33
  use,
28
- usersOf,
29
34
  whoami,
30
35
  writePlatformConfig
31
- } from "./chunk-ADZ23DPF.js";
36
+ } from "./chunk-5MOIXIMJ.js";
32
37
 
33
38
  // src/cli.ts
34
39
  import { Command as Command16 } from "commander";
@@ -36,12 +41,12 @@ import { Command as Command16 } from "commander";
36
41
  // package.json
37
42
  var package_default = {
38
43
  name: "@openbkn/bkn-sdk",
39
- version: "0.1.1-alpha.1",
44
+ version: "0.1.1-alpha.2",
40
45
  description: "Unified TypeScript SDK + CLI for the BKN (Business Knowledge Network) platform.",
41
46
  type: "module",
42
47
  license: "Apache-2.0",
43
48
  engines: {
44
- node: ">=22"
49
+ node: ">=18"
45
50
  },
46
51
  bin: {
47
52
  openbkn: "./dist/cli.js"
@@ -148,6 +153,19 @@ function installGroupedHelp(root) {
148
153
  apply(root);
149
154
  }
150
155
 
156
+ // src/utils/org-tree.ts
157
+ function renderOrgTree(nodes, prefix = "") {
158
+ const lines = [];
159
+ nodes.forEach((node, i) => {
160
+ const last = i === nodes.length - 1;
161
+ lines.push(`${prefix}${last ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${node.name} (id: ${node.id})`);
162
+ if (node.children.length) {
163
+ lines.push(renderOrgTree(node.children, `${prefix}${last ? " " : "\u2502 "}`));
164
+ }
165
+ });
166
+ return lines.join("\n");
167
+ }
168
+
151
169
  // src/utils/output.ts
152
170
  function printJson(value, opts = {}) {
153
171
  if (opts.json || opts.compact) {
@@ -157,10 +175,11 @@ function printJson(value, opts = {}) {
157
175
  }
158
176
  const rows = toRows(value);
159
177
  if (rows) {
160
- const columns = opts.full ? columnsOf(rows).filter((c) => rows.some((r) => stringifyCell(r[c]) !== "")) : selectColumns(rows);
178
+ const fullColumns = columnsOf(rows).filter((c) => rows.some((r) => stringifyCell(r[c]) !== ""));
179
+ const columns = opts.full ? fullColumns : selectColumns(rows);
161
180
  if (columns.length > 0) {
162
181
  printTable(rows, columns);
163
- const hidden = columnsOf(rows).length - columns.length;
182
+ const hidden = fullColumns.length - columns.length;
164
183
  if (hidden > 0 && !opts.full) {
165
184
  process.stdout.write(`\u2026 ${hidden} more column(s); use --full or --json for everything
166
185
  `);
@@ -168,10 +187,31 @@ function printJson(value, opts = {}) {
168
187
  return;
169
188
  }
170
189
  }
190
+ if (isEmptyEnvelope(value)) {
191
+ process.stdout.write("(no results)\n");
192
+ return;
193
+ }
171
194
  process.stdout.write(`${JSON.stringify(value, null, 2)}
172
195
  `);
173
196
  }
174
- var ROW_ENVELOPES = ["entries", "data", "cases", "reports", "results", "list", "recurringRules"];
197
+ function isEmptyEnvelope(value) {
198
+ if (!value || typeof value !== "object") return false;
199
+ const o = value;
200
+ return ROW_ENVELOPES.some((k) => Array.isArray(o[k]) && o[k].length === 0);
201
+ }
202
+ var ROW_ENVELOPES = [
203
+ "entries",
204
+ "data",
205
+ "cases",
206
+ "reports",
207
+ "results",
208
+ "list",
209
+ "recurringRules",
210
+ "users",
211
+ "roles",
212
+ "departments",
213
+ "members"
214
+ ];
175
215
  function toRows(value) {
176
216
  const isRowArray = (v) => Array.isArray(v) && v.length > 0 && v.every((x) => x !== null && typeof x === "object" && !Array.isArray(x));
177
217
  if (isRowArray(value)) return value;
@@ -286,340 +326,144 @@ function readBody(opts) {
286
326
  }
287
327
 
288
328
  // src/commands/auth.ts
289
- import { readFileSync as readFileSync2 } from "fs";
329
+ import { createInterface } from "readline";
290
330
  import { Command } from "commander";
291
331
 
292
- // src/auth/oauth.ts
293
- import { spawn } from "child_process";
294
- import { createHash, constants as cryptoConstants, publicEncrypt, randomBytes } from "crypto";
295
- import { createServer } from "http";
296
- var DEFAULT_REDIRECT_PORT = 9010;
297
- var DEFAULT_SCOPE = "openid offline all";
298
- var STUDIOWEB_LOGIN_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
299
- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyOstgbYuubBi2PUqeVj
300
- GKlkwVUY6w1Y8d4k116dI2SkZI8fxcjHALv77kItO4jYLVplk9gO4HAtsisnNE2o
301
- wlYIqdmyEPMwupaeFFFcg751oiTXJiYbtX7ABzU5KQYPjRSEjMq6i5qu/mL67XTk
302
- hvKwrC83zme66qaKApmKupDODPb0RRkutK/zHfd1zL7sciBQ6psnNadh8pE24w8O
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`, {
332
+ // src/api/eacp-crypto.ts
333
+ import {
334
+ constants,
335
+ createPrivateKey,
336
+ createPublicKey,
337
+ publicEncrypt
338
+ } from "crypto";
339
+
340
+ // src/api/admin.ts
341
+ async function changePasswordSafe(ctx, account, oldPassword, newPassword) {
342
+ await request(ctx, "/api/safe/v1/auth/change-password", {
375
343
  method: "POST",
376
- headers,
377
- body: new URLSearchParams(params).toString()
378
- });
379
- if (!res.ok) {
380
- throw new Error(
381
- `Token exchange failed (${res.status}): ${await res.text() || res.statusText}`
382
- );
383
- }
384
- return mapToken(await res.json());
385
- }
386
- function startCallbackServer(port) {
387
- return new Promise((resolve2, reject) => {
388
- const server = createServer((req2, res) => {
389
- const u = new URL(req2.url ?? "/", `http://127.0.0.1:${port}`);
390
- if (u.pathname !== "/callback") {
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");
344
+ body: { account, old_password: oldPassword, new_password: newPassword }
418
345
  });
346
+ return { ok: true };
419
347
  }
420
- function openBrowser(url) {
421
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
348
+
349
+ // src/commands/auth.ts
350
+ async function resolveAccount(baseUrl, accessToken, insecure, idToken) {
351
+ const sub = decodeJwt(idToken ?? accessToken)?.sub;
352
+ if (!sub) return void 0;
422
353
  try {
423
- spawn(cmd, [url], {
424
- stdio: "ignore",
425
- detached: true,
426
- shell: process.platform === "win32"
427
- }).unref();
354
+ const u = await getUserSafe(
355
+ { baseUrl, token: accessToken, businessDomain: DEFAULT_BUSINESS_DOMAIN, insecure },
356
+ sub
357
+ );
358
+ return u.account;
428
359
  } catch {
360
+ return void 0;
429
361
  }
430
362
  }
431
- async function browserLogin(baseUrl, opts = {}) {
432
- const base = normalizeBaseUrl(baseUrl);
433
- const port = opts.port ?? DEFAULT_REDIRECT_PORT;
434
- const redirectUri = `http://127.0.0.1:${port}/callback`;
435
- const scope = opts.scope ?? DEFAULT_SCOPE;
436
- const client = opts.clientId ? { clientId: opts.clientId } : await registerClient(base, redirectUri, scope);
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);
451
- }
452
- const { code, state: returned, close } = await waiter;
453
- close();
454
- if (returned && returned !== state) throw new Error("OAuth state mismatch \u2014 possible CSRF.");
455
- return exchangeCode(base, code, redirectUri, client, verifier);
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}).`);
363
+ function promptLine(query, hidden = false) {
364
+ return new Promise((resolve2) => {
365
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
366
+ if (hidden) {
367
+ const mutable = rl;
368
+ mutable._writeToOutput = (s) => {
369
+ if (s.startsWith(query)) process.stdout.write(query);
370
+ };
494
371
  }
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;
507
- }
508
- throw new Error("Too many OAuth redirects.");
372
+ rl.question(query, (answer) => {
373
+ rl.close();
374
+ if (hidden) process.stdout.write("\n");
375
+ resolve2(answer.trim());
376
+ });
377
+ });
509
378
  }
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}`);
379
+ function renderSessions(items) {
380
+ const byPlatform = /* @__PURE__ */ new Map();
381
+ for (const it of items) {
382
+ const arr = byPlatform.get(it.baseUrl) ?? [];
383
+ arr.push(it);
384
+ byPlatform.set(it.baseUrl, arr);
529
385
  }
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
- );
386
+ const lines = [];
387
+ for (const [platform, users] of byPlatform) {
388
+ lines.push(platform);
389
+ for (const u of users) lines.push(` ${u.active ? "*" : " "} ${u.username ?? u.userId}`);
589
390
  }
590
- return exchangeCode(base, code, redirectUri, client, verifier);
391
+ return lines.join("\n") || "(no saved sessions)";
591
392
  }
592
-
593
- // src/commands/auth.ts
594
393
  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("--port <n>", "local callback port", (v) => Number.parseInt(v, 10)).option(
596
- "--signin-public-key-file <path>",
597
- "override the RSA public key (PEM) for password signin"
598
- ).option("--product <name>", "OAuth product query (default 'adp')").option("--no-browser", "headless: print the authorize URL instead of opening a browser").action(async (url, opts, cmd2) => {
394
+ 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(
395
+ "--port <n>",
396
+ "loopback redirect port for the auth_code flow",
397
+ (v) => Number.parseInt(v, 10)
398
+ ).option("--device", "headless device-code login (RFC 8628) \u2014 no callback server, no password").option("--audience <aud>", "device-code token audience", "bkn-safe").option(
399
+ "--timeout <s>",
400
+ "device-login wait before timing out",
401
+ (v) => Number.parseInt(v, 10),
402
+ 120
403
+ ).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").action(async (url, opts, cmd2) => {
599
404
  const g = cmd2.optsWithGlobals();
600
405
  if (g.insecure) process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
406
+ const out = outputOptions(cmd2);
407
+ const report = (r) => {
408
+ if (out.json || out.compact) {
409
+ printJson({ loggedIn: true, ...r }, out);
410
+ } else {
411
+ process.stdout.write(`Logged in to ${r.baseUrl ?? url} as ${r.username ?? r.userId}
412
+ `);
413
+ }
414
+ };
601
415
  const token = opts.token ?? g.token;
602
416
  if (token) {
603
- const r2 = attachToken(url, token, { insecure: g.insecure });
604
- printJson({ loggedIn: true, ...r2 }, outputOptions(cmd2));
417
+ report(attachToken(url, token, { insecure: g.insecure }));
605
418
  return;
606
419
  }
607
- const signinKey = opts.signinPublicKeyFile ? readFileSync2(opts.signinPublicKeyFile, "utf8") : void 0;
608
- const tokens = opts.username ? await passwordLogin(url, opts.username, opts.password ?? "", {
609
- clientId: opts.clientId,
610
- port: opts.port,
611
- signinPublicKeyPem: signinKey
612
- }) : await browserLogin(url, {
613
- clientId: opts.clientId,
614
- port: opts.port,
615
- noBrowser: opts.browser === false
616
- });
617
- const r = attachToken(url, tokens.accessToken, {
618
- refreshToken: tokens.refreshToken,
619
- idToken: tokens.idToken,
620
- insecure: g.insecure
621
- });
622
- printJson({ loggedIn: true, ...r }, outputOptions(cmd2));
420
+ let tokens;
421
+ let account;
422
+ if (opts.username || opts.password) {
423
+ const username = opts.username ?? await promptLine("Username: ");
424
+ account = username;
425
+ const password = opts.password ?? await promptLine("Password: ", true);
426
+ tokens = await credentialDeviceLogin(url, username, password, {
427
+ clientId: opts.clientId,
428
+ audience: opts.audience,
429
+ timeoutMs: opts.timeout * 1e3
430
+ });
431
+ } else {
432
+ const openInBrowser = !opts.device && opts.browser !== false;
433
+ tokens = await deviceLogin(url, {
434
+ clientId: opts.clientId,
435
+ audience: opts.audience,
436
+ timeoutMs: opts.timeout * 1e3,
437
+ onPrompt: ({ userCode, verificationUri, verificationUriComplete }) => {
438
+ const target = verificationUriComplete ?? verificationUri;
439
+ process.stderr.write(
440
+ `
441
+ Open this URL to sign in and authorize:
442
+ ${target}
443
+ User code: ${userCode}
444
+ `
445
+ );
446
+ if (openInBrowser) openBrowser(target);
447
+ process.stderr.write("Waiting for authorization\u2026\n");
448
+ }
449
+ });
450
+ }
451
+ if (!account) {
452
+ account = await resolveAccount(
453
+ url,
454
+ tokens.accessToken,
455
+ Boolean(g.insecure),
456
+ tokens.idToken
457
+ );
458
+ }
459
+ report(
460
+ attachToken(url, tokens.accessToken, {
461
+ refreshToken: tokens.refreshToken,
462
+ idToken: tokens.idToken,
463
+ insecure: g.insecure,
464
+ username: account
465
+ })
466
+ );
623
467
  });
624
468
  cmd.command("status").description("Show base URL and whether a token is configured").action((_opts, cmd2) => printJson(status(), outputOptions(cmd2)));
625
469
  cmd.command("token").description("Print the current access token (keep secret)").action(() => {
@@ -629,7 +473,13 @@ function registerAuthLeaves(cmd) {
629
473
  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
474
  (_url, _opts, cmd2) => printJson(whoami(), outputOptions(cmd2))
631
475
  );
632
- cmd.command("list").alias("ls").description("List platforms with a saved session").action((_opts, cmd2) => printJson(listPlatforms(), outputOptions(cmd2)));
476
+ cmd.command("list").alias("ls").description("List saved sessions (platform \u2192 users; * = active)").action((_opts, cmd2) => {
477
+ const items = listPlatforms();
478
+ const out = outputOptions(cmd2);
479
+ if (out.json || out.compact) printJson(items, out);
480
+ else process.stdout.write(`${renderSessions(items)}
481
+ `);
482
+ });
633
483
  cmd.command("use <url>").description("Switch the active platform").action((url, _opts, cmd2) => {
634
484
  use(url);
635
485
  printJson(status(), outputOptions(cmd2));
@@ -638,28 +488,56 @@ function registerAuthLeaves(cmd) {
638
488
  cmd.command("delete <url>").description("Delete saved credentials for a platform").action(
639
489
  (url, _opts, cmd2) => printJson({ deleted: deletePlatform(url) }, outputOptions(cmd2))
640
490
  );
641
- cmd.command("switch <url> <user-id>").description("Switch the active user for a platform").action((url, userId, _opts, cmd2) => {
642
- printJson(switchUser(url, userId), outputOptions(cmd2));
491
+ cmd.command("switch <url> <user>").description("Switch the active user for a platform (by username or user id)").action((url, user, _opts, cmd2) => {
492
+ const r = switchUser(url, user);
493
+ const out = outputOptions(cmd2);
494
+ if (out.json || out.compact) printJson(r, out);
495
+ else process.stdout.write(`Switched to ${r.username ?? r.userId} on ${r.baseUrl}
496
+ `);
643
497
  });
644
- cmd.command("users <url>").description("List saved user profiles for a platform").action((url, _opts, cmd2) => {
645
- printJson(usersOf(url), outputOptions(cmd2));
498
+ cmd.command("users <url>").description("List saved users for a platform (* = active)").action((url, _opts, cmd2) => {
499
+ const norm = url.replace(/\/+$/, "");
500
+ const items = listPlatforms().filter((i) => i.baseUrl === norm);
501
+ const out = outputOptions(cmd2);
502
+ if (out.json || out.compact) printJson(items, out);
503
+ else process.stdout.write(`${renderSessions(items)}
504
+ `);
646
505
  });
647
506
  cmd.command("export").description("Export the active session's tokens (for a headless host)").action((_opts, cmd2) => {
648
507
  printJson(exportCreds(), outputOptions(cmd2));
649
508
  });
650
- cmd.command("change-password [url]").description("Change your account password (EACP, RSA-encrypted in transit)").requiredOption("-a, --account <name>", "account / login name").requiredOption("--old-password <pwd>", "current password").requiredOption("--new-password <pwd>", "new password").option("--public-key-file <path>", "override the RSA public key (PEM) for password encryption").action(async (_url, opts, cmd2) => {
509
+ 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
510
  const g = cmd2.optsWithGlobals();
511
+ if (g.insecure) process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
652
512
  const ctx = resolveContext({
653
- baseUrl: g.baseUrl,
513
+ baseUrl: url ?? g.baseUrl,
654
514
  token: g.token,
655
515
  user: g.user,
656
516
  businessDomain: g.bizDomain,
657
517
  insecure: g.insecure
658
518
  });
659
- printJson(
660
- await changePassword(ctx, opts.account, opts.oldPassword, opts.newPassword),
661
- outputOptions(cmd2)
662
- );
519
+ const account = opts.account ?? await promptLine("Account: ");
520
+ const oldPassword = opts.oldPassword ?? await promptLine("Current password: ", true);
521
+ let newPassword = opts.newPassword;
522
+ if (!newPassword) {
523
+ newPassword = await promptLine("New password: ", true);
524
+ const confirm = await promptLine("Confirm new password: ", true);
525
+ if (newPassword !== confirm) throw new Error("New passwords do not match.");
526
+ }
527
+ try {
528
+ printJson(
529
+ await changePasswordSafe(ctx, account, oldPassword, newPassword),
530
+ outputOptions(cmd2)
531
+ );
532
+ } catch (e) {
533
+ if (e instanceof HttpError && e.status === 401) {
534
+ throw new InputError("Wrong account or current password.");
535
+ }
536
+ if (e instanceof HttpError && e.status === 400) {
537
+ throw new InputError("New password must differ from the current one.");
538
+ }
539
+ throw e;
540
+ }
663
541
  });
664
542
  }
665
543
  function authCommand() {
@@ -840,6 +718,41 @@ function adminCommand() {
840
718
  outputOptions(cmd)
841
719
  );
842
720
  });
721
+ 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) => {
722
+ printJson(
723
+ await clientFrom(cmd).admin.roleCreate(opts.name, opts.description),
724
+ outputOptions(cmd)
725
+ );
726
+ });
727
+ 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) => {
728
+ printJson(
729
+ await clientFrom(cmd).admin.roleUpdate(roleId, {
730
+ name: opts.name,
731
+ description: opts.description
732
+ }),
733
+ outputOptions(cmd)
734
+ );
735
+ });
736
+ role.command("delete <role>").description("Delete a custom role (403 on built-in)").option("-y, --yes", "skip confirmation").action(async (roleId, _opts, cmd) => {
737
+ printJson(await clientFrom(cmd).admin.roleDelete(roleId), outputOptions(cmd));
738
+ });
739
+ for (const [verb, grant] of [
740
+ ["grant-perm", true],
741
+ ["revoke-perm", false]
742
+ ]) {
743
+ 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) => {
744
+ printJson(
745
+ await clientFrom(cmd).admin.rolePermission(
746
+ roleId,
747
+ grant,
748
+ opts.resourceType,
749
+ opts.resourceId,
750
+ csv(opts.operations) ?? []
751
+ ),
752
+ outputOptions(cmd)
753
+ );
754
+ });
755
+ }
843
756
  const modelBody = (opts) => {
844
757
  if (opts.body || opts.bodyFile) return readBody(opts);
845
758
  const mc = {};
@@ -1075,7 +988,7 @@ function agentCommand() {
1075
988
  import { Command as Command4 } from "commander";
1076
989
 
1077
990
  // src/utils/bkn-validate.ts
1078
- import { existsSync, readFileSync as readFileSync3, readdirSync, statSync } from "fs";
991
+ import { existsSync, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
1079
992
  import { join, resolve } from "path";
1080
993
  var BKN_OBJECT_NAME_MAX_LENGTH = 40;
1081
994
  function parseFrontmatter(text) {
@@ -1128,7 +1041,7 @@ function validateBknDirectory(dirPath) {
1128
1041
  if (!existsSync(networkPath)) {
1129
1042
  errors.push("Missing network.bkn at the BKN root.");
1130
1043
  } else {
1131
- const fm = parseFrontmatter(readFileSync3(networkPath, "utf8"));
1044
+ const fm = parseFrontmatter(readFileSync2(networkPath, "utf8"));
1132
1045
  if (!fm) errors.push("network.bkn has no frontmatter block.");
1133
1046
  else {
1134
1047
  if (fm.type !== "knowledge_network")
@@ -1140,7 +1053,7 @@ function validateBknDirectory(dirPath) {
1140
1053
  const otIds = /* @__PURE__ */ new Set();
1141
1054
  const otFiles = bknFiles(join(dir, "object_types"));
1142
1055
  for (const file of otFiles) {
1143
- const fm = parseFrontmatter(readFileSync3(file, "utf8"));
1056
+ const fm = parseFrontmatter(readFileSync2(file, "utf8"));
1144
1057
  const rel = file.slice(dir.length + 1);
1145
1058
  if (!fm || fm.type !== "object_type") {
1146
1059
  errors.push(`${rel}: not a valid object_type (missing/wrong frontmatter type).`);
@@ -1160,7 +1073,7 @@ function validateBknDirectory(dirPath) {
1160
1073
  }
1161
1074
  const rtFiles = bknFiles(join(dir, "relation_types"));
1162
1075
  for (const file of rtFiles) {
1163
- const text = readFileSync3(file, "utf8");
1076
+ const text = readFileSync2(file, "utf8");
1164
1077
  const fm = parseFrontmatter(text);
1165
1078
  const rel = file.slice(dir.length + 1);
1166
1079
  if (!fm || fm.type !== "relation_type") {
@@ -1754,7 +1667,7 @@ function dataflowCommand() {
1754
1667
  }
1755
1668
 
1756
1669
  // src/commands/explore.ts
1757
- import { createServer as createServer2 } from "http";
1670
+ import { createServer } from "http";
1758
1671
  import { Command as Command9 } from "commander";
1759
1672
  var int6 = (v) => Number.parseInt(v, 10);
1760
1673
  var ROUTES = {
@@ -1806,7 +1719,7 @@ function exploreCommand() {
1806
1719
  );
1807
1720
  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
1721
  const client = clientFrom(command);
1809
- const server = createServer2((reqMsg, res) => {
1722
+ const server = createServer((reqMsg, res) => {
1810
1723
  void handle(client, reqMsg, res);
1811
1724
  });
1812
1725
  server.listen(opts.port, opts.host, () => {
@@ -2147,11 +2060,11 @@ function toolCommand() {
2147
2060
  }
2148
2061
 
2149
2062
  // src/commands/trace.ts
2150
- import { readFileSync as readFileSync5, writeFileSync } from "fs";
2063
+ import { readFileSync as readFileSync4, writeFileSync } from "fs";
2151
2064
  import { Command as Command14 } from "commander";
2152
2065
 
2153
2066
  // src/trace-ai/schema-validate.ts
2154
- import { readFileSync as readFileSync4 } from "fs";
2067
+ import { readFileSync as readFileSync3 } from "fs";
2155
2068
  import { extname } from "path";
2156
2069
  import yaml from "js-yaml";
2157
2070
  import { z } from "zod";
@@ -2188,7 +2101,7 @@ var DiagnosisRule = z.object({
2188
2101
  params: z.record(z.string(), z.unknown()).optional()
2189
2102
  });
2190
2103
  function parseFile(file) {
2191
- const text = readFileSync4(file, "utf8");
2104
+ const text = readFileSync3(file, "utf8");
2192
2105
  const ext = extname(file).toLowerCase();
2193
2106
  if (ext === ".yaml" || ext === ".yml") return yaml.load(text);
2194
2107
  return JSON.parse(text);
@@ -2255,7 +2168,7 @@ function traceCommand() {
2255
2168
  });
2256
2169
  const evalSet = cmd.command("eval-set").description("Build + run trace eval sets");
2257
2170
  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(readFileSync5(queriesFile, "utf8"));
2171
+ const raw = JSON.parse(readFileSync4(queriesFile, "utf8"));
2259
2172
  const cases = clientFrom(cmd2).trace.evalSetBuild(raw);
2260
2173
  if (opts.out) {
2261
2174
  writeFileSync(opts.out, JSON.stringify({ cases }, null, 2));
@@ -2265,7 +2178,7 @@ function traceCommand() {
2265
2178
  }
2266
2179
  });
2267
2180
  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(readFileSync5(casesFile, "utf8"));
2181
+ const raw = JSON.parse(readFileSync4(casesFile, "utf8"));
2269
2182
  const cases = clientFrom(cmd2).trace.evalSetBuild(raw);
2270
2183
  const result = await clientFrom(cmd2).trace.evalSetTest(opts.agent, cases, {
2271
2184
  version: opts.version,