@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.
- package/README.md +7 -2
- package/README.zh.md +6 -2
- package/dist/api/toolboxes.js +4 -4
- package/dist/auth/eacp-modify-password.d.ts +25 -0
- package/dist/auth/eacp-modify-password.js +84 -0
- package/dist/auth/oauth.d.ts +71 -3
- package/dist/auth/oauth.js +140 -18
- package/dist/cli.js +2 -1
- package/dist/commands/agent-members.d.ts +68 -0
- package/dist/commands/agent-members.js +383 -0
- package/dist/commands/agent.js +4 -0
- package/dist/commands/auth.js +298 -47
- package/dist/commands/config.js +80 -28
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/package.json +1 -1
package/dist/commands/auth.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
257
|
-
if (!
|
|
258
|
-
|
|
259
|
-
|
|
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 (!
|
|
262
|
-
console.error(
|
|
263
|
-
"
|
|
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: ${
|
|
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
|
|
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
|
|
507
|
-
if (!
|
|
508
|
-
|
|
509
|
-
|
|
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 (!
|
|
512
|
-
console.error(
|
|
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
|
|
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
|
-
|
|
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 (
|
|
524
|
-
|
|
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: ${
|
|
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
|
+
}
|
package/dist/commands/config.js
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
import { listBusinessDomains } from "../api/business-domains.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
33
|
-
if (!
|
|
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
|
|
31
|
+
const bdSource = process.env.KWEAVER_BUSINESS_DOMAIN
|
|
39
32
|
? "env"
|
|
40
33
|
: loadPlatformBusinessDomain(platform)
|
|
41
34
|
? "config"
|
|
42
35
|
: "default";
|
|
43
|
-
const
|
|
44
|
-
console.log(`Platform: ${platform}${
|
|
45
|
-
console.log(`Business Domain: ${bd} (${
|
|
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
|
|
55
|
-
if (!
|
|
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
|
-
|
|
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
|
|
65
|
-
if (!
|
|
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) =>
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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