@openbkn/bkn-sdk 0.1.1-alpha.1 → 0.1.1-alpha.3

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,41 @@
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
+ decodeJwt,
11
15
  deletePlatform,
16
+ deviceLogin,
12
17
  exportCreds,
18
+ fetchAuthStatus,
13
19
  formatError,
20
+ getUserSafe,
14
21
  listPlatforms,
15
22
  logout,
23
+ openBrowser,
16
24
  parseEmbeddingFields,
17
25
  parsePkMap,
18
26
  rawCall,
19
27
  readPlatformConfig,
20
- renderOrgTree,
21
28
  renderReportMarkdown,
29
+ request,
22
30
  resolveContext,
23
31
  setActivePlatform,
24
32
  status,
25
33
  switchUser,
26
34
  toExitCode,
27
35
  use,
28
- usersOf,
29
36
  whoami,
30
37
  writePlatformConfig
31
- } from "./chunk-ADZ23DPF.js";
38
+ } from "./chunk-APJNRHLS.js";
32
39
 
33
40
  // src/cli.ts
34
41
  import { Command as Command16 } from "commander";
@@ -36,12 +43,12 @@ import { Command as Command16 } from "commander";
36
43
  // package.json
37
44
  var package_default = {
38
45
  name: "@openbkn/bkn-sdk",
39
- version: "0.1.1-alpha.1",
46
+ version: "0.1.1-alpha.3",
40
47
  description: "Unified TypeScript SDK + CLI for the BKN (Business Knowledge Network) platform.",
41
48
  type: "module",
42
49
  license: "Apache-2.0",
43
50
  engines: {
44
- node: ">=22"
51
+ node: ">=18"
45
52
  },
46
53
  bin: {
47
54
  openbkn: "./dist/cli.js"
@@ -148,6 +155,19 @@ function installGroupedHelp(root) {
148
155
  apply(root);
149
156
  }
150
157
 
158
+ // src/utils/org-tree.ts
159
+ function renderOrgTree(nodes, prefix = "") {
160
+ const lines = [];
161
+ nodes.forEach((node, i) => {
162
+ const last = i === nodes.length - 1;
163
+ lines.push(`${prefix}${last ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${node.name} (id: ${node.id})`);
164
+ if (node.children.length) {
165
+ lines.push(renderOrgTree(node.children, `${prefix}${last ? " " : "\u2502 "}`));
166
+ }
167
+ });
168
+ return lines.join("\n");
169
+ }
170
+
151
171
  // src/utils/output.ts
152
172
  function printJson(value, opts = {}) {
153
173
  if (opts.json || opts.compact) {
@@ -157,10 +177,11 @@ function printJson(value, opts = {}) {
157
177
  }
158
178
  const rows = toRows(value);
159
179
  if (rows) {
160
- const columns = opts.full ? columnsOf(rows).filter((c) => rows.some((r) => stringifyCell(r[c]) !== "")) : selectColumns(rows);
180
+ const fullColumns = columnsOf(rows).filter((c) => rows.some((r) => stringifyCell(r[c]) !== ""));
181
+ const columns = opts.full ? fullColumns : selectColumns(rows);
161
182
  if (columns.length > 0) {
162
183
  printTable(rows, columns);
163
- const hidden = columnsOf(rows).length - columns.length;
184
+ const hidden = fullColumns.length - columns.length;
164
185
  if (hidden > 0 && !opts.full) {
165
186
  process.stdout.write(`\u2026 ${hidden} more column(s); use --full or --json for everything
166
187
  `);
@@ -168,10 +189,31 @@ function printJson(value, opts = {}) {
168
189
  return;
169
190
  }
170
191
  }
192
+ if (isEmptyEnvelope(value)) {
193
+ process.stdout.write("(no results)\n");
194
+ return;
195
+ }
171
196
  process.stdout.write(`${JSON.stringify(value, null, 2)}
172
197
  `);
173
198
  }
174
- var ROW_ENVELOPES = ["entries", "data", "cases", "reports", "results", "list", "recurringRules"];
199
+ function isEmptyEnvelope(value) {
200
+ if (!value || typeof value !== "object") return false;
201
+ const o = value;
202
+ return ROW_ENVELOPES.some((k) => Array.isArray(o[k]) && o[k].length === 0);
203
+ }
204
+ var ROW_ENVELOPES = [
205
+ "entries",
206
+ "data",
207
+ "cases",
208
+ "reports",
209
+ "results",
210
+ "list",
211
+ "recurringRules",
212
+ "users",
213
+ "roles",
214
+ "departments",
215
+ "members"
216
+ ];
175
217
  function toRows(value) {
176
218
  const isRowArray = (v) => Array.isArray(v) && v.length > 0 && v.every((x) => x !== null && typeof x === "object" && !Array.isArray(x));
177
219
  if (isRowArray(value)) return value;
@@ -286,340 +328,169 @@ function readBody(opts) {
286
328
  }
287
329
 
288
330
  // src/commands/auth.ts
289
- import { readFileSync as readFileSync2 } from "fs";
331
+ import { createInterface } from "readline";
290
332
  import { Command } from "commander";
291
333
 
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`, {
334
+ // src/api/eacp-crypto.ts
335
+ import {
336
+ constants,
337
+ createPrivateKey,
338
+ createPublicKey,
339
+ publicEncrypt
340
+ } from "crypto";
341
+
342
+ // src/api/admin.ts
343
+ async function changePasswordSafe(ctx, account, oldPassword, newPassword) {
344
+ await request(ctx, "/api/safe/v1/auth/change-password", {
375
345
  method: "POST",
376
- headers,
377
- body: new URLSearchParams(params).toString()
346
+ body: { account, old_password: oldPassword, new_password: newPassword }
378
347
  });
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());
348
+ return { ok: true };
385
349
  }
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");
418
- });
419
- }
420
- function openBrowser(url) {
421
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
350
+
351
+ // src/commands/auth.ts
352
+ async function resolveAccount(baseUrl, accessToken, insecure, idToken) {
353
+ const sub = decodeJwt(idToken ?? accessToken)?.sub;
354
+ if (!sub) return void 0;
422
355
  try {
423
- spawn(cmd, [url], {
424
- stdio: "ignore",
425
- detached: true,
426
- shell: process.platform === "win32"
427
- }).unref();
356
+ const u = await getUserSafe(
357
+ { baseUrl, token: accessToken, businessDomain: DEFAULT_BUSINESS_DOMAIN, insecure },
358
+ sub
359
+ );
360
+ return u.account;
428
361
  } catch {
362
+ return void 0;
429
363
  }
430
364
  }
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}).`);
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;
365
+ function promptLine(query, hidden = false) {
366
+ return new Promise((resolve2) => {
367
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
368
+ if (hidden) {
369
+ const mutable = rl;
370
+ mutable._writeToOutput = (s) => {
371
+ if (s.startsWith(query)) process.stdout.write(query);
372
+ };
505
373
  }
506
- url = next.href;
507
- }
508
- throw new Error("Too many OAuth redirects.");
374
+ rl.question(query, (answer) => {
375
+ rl.close();
376
+ if (hidden) process.stdout.write("\n");
377
+ resolve2(answer.trim());
378
+ });
379
+ });
509
380
  }
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}`);
381
+ function renderSessions(items) {
382
+ const byPlatform = /* @__PURE__ */ new Map();
383
+ for (const it of items) {
384
+ const arr = byPlatform.get(it.baseUrl) ?? [];
385
+ arr.push(it);
386
+ byPlatform.set(it.baseUrl, arr);
529
387
  }
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
- );
388
+ const lines = [];
389
+ for (const [platform, users] of byPlatform) {
390
+ lines.push(platform);
391
+ for (const u of users) lines.push(` ${u.active ? "*" : " "} ${u.username ?? u.userId}`);
589
392
  }
590
- return exchangeCode(base, code, redirectUri, client, verifier);
393
+ return lines.join("\n") || "(no saved sessions)";
591
394
  }
592
-
593
- // src/commands/auth.ts
594
395
  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) => {
396
+ 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(
397
+ "--port <n>",
398
+ "loopback redirect port for the auth_code flow",
399
+ (v) => Number.parseInt(v, 10)
400
+ ).option("--device", "headless device-code login (RFC 8628) \u2014 no callback server, no password").option("--audience <aud>", "device-code token audience", "bkn-safe").option(
401
+ "--timeout <s>",
402
+ "device-login wait before timing out",
403
+ (v) => Number.parseInt(v, 10),
404
+ 120
405
+ ).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
406
  const g = cmd2.optsWithGlobals();
600
407
  if (g.insecure) process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
408
+ const out = outputOptions(cmd2);
409
+ const report = (r) => {
410
+ if (out.json || out.compact) {
411
+ printJson({ loggedIn: true, ...r }, out);
412
+ } else if (r.noAuth) {
413
+ process.stdout.write(`Registered ${r.baseUrl ?? url} (no authentication)
414
+ `);
415
+ } else {
416
+ process.stdout.write(`Logged in to ${r.baseUrl ?? url} as ${r.username ?? r.userId}
417
+ `);
418
+ }
419
+ };
601
420
  const token = opts.token ?? g.token;
602
421
  if (token) {
603
- const r2 = attachToken(url, token, { insecure: g.insecure });
604
- printJson({ loggedIn: true, ...r2 }, outputOptions(cmd2));
422
+ report(attachToken(url, token, { insecure: g.insecure }));
605
423
  return;
606
424
  }
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));
425
+ if (opts.auth === false) {
426
+ report(attachNoAuth(url, { insecure: g.insecure }));
427
+ return;
428
+ }
429
+ const authStatus = await fetchAuthStatus(url);
430
+ if (authStatus && !authStatus.enabled) {
431
+ process.stderr.write(
432
+ `Platform auth is disabled (stack: ${authStatus.stack ?? "none"}) \u2014 registering without auth.
433
+ `
434
+ );
435
+ report(attachNoAuth(url, { insecure: g.insecure }));
436
+ return;
437
+ }
438
+ let tokens;
439
+ let account;
440
+ try {
441
+ if (opts.username || opts.password) {
442
+ const username = opts.username ?? await promptLine("Username: ");
443
+ account = username;
444
+ const password = opts.password ?? await promptLine("Password: ", true);
445
+ tokens = await credentialDeviceLogin(url, username, password, {
446
+ clientId: opts.clientId,
447
+ audience: opts.audience,
448
+ timeoutMs: opts.timeout * 1e3
449
+ });
450
+ } else {
451
+ const openInBrowser = !opts.device && opts.browser !== false;
452
+ tokens = await deviceLogin(url, {
453
+ clientId: opts.clientId,
454
+ audience: opts.audience,
455
+ timeoutMs: opts.timeout * 1e3,
456
+ onPrompt: ({ userCode, verificationUri, verificationUriComplete }) => {
457
+ const target = verificationUriComplete ?? verificationUri;
458
+ process.stderr.write(
459
+ `
460
+ Open this URL to sign in and authorize:
461
+ ${target}
462
+ User code: ${userCode}
463
+ `
464
+ );
465
+ if (openInBrowser) openBrowser(target);
466
+ process.stderr.write("Waiting for authorization\u2026\n");
467
+ }
468
+ });
469
+ }
470
+ } catch (e) {
471
+ if (e instanceof Error && /Device auth failed \(404\)/.test(e.message)) {
472
+ process.stderr.write("No auth endpoint found \u2014 registering platform without auth.\n");
473
+ report(attachNoAuth(url, { insecure: g.insecure }));
474
+ return;
475
+ }
476
+ throw e;
477
+ }
478
+ if (!account) {
479
+ account = await resolveAccount(
480
+ url,
481
+ tokens.accessToken,
482
+ Boolean(g.insecure),
483
+ tokens.idToken
484
+ );
485
+ }
486
+ report(
487
+ attachToken(url, tokens.accessToken, {
488
+ refreshToken: tokens.refreshToken,
489
+ idToken: tokens.idToken,
490
+ insecure: g.insecure,
491
+ username: account
492
+ })
493
+ );
623
494
  });
624
495
  cmd.command("status").description("Show base URL and whether a token is configured").action((_opts, cmd2) => printJson(status(), outputOptions(cmd2)));
625
496
  cmd.command("token").description("Print the current access token (keep secret)").action(() => {
@@ -629,7 +500,13 @@ function registerAuthLeaves(cmd) {
629
500
  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
501
  (_url, _opts, cmd2) => printJson(whoami(), outputOptions(cmd2))
631
502
  );
632
- cmd.command("list").alias("ls").description("List platforms with a saved session").action((_opts, cmd2) => printJson(listPlatforms(), outputOptions(cmd2)));
503
+ cmd.command("list").alias("ls").description("List saved sessions (platform \u2192 users; * = active)").action((_opts, cmd2) => {
504
+ const items = listPlatforms();
505
+ const out = outputOptions(cmd2);
506
+ if (out.json || out.compact) printJson(items, out);
507
+ else process.stdout.write(`${renderSessions(items)}
508
+ `);
509
+ });
633
510
  cmd.command("use <url>").description("Switch the active platform").action((url, _opts, cmd2) => {
634
511
  use(url);
635
512
  printJson(status(), outputOptions(cmd2));
@@ -638,28 +515,56 @@ function registerAuthLeaves(cmd) {
638
515
  cmd.command("delete <url>").description("Delete saved credentials for a platform").action(
639
516
  (url, _opts, cmd2) => printJson({ deleted: deletePlatform(url) }, outputOptions(cmd2))
640
517
  );
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));
518
+ cmd.command("switch <url> <user>").description("Switch the active user for a platform (by username or user id)").action((url, user, _opts, cmd2) => {
519
+ const r = switchUser(url, user);
520
+ const out = outputOptions(cmd2);
521
+ if (out.json || out.compact) printJson(r, out);
522
+ else process.stdout.write(`Switched to ${r.username ?? r.userId} on ${r.baseUrl}
523
+ `);
643
524
  });
644
- cmd.command("users <url>").description("List saved user profiles for a platform").action((url, _opts, cmd2) => {
645
- printJson(usersOf(url), outputOptions(cmd2));
525
+ cmd.command("users <url>").description("List saved users for a platform (* = active)").action((url, _opts, cmd2) => {
526
+ const norm = url.replace(/\/+$/, "");
527
+ const items = listPlatforms().filter((i) => i.baseUrl === norm);
528
+ const out = outputOptions(cmd2);
529
+ if (out.json || out.compact) printJson(items, out);
530
+ else process.stdout.write(`${renderSessions(items)}
531
+ `);
646
532
  });
647
533
  cmd.command("export").description("Export the active session's tokens (for a headless host)").action((_opts, cmd2) => {
648
534
  printJson(exportCreds(), outputOptions(cmd2));
649
535
  });
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) => {
536
+ 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
537
  const g = cmd2.optsWithGlobals();
538
+ if (g.insecure) process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
652
539
  const ctx = resolveContext({
653
- baseUrl: g.baseUrl,
540
+ baseUrl: url ?? g.baseUrl,
654
541
  token: g.token,
655
542
  user: g.user,
656
543
  businessDomain: g.bizDomain,
657
544
  insecure: g.insecure
658
545
  });
659
- printJson(
660
- await changePassword(ctx, opts.account, opts.oldPassword, opts.newPassword),
661
- outputOptions(cmd2)
662
- );
546
+ const account = opts.account ?? await promptLine("Account: ");
547
+ const oldPassword = opts.oldPassword ?? await promptLine("Current password: ", true);
548
+ let newPassword = opts.newPassword;
549
+ if (!newPassword) {
550
+ newPassword = await promptLine("New password: ", true);
551
+ const confirm = await promptLine("Confirm new password: ", true);
552
+ if (newPassword !== confirm) throw new Error("New passwords do not match.");
553
+ }
554
+ try {
555
+ printJson(
556
+ await changePasswordSafe(ctx, account, oldPassword, newPassword),
557
+ outputOptions(cmd2)
558
+ );
559
+ } catch (e) {
560
+ if (e instanceof HttpError && e.status === 401) {
561
+ throw new InputError("Wrong account or current password.");
562
+ }
563
+ if (e instanceof HttpError && e.status === 400) {
564
+ throw new InputError("New password must differ from the current one.");
565
+ }
566
+ throw e;
567
+ }
663
568
  });
664
569
  }
665
570
  function authCommand() {
@@ -840,6 +745,41 @@ function adminCommand() {
840
745
  outputOptions(cmd)
841
746
  );
842
747
  });
748
+ 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) => {
749
+ printJson(
750
+ await clientFrom(cmd).admin.roleCreate(opts.name, opts.description),
751
+ outputOptions(cmd)
752
+ );
753
+ });
754
+ 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) => {
755
+ printJson(
756
+ await clientFrom(cmd).admin.roleUpdate(roleId, {
757
+ name: opts.name,
758
+ description: opts.description
759
+ }),
760
+ outputOptions(cmd)
761
+ );
762
+ });
763
+ role.command("delete <role>").description("Delete a custom role (403 on built-in)").option("-y, --yes", "skip confirmation").action(async (roleId, _opts, cmd) => {
764
+ printJson(await clientFrom(cmd).admin.roleDelete(roleId), outputOptions(cmd));
765
+ });
766
+ for (const [verb, grant] of [
767
+ ["grant-perm", true],
768
+ ["revoke-perm", false]
769
+ ]) {
770
+ 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) => {
771
+ printJson(
772
+ await clientFrom(cmd).admin.rolePermission(
773
+ roleId,
774
+ grant,
775
+ opts.resourceType,
776
+ opts.resourceId,
777
+ csv(opts.operations) ?? []
778
+ ),
779
+ outputOptions(cmd)
780
+ );
781
+ });
782
+ }
843
783
  const modelBody = (opts) => {
844
784
  if (opts.body || opts.bodyFile) return readBody(opts);
845
785
  const mc = {};
@@ -1075,7 +1015,7 @@ function agentCommand() {
1075
1015
  import { Command as Command4 } from "commander";
1076
1016
 
1077
1017
  // src/utils/bkn-validate.ts
1078
- import { existsSync, readFileSync as readFileSync3, readdirSync, statSync } from "fs";
1018
+ import { existsSync, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
1079
1019
  import { join, resolve } from "path";
1080
1020
  var BKN_OBJECT_NAME_MAX_LENGTH = 40;
1081
1021
  function parseFrontmatter(text) {
@@ -1128,7 +1068,7 @@ function validateBknDirectory(dirPath) {
1128
1068
  if (!existsSync(networkPath)) {
1129
1069
  errors.push("Missing network.bkn at the BKN root.");
1130
1070
  } else {
1131
- const fm = parseFrontmatter(readFileSync3(networkPath, "utf8"));
1071
+ const fm = parseFrontmatter(readFileSync2(networkPath, "utf8"));
1132
1072
  if (!fm) errors.push("network.bkn has no frontmatter block.");
1133
1073
  else {
1134
1074
  if (fm.type !== "knowledge_network")
@@ -1140,7 +1080,7 @@ function validateBknDirectory(dirPath) {
1140
1080
  const otIds = /* @__PURE__ */ new Set();
1141
1081
  const otFiles = bknFiles(join(dir, "object_types"));
1142
1082
  for (const file of otFiles) {
1143
- const fm = parseFrontmatter(readFileSync3(file, "utf8"));
1083
+ const fm = parseFrontmatter(readFileSync2(file, "utf8"));
1144
1084
  const rel = file.slice(dir.length + 1);
1145
1085
  if (!fm || fm.type !== "object_type") {
1146
1086
  errors.push(`${rel}: not a valid object_type (missing/wrong frontmatter type).`);
@@ -1160,7 +1100,7 @@ function validateBknDirectory(dirPath) {
1160
1100
  }
1161
1101
  const rtFiles = bknFiles(join(dir, "relation_types"));
1162
1102
  for (const file of rtFiles) {
1163
- const text = readFileSync3(file, "utf8");
1103
+ const text = readFileSync2(file, "utf8");
1164
1104
  const fm = parseFrontmatter(text);
1165
1105
  const rel = file.slice(dir.length + 1);
1166
1106
  if (!fm || fm.type !== "relation_type") {
@@ -1754,7 +1694,7 @@ function dataflowCommand() {
1754
1694
  }
1755
1695
 
1756
1696
  // src/commands/explore.ts
1757
- import { createServer as createServer2 } from "http";
1697
+ import { createServer } from "http";
1758
1698
  import { Command as Command9 } from "commander";
1759
1699
  var int6 = (v) => Number.parseInt(v, 10);
1760
1700
  var ROUTES = {
@@ -1806,7 +1746,7 @@ function exploreCommand() {
1806
1746
  );
1807
1747
  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
1748
  const client = clientFrom(command);
1809
- const server = createServer2((reqMsg, res) => {
1749
+ const server = createServer((reqMsg, res) => {
1810
1750
  void handle(client, reqMsg, res);
1811
1751
  });
1812
1752
  server.listen(opts.port, opts.host, () => {
@@ -2147,11 +2087,11 @@ function toolCommand() {
2147
2087
  }
2148
2088
 
2149
2089
  // src/commands/trace.ts
2150
- import { readFileSync as readFileSync5, writeFileSync } from "fs";
2090
+ import { readFileSync as readFileSync4, writeFileSync } from "fs";
2151
2091
  import { Command as Command14 } from "commander";
2152
2092
 
2153
2093
  // src/trace-ai/schema-validate.ts
2154
- import { readFileSync as readFileSync4 } from "fs";
2094
+ import { readFileSync as readFileSync3 } from "fs";
2155
2095
  import { extname } from "path";
2156
2096
  import yaml from "js-yaml";
2157
2097
  import { z } from "zod";
@@ -2188,7 +2128,7 @@ var DiagnosisRule = z.object({
2188
2128
  params: z.record(z.string(), z.unknown()).optional()
2189
2129
  });
2190
2130
  function parseFile(file) {
2191
- const text = readFileSync4(file, "utf8");
2131
+ const text = readFileSync3(file, "utf8");
2192
2132
  const ext = extname(file).toLowerCase();
2193
2133
  if (ext === ".yaml" || ext === ".yml") return yaml.load(text);
2194
2134
  return JSON.parse(text);
@@ -2255,7 +2195,7 @@ function traceCommand() {
2255
2195
  });
2256
2196
  const evalSet = cmd.command("eval-set").description("Build + run trace eval sets");
2257
2197
  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"));
2198
+ const raw = JSON.parse(readFileSync4(queriesFile, "utf8"));
2259
2199
  const cases = clientFrom(cmd2).trace.evalSetBuild(raw);
2260
2200
  if (opts.out) {
2261
2201
  writeFileSync(opts.out, JSON.stringify({ cases }, null, 2));
@@ -2265,7 +2205,7 @@ function traceCommand() {
2265
2205
  }
2266
2206
  });
2267
2207
  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"));
2208
+ const raw = JSON.parse(readFileSync4(casesFile, "utf8"));
2269
2209
  const cases = clientFrom(cmd2).trace.evalSetBuild(raw);
2270
2210
  const result = await clientFrom(cmd2).trace.evalSetTest(opts.agent, cases, {
2271
2211
  version: opts.version,