@kweaver-ai/kweaver-sdk 0.6.5 → 0.6.6

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 CHANGED
@@ -153,8 +153,8 @@ const skillMd = await client.skills.fetchContent("skill-id");
153
153
  ## CLI Reference
154
154
 
155
155
  ```
156
- kweaver auth login <url> [--alias name] [--no-auth] [--no-browser] [-u user] [-p pass] [--http-signin] [--playwright] [--insecure|-k]
157
- # -u/-p: tries HTTP /oauth2/signin first (refresh_token). If studioweb is missing: falls back to Playwright when installed, else prints install hint. --http-signin: HTTP only. --playwright: force browser automation.
156
+ kweaver auth login <url> [--alias name] [--no-auth] [--no-browser] [-u user] [-p pass] [--http-signin] [--insecure|-k]
157
+ # -u/-p (with or without --http-signin): HTTP POST /oauth2/signin (yields refresh_token). Missing -u/-p are prompted from stdin (password hidden when TTY).
158
158
  kweaver auth login <url> --client-id ID --client-secret S --refresh-token T (headless login)
159
159
  kweaver auth export [url|alias] [--json] (export command to run on a headless host)
160
160
  kweaver auth status / whoami [url|alias] [--json] # whoami: --json; with KWEAVER_BASE_URL+KWEAVER_TOKEN when no ~/.kweaver/ platform
package/README.zh.md CHANGED
@@ -146,8 +146,8 @@ const skillMd = await client.skills.fetchContent("skill-id");
146
146
  ## 命令速查
147
147
 
148
148
  ```
149
- kweaver auth login <url> [--alias name] [--no-auth] [--no-browser] [-u user] [-p pass] [--http-signin] [--playwright] [--insecure|-k]
150
- # -u/-p:默认先试 HTTP /oauth2/signin(可拿 refresh_token);无 studioweb 时:已装 Playwright 则回退无头浏览器,否则提示安装 Playwright;--http-signin 仅 HTTP;--playwright 强制浏览器
149
+ kweaver auth login <url> [--alias name] [--no-auth] [--no-browser] [-u user] [-p pass] [--http-signin] [--insecure|-k]
150
+ # -u/-p(无论是否带 --http-signin):HTTP POST /oauth2/signin(可拿 refresh_token);缺失的用户名/密码会从 stdin 提示输入(TTY 下密码隐藏)
151
151
  kweaver auth login <url> --client-id ID --client-secret S --refresh-token T (无浏览器登录)
152
152
  kweaver auth export [url|alias] [--json] (导出在无浏览器机器上运行的命令)
153
153
  kweaver auth status / whoami [url|alias] [--json] # whoami 支持 --json;无 ~/.kweaver/ 当前平台时可配 KWEAVER_BASE_URL+KWEAVER_TOKEN
@@ -32,6 +32,40 @@ export declare function normalizeBaseUrl(value: string): string;
32
32
  */
33
33
  /** @internal Exported for CLI env-only identity resolution (`env-snapshot.ts`). */
34
34
  export declare function runWithTlsInsecure<T>(tlsInsecure: boolean | undefined, fn: () => Promise<T>): Promise<T>;
35
+ /**
36
+ * Headless login: read authorization code from stdin (full callback URL or raw code).
37
+ * Used with `--no-browser` or when automatic browser launch fails.
38
+ *
39
+ * `io` is injectable for tests; defaults to `process.stdin` / `process.stderr`.
40
+ */
41
+ export declare function promptForCode(authUrl: string, state: string, port: number, pasteMode?: "explicit" | "fallback", io?: {
42
+ input?: NodeJS.ReadableStream;
43
+ output?: NodeJS.WritableStream;
44
+ }): Promise<string>;
45
+ /**
46
+ * Prompt the user for a username on stderr (input echoed).
47
+ *
48
+ * `io` is injectable for tests; defaults to `process.stdin` / `process.stderr`.
49
+ */
50
+ export declare function promptForUsername(promptLabel?: string, io?: {
51
+ input?: NodeJS.ReadableStream;
52
+ output?: NodeJS.WritableStream;
53
+ }): Promise<string>;
54
+ /**
55
+ * Prompt the user for a password on stderr without echoing keystrokes (TTY only).
56
+ *
57
+ * Falls back to a regular readline prompt when stdin is not a TTY (e.g. piped input
58
+ * during scripted use); callers needing strict no-echo should detect this case themselves.
59
+ *
60
+ * `io` is injectable for tests.
61
+ */
62
+ export declare function promptForPassword(promptLabel?: string, io?: {
63
+ input?: NodeJS.ReadableStream & {
64
+ isTTY?: boolean;
65
+ setRawMode?: (mode: boolean) => unknown;
66
+ };
67
+ output?: NodeJS.WritableStream;
68
+ }): Promise<string>;
35
69
  /**
36
70
  * OAuth2 Authorization Code login flow.
37
71
  * 1. Register client (if not already registered), OR use a provided client ID
@@ -53,24 +87,6 @@ export declare function oauth2Login(baseUrl: string, options?: {
53
87
  */
54
88
  noBrowser?: boolean;
55
89
  }): Promise<TokenConfig>;
56
- /**
57
- * Playwright-automated OAuth2 login.
58
- *
59
- * Uses the full OAuth2 authorization code flow (same as `oauth2Login`) but
60
- * automates the browser interaction with Playwright. This produces a
61
- * refresh_token so the CLI can auto-refresh without re-login.
62
- *
63
- * When `username` and `password` are provided the browser runs headless and
64
- * fills the login form automatically. Otherwise it opens a visible browser
65
- * window for manual login (same UX as the old cookie-based flow).
66
- */
67
- export declare function playwrightLogin(baseUrl: string, options?: {
68
- username?: string;
69
- password?: string;
70
- port?: number;
71
- scope?: string;
72
- tlsInsecure?: boolean;
73
- }): Promise<TokenConfig>;
74
90
  /**
75
91
  * Parse Next.js `__NEXT_DATA__` from the OAuth2 sign-in HTML shell (CSRF + optional challenge/remember for POST /oauth2/signin).
76
92
  * Hydra `login_challenge` may appear only in the sign-in URL; use that when `pageProps.challenge` is absent.
@@ -83,10 +99,25 @@ export declare function parseSigninPageHtmlProps(html: string): {
83
99
  rsaPublicKeyMaterial?: string;
84
100
  };
85
101
  /**
86
- * True when {@link oauth2PasswordSigninLogin} failed because the Studio web sign-in shell
87
- * (`/interface/studioweb/login`) is missing or unreachable — callers may fall back to Playwright.
102
+ * Build the JSON body for `POST /oauth2/signin` (matches the browser `oauth2-ui` form).
103
+ *
104
+ * `device.client_type` MUST be a value present in the EACP whitelist defined by
105
+ * `kweaver/deploy/auto_cofig/auto_config.sh`. `console_web` is the canonical CLI value
106
+ * (also used by `kweaver-admin`); other values such as `unknown` are rejected by strict
107
+ * deployments with `管理员已禁止此类客户端登录` — surfaced upstream as a `request_forbidden`
108
+ * `No CSRF value available in the session cookie` error after Hydra discards the rejected
109
+ * login challenge.
110
+ *
111
+ * `vcode` and `dualfactorauthinfo` must be present even when empty; otherwise eachttpserver
112
+ * returns HTTP 400 (invalid parameter).
88
113
  */
89
- export declare function isStudiowebShellUnavailableError(err: unknown): boolean;
114
+ export declare function buildOauth2SigninPostBody(opts: {
115
+ csrftoken: string;
116
+ challenge: string;
117
+ account: string;
118
+ passwordCipher: string;
119
+ remember: boolean;
120
+ }): Record<string, unknown>;
90
121
  /**
91
122
  * OAuth2 Authorization Code login using HTTP **only**: `GET /oauth2/signin` (Next.js shell) and
92
123
  * `POST /oauth2/signin` with an RSA PKCS#1 v1.5–encrypted password (same as the browser `rsa.min` / Studio
@@ -463,23 +463,36 @@ function stderrEmphasis(text) {
463
463
  /**
464
464
  * Headless login: read authorization code from stdin (full callback URL or raw code).
465
465
  * Used with `--no-browser` or when automatic browser launch fails.
466
+ *
467
+ * `io` is injectable for tests; defaults to `process.stdin` / `process.stderr`.
466
468
  */
467
- async function promptForCode(authUrl, state, port, pasteMode = "explicit") {
469
+ export async function promptForCode(authUrl, state, port, pasteMode = "explicit", io) {
468
470
  const { createInterface } = await import("node:readline");
471
+ const stdin = io?.input ?? process.stdin;
472
+ const stderr = io?.output ?? process.stderr;
469
473
  const intro = pasteMode === "explicit"
470
474
  ? "Open this URL on any device (use a private/incognito window if you need the full sign-in form):\n\n"
471
475
  : "Could not open a browser automatically. Open this URL on any device:\n\n";
472
476
  const pasteInstructions = "After login, the browser may show an error page (this is expected if nothing listens on localhost).\n" +
473
477
  "Copy the FULL URL from the address bar and paste it here, or paste only the authorization code.\n" +
474
478
  `The URL looks like: http://127.0.0.1:${port}/callback?code=THIS_PART&state=...\n\n`;
475
- process.stderr.write("\n" +
479
+ stderr.write("\n" +
476
480
  intro +
477
481
  ` ${authUrl}\n\n` +
478
482
  stderrEmphasis(pasteInstructions));
479
- const rl = createInterface({ input: process.stdin, output: process.stderr });
483
+ const rl = createInterface({ input: stdin, output: stderr });
484
+ // The `close` listener exists to surface Ctrl-D / EOF before the user answers.
485
+ // It MUST be a no-op once the question callback has fired, because `rl.close()`
486
+ // emits `close` synchronously and would otherwise reject the promise before
487
+ // `resolve(answer)` runs (race condition that turns valid input into "Login cancelled.").
480
488
  const input = await new Promise((resolve, reject) => {
481
- rl.on("close", () => reject(new Error("Login cancelled.")));
489
+ let answered = false;
490
+ rl.on("close", () => {
491
+ if (!answered)
492
+ reject(new Error("Login cancelled."));
493
+ });
482
494
  rl.question("Paste URL or code> ", (answer) => {
495
+ answered = true;
483
496
  rl.close();
484
497
  resolve(answer.trim());
485
498
  });
@@ -512,6 +525,128 @@ async function promptForCode(authUrl, state, port, pasteMode = "explicit") {
512
525
  }
513
526
  return input;
514
527
  }
528
+ /**
529
+ * Prompt the user for a username on stderr (input echoed).
530
+ *
531
+ * `io` is injectable for tests; defaults to `process.stdin` / `process.stderr`.
532
+ */
533
+ export async function promptForUsername(promptLabel = "Username", io) {
534
+ const { createInterface } = await import("node:readline");
535
+ const stdin = io?.input ?? process.stdin;
536
+ const stderr = io?.output ?? process.stderr;
537
+ const rl = createInterface({ input: stdin, output: stderr });
538
+ const value = await new Promise((resolve, reject) => {
539
+ let answered = false;
540
+ rl.on("close", () => {
541
+ if (!answered)
542
+ reject(new Error("Login cancelled."));
543
+ });
544
+ rl.question(`${promptLabel}: `, (answer) => {
545
+ answered = true;
546
+ rl.close();
547
+ resolve(answer.trim());
548
+ });
549
+ });
550
+ if (!value) {
551
+ throw new Error(`${promptLabel} is required.`);
552
+ }
553
+ return value;
554
+ }
555
+ /**
556
+ * Prompt the user for a password on stderr without echoing keystrokes (TTY only).
557
+ *
558
+ * Falls back to a regular readline prompt when stdin is not a TTY (e.g. piped input
559
+ * during scripted use); callers needing strict no-echo should detect this case themselves.
560
+ *
561
+ * `io` is injectable for tests.
562
+ */
563
+ export async function promptForPassword(promptLabel = "Password", io) {
564
+ const stdin = io?.input ?? process.stdin;
565
+ const stderr = io?.output ?? process.stderr;
566
+ // Non-TTY (piped, redirected, tests): use regular readline — no masking is possible
567
+ // without raw mode, so we accept echoed input rather than block forever.
568
+ if (!stdin.isTTY || typeof stdin.setRawMode !== "function") {
569
+ const { createInterface } = await import("node:readline");
570
+ const rl = createInterface({ input: stdin, output: stderr });
571
+ const value = await new Promise((resolve, reject) => {
572
+ let answered = false;
573
+ rl.on("close", () => {
574
+ if (!answered)
575
+ reject(new Error("Login cancelled."));
576
+ });
577
+ rl.question(`${promptLabel}: `, (answer) => {
578
+ answered = true;
579
+ rl.close();
580
+ resolve(answer);
581
+ });
582
+ });
583
+ if (!value)
584
+ throw new Error(`${promptLabel} is required.`);
585
+ return value;
586
+ }
587
+ // TTY: read byte-by-byte in raw mode so keystrokes are not echoed.
588
+ return new Promise((resolve, reject) => {
589
+ stderr.write(`${promptLabel}: `);
590
+ let buf = "";
591
+ const onData = (chunk) => {
592
+ const s = chunk.toString("utf8");
593
+ for (const ch of s) {
594
+ const code = ch.charCodeAt(0);
595
+ if (ch === "\n" || ch === "\r") {
596
+ cleanup();
597
+ stderr.write("\n");
598
+ if (!buf) {
599
+ reject(new Error(`${promptLabel} is required.`));
600
+ }
601
+ else {
602
+ resolve(buf);
603
+ }
604
+ return;
605
+ }
606
+ if (code === 3) {
607
+ // Ctrl-C
608
+ cleanup();
609
+ stderr.write("\n");
610
+ reject(new Error("Login cancelled."));
611
+ return;
612
+ }
613
+ if (code === 4 && buf.length === 0) {
614
+ // Ctrl-D on empty buffer -> cancel
615
+ cleanup();
616
+ stderr.write("\n");
617
+ reject(new Error("Login cancelled."));
618
+ return;
619
+ }
620
+ if (code === 8 || code === 127) {
621
+ // Backspace / DEL
622
+ buf = buf.slice(0, -1);
623
+ continue;
624
+ }
625
+ if (code < 32)
626
+ continue; // ignore other control chars
627
+ buf += ch;
628
+ }
629
+ };
630
+ const cleanup = () => {
631
+ try {
632
+ stdin.setRawMode(false);
633
+ }
634
+ catch { /* noop */ }
635
+ stdin.removeListener("data", onData);
636
+ if (typeof stdin.pause === "function") {
637
+ stdin.pause();
638
+ }
639
+ };
640
+ try {
641
+ stdin.setRawMode(true);
642
+ }
643
+ catch { /* noop */ }
644
+ if (typeof stdin.resume === "function") {
645
+ stdin.resume();
646
+ }
647
+ stdin.on("data", onData);
648
+ });
649
+ }
515
650
  /**
516
651
  * OAuth2 Authorization Code login flow.
517
652
  * 1. Register client (if not already registered), OR use a provided client ID
@@ -755,174 +890,6 @@ async function exchangeCodeForToken(baseUrl, code, clientId, clientSecret, redir
755
890
  saveTokenConfig(token);
756
891
  return token;
757
892
  }
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
893
  function mergeCookieJarForSignin(existing, response) {
927
894
  const setCookies = typeof response.headers.getSetCookie === "function"
928
895
  ? response.headers.getSetCookie()
@@ -1041,7 +1008,7 @@ async function followSigninRedirectsUntilCallback(startUrl, initialJar, state, r
1041
1008
  return consentResult;
1042
1009
  }
1043
1010
  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 or Playwright.`);
1011
+ `If this is a consent or MFA screen, use browser login (kweaver auth login <url>).`);
1045
1012
  }
1046
1013
  const text = await resp.text().catch(() => "");
1047
1014
  throw new HttpError(resp.status, resp.statusText, text);
@@ -1094,17 +1061,38 @@ async function tryAcceptConsentAfterSignin(base, pageUrl, html, jar, scope, stat
1094
1061
  }
1095
1062
  return null;
1096
1063
  }
1097
- const STUDIOWEB_SHELL_UNAVAILABLE_SNIPPETS = [
1098
- "Studioweb signin endpoint not available",
1099
- "Cannot reach studioweb signin endpoint",
1100
- ];
1101
1064
  /**
1102
- * True when {@link oauth2PasswordSigninLogin} failed because the Studio web sign-in shell
1103
- * (`/interface/studioweb/login`) is missing or unreachable — callers may fall back to Playwright.
1065
+ * Build the JSON body for `POST /oauth2/signin` (matches the browser `oauth2-ui` form).
1066
+ *
1067
+ * `device.client_type` MUST be a value present in the EACP whitelist defined by
1068
+ * `kweaver/deploy/auto_cofig/auto_config.sh`. `console_web` is the canonical CLI value
1069
+ * (also used by `kweaver-admin`); other values such as `unknown` are rejected by strict
1070
+ * deployments with `管理员已禁止此类客户端登录` — surfaced upstream as a `request_forbidden`
1071
+ * `No CSRF value available in the session cookie` error after Hydra discards the rejected
1072
+ * login challenge.
1073
+ *
1074
+ * `vcode` and `dualfactorauthinfo` must be present even when empty; otherwise eachttpserver
1075
+ * returns HTTP 400 (invalid parameter).
1104
1076
  */
1105
- export function isStudiowebShellUnavailableError(err) {
1106
- const msg = err instanceof Error ? err.message : String(err);
1107
- return STUDIOWEB_SHELL_UNAVAILABLE_SNIPPETS.some((s) => msg.includes(s));
1077
+ export function buildOauth2SigninPostBody(opts) {
1078
+ return {
1079
+ _csrf: opts.csrftoken,
1080
+ challenge: opts.challenge,
1081
+ account: opts.account,
1082
+ password: opts.passwordCipher,
1083
+ vcode: { id: "", content: "" },
1084
+ dualfactorauthinfo: {
1085
+ validcode: { vcode: "" },
1086
+ OTP: { OTP: "" },
1087
+ },
1088
+ remember: opts.remember,
1089
+ device: {
1090
+ name: "",
1091
+ description: "",
1092
+ client_type: "console_web",
1093
+ udids: [],
1094
+ },
1095
+ };
1108
1096
  }
1109
1097
  /**
1110
1098
  * OAuth2 Authorization Code login using HTTP **only**: `GET /oauth2/signin` (Next.js shell) and
@@ -1127,27 +1115,10 @@ export async function oauth2PasswordSigninLogin(baseUrl, options) {
1127
1115
  (typeof process.env.KWEAVER_OAUTH_PRODUCT === "string" && process.env.KWEAVER_OAUTH_PRODUCT.trim()
1128
1116
  ? process.env.KWEAVER_OAUTH_PRODUCT.trim()
1129
1117
  : "adp");
1130
- // Pre-flight: verify studioweb signin shell exists (same entry as deploy auto_config.sh get_token).
1131
- // If the deployment lacks studioweb, abort before OAuth client registration.
1132
- const studiowebProbeUrl = `${base}/interface/studioweb/login?lang=zh-cn&state=${encodeURIComponent(state)}` +
1133
- `&x-forwarded-prefix=&integrated=false&product=${encodeURIComponent(oauthProduct)}&_t=${Date.now()}`;
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
- }
1118
+ // Note: previously we pre-flighted `/interface/studioweb/login` to detect deployments
1119
+ // missing the Studio web shell. The probe added an extra round-trip and was unreliable
1120
+ // (see kweaver-admin which works fine without it). HTTP sign-in only needs `/oauth2/auth`
1121
+ // and `/oauth2/signin`; if either is missing the request below will surface a precise error.
1151
1122
  let client;
1152
1123
  try {
1153
1124
  client = await resolveOrRegisterClient(base, redirectUri, scope, {
@@ -1231,26 +1202,13 @@ export async function oauth2PasswordSigninLogin(baseUrl, options) {
1231
1202
  : options.signinPasswordBase64Plain === false
1232
1203
  ? false
1233
1204
  : process.env.KWEAVER_SIGNIN_PASSWORD_B64_RSA_MIN !== "1";
1234
- // Body shape matches browser `POST /oauth2/signin` (EACP / oauth2-ui); omitting vcode/dualfactorauthinfo
1235
- // causes 400 from eachttpserver (invalid parameter).
1236
- const postBody = {
1237
- _csrf: csrftoken,
1205
+ const postBody = buildOauth2SigninPostBody({
1206
+ csrftoken,
1238
1207
  challenge: loginChallenge,
1239
1208
  account: options.username,
1240
- password: "",
1241
- vcode: { id: "", content: "" },
1242
- dualfactorauthinfo: {
1243
- validcode: { vcode: "" },
1244
- OTP: { OTP: "" },
1245
- },
1209
+ passwordCipher: "",
1246
1210
  remember,
1247
- device: {
1248
- name: "",
1249
- description: "",
1250
- client_type: "unknown",
1251
- udids: [],
1252
- },
1253
- };
1211
+ });
1254
1212
  const origin = new URL(base).origin;
1255
1213
  /** Some gateways (e.g. DIP) return HTTP 200 + `{"redirect":"..."}` instead of 3xx Location. */
1256
1214
  let signinRedirectFromJson;
package/dist/cli.js CHANGED
@@ -23,7 +23,7 @@ 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] [--http-signin] [--playwright] [--insecure|-k]
26
+ kweaver auth <platform-url> [--alias name] [--no-auth] [--no-browser] [-u user] [-p pass] [--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
29
  kweaver auth whoami [platform-url|alias] [--json]
@@ -1,18 +1,7 @@
1
1
  import { isNoAuth } from "../config/no-auth.js";
2
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";
3
3
  import { decodeJwtPayload } from "../config/jwt.js";
4
- import { buildCopyCommand, formatHttpError, isStudiowebShellUnavailableError, normalizeBaseUrl, oauth2Login, oauth2PasswordSigninLogin, playwrightLogin, refreshTokenLogin, } from "../auth/oauth.js";
5
- /** True when the `playwright` npm package can be imported (browser binaries may still need `npx playwright install`). */
6
- async function isPlaywrightPackageResolvable() {
7
- try {
8
- const modName = "playwright";
9
- await import(/* webpackIgnore: true */ modName);
10
- return true;
11
- }
12
- catch {
13
- return false;
14
- }
15
- }
4
+ import { buildCopyCommand, formatHttpError, normalizeBaseUrl, oauth2Login, oauth2PasswordSigninLogin, promptForUsername, promptForPassword, refreshTokenLogin, } from "../auth/oauth.js";
16
5
  export async function runAuthCommand(args) {
17
6
  const target = args[0];
18
7
  const rest = args.slice(1);
@@ -39,24 +28,24 @@ Login options:
39
28
  Requires --client-id and --client-secret.
40
29
  Get these from the callback page after browser login or \`auth export\`.
41
30
  --port <n> Local callback port (default: 9010). Use when 9010 is occupied.
42
- --no-browser Do not open a browser; print the auth URL and prompt for the callback URL or code (stdin).
43
- Use on headless servers or when automatic browser launch fails.
44
- -u, --username Username (with -p: tries HTTP /oauth2/signin first when the Studio web shell is available)
45
- -p, --password Password (with -u: falls back to Playwright headless only when studioweb is unavailable and Playwright is installed)
46
- --http-signin With -u/-p: HTTP POST /oauth2/signin only (no Playwright fallback). Uses the built-in RSA public key.
47
- --playwright With -u/-p: force Playwright (skip HTTP sign-in). Without -u/-p: open Playwright for manual login.
31
+ --no-browser Do not open a browser. Without -u/-p: print the auth URL and prompt for the
32
+ callback URL or code (stdin). With -u and/or -p: route through HTTP sign-in
33
+ (any missing credential is prompted; password is hidden when stdin is a TTY).
34
+ -u, --username Username for HTTP /oauth2/signin (POST). If -p is omitted, password is prompted.
35
+ -p, --password Password for HTTP /oauth2/signin (POST). If -u is omitted, username is prompted.
36
+ --http-signin Force HTTP /oauth2/signin (no browser). Missing -u/-p are prompted from stdin.
48
37
  --insecure, -k Skip TLS certificate verification (self-signed / dev HTTPS only)
49
38
  --no-auth Save platform without OAuth (servers with no authentication). Same as detecting OAuth 404 during login.`);
50
39
  return 0;
51
40
  }
52
41
  if (target === "login") {
53
42
  if (rest[0] === "--help" || rest[0] === "-h") {
54
- console.log(`kweaver auth login <platform-url> [--alias <name>] [--no-auth] [--no-browser] [-u user] [-p pass] [--http-signin] [--playwright] [--refresh-token T --client-id ID --client-secret S]`);
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]`);
55
44
  return 0;
56
45
  }
57
46
  const url = rest[0];
58
47
  if (!url || url.startsWith("-")) {
59
- console.error("Usage: kweaver auth login <platform-url> [--alias <name>] [-u user] [-p pass] [--playwright]");
48
+ console.error("Usage: kweaver auth login <platform-url> [--alias <name>] [-u user] [-p pass]");
60
49
  return 1;
61
50
  }
62
51
  return runAuthCommand([url, ...rest.slice(1)]);
@@ -78,9 +67,8 @@ Login options:
78
67
  try {
79
68
  const normalizedTarget = normalizeBaseUrl(target);
80
69
  const alias = readOption(args, "--alias");
81
- const username = readOption(args, "--username") ?? readOption(args, "-u");
82
- const password = readOption(args, "--password") ?? readOption(args, "-p");
83
- const usePlaywright = args.includes("--playwright");
70
+ let username = readOption(args, "--username") ?? readOption(args, "-u");
71
+ let password = readOption(args, "--password") ?? readOption(args, "-p");
84
72
  const httpSignin = args.includes("--http-signin");
85
73
  const oauthProduct = readOption(args, "--oauth-product");
86
74
  const signinPublicKeyFile = readOption(args, "--signin-public-key-file");
@@ -101,7 +89,7 @@ Login options:
101
89
  "--http-signin",
102
90
  "--oauth-product",
103
91
  "--signin-public-key-file",
104
- "--playwright", "--insecure", "-k", "--no-auth", "--redirect-uri",
92
+ "--insecure", "-k", "--no-auth", "--redirect-uri",
105
93
  ]);
106
94
  const KNOWN_VALUE_FLAGS = new Set([
107
95
  "--alias", "--client-id", "--client-secret", "--refresh-token",
@@ -130,30 +118,34 @@ Login options:
130
118
  if (noAuth && noBrowser) {
131
119
  console.error("--no-auth does not require a browser; --no-browser is ignored.");
132
120
  }
133
- if (noAuth && (username || password || usePlaywright || httpSignin)) {
134
- console.error("--no-auth cannot be used with Playwright login, HTTP sign-in, or -u/-p.");
121
+ if (noAuth && (username || password || httpSignin)) {
122
+ console.error("--no-auth cannot be used with HTTP sign-in or -u/-p.");
135
123
  return 1;
136
124
  }
137
- if (noBrowser && (username || password || usePlaywright || httpSignin)) {
138
- console.error("--no-browser cannot be used with Playwright login, HTTP sign-in, or -u/-p.");
139
- return 1;
140
- }
141
- if (httpSignin && usePlaywright) {
142
- console.error("--http-signin cannot be used with --playwright.");
143
- return 1;
125
+ if (noBrowser && httpSignin) {
126
+ // HTTP sign-in already runs without a browser; --no-browser is a no-op signal here.
127
+ console.error("--http-signin already runs without a browser; --no-browser is redundant and ignored.");
144
128
  }
145
129
  if (httpSignin && refreshToken) {
146
130
  console.error("--http-signin cannot be used with --refresh-token.");
147
131
  return 1;
148
132
  }
149
- if (httpSignin && (!username || !password)) {
150
- console.error("--http-signin requires -u/--username and -p/--password.");
151
- return 1;
152
- }
153
133
  if (noBrowser && refreshToken) {
154
134
  console.error("--no-browser cannot be used with --refresh-token.");
155
135
  return 1;
156
136
  }
137
+ // Headless credential login: if the user signalled HTTP sign-in (--http-signin,
138
+ // or partial -u/-p, or --no-browser combined with -u/-p) but didn't provide both
139
+ // credentials inline, prompt for the missing one(s) on stderr. Password is read
140
+ // without echo when stdin is a TTY.
141
+ const wantsCredentialLogin = !noAuth && !refreshToken &&
142
+ (httpSignin || (noBrowser && (username || password)) || (!!username !== !!password));
143
+ if (wantsCredentialLogin) {
144
+ if (!username)
145
+ username = await promptForUsername("Username");
146
+ if (!password)
147
+ password = await promptForPassword("Password");
148
+ }
157
149
  let token;
158
150
  if (noAuth) {
159
151
  token = saveNoAuthPlatform(normalizedTarget, { tlsInsecure });
@@ -182,17 +174,9 @@ Login options:
182
174
  signinPublicKeyPemPath: signinPublicKeyFile ?? undefined,
183
175
  });
184
176
  }
185
- else if (username && password && usePlaywright) {
186
- console.log("Logging in (headless, Playwright)...");
187
- token = await playwrightLogin(normalizedTarget, {
188
- username,
189
- password,
190
- tlsInsecure,
191
- port: customPort,
192
- });
193
- }
194
177
  else if (username && password) {
195
- const signinOpts = {
178
+ console.log("Logging in (HTTP /oauth2/signin)...");
179
+ token = await oauth2PasswordSigninLogin(normalizedTarget, {
196
180
  username,
197
181
  password,
198
182
  tlsInsecure,
@@ -201,39 +185,6 @@ Login options:
201
185
  clientSecret: clientSecret ?? undefined,
202
186
  oauthProduct: oauthProduct ?? undefined,
203
187
  signinPublicKeyPemPath: signinPublicKeyFile ?? undefined,
204
- };
205
- console.log("Logging in (HTTP /oauth2/signin)...");
206
- try {
207
- token = await oauth2PasswordSigninLogin(normalizedTarget, signinOpts);
208
- }
209
- catch (err) {
210
- if (!isStudiowebShellUnavailableError(err)) {
211
- throw err;
212
- }
213
- const playwrightOk = await isPlaywrightPackageResolvable();
214
- if (playwrightOk) {
215
- process.stderr.write("Studio web sign-in shell is not available; falling back to Playwright headless login.\n");
216
- console.log("Logging in (headless, Playwright)...");
217
- token = await playwrightLogin(normalizedTarget, {
218
- username,
219
- password,
220
- tlsInsecure,
221
- port: customPort,
222
- });
223
- }
224
- else {
225
- console.error("Studio web sign-in shell is not available on this platform, and the Playwright package is not installed.");
226
- console.error("Install Playwright for headless browser login: npm install playwright && npx playwright install chromium");
227
- console.error("Alternatively, use OAuth without credentials:");
228
- console.error(` kweaver auth login ${normalizedTarget} --no-browser`);
229
- throw err;
230
- }
231
- }
232
- }
233
- else if (usePlaywright) {
234
- console.log("Opening browser for login (Playwright)...");
235
- token = await playwrightLogin(normalizedTarget, {
236
- tlsInsecure, port: customPort,
237
188
  });
238
189
  }
239
190
  else {
@@ -456,7 +407,7 @@ Login options:
456
407
  console.log(`Run \`kweaver auth login ${logoutTarget}\` to sign in again.`);
457
408
  return 0;
458
409
  }
459
- console.error("Usage: kweaver auth login <platform-url> [--alias <name>] [-u user] [-p pass] [--playwright]");
410
+ console.error("Usage: kweaver auth login <platform-url> [--alias <name>] [-u user] [-p pass]");
460
411
  console.error(" kweaver auth whoami [platform-url|alias] [--json]");
461
412
  console.error(" kweaver auth export [platform-url|alias] [--json]");
462
413
  console.error(" kweaver auth status [platform-url|alias]");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kweaver-ai/kweaver-sdk",
3
- "version": "0.6.5",
3
+ "version": "0.6.6",
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",
@@ -51,21 +51,11 @@
51
51
  "@types/node": "^24.6.0",
52
52
  "@types/react": "^19.2.14",
53
53
  "@types/yargs": "^17.0.35",
54
- "playwright": "^1.58.2",
55
54
  "tsx": "^4.20.5",
56
55
  "typescript": "^5.9.3"
57
56
  },
58
- "peerDependencies": {
59
- "playwright": ">=1.40.0"
60
- },
61
- "peerDependenciesMeta": {
62
- "playwright": {
63
- "optional": true
64
- }
65
- },
66
57
  "dependencies": {
67
58
  "@kweaver-ai/bkn": "^0.1.0",
68
- "@playwright/test": "^1.58.2",
69
59
  "chardet": "^2.1.1",
70
60
  "columnify": "^1.6.0",
71
61
  "csv-parse": "^6.2.1",