@kweaver-ai/kweaver-sdk 0.6.6 → 0.6.8

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.
@@ -1,7 +1,8 @@
1
1
  import { isNoAuth } from "../config/no-auth.js";
2
- import { autoSelectBusinessDomain, clearPlatformSession, deletePlatform, deleteUser, getActiveUser, getConfigDir, getCurrentPlatform, getPlatformAlias, hasPlatform, listPlatforms, listUserProfiles, loadClientConfig, loadTokenConfig, resolveBusinessDomain, resolvePlatformIdentifier, resolveUserId, saveNoAuthPlatform, setActiveUser, setCurrentPlatform, setPlatformAlias, } from "../config/store.js";
2
+ import { autoSelectBusinessDomain, clearPlatformSession, deletePlatform, deleteUser, getActiveUser, getConfigDir, getCurrentPlatform, getPlatformAlias, hasPlatform, listPlatforms, listUserProfiles, loadClientConfig, loadTokenConfig, loadUserTokenConfig, resolveBusinessDomain, resolvePlatformIdentifier, resolveUserId, saveNoAuthPlatform, setActiveUser, setCurrentPlatform, setPlatformAlias, } from "../config/store.js";
3
3
  import { decodeJwtPayload } from "../config/jwt.js";
4
- import { buildCopyCommand, formatHttpError, normalizeBaseUrl, oauth2Login, oauth2PasswordSigninLogin, promptForUsername, promptForPassword, refreshTokenLogin, } from "../auth/oauth.js";
4
+ import { eacpModifyPassword } from "../auth/eacp-modify-password.js";
5
+ import { buildCopyCommand, fetchEacpUserInfo, formatHttpError, InitialPasswordChangeRequiredError, normalizeBaseUrl, oauth2Login, oauth2PasswordSigninLogin, promptForUsername, promptForPassword, refreshTokenLogin, resolveActivePlatform, } from "../auth/oauth.js";
5
6
  export async function runAuthCommand(args) {
6
7
  const target = args[0];
7
8
  const rest = args.slice(1);
@@ -17,6 +18,7 @@ kweaver auth users [url|alias] List all user profiles (with usernames) for
17
18
  kweaver auth switch [url|alias] --user <id|username> Switch active user for a platform
18
19
  kweaver auth logout [url|alias] [--user <id>] Logout (clear local token)
19
20
  kweaver auth delete <url|alias> [--user <id>] Delete saved credentials
21
+ kweaver auth change-password [<url>] [-u <account>] [-o <old>] [-n <new>] Change password
20
22
 
21
23
  Login options:
22
24
  --alias <name> Save platform with a short alias (use with use / status / logout)
@@ -34,13 +36,14 @@ Login options:
34
36
  -u, --username Username for HTTP /oauth2/signin (POST). If -p is omitted, password is prompted.
35
37
  -p, --password Password for HTTP /oauth2/signin (POST). If -u is omitted, username is prompted.
36
38
  --http-signin Force HTTP /oauth2/signin (no browser). Missing -u/-p are prompted from stdin.
39
+ --new-password <pwd> After HTTP sign-in error 401001017 (initial password), set the new password non-interactively, then retry login.
37
40
  --insecure, -k Skip TLS certificate verification (self-signed / dev HTTPS only)
38
41
  --no-auth Save platform without OAuth (servers with no authentication). Same as detecting OAuth 404 during login.`);
39
42
  return 0;
40
43
  }
41
44
  if (target === "login") {
42
45
  if (rest[0] === "--help" || rest[0] === "-h") {
43
- console.log(`kweaver auth login <platform-url> [--alias <name>] [--no-auth] [--no-browser] [-u user] [-p pass] [--http-signin] [--refresh-token T --client-id ID --client-secret S]`);
46
+ console.log(`kweaver auth login <platform-url> [--alias <name>] [--no-auth] [--no-browser] [-u user] [-p pass] [--new-password <pwd>] [--http-signin] [--refresh-token T --client-id ID --client-secret S]`);
44
47
  return 0;
45
48
  }
46
49
  const url = rest[0];
@@ -51,7 +54,7 @@ Login options:
51
54
  return runAuthCommand([url, ...rest.slice(1)]);
52
55
  }
53
56
  if (target === "whoami") {
54
- return runAuthWhoamiCommand(rest);
57
+ return await runAuthWhoamiCommand(rest);
55
58
  }
56
59
  if (target === "export") {
57
60
  return runAuthExportCommand(rest);
@@ -62,6 +65,9 @@ Login options:
62
65
  if (target === "switch") {
63
66
  return runAuthSwitchCommand(rest);
64
67
  }
68
+ if (target === "change-password") {
69
+ return runAuthChangePasswordCommand(rest);
70
+ }
65
71
  const LOGIN_SUBCOMMANDS = new Set(["status", "list", "use", "delete", "logout", "export", "whoami", "users", "switch"]);
66
72
  if (target && !LOGIN_SUBCOMMANDS.has(target)) {
67
73
  try {
@@ -80,6 +86,7 @@ Login options:
80
86
  const tlsInsecure = args.includes("--insecure") || args.includes("-k");
81
87
  const noAuth = args.includes("--no-auth");
82
88
  const noBrowser = args.includes("--no-browser");
89
+ const newPasswordFlag = readOption(args, "--new-password");
83
90
  if (args.includes("--redirect-uri")) {
84
91
  console.error("Warning: --redirect-uri is deprecated and ignored. The redirect URI is always http://127.0.0.1:<port>/callback.");
85
92
  }
@@ -87,6 +94,7 @@ Login options:
87
94
  "--alias", "--client-id", "--client-secret", "--refresh-token",
88
95
  "--port", "--no-browser", "--username", "-u", "--password", "-p",
89
96
  "--http-signin",
97
+ "--new-password",
90
98
  "--oauth-product",
91
99
  "--signin-public-key-file",
92
100
  "--insecure", "-k", "--no-auth", "--redirect-uri",
@@ -94,6 +102,7 @@ Login options:
94
102
  const KNOWN_VALUE_FLAGS = new Set([
95
103
  "--alias", "--client-id", "--client-secret", "--refresh-token",
96
104
  "--port", "--username", "-u", "--password", "-p", "--redirect-uri",
105
+ "--new-password",
97
106
  "--oauth-product",
98
107
  "--signin-public-key-file",
99
108
  ]);
@@ -122,6 +131,10 @@ Login options:
122
131
  console.error("--no-auth cannot be used with HTTP sign-in or -u/-p.");
123
132
  return 1;
124
133
  }
134
+ if (newPasswordFlag !== undefined && (!username || !password)) {
135
+ console.error("--new-password requires -u/--username and -p/--password (HTTP sign-in).");
136
+ return 1;
137
+ }
125
138
  if (noBrowser && httpSignin) {
126
139
  // HTTP sign-in already runs without a browser; --no-browser is a no-op signal here.
127
140
  console.error("--http-signin already runs without a browser; --no-browser is redundant and ignored.");
@@ -161,22 +174,9 @@ Login options:
161
174
  clientId, clientSecret, refreshToken, tlsInsecure,
162
175
  });
163
176
  }
164
- else if (username && password && httpSignin) {
165
- console.log("Logging in (HTTP /oauth2/signin)...");
166
- token = await oauth2PasswordSigninLogin(normalizedTarget, {
167
- username,
168
- password,
169
- tlsInsecure,
170
- port: customPort,
171
- clientId: clientId ?? undefined,
172
- clientSecret: clientSecret ?? undefined,
173
- oauthProduct: oauthProduct ?? undefined,
174
- signinPublicKeyPemPath: signinPublicKeyFile ?? undefined,
175
- });
176
- }
177
177
  else if (username && password) {
178
178
  console.log("Logging in (HTTP /oauth2/signin)...");
179
- token = await oauth2PasswordSigninLogin(normalizedTarget, {
179
+ token = await loginWithInitialPasswordRecovery(normalizedTarget, {
180
180
  username,
181
181
  password,
182
182
  tlsInsecure,
@@ -185,7 +185,7 @@ Login options:
185
185
  clientSecret: clientSecret ?? undefined,
186
186
  oauthProduct: oauthProduct ?? undefined,
187
187
  signinPublicKeyPemPath: signinPublicKeyFile ?? undefined,
188
- });
188
+ }, { newPasswordFlag, tlsInsecure });
189
189
  }
190
190
  else {
191
191
  if (noBrowser) {
@@ -253,22 +253,26 @@ Login options:
253
253
  if (target === "status") {
254
254
  const resolvedTarget = args[1] ? resolvePlatformIdentifier(args[1]) : undefined;
255
255
  const statusTarget = resolvedTarget && /^https?:\/\//.test(resolvedTarget) ? normalizeBaseUrl(resolvedTarget) : resolvedTarget ?? undefined;
256
- const platform = statusTarget ?? getCurrentPlatform();
257
- if (!platform) {
258
- const envRaw = process.env.KWEAVER_BASE_URL?.trim();
259
- const envUrl = envRaw ? normalizeBaseUrl(envRaw) : undefined;
256
+ const active = resolveActivePlatform(statusTarget);
257
+ if (!active) {
258
+ console.error("No active platform. Run `kweaver auth login <platform-url>` first.\n" +
259
+ " Tip: set KWEAVER_BASE_URL and KWEAVER_TOKEN to use this command without a saved login.");
260
+ return 1;
261
+ }
262
+ if (active.source === "env") {
260
263
  const envToken = process.env.KWEAVER_TOKEN?.trim();
261
- if (!envUrl || !envToken) {
262
- console.error("No active platform. Run `kweaver auth login <platform-url>` first.\n" +
263
- " Tip: set KWEAVER_BASE_URL and KWEAVER_TOKEN to use this command without a saved login.");
264
+ if (!envToken) {
265
+ console.error(`KWEAVER_BASE_URL is set to ${active.url} but KWEAVER_TOKEN is missing. ` +
266
+ "Set KWEAVER_TOKEN, or unset KWEAVER_BASE_URL to fall back to the saved session.");
264
267
  return 1;
265
268
  }
266
269
  console.log(`Config directory: ${getConfigDir()}`);
267
- console.log(`Platform: ${envUrl} (KWEAVER_BASE_URL)`);
270
+ console.log(`Platform: ${active.url} (KWEAVER_BASE_URL)`);
268
271
  console.log(`Token present: yes (KWEAVER_TOKEN)`);
269
272
  console.log(`Refresh token: n/a (env)`);
270
273
  return 0;
271
274
  }
275
+ const platform = active.url;
272
276
  const token = loadTokenConfig(platform);
273
277
  if (!token) {
274
278
  console.error(statusTarget ? `No saved token for ${statusTarget}.` : "No saved token found.");
@@ -490,11 +494,13 @@ You can specify either the userId (sub claim) or the username (preferred_usernam
490
494
  console.log(`Switched to user ${resolvedId}${displayName} on ${platform}`);
491
495
  return 0;
492
496
  }
493
- function runAuthWhoamiCommand(args) {
497
+ async function runAuthWhoamiCommand(args) {
494
498
  if (args[0] === "--help" || args[0] === "-h") {
495
499
  console.log(`kweaver auth whoami [platform-url|alias] [--json]
496
500
 
497
- Show current user identity decoded from the saved id_token.
501
+ Show current user identity. For env-token mode (KWEAVER_TOKEN), the bound
502
+ identity is resolved live from EACP /api/eacp/v1/user/get; for saved sessions
503
+ it is decoded from the local id_token.
498
504
 
499
505
  Options:
500
506
  --json Output as JSON (machine-readable)`);
@@ -503,40 +509,71 @@ Options:
503
509
  const jsonOutput = args.includes("--json");
504
510
  const positional = args.find((a) => !a.startsWith("-"));
505
511
  const resolved = positional ? resolvePlatformIdentifier(positional) : null;
506
- const platform = resolved && /^https?:\/\//.test(resolved) ? normalizeBaseUrl(resolved) : resolved ?? getCurrentPlatform();
507
- if (!platform) {
508
- const envRaw = process.env.KWEAVER_BASE_URL?.trim();
509
- const envUrl = envRaw ? normalizeBaseUrl(envRaw) : undefined;
512
+ const active = resolveActivePlatform(resolved);
513
+ if (!active) {
514
+ console.error("No active platform. Run `kweaver auth login <platform-url>` first.");
515
+ return 1;
516
+ }
517
+ // Env mode requires both URL and token. resolveActivePlatform() returns
518
+ // source="env" as soon as KWEAVER_BASE_URL is set; if KWEAVER_TOKEN is
519
+ // missing we cannot inspect identity at all, so guide the user explicitly.
520
+ if (active.source === "env") {
510
521
  const envToken = process.env.KWEAVER_TOKEN?.trim();
511
- if (!envUrl || !envToken) {
512
- console.error("No active platform. Run `kweaver auth login <platform-url>` first.");
522
+ if (!envToken) {
523
+ console.error(`KWEAVER_BASE_URL is set to ${active.url} but KWEAVER_TOKEN is missing. ` +
524
+ "Set KWEAVER_TOKEN, or unset KWEAVER_BASE_URL to fall back to the saved session.");
513
525
  return 1;
514
526
  }
515
527
  const accessToken = envToken.replace(/^Bearer\s+/i, "");
516
- const payload = decodeJwtPayload(accessToken);
528
+ const envUrl = active.url;
529
+ const userInfo = await fetchEacpUserInfo(envUrl, accessToken);
530
+ // Always decode the JWT in env mode so we can render Issuer/Issued/Expires
531
+ // alongside EACP's Type/Account/Name. The two carry different facts: EACP
532
+ // tells us *who* the token belongs to (works for opaque tokens too); the
533
+ // JWT claims tell us *when* it was issued and when it expires (only
534
+ // available when the token is a JWT). Showing both gives the user the
535
+ // complete picture without forcing them to pick a mode.
536
+ const jwtPayload = decodeJwtPayload(accessToken);
517
537
  if (jsonOutput) {
518
- console.log(JSON.stringify({ platform: envUrl, source: "env", ...(payload ?? {}) }, null, 2));
538
+ const out = { platform: envUrl, source: "env" };
539
+ if (userInfo)
540
+ out.userInfo = userInfo;
541
+ if (jwtPayload)
542
+ Object.assign(out, jwtPayload);
543
+ console.log(JSON.stringify(out, null, 2));
519
544
  return 0;
520
545
  }
521
546
  console.log(`Platform: ${envUrl}`);
522
547
  console.log(`Source: env (KWEAVER_TOKEN)`);
523
- if (payload) {
524
- const uname = payload.preferred_username ?? payload.name;
548
+ if (userInfo) {
549
+ console.log(`Type: ${userInfo.type}`);
550
+ console.log(`User ID: ${userInfo.id}`);
551
+ if (userInfo.account)
552
+ console.log(`Account: ${userInfo.account}`);
553
+ if (userInfo.name)
554
+ console.log(`Name: ${userInfo.name}`);
555
+ }
556
+ else if (jwtPayload) {
557
+ const uname = jwtPayload.preferred_username ?? jwtPayload.name;
525
558
  if (uname)
526
559
  console.log(`Username: ${uname}`);
527
- console.log(`User ID: ${payload.sub ?? "(unknown)"}`);
528
- console.log(`Issuer: ${payload.iss ?? "(unknown)"}`);
529
- if (payload.iat)
530
- console.log(`Issued: ${new Date(payload.iat * 1000).toISOString()}`);
531
- if (payload.exp)
532
- console.log(`Expires: ${new Date(payload.exp * 1000).toISOString()}`);
560
+ console.log(`User ID: ${jwtPayload.sub ?? "(unknown)"}`);
533
561
  }
534
562
  else {
535
- console.log(`User info unavailable: opaque access token.`);
536
- console.log(`Hint: run \`kweaver auth login ${envUrl}\` to obtain a full session.`);
563
+ console.log(`User info unavailable: opaque access token and EACP did not respond.`);
564
+ console.log(`Hint: run \`kweaver auth login ${envUrl}\` to obtain a full session, or check connectivity to ${envUrl}.`);
565
+ }
566
+ if (jwtPayload) {
567
+ if (jwtPayload.iss)
568
+ console.log(`Issuer: ${jwtPayload.iss}`);
569
+ if (jwtPayload.iat)
570
+ console.log(`Issued: ${new Date(jwtPayload.iat * 1000).toISOString()}`);
571
+ if (jwtPayload.exp)
572
+ console.log(`Expires: ${new Date(jwtPayload.exp * 1000).toISOString()}`);
537
573
  }
538
574
  return 0;
539
575
  }
576
+ const platform = active.url;
540
577
  const token = loadTokenConfig(platform);
541
578
  if (!token) {
542
579
  console.error(`No saved token for ${platform}.`);
@@ -635,3 +672,217 @@ function readOption(args, name) {
635
672
  }
636
673
  return args[index + 1];
637
674
  }
675
+ const EACP_NEW_PWD_MIN = 6;
676
+ const EACP_NEW_PWD_MAX = 100;
677
+ function validateNewPasswordLengthForEacp(pwd) {
678
+ if (pwd.length < EACP_NEW_PWD_MIN || pwd.length > EACP_NEW_PWD_MAX) {
679
+ throw new Error(`New password must be between ${EACP_NEW_PWD_MIN} and ${EACP_NEW_PWD_MAX} characters.`);
680
+ }
681
+ }
682
+ function formatEacpModifyFailure(status, json, body) {
683
+ if (json && typeof json === "object" && json !== null) {
684
+ const o = json;
685
+ const msg = typeof o.message === "string" && o.message.trim() !== ""
686
+ ? o.message
687
+ : typeof o.cause === "string"
688
+ ? o.cause
689
+ : "";
690
+ if (msg)
691
+ return `Password change failed (HTTP ${status}): ${msg}`;
692
+ }
693
+ return `Password change failed (HTTP ${status}): ${body.slice(0, 500)}`;
694
+ }
695
+ async function promptYesNo(message) {
696
+ const { createInterface } = await import("node:readline");
697
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
698
+ return await new Promise((resolve, reject) => {
699
+ let answered = false;
700
+ rl.on("close", () => {
701
+ if (!answered)
702
+ reject(new Error("Login cancelled."));
703
+ });
704
+ rl.question(`${message} [Y/n] `, (answer) => {
705
+ answered = true;
706
+ rl.close();
707
+ const a = answer.trim().toLowerCase();
708
+ resolve(a === "" || a === "y" || a === "yes");
709
+ });
710
+ });
711
+ }
712
+ async function loginWithInitialPasswordRecovery(normalizedTarget, signinOpts, recovery) {
713
+ try {
714
+ return await oauth2PasswordSigninLogin(normalizedTarget, signinOpts);
715
+ }
716
+ catch (e) {
717
+ if (!(e instanceof InitialPasswordChangeRequiredError))
718
+ throw e;
719
+ const err = e;
720
+ const account = signinOpts.username;
721
+ const oldPwd = signinOpts.password;
722
+ let newPwd = recovery.newPasswordFlag;
723
+ if (newPwd !== undefined) {
724
+ validateNewPasswordLengthForEacp(newPwd);
725
+ }
726
+ else if (process.stderr.isTTY) {
727
+ process.stderr.write(`${err.serverMessage}\n`);
728
+ const ok = await promptYesNo(`Account "${account}" must change its initial password. Proceed with password change now?`);
729
+ if (!ok) {
730
+ throw new Error("Initial password change declined. Run again when ready.");
731
+ }
732
+ const np1 = await promptForPassword("New password (6-100 characters)");
733
+ const np2 = await promptForPassword("Confirm new password");
734
+ if (np1 !== np2) {
735
+ throw new Error("New passwords do not match.");
736
+ }
737
+ validateNewPasswordLengthForEacp(np1);
738
+ newPwd = np1;
739
+ }
740
+ else {
741
+ throw new Error("This account must change its initial password (error 401001017). Re-run with --new-password <password> (non-interactive).");
742
+ }
743
+ const mod = await eacpModifyPassword(normalizedTarget, {
744
+ account,
745
+ oldPassword: oldPwd,
746
+ newPassword: newPwd,
747
+ tlsInsecure: recovery.tlsInsecure,
748
+ });
749
+ if (!mod.ok) {
750
+ throw new Error(formatEacpModifyFailure(mod.status, mod.json, mod.body));
751
+ }
752
+ return oauth2PasswordSigninLogin(normalizedTarget, {
753
+ ...signinOpts,
754
+ password: newPwd,
755
+ });
756
+ }
757
+ }
758
+ async function runAuthChangePasswordCommand(args) {
759
+ if (args[0] === "--help" || args[0] === "-h") {
760
+ console.log(`kweaver auth change-password [<platform-url>] [options]
761
+
762
+ Change the EACP account password via POST /api/eacp/v1/auth1/modifypassword.
763
+ No saved OAuth token is required.
764
+
765
+ Options:
766
+ -u, --account <name> Account / login name. On TTY, defaults to the current active user
767
+ after a confirmation prompt. Required in non-interactive mode.
768
+ -o, --old-password <pwd> Current password (omit on TTY to be prompted)
769
+ -n, --new-password <pwd> New password, 6-100 characters (omit on TTY to be prompted)
770
+ --insecure, -k Skip TLS certificate verification (defaults to the platform's saved
771
+ preference set at login with -k; pass to override per-call)
772
+
773
+ Platform URL is optional; defaults to the current active platform (kweaver auth use).`);
774
+ return 0;
775
+ }
776
+ const KNOWN_CP_FLAGS = new Set([
777
+ "-u",
778
+ "--account",
779
+ "-o",
780
+ "--old-password",
781
+ "-n",
782
+ "--new-password",
783
+ "--insecure",
784
+ "-k",
785
+ "--help",
786
+ "-h",
787
+ ]);
788
+ const KNOWN_CP_VALUE = new Set([
789
+ "-u",
790
+ "--account",
791
+ "-o",
792
+ "--old-password",
793
+ "-n",
794
+ "--new-password",
795
+ ]);
796
+ // First positional (if present and not a flag) is the platform URL or alias.
797
+ const positional = args[0] && !args[0].startsWith("-") ? args[0] : undefined;
798
+ const flagArgs = positional ? args.slice(1) : args;
799
+ for (let i = 0; i < flagArgs.length; i++) {
800
+ const a = flagArgs[i];
801
+ if (a.startsWith("-") && !KNOWN_CP_FLAGS.has(a)) {
802
+ console.error(`Unknown option: ${a}`);
803
+ console.error("Run 'kweaver auth change-password --help' for usage.");
804
+ return 1;
805
+ }
806
+ if (KNOWN_CP_VALUE.has(a))
807
+ i++;
808
+ }
809
+ const normalizedTarget = resolvePlatformArg(positional ? [positional] : []);
810
+ if (!normalizedTarget) {
811
+ console.error("No platform resolved. Pass <platform-url|alias> or run `kweaver auth use <url|alias>` first.");
812
+ return 1;
813
+ }
814
+ let account = readOption(flagArgs, "--account") ?? readOption(flagArgs, "-u");
815
+ let oldPassword = readOption(flagArgs, "--old-password") ?? readOption(flagArgs, "-o");
816
+ let newPassword = readOption(flagArgs, "--new-password") ?? readOption(flagArgs, "-n");
817
+ const explicitTlsInsecure = flagArgs.includes("--insecure") || flagArgs.includes("-k");
818
+ // Resolve the active user's saved token; we use it both to default the account
819
+ // and to inherit the platform's saved tlsInsecure preference (set at login with -k).
820
+ const activeUser = getActiveUser(normalizedTarget);
821
+ const activeToken = activeUser ? loadUserTokenConfig(normalizedTarget, activeUser) : null;
822
+ const tlsInsecure = explicitTlsInsecure || activeToken?.tlsInsecure === true;
823
+ const interactive = process.stdin.isTTY === true && process.stderr.isTTY === true;
824
+ const accountWasExplicit = !!account?.trim();
825
+ // Account resolution (with safety guards):
826
+ // - Explicit -u always wins.
827
+ // - Non-TTY + no -u: REFUSE. Silently using the active account in CI / pipes
828
+ // would let scripts modify the wrong account's password without warning.
829
+ // - TTY + no -u: default to the active user's displayName, but require an
830
+ // interactive yes/no confirmation before proceeding.
831
+ if (!accountWasExplicit) {
832
+ const defaultAccount = activeToken?.displayName?.trim();
833
+ if (!defaultAccount) {
834
+ console.error("Cannot determine current account on the platform. Pass -u/--account, or log in first (kweaver auth login ...).");
835
+ return 1;
836
+ }
837
+ if (!interactive) {
838
+ console.error(`Refusing to default account in non-interactive mode. Pass -u/--account explicitly (would have used "${defaultAccount}").`);
839
+ return 1;
840
+ }
841
+ const ok = await promptYesNo(`Change password for account "${defaultAccount}" on ${normalizedTarget}?`);
842
+ if (!ok) {
843
+ console.error("Aborted by user.");
844
+ return 1;
845
+ }
846
+ account = defaultAccount;
847
+ }
848
+ const trimmedAccount = account.trim();
849
+ try {
850
+ if (!interactive) {
851
+ if (!oldPassword || !newPassword) {
852
+ console.error("In non-interactive mode, --old-password and --new-password are required.");
853
+ return 1;
854
+ }
855
+ }
856
+ else {
857
+ if (!oldPassword) {
858
+ oldPassword = await promptForPassword("Old password");
859
+ }
860
+ if (!newPassword) {
861
+ const n1 = await promptForPassword("New password (6-100 characters)");
862
+ const n2 = await promptForPassword("Confirm new password");
863
+ if (n1 !== n2) {
864
+ console.error("New passwords do not match.");
865
+ return 1;
866
+ }
867
+ newPassword = n1;
868
+ }
869
+ }
870
+ validateNewPasswordLengthForEacp(newPassword);
871
+ const result = await eacpModifyPassword(normalizedTarget, {
872
+ account: trimmedAccount,
873
+ oldPassword: oldPassword,
874
+ newPassword: newPassword,
875
+ tlsInsecure,
876
+ });
877
+ if (!result.ok) {
878
+ console.error(`${formatEacpModifyFailure(result.status, result.json, result.body)} (account="${trimmedAccount}")`);
879
+ return 1;
880
+ }
881
+ console.log(`Password changed for ${trimmedAccount} on ${normalizedTarget}`);
882
+ return 0;
883
+ }
884
+ catch (e) {
885
+ console.error(`${formatHttpError(e)}\n(account="${trimmedAccount}")`);
886
+ return 1;
887
+ }
888
+ }
@@ -1,15 +1,7 @@
1
1
  import { listBusinessDomains } from "../api/business-domains.js";
2
- import { normalizeBaseUrl, withTokenRetry } from "../auth/oauth.js";
3
- // Resolve platform URL: saved current platform > KWEAVER_BASE_URL (normalized to
4
- // match what `auth login` writes, so env users share the same platforms/<key>/ dir).
5
- function resolvePlatformUrl() {
6
- const saved = getCurrentPlatform();
7
- if (saved)
8
- return saved;
9
- const env = process.env.KWEAVER_BASE_URL?.trim();
10
- return env ? normalizeBaseUrl(env) : undefined;
11
- }
12
- import { getCurrentPlatform, loadPlatformBusinessDomain, resolveBusinessDomain, savePlatformBusinessDomain, } from "../config/store.js";
2
+ import { fetchEacpUserInfo, resolveActivePlatform, withTokenRetry } from "../auth/oauth.js";
3
+ import { HttpError } from "../utils/http.js";
4
+ import { loadPlatformBusinessDomain, resolveBusinessDomain, savePlatformBusinessDomain, } from "../config/store.js";
13
5
  const HELP = `kweaver config
14
6
 
15
7
  Subcommands:
@@ -29,20 +21,21 @@ export async function runConfigCommand(args) {
29
21
  return 0;
30
22
  }
31
23
  if (sub === "show") {
32
- const platform = resolvePlatformUrl();
33
- if (!platform) {
24
+ const active = resolveActivePlatform();
25
+ if (!active) {
34
26
  console.error("No active platform. Run `kweaver auth login <url>` first.\n Tip: set KWEAVER_BASE_URL to use this command without a saved login.");
35
27
  return 1;
36
28
  }
29
+ const platform = active.url;
37
30
  const bd = resolveBusinessDomain(platform);
38
- const source = process.env.KWEAVER_BUSINESS_DOMAIN
31
+ const bdSource = process.env.KWEAVER_BUSINESS_DOMAIN
39
32
  ? "env"
40
33
  : loadPlatformBusinessDomain(platform)
41
34
  ? "config"
42
35
  : "default";
43
- const platformSource = getCurrentPlatform() ? "" : " (KWEAVER_BASE_URL)";
44
- console.log(`Platform: ${platform}${platformSource}`);
45
- console.log(`Business Domain: ${bd} (${source})`);
36
+ const platformSuffix = active.source === "env" ? " (KWEAVER_BASE_URL)" : "";
37
+ console.log(`Platform: ${platform}${platformSuffix}`);
38
+ console.log(`Business Domain: ${bd} (${bdSource})`);
46
39
  return 0;
47
40
  }
48
41
  if (sub === "set-bd") {
@@ -51,27 +44,36 @@ export async function runConfigCommand(args) {
51
44
  console.error("Usage: kweaver config set-bd <value>");
52
45
  return 1;
53
46
  }
54
- const platform = resolvePlatformUrl();
55
- if (!platform) {
47
+ const active = resolveActivePlatform();
48
+ if (!active) {
56
49
  console.error("No active platform. Run `kweaver auth login <url>` first.\n Tip: set KWEAVER_BASE_URL to write the business domain for that platform.");
57
50
  return 1;
58
51
  }
52
+ const platform = active.url;
59
53
  savePlatformBusinessDomain(platform, value);
60
- console.log(`Business domain set to: ${value} (${getCurrentPlatform() ? platform : `${platform} via KWEAVER_BASE_URL`})`);
54
+ const provenance = active.source === "env" ? `${platform} via KWEAVER_BASE_URL` : platform;
55
+ console.log(`Business domain set to: ${value} (${provenance})`);
61
56
  return 0;
62
57
  }
63
58
  if (sub === "list-bd") {
64
- const platform = resolvePlatformUrl();
65
- if (!platform) {
59
+ const active = resolveActivePlatform();
60
+ if (!active) {
66
61
  console.error("No active platform. Run `kweaver auth login <url>` first.\n Tip: set KWEAVER_BASE_URL and KWEAVER_TOKEN to use this command without a saved login.");
67
62
  return 1;
68
63
  }
64
+ const platform = active.url;
65
+ let lastAccessToken = "";
66
+ let lastTlsInsecure;
69
67
  try {
70
- const rows = await withTokenRetry((token) => listBusinessDomains({
71
- baseUrl: platform,
72
- accessToken: token.accessToken,
73
- tlsInsecure: token.tlsInsecure,
74
- }));
68
+ const rows = await withTokenRetry((token) => {
69
+ lastAccessToken = token.accessToken;
70
+ lastTlsInsecure = token.tlsInsecure;
71
+ return listBusinessDomains({
72
+ baseUrl: platform,
73
+ accessToken: token.accessToken,
74
+ tlsInsecure: token.tlsInsecure,
75
+ });
76
+ });
75
77
  const currentId = resolveBusinessDomain(platform);
76
78
  const payload = {
77
79
  currentId,
@@ -84,7 +86,12 @@ export async function runConfigCommand(args) {
84
86
  return 0;
85
87
  }
86
88
  catch (error) {
87
- const message = error instanceof Error ? error.message : String(error);
89
+ // Backend returns 401 + `invalid user_id` when the caller is an app
90
+ // (service) token with no bound user. Probe EACP to confirm — only swap
91
+ // the cryptic backend body for a one-liner when we can prove `type:"app"`.
92
+ // See kweaver-core#263.
93
+ const friendly = await maybeAppAccountMessage(error, platform, lastAccessToken, lastTlsInsecure);
94
+ const message = friendly ?? (error instanceof Error ? error.message : String(error));
88
95
  console.error(`Failed to list business domains: ${message}`);
89
96
  return 1;
90
97
  }
@@ -93,3 +100,48 @@ export async function runConfigCommand(args) {
93
100
  console.log(HELP);
94
101
  return 1;
95
102
  }
103
+ /**
104
+ * Detect "app account hit a user-scoped endpoint" by signature, then confirm
105
+ * with EACP. Returns a short user-facing message if the call really came from
106
+ * an app token, otherwise `null` (caller falls back to the original error).
107
+ *
108
+ * Two layers of evidence (signature + identity) keep us from probing EACP on
109
+ * every random failure and from mislabeling real auth problems.
110
+ */
111
+ async function maybeAppAccountMessage(error, baseUrl, accessToken, tlsInsecure) {
112
+ // Detect 401 either as a direct HttpError or via the wrapping Error's
113
+ // message ("Authentication failed (401)..." / "...status 401..."). We don't
114
+ // rely solely on the `cause` chain because withTokenRetry may attempt a
115
+ // token refresh and then wrap with the *refresh* error as cause, dropping
116
+ // the original list-bd HttpError.
117
+ if (!is401Error(error))
118
+ return null;
119
+ if (!accessToken)
120
+ return null;
121
+ const info = await fetchEacpUserInfo(baseUrl, accessToken, tlsInsecure);
122
+ if (info?.type !== "app")
123
+ return null;
124
+ return "This command does not support app accounts.";
125
+ }
126
+ /** True if the error or any cause looks like a 401 from the platform. */
127
+ function is401Error(error) {
128
+ const seen = new Set();
129
+ const queue = [error];
130
+ while (queue.length) {
131
+ const cur = queue.shift();
132
+ if (!cur || seen.has(cur))
133
+ continue;
134
+ seen.add(cur);
135
+ if (cur instanceof HttpError && cur.status === 401)
136
+ return true;
137
+ if (cur instanceof Error) {
138
+ if (/\b401\b/.test(cur.message))
139
+ return true;
140
+ if (cur.cause)
141
+ queue.push(cur.cause);
142
+ }
143
+ if (cur instanceof AggregateError)
144
+ queue.push(...cur.errors);
145
+ }
146
+ return false;
147
+ }
package/dist/index.d.ts CHANGED
@@ -62,4 +62,5 @@ export type { TokenConfig, ContextLoaderEntry, ContextLoaderConfig, } from "./co
62
62
  export type { UserProfile } from "./config/store.js";
63
63
  export { NO_AUTH_TOKEN, isNoAuth, saveNoAuthPlatform, autoSelectBusinessDomain, getConfigDir, getCurrentPlatform, getActiveUser, setActiveUser, listUsers, listUserProfiles, resolveUserId, extractUserId, } from "./config/store.js";
64
64
  export { decodeJwtPayload, extractUserIdFromJwt } from "./config/jwt.js";
65
- export { DEFAULT_SIGNIN_RSA_MODULUS_HEX, oauth2PasswordSigninLogin, parseSigninPageHtmlProps, rsaModulusHexToSpkiPem, STUDIOWEB_LOGIN_PUBLIC_KEY_PEM, } from "./auth/oauth.js";
65
+ export { DEFAULT_SIGNIN_RSA_MODULUS_HEX, InitialPasswordChangeRequiredError, oauth2PasswordSigninLogin, parseSigninPageHtmlProps, rsaModulusHexToSpkiPem, STUDIOWEB_LOGIN_PUBLIC_KEY_PEM, } from "./auth/oauth.js";
66
+ export { eacpModifyPassword, encryptModifyPwd } from "./auth/eacp-modify-password.js";
package/dist/index.js CHANGED
@@ -50,4 +50,5 @@ export { NO_AUTH_TOKEN, isNoAuth, saveNoAuthPlatform, autoSelectBusinessDomain,
50
50
  // ── JWT utilities ─────────────────────────────────────────────────────────────
51
51
  export { decodeJwtPayload, extractUserIdFromJwt } from "./config/jwt.js";
52
52
  // ── OAuth (advanced — CLI uses these internally; optional for custom login tools) ─
53
- export { DEFAULT_SIGNIN_RSA_MODULUS_HEX, oauth2PasswordSigninLogin, parseSigninPageHtmlProps, rsaModulusHexToSpkiPem, STUDIOWEB_LOGIN_PUBLIC_KEY_PEM, } from "./auth/oauth.js";
53
+ export { DEFAULT_SIGNIN_RSA_MODULUS_HEX, InitialPasswordChangeRequiredError, oauth2PasswordSigninLogin, parseSigninPageHtmlProps, rsaModulusHexToSpkiPem, STUDIOWEB_LOGIN_PUBLIC_KEY_PEM, } from "./auth/oauth.js";
54
+ export { eacpModifyPassword, encryptModifyPwd } from "./auth/eacp-modify-password.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kweaver-ai/kweaver-sdk",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
4
4
  "description": "KWeaver TypeScript SDK — CLI tool and programmatic API for knowledge networks and Decision Agents.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",