@kweaver-ai/kweaver-sdk 0.6.5 → 0.6.7
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 +5 -2
- package/README.zh.md +4 -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 +65 -21
- package/dist/auth/oauth.js +216 -220
- package/dist/cli.js +2 -1
- package/dist/commands/auth.js +259 -94
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/package.json +1 -11
package/dist/auth/oauth.js
CHANGED
|
@@ -3,6 +3,21 @@ import { createPublicKey } from "node:crypto";
|
|
|
3
3
|
import { isNoAuth } from "../config/no-auth.js";
|
|
4
4
|
import { deleteClientConfig, getCurrentPlatform, loadClientConfig, loadTokenConfig, loadUserTokenConfig, resolveUserId, saveClientConfig, saveNoAuthPlatform, saveTokenConfig, setCurrentPlatform, } from "../config/store.js";
|
|
5
5
|
import { HttpError, NetworkRequestError, fetchWithRetry } from "../utils/http.js";
|
|
6
|
+
/** Thrown when `POST /oauth2/signin` returns HTTP 401 with EACP code `401001017` (initial password must be changed). */
|
|
7
|
+
export class InitialPasswordChangeRequiredError extends Error {
|
|
8
|
+
code = 401001017;
|
|
9
|
+
account;
|
|
10
|
+
baseUrl;
|
|
11
|
+
httpStatus = 401;
|
|
12
|
+
serverMessage;
|
|
13
|
+
constructor(opts) {
|
|
14
|
+
super(opts.serverMessage);
|
|
15
|
+
this.name = "InitialPasswordChangeRequiredError";
|
|
16
|
+
this.account = opts.account;
|
|
17
|
+
this.baseUrl = opts.baseUrl;
|
|
18
|
+
this.serverMessage = opts.serverMessage;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
6
21
|
const TOKEN_TTL_SECONDS = 3600;
|
|
7
22
|
/** Seconds before access token expiry to trigger refresh (matches Python ConfigAuth). */
|
|
8
23
|
const REFRESH_THRESHOLD_SEC = 60;
|
|
@@ -463,23 +478,36 @@ function stderrEmphasis(text) {
|
|
|
463
478
|
/**
|
|
464
479
|
* Headless login: read authorization code from stdin (full callback URL or raw code).
|
|
465
480
|
* Used with `--no-browser` or when automatic browser launch fails.
|
|
481
|
+
*
|
|
482
|
+
* `io` is injectable for tests; defaults to `process.stdin` / `process.stderr`.
|
|
466
483
|
*/
|
|
467
|
-
async function promptForCode(authUrl, state, port, pasteMode = "explicit") {
|
|
484
|
+
export async function promptForCode(authUrl, state, port, pasteMode = "explicit", io) {
|
|
468
485
|
const { createInterface } = await import("node:readline");
|
|
486
|
+
const stdin = io?.input ?? process.stdin;
|
|
487
|
+
const stderr = io?.output ?? process.stderr;
|
|
469
488
|
const intro = pasteMode === "explicit"
|
|
470
489
|
? "Open this URL on any device (use a private/incognito window if you need the full sign-in form):\n\n"
|
|
471
490
|
: "Could not open a browser automatically. Open this URL on any device:\n\n";
|
|
472
491
|
const pasteInstructions = "After login, the browser may show an error page (this is expected if nothing listens on localhost).\n" +
|
|
473
492
|
"Copy the FULL URL from the address bar and paste it here, or paste only the authorization code.\n" +
|
|
474
493
|
`The URL looks like: http://127.0.0.1:${port}/callback?code=THIS_PART&state=...\n\n`;
|
|
475
|
-
|
|
494
|
+
stderr.write("\n" +
|
|
476
495
|
intro +
|
|
477
496
|
` ${authUrl}\n\n` +
|
|
478
497
|
stderrEmphasis(pasteInstructions));
|
|
479
|
-
const rl = createInterface({ input:
|
|
498
|
+
const rl = createInterface({ input: stdin, output: stderr });
|
|
499
|
+
// The `close` listener exists to surface Ctrl-D / EOF before the user answers.
|
|
500
|
+
// It MUST be a no-op once the question callback has fired, because `rl.close()`
|
|
501
|
+
// emits `close` synchronously and would otherwise reject the promise before
|
|
502
|
+
// `resolve(answer)` runs (race condition that turns valid input into "Login cancelled.").
|
|
480
503
|
const input = await new Promise((resolve, reject) => {
|
|
481
|
-
|
|
504
|
+
let answered = false;
|
|
505
|
+
rl.on("close", () => {
|
|
506
|
+
if (!answered)
|
|
507
|
+
reject(new Error("Login cancelled."));
|
|
508
|
+
});
|
|
482
509
|
rl.question("Paste URL or code> ", (answer) => {
|
|
510
|
+
answered = true;
|
|
483
511
|
rl.close();
|
|
484
512
|
resolve(answer.trim());
|
|
485
513
|
});
|
|
@@ -512,6 +540,128 @@ async function promptForCode(authUrl, state, port, pasteMode = "explicit") {
|
|
|
512
540
|
}
|
|
513
541
|
return input;
|
|
514
542
|
}
|
|
543
|
+
/**
|
|
544
|
+
* Prompt the user for a username on stderr (input echoed).
|
|
545
|
+
*
|
|
546
|
+
* `io` is injectable for tests; defaults to `process.stdin` / `process.stderr`.
|
|
547
|
+
*/
|
|
548
|
+
export async function promptForUsername(promptLabel = "Username", io) {
|
|
549
|
+
const { createInterface } = await import("node:readline");
|
|
550
|
+
const stdin = io?.input ?? process.stdin;
|
|
551
|
+
const stderr = io?.output ?? process.stderr;
|
|
552
|
+
const rl = createInterface({ input: stdin, output: stderr });
|
|
553
|
+
const value = await new Promise((resolve, reject) => {
|
|
554
|
+
let answered = false;
|
|
555
|
+
rl.on("close", () => {
|
|
556
|
+
if (!answered)
|
|
557
|
+
reject(new Error("Login cancelled."));
|
|
558
|
+
});
|
|
559
|
+
rl.question(`${promptLabel}: `, (answer) => {
|
|
560
|
+
answered = true;
|
|
561
|
+
rl.close();
|
|
562
|
+
resolve(answer.trim());
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
if (!value) {
|
|
566
|
+
throw new Error(`${promptLabel} is required.`);
|
|
567
|
+
}
|
|
568
|
+
return value;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Prompt the user for a password on stderr without echoing keystrokes (TTY only).
|
|
572
|
+
*
|
|
573
|
+
* Falls back to a regular readline prompt when stdin is not a TTY (e.g. piped input
|
|
574
|
+
* during scripted use); callers needing strict no-echo should detect this case themselves.
|
|
575
|
+
*
|
|
576
|
+
* `io` is injectable for tests.
|
|
577
|
+
*/
|
|
578
|
+
export async function promptForPassword(promptLabel = "Password", io) {
|
|
579
|
+
const stdin = io?.input ?? process.stdin;
|
|
580
|
+
const stderr = io?.output ?? process.stderr;
|
|
581
|
+
// Non-TTY (piped, redirected, tests): use regular readline — no masking is possible
|
|
582
|
+
// without raw mode, so we accept echoed input rather than block forever.
|
|
583
|
+
if (!stdin.isTTY || typeof stdin.setRawMode !== "function") {
|
|
584
|
+
const { createInterface } = await import("node:readline");
|
|
585
|
+
const rl = createInterface({ input: stdin, output: stderr });
|
|
586
|
+
const value = await new Promise((resolve, reject) => {
|
|
587
|
+
let answered = false;
|
|
588
|
+
rl.on("close", () => {
|
|
589
|
+
if (!answered)
|
|
590
|
+
reject(new Error("Login cancelled."));
|
|
591
|
+
});
|
|
592
|
+
rl.question(`${promptLabel}: `, (answer) => {
|
|
593
|
+
answered = true;
|
|
594
|
+
rl.close();
|
|
595
|
+
resolve(answer);
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
if (!value)
|
|
599
|
+
throw new Error(`${promptLabel} is required.`);
|
|
600
|
+
return value;
|
|
601
|
+
}
|
|
602
|
+
// TTY: read byte-by-byte in raw mode so keystrokes are not echoed.
|
|
603
|
+
return new Promise((resolve, reject) => {
|
|
604
|
+
stderr.write(`${promptLabel}: `);
|
|
605
|
+
let buf = "";
|
|
606
|
+
const onData = (chunk) => {
|
|
607
|
+
const s = chunk.toString("utf8");
|
|
608
|
+
for (const ch of s) {
|
|
609
|
+
const code = ch.charCodeAt(0);
|
|
610
|
+
if (ch === "\n" || ch === "\r") {
|
|
611
|
+
cleanup();
|
|
612
|
+
stderr.write("\n");
|
|
613
|
+
if (!buf) {
|
|
614
|
+
reject(new Error(`${promptLabel} is required.`));
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
resolve(buf);
|
|
618
|
+
}
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if (code === 3) {
|
|
622
|
+
// Ctrl-C
|
|
623
|
+
cleanup();
|
|
624
|
+
stderr.write("\n");
|
|
625
|
+
reject(new Error("Login cancelled."));
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (code === 4 && buf.length === 0) {
|
|
629
|
+
// Ctrl-D on empty buffer -> cancel
|
|
630
|
+
cleanup();
|
|
631
|
+
stderr.write("\n");
|
|
632
|
+
reject(new Error("Login cancelled."));
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (code === 8 || code === 127) {
|
|
636
|
+
// Backspace / DEL
|
|
637
|
+
buf = buf.slice(0, -1);
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
if (code < 32)
|
|
641
|
+
continue; // ignore other control chars
|
|
642
|
+
buf += ch;
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
const cleanup = () => {
|
|
646
|
+
try {
|
|
647
|
+
stdin.setRawMode(false);
|
|
648
|
+
}
|
|
649
|
+
catch { /* noop */ }
|
|
650
|
+
stdin.removeListener("data", onData);
|
|
651
|
+
if (typeof stdin.pause === "function") {
|
|
652
|
+
stdin.pause();
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
try {
|
|
656
|
+
stdin.setRawMode(true);
|
|
657
|
+
}
|
|
658
|
+
catch { /* noop */ }
|
|
659
|
+
if (typeof stdin.resume === "function") {
|
|
660
|
+
stdin.resume();
|
|
661
|
+
}
|
|
662
|
+
stdin.on("data", onData);
|
|
663
|
+
});
|
|
664
|
+
}
|
|
515
665
|
/**
|
|
516
666
|
* OAuth2 Authorization Code login flow.
|
|
517
667
|
* 1. Register client (if not already registered), OR use a provided client ID
|
|
@@ -755,174 +905,6 @@ async function exchangeCodeForToken(baseUrl, code, clientId, clientSecret, redir
|
|
|
755
905
|
saveTokenConfig(token);
|
|
756
906
|
return token;
|
|
757
907
|
}
|
|
758
|
-
/**
|
|
759
|
-
* Playwright-automated OAuth2 login.
|
|
760
|
-
*
|
|
761
|
-
* Uses the full OAuth2 authorization code flow (same as `oauth2Login`) but
|
|
762
|
-
* automates the browser interaction with Playwright. This produces a
|
|
763
|
-
* refresh_token so the CLI can auto-refresh without re-login.
|
|
764
|
-
*
|
|
765
|
-
* When `username` and `password` are provided the browser runs headless and
|
|
766
|
-
* fills the login form automatically. Otherwise it opens a visible browser
|
|
767
|
-
* window for manual login (same UX as the old cookie-based flow).
|
|
768
|
-
*/
|
|
769
|
-
export async function playwrightLogin(baseUrl, options) {
|
|
770
|
-
return runWithTlsInsecure(options?.tlsInsecure, async () => {
|
|
771
|
-
const { createServer } = await import("node:http");
|
|
772
|
-
const { randomBytes } = await import("node:crypto");
|
|
773
|
-
let chromium;
|
|
774
|
-
try {
|
|
775
|
-
const modName = "playwright";
|
|
776
|
-
const pw = await import(/* webpackIgnore: true */ modName);
|
|
777
|
-
chromium = pw.chromium;
|
|
778
|
-
}
|
|
779
|
-
catch {
|
|
780
|
-
throw new Error("Playwright is not installed. Run:\n npm install playwright && npx playwright install chromium");
|
|
781
|
-
}
|
|
782
|
-
const base = normalizeBaseUrl(baseUrl);
|
|
783
|
-
const port = options?.port ?? DEFAULT_REDIRECT_PORT;
|
|
784
|
-
const scope = options?.scope ?? DEFAULT_SCOPE;
|
|
785
|
-
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
786
|
-
const hasCredentials = !!(options?.username && options?.password);
|
|
787
|
-
// Step 1: Ensure registered OAuth2 client (with stale-client auto-recovery)
|
|
788
|
-
let client;
|
|
789
|
-
try {
|
|
790
|
-
client = await resolveOrRegisterClient(base, redirectUri, scope);
|
|
791
|
-
}
|
|
792
|
-
catch (e) {
|
|
793
|
-
if (e instanceof HttpError && e.status === 404) {
|
|
794
|
-
process.stderr.write("OAuth2 endpoint not found (404). Saving platform in no-auth mode.\n");
|
|
795
|
-
return saveNoAuthPlatform(base, { tlsInsecure: options?.tlsInsecure });
|
|
796
|
-
}
|
|
797
|
-
throw e;
|
|
798
|
-
}
|
|
799
|
-
// Step 2: Generate CSRF state
|
|
800
|
-
const state = randomBytes(12).toString("hex");
|
|
801
|
-
// Step 3: Build authorization URL
|
|
802
|
-
const authParams = new URLSearchParams({
|
|
803
|
-
redirect_uri: redirectUri,
|
|
804
|
-
"x-forwarded-prefix": "",
|
|
805
|
-
client_id: client.clientId,
|
|
806
|
-
scope,
|
|
807
|
-
response_type: "code",
|
|
808
|
-
state,
|
|
809
|
-
lang: "zh-cn",
|
|
810
|
-
product: "adp",
|
|
811
|
-
});
|
|
812
|
-
const authUrl = `${base}/oauth2/auth?${authParams.toString()}`;
|
|
813
|
-
// Step 4: Start local callback server; exchange code inside handler, then show credentials HTML
|
|
814
|
-
let browser;
|
|
815
|
-
const token = await new Promise((resolve, reject) => {
|
|
816
|
-
const TIMEOUT_MS = hasCredentials ? 30_000 : 120_000;
|
|
817
|
-
let server;
|
|
818
|
-
const timeoutId = setTimeout(() => {
|
|
819
|
-
server?.close();
|
|
820
|
-
browser?.close();
|
|
821
|
-
reject(new Error(`OAuth2 login timed out (${TIMEOUT_MS / 1000}s). No authorization code received.`));
|
|
822
|
-
}, TIMEOUT_MS);
|
|
823
|
-
server = createServer((req, res) => {
|
|
824
|
-
void (async () => {
|
|
825
|
-
try {
|
|
826
|
-
const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
|
|
827
|
-
if (url.pathname !== "/callback") {
|
|
828
|
-
res.writeHead(404);
|
|
829
|
-
res.end();
|
|
830
|
-
return;
|
|
831
|
-
}
|
|
832
|
-
const receivedState = url.searchParams.get("state");
|
|
833
|
-
const receivedCode = url.searchParams.get("code");
|
|
834
|
-
const callbackError = url.searchParams.get("error");
|
|
835
|
-
const callbackErrorDesc = url.searchParams.get("error_description");
|
|
836
|
-
if (receivedState !== state) {
|
|
837
|
-
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
838
|
-
res.end(buildCallbackExchangeErrorHtml("OAuth2 state mismatch — possible CSRF attack."));
|
|
839
|
-
clearTimeout(timeoutId);
|
|
840
|
-
server.close();
|
|
841
|
-
browser?.close();
|
|
842
|
-
reject(new Error("OAuth2 state mismatch — possible CSRF attack."));
|
|
843
|
-
return;
|
|
844
|
-
}
|
|
845
|
-
if (callbackError) {
|
|
846
|
-
const msg = callbackErrorDesc
|
|
847
|
-
? `Authorization failed: ${callbackError} — ${callbackErrorDesc}`
|
|
848
|
-
: `Authorization failed: ${callbackError}`;
|
|
849
|
-
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
850
|
-
res.end(buildCallbackExchangeErrorHtml(msg));
|
|
851
|
-
clearTimeout(timeoutId);
|
|
852
|
-
server.close();
|
|
853
|
-
browser?.close();
|
|
854
|
-
reject(new Error(msg));
|
|
855
|
-
return;
|
|
856
|
-
}
|
|
857
|
-
if (!receivedCode) {
|
|
858
|
-
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
859
|
-
res.end(buildCallbackExchangeErrorHtml("No authorization code received in callback."));
|
|
860
|
-
clearTimeout(timeoutId);
|
|
861
|
-
server.close();
|
|
862
|
-
browser?.close();
|
|
863
|
-
reject(new Error("No authorization code received in callback."));
|
|
864
|
-
return;
|
|
865
|
-
}
|
|
866
|
-
const exchanged = await exchangeCodeForToken(base, receivedCode, client.clientId, client.clientSecret, redirectUri, undefined, options?.tlsInsecure);
|
|
867
|
-
const copyCommand = buildCopyCommand(base, client.clientId, client.clientSecret, exchanged.refreshToken, options?.tlsInsecure);
|
|
868
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
869
|
-
res.end(buildCallbackHtml(copyCommand));
|
|
870
|
-
clearTimeout(timeoutId);
|
|
871
|
-
server.close();
|
|
872
|
-
browser?.close();
|
|
873
|
-
resolve(exchanged);
|
|
874
|
-
}
|
|
875
|
-
catch (err) {
|
|
876
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
877
|
-
try {
|
|
878
|
-
res.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
|
|
879
|
-
res.end(buildCallbackExchangeErrorHtml(message));
|
|
880
|
-
}
|
|
881
|
-
catch {
|
|
882
|
-
/* response may already be sent */
|
|
883
|
-
}
|
|
884
|
-
clearTimeout(timeoutId);
|
|
885
|
-
server.close();
|
|
886
|
-
browser?.close();
|
|
887
|
-
reject(err instanceof Error ? err : new Error(message));
|
|
888
|
-
}
|
|
889
|
-
})();
|
|
890
|
-
});
|
|
891
|
-
server.listen(port, "127.0.0.1", async () => {
|
|
892
|
-
try {
|
|
893
|
-
browser = await chromium.launch({ headless: hasCredentials });
|
|
894
|
-
const context = await browser.newContext({ ignoreHTTPSErrors: !!options?.tlsInsecure });
|
|
895
|
-
const page = await context.newPage();
|
|
896
|
-
// Navigate to OAuth2 auth URL — redirects to signin page
|
|
897
|
-
await page.goto(authUrl, { waitUntil: "networkidle", timeout: 30_000 });
|
|
898
|
-
if (hasCredentials) {
|
|
899
|
-
// Auto-fill credentials
|
|
900
|
-
await page.waitForSelector('input[name="account"]', { timeout: 10_000 });
|
|
901
|
-
await page.fill('input[name="account"]', options.username);
|
|
902
|
-
await page.fill('input[name="password"]', options.password);
|
|
903
|
-
await page.click("button.ant-btn-primary");
|
|
904
|
-
}
|
|
905
|
-
// else: visible browser — user logs in manually
|
|
906
|
-
// The OAuth2 callback will fire when login completes, resolving the promise above
|
|
907
|
-
}
|
|
908
|
-
catch (err) {
|
|
909
|
-
clearTimeout(timeoutId);
|
|
910
|
-
server.close();
|
|
911
|
-
browser?.close();
|
|
912
|
-
reject(err);
|
|
913
|
-
}
|
|
914
|
-
});
|
|
915
|
-
});
|
|
916
|
-
if (hasCredentials) {
|
|
917
|
-
const copyCommand = buildCopyCommand(base, client.clientId, client.clientSecret, token.refreshToken, options?.tlsInsecure);
|
|
918
|
-
process.stderr.write("\nHeadless login: copy this command and run it on a machine without a browser, or use `kweaver auth export`:\n\n" +
|
|
919
|
-
copyCommand +
|
|
920
|
-
"\n\n");
|
|
921
|
-
}
|
|
922
|
-
setCurrentPlatform(base);
|
|
923
|
-
return token;
|
|
924
|
-
});
|
|
925
|
-
}
|
|
926
908
|
function mergeCookieJarForSignin(existing, response) {
|
|
927
909
|
const setCookies = typeof response.headers.getSetCookie === "function"
|
|
928
910
|
? response.headers.getSetCookie()
|
|
@@ -1041,7 +1023,7 @@ async function followSigninRedirectsUntilCallback(startUrl, initialJar, state, r
|
|
|
1041
1023
|
return consentResult;
|
|
1042
1024
|
}
|
|
1043
1025
|
throw new Error(`Unexpected OAuth page (HTTP 200) at ${url.slice(0, 120)}… ` +
|
|
1044
|
-
`If this is a consent or MFA screen, use browser login
|
|
1026
|
+
`If this is a consent or MFA screen, use browser login (kweaver auth login <url>).`);
|
|
1045
1027
|
}
|
|
1046
1028
|
const text = await resp.text().catch(() => "");
|
|
1047
1029
|
throw new HttpError(resp.status, resp.statusText, text);
|
|
@@ -1094,17 +1076,38 @@ async function tryAcceptConsentAfterSignin(base, pageUrl, html, jar, scope, stat
|
|
|
1094
1076
|
}
|
|
1095
1077
|
return null;
|
|
1096
1078
|
}
|
|
1097
|
-
const STUDIOWEB_SHELL_UNAVAILABLE_SNIPPETS = [
|
|
1098
|
-
"Studioweb signin endpoint not available",
|
|
1099
|
-
"Cannot reach studioweb signin endpoint",
|
|
1100
|
-
];
|
|
1101
1079
|
/**
|
|
1102
|
-
*
|
|
1103
|
-
*
|
|
1080
|
+
* Build the JSON body for `POST /oauth2/signin` (matches the browser `oauth2-ui` form).
|
|
1081
|
+
*
|
|
1082
|
+
* `device.client_type` MUST be a value present in the EACP whitelist defined by
|
|
1083
|
+
* `kweaver/deploy/auto_cofig/auto_config.sh`. `console_web` is the canonical CLI value
|
|
1084
|
+
* (also used by `kweaver-admin`); other values such as `unknown` are rejected by strict
|
|
1085
|
+
* deployments with `管理员已禁止此类客户端登录` — surfaced upstream as a `request_forbidden`
|
|
1086
|
+
* `No CSRF value available in the session cookie` error after Hydra discards the rejected
|
|
1087
|
+
* login challenge.
|
|
1088
|
+
*
|
|
1089
|
+
* `vcode` and `dualfactorauthinfo` must be present even when empty; otherwise eachttpserver
|
|
1090
|
+
* returns HTTP 400 (invalid parameter).
|
|
1104
1091
|
*/
|
|
1105
|
-
export function
|
|
1106
|
-
|
|
1107
|
-
|
|
1092
|
+
export function buildOauth2SigninPostBody(opts) {
|
|
1093
|
+
return {
|
|
1094
|
+
_csrf: opts.csrftoken,
|
|
1095
|
+
challenge: opts.challenge,
|
|
1096
|
+
account: opts.account,
|
|
1097
|
+
password: opts.passwordCipher,
|
|
1098
|
+
vcode: { id: "", content: "" },
|
|
1099
|
+
dualfactorauthinfo: {
|
|
1100
|
+
validcode: { vcode: "" },
|
|
1101
|
+
OTP: { OTP: "" },
|
|
1102
|
+
},
|
|
1103
|
+
remember: opts.remember,
|
|
1104
|
+
device: {
|
|
1105
|
+
name: "",
|
|
1106
|
+
description: "",
|
|
1107
|
+
client_type: "console_web",
|
|
1108
|
+
udids: [],
|
|
1109
|
+
},
|
|
1110
|
+
};
|
|
1108
1111
|
}
|
|
1109
1112
|
/**
|
|
1110
1113
|
* OAuth2 Authorization Code login using HTTP **only**: `GET /oauth2/signin` (Next.js shell) and
|
|
@@ -1127,27 +1130,10 @@ export async function oauth2PasswordSigninLogin(baseUrl, options) {
|
|
|
1127
1130
|
(typeof process.env.KWEAVER_OAUTH_PRODUCT === "string" && process.env.KWEAVER_OAUTH_PRODUCT.trim()
|
|
1128
1131
|
? process.env.KWEAVER_OAUTH_PRODUCT.trim()
|
|
1129
1132
|
: "adp");
|
|
1130
|
-
//
|
|
1131
|
-
//
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
let probeResp;
|
|
1135
|
-
try {
|
|
1136
|
-
probeResp = await fetch(studiowebProbeUrl, { method: "GET", redirect: "manual" });
|
|
1137
|
-
}
|
|
1138
|
-
catch (cause) {
|
|
1139
|
-
throw new Error(`Cannot reach studioweb signin endpoint at ${base}/interface/studioweb/login. ` +
|
|
1140
|
-
`The deployment may not include studioweb. Use \`kweaver auth login ${base}\` ` +
|
|
1141
|
-
`(OAuth code flow) instead.\n Cause: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
1142
|
-
}
|
|
1143
|
-
const probeOk2xx = probeResp.status >= 200 && probeResp.status < 300;
|
|
1144
|
-
const probeOkRedirect = [301, 302, 303, 307, 308].includes(probeResp.status);
|
|
1145
|
-
await probeResp.text().catch(() => "");
|
|
1146
|
-
if (!probeOk2xx && !probeOkRedirect) {
|
|
1147
|
-
throw new Error(`Studioweb signin endpoint not available at ${base}/interface/studioweb/login ` +
|
|
1148
|
-
`(HTTP ${probeResp.status}). The deployment may not include studioweb. ` +
|
|
1149
|
-
`Use \`kweaver auth login ${base}\` (OAuth code flow) instead.`);
|
|
1150
|
-
}
|
|
1133
|
+
// Note: previously we pre-flighted `/interface/studioweb/login` to detect deployments
|
|
1134
|
+
// missing the Studio web shell. The probe added an extra round-trip and was unreliable
|
|
1135
|
+
// (see kweaver-admin which works fine without it). HTTP sign-in only needs `/oauth2/auth`
|
|
1136
|
+
// and `/oauth2/signin`; if either is missing the request below will surface a precise error.
|
|
1151
1137
|
let client;
|
|
1152
1138
|
try {
|
|
1153
1139
|
client = await resolveOrRegisterClient(base, redirectUri, scope, {
|
|
@@ -1231,26 +1217,13 @@ export async function oauth2PasswordSigninLogin(baseUrl, options) {
|
|
|
1231
1217
|
: options.signinPasswordBase64Plain === false
|
|
1232
1218
|
? false
|
|
1233
1219
|
: process.env.KWEAVER_SIGNIN_PASSWORD_B64_RSA_MIN !== "1";
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
const postBody = {
|
|
1237
|
-
_csrf: csrftoken,
|
|
1220
|
+
const postBody = buildOauth2SigninPostBody({
|
|
1221
|
+
csrftoken,
|
|
1238
1222
|
challenge: loginChallenge,
|
|
1239
1223
|
account: options.username,
|
|
1240
|
-
|
|
1241
|
-
vcode: { id: "", content: "" },
|
|
1242
|
-
dualfactorauthinfo: {
|
|
1243
|
-
validcode: { vcode: "" },
|
|
1244
|
-
OTP: { OTP: "" },
|
|
1245
|
-
},
|
|
1224
|
+
passwordCipher: "",
|
|
1246
1225
|
remember,
|
|
1247
|
-
|
|
1248
|
-
name: "",
|
|
1249
|
-
description: "",
|
|
1250
|
-
client_type: "unknown",
|
|
1251
|
-
udids: [],
|
|
1252
|
-
},
|
|
1253
|
-
};
|
|
1226
|
+
});
|
|
1254
1227
|
const origin = new URL(base).origin;
|
|
1255
1228
|
/** Some gateways (e.g. DIP) return HTTP 200 + `{"redirect":"..."}` instead of 3xx Location. */
|
|
1256
1229
|
let signinRedirectFromJson;
|
|
@@ -1310,6 +1283,26 @@ export async function oauth2PasswordSigninLogin(baseUrl, options) {
|
|
|
1310
1283
|
}
|
|
1311
1284
|
}
|
|
1312
1285
|
else {
|
|
1286
|
+
if (postResp.status === 401) {
|
|
1287
|
+
try {
|
|
1288
|
+
const j = JSON.parse(bodyText);
|
|
1289
|
+
const c = j.code;
|
|
1290
|
+
if (c === 401001017 || c === "401001017") {
|
|
1291
|
+
const msg = typeof j.message === "string" && j.message.trim() !== ""
|
|
1292
|
+
? j.message.trim()
|
|
1293
|
+
: "Initial password must be changed before login.";
|
|
1294
|
+
throw new InitialPasswordChangeRequiredError({
|
|
1295
|
+
account: options.username,
|
|
1296
|
+
baseUrl: base,
|
|
1297
|
+
serverMessage: msg,
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
catch (e) {
|
|
1302
|
+
if (e instanceof InitialPasswordChangeRequiredError)
|
|
1303
|
+
throw e;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1313
1306
|
throw new HttpError(postResp.status, postResp.statusText, bodyText);
|
|
1314
1307
|
}
|
|
1315
1308
|
}
|
|
@@ -1652,6 +1645,9 @@ function isTlsVerificationDisabledForProcess() {
|
|
|
1652
1645
|
process.env.KWEAVER_TLS_INSECURE === "true");
|
|
1653
1646
|
}
|
|
1654
1647
|
export function formatHttpError(error) {
|
|
1648
|
+
if (error instanceof InitialPasswordChangeRequiredError) {
|
|
1649
|
+
return `${error.serverMessage} (code ${error.code})`;
|
|
1650
|
+
}
|
|
1655
1651
|
if (error instanceof HttpError) {
|
|
1656
1652
|
const oauthMessage = formatOAuthErrorBody(error.body);
|
|
1657
1653
|
if (oauthMessage) {
|
package/dist/cli.js
CHANGED
|
@@ -23,9 +23,10 @@ Usage:
|
|
|
23
23
|
kweaver --version | -V
|
|
24
24
|
kweaver --help | -h
|
|
25
25
|
|
|
26
|
-
kweaver auth <platform-url> [--alias name] [--no-auth] [--no-browser] [-u user] [-p pass] [--
|
|
26
|
+
kweaver auth <platform-url> [--alias name] [--no-auth] [--no-browser] [-u user] [-p pass] [--new-password <pwd>] [--http-signin] [--insecure|-k]
|
|
27
27
|
kweaver auth login <platform-url> (alias for auth <url>)
|
|
28
28
|
kweaver auth login <url> --client-id ID --client-secret S --refresh-token T (run on host without browser)
|
|
29
|
+
kweaver auth change-password [<platform-url>] [-u <account>] [-o <old>] [-n <new>] [--insecure|-k]
|
|
29
30
|
kweaver auth whoami [platform-url|alias] [--json]
|
|
30
31
|
kweaver auth export [platform-url|alias] [--json]
|
|
31
32
|
kweaver auth status [platform-url|alias]
|