@kweaver-ai/kweaver-sdk 0.6.3 → 0.6.4
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 -3
- package/README.zh.md +7 -3
- package/dist/api/dataflow.js +4 -1
- package/dist/auth/oauth.d.ts +69 -0
- package/dist/auth/oauth.js +647 -1
- package/dist/cli.js +1 -1
- package/dist/commands/auth.js +145 -18
- package/dist/commands/config.js +19 -9
- package/dist/commands/context-loader.js +8 -2
- package/dist/commands/ds.d.ts +1 -0
- package/dist/commands/ds.js +10 -10
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,6 +31,8 @@ export KWEAVER_BASE_URL=https://your-kweaver-instance.com
|
|
|
31
31
|
export KWEAVER_TOKEN=your-token
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
With both set, API commands use that token even if you never ran `auth login`. You can also run **`kweaver auth status`**, **`kweaver auth whoami`** (supports `--json`), and **`kweaver config show`** when there is **no** current platform in `~/.kweaver/` — the CLI decodes the token locally (JWT only). If the token is opaque, identity fields are omitted and a short hint is printed.
|
|
35
|
+
|
|
34
36
|
### Business domain (platform)
|
|
35
37
|
|
|
36
38
|
Set or verify **before** calling list/query APIs that scope by tenant. DIP deployments often need a UUID, not only `bd_public`.
|
|
@@ -151,11 +153,13 @@ const skillMd = await client.skills.fetchContent("skill-id");
|
|
|
151
153
|
## CLI Reference
|
|
152
154
|
|
|
153
155
|
```
|
|
154
|
-
kweaver auth login <url> [--alias name] [--no-auth] [--no-browser] [-u user] [-p pass] [--playwright] [--insecure|-k]
|
|
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.
|
|
155
158
|
kweaver auth login <url> --client-id ID --client-secret S --refresh-token T (headless login)
|
|
156
159
|
kweaver auth export [url|alias] [--json] (export command to run on a headless host)
|
|
157
|
-
kweaver auth status/
|
|
158
|
-
kweaver
|
|
160
|
+
kweaver auth status / whoami [url|alias] [--json] # whoami: --json; with KWEAVER_BASE_URL+KWEAVER_TOKEN when no ~/.kweaver/ platform
|
|
161
|
+
kweaver auth list/use/delete/logout
|
|
162
|
+
kweaver config show / list-bd / set-bd <value> # platform business domain — show/list-bd work with KWEAVER_BASE_URL (+ KWEAVER_TOKEN for list-bd)
|
|
159
163
|
kweaver token
|
|
160
164
|
kweaver ds list/get/delete/tables/connect
|
|
161
165
|
kweaver ds import-csv <ds_id> --files <glob> [--table-prefix <p>] [--batch-size 500] [--recreate]
|
package/README.zh.md
CHANGED
|
@@ -31,6 +31,8 @@ export KWEAVER_BASE_URL=https://your-kweaver-instance.com
|
|
|
31
31
|
export KWEAVER_TOKEN=your-token
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
两者同时设置时,即使未执行 `auth login`,业务命令也会使用该 token。若 **`~/.kweaver/` 无当前平台**,仍可使用 **`kweaver auth status`**、**`kweaver auth whoami`**(支持 `--json`)、**`kweaver config show`**:CLI 会在本地解 JWT 展示身份;若 token 为 opaque,则省略身份字段并给出简短提示。
|
|
35
|
+
|
|
34
36
|
### 业务域(平台配置)
|
|
35
37
|
|
|
36
38
|
在调用依赖租户范围的接口前,应先确认业务域;DIP 环境通常使用 **UUID**,不能长期只依赖默认 `bd_public`。
|
|
@@ -144,11 +146,13 @@ const skillMd = await client.skills.fetchContent("skill-id");
|
|
|
144
146
|
## 命令速查
|
|
145
147
|
|
|
146
148
|
```
|
|
147
|
-
kweaver auth login <url> [--alias name] [--no-auth] [--no-browser] [-u user] [-p pass] [--playwright] [--insecure|-k]
|
|
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 强制浏览器
|
|
148
151
|
kweaver auth login <url> --client-id ID --client-secret S --refresh-token T (无浏览器登录)
|
|
149
152
|
kweaver auth export [url|alias] [--json] (导出在无浏览器机器上运行的命令)
|
|
150
|
-
kweaver auth status/
|
|
151
|
-
kweaver
|
|
153
|
+
kweaver auth status / whoami [url|alias] [--json] # whoami 支持 --json;无 ~/.kweaver/ 当前平台时可配 KWEAVER_BASE_URL+KWEAVER_TOKEN
|
|
154
|
+
kweaver auth list/use/delete/logout
|
|
155
|
+
kweaver config show / list-bd / set-bd <value> # 业务域;show/list-bd 在无已保存平台时可与 env 配对
|
|
152
156
|
kweaver token
|
|
153
157
|
kweaver ds list/get/delete/tables/connect
|
|
154
158
|
kweaver dataflow list/run/runs/logs
|
package/dist/api/dataflow.js
CHANGED
|
@@ -82,7 +82,10 @@ export async function pollDataflowResults(options) {
|
|
|
82
82
|
return latest;
|
|
83
83
|
}
|
|
84
84
|
if (latest.status === "failed" || latest.status === "error") {
|
|
85
|
-
const
|
|
85
|
+
const reasonVal = latest.reason;
|
|
86
|
+
const reason = reasonVal
|
|
87
|
+
? `: ${typeof reasonVal === "string" ? reasonVal : JSON.stringify(reasonVal)}`
|
|
88
|
+
: "";
|
|
86
89
|
throw new Error(`Dataflow run ${latest.status}${reason}`);
|
|
87
90
|
}
|
|
88
91
|
}
|
package/dist/auth/oauth.d.ts
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import { type TokenConfig } from "../config/store.js";
|
|
2
|
+
/**
|
|
3
|
+
* Studioweb hardcoded LOGIN public key (PEM) — the single key used for HTTP `/oauth2/signin`.
|
|
4
|
+
* Source: kweaver-ai/kweaver `deploy/auto_cofig/auto_config.sh` `LOGIN_PUBLIC_KEY`.
|
|
5
|
+
*/
|
|
6
|
+
export declare const STUDIOWEB_LOGIN_PUBLIC_KEY_PEM = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyOstgbYuubBi2PUqeVj\nGKlkwVUY6w1Y8d4k116dI2SkZI8fxcjHALv77kItO4jYLVplk9gO4HAtsisnNE2o\nwlYIqdmyEPMwupaeFFFcg751oiTXJiYbtX7ABzU5KQYPjRSEjMq6i5qu/mL67XTk\nhvKwrC83zme66qaKApmKupDODPb0RRkutK/zHfd1zL7sciBQ6psnNadh8pE24w8O\n2XVy1v2bgSNkGHABgncR7seyIg81JQ3c/Axxd6GsTztjLnlvGAlmT1TphE84mi99\nfUaGD2A1u1qdIuNc+XuisFeNcUW6fct0+x97eS2eEGRr/7qxWmO/P20sFVzXc2bF\n1QIDAQAB\n-----END PUBLIC KEY-----";
|
|
7
|
+
/**
|
|
8
|
+
* Default RSA modulus (hex) for `/oauth2/signin` when `__NEXT_DATA__` has no `publicKey` / `modulus`.
|
|
9
|
+
* DIP / EACP / AnyShare-style deployments use the ISFWeb `core/auth` PUBLIC_KEY (1024-bit, exp 65537).
|
|
10
|
+
* Prefer key material from the sign-in page when present.
|
|
11
|
+
*/
|
|
12
|
+
export declare const DEFAULT_SIGNIN_RSA_MODULUS_HEX = "C1D9F84B95AF6B331FBA2D64D76A39CAD7529DA79DB4B3543E4DF3DF21723FEC6F7E2F6602E11037339AE0462DF6B39F94150FC256A505A8CA95BB3699E25C3FB84764D6A1DC3F483A2C1DC4F70925D85725151D0CFBF1EB5A6C4FA0E37ED32FED150C717CD82C528745CDB761D17635AC855421B3CBBEE7D405B2CA5C70CFA7";
|
|
13
|
+
/**
|
|
14
|
+
* Build an SPKI PEM from an RSA modulus (hex) and public exponent (default 65537 / 0x10001).
|
|
15
|
+
*/
|
|
16
|
+
export declare function rsaModulusHexToSpkiPem(modulusHex: string, exponent?: number): string;
|
|
2
17
|
/** POSIX shell single-quote escaping for copy-paste commands. */
|
|
3
18
|
export declare function shellQuoteForShell(value: string): string;
|
|
4
19
|
/**
|
|
@@ -11,6 +26,12 @@ export declare function buildCopyCommand(baseUrl: string, clientId: string, clie
|
|
|
11
26
|
*/
|
|
12
27
|
export declare function buildCallbackHtml(copyCommand: string): string;
|
|
13
28
|
export declare function normalizeBaseUrl(value: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Temporarily disable TLS certificate verification for Node `fetch` (sets
|
|
31
|
+
* NODE_TLS_REJECT_UNAUTHORIZED). Used for `--insecure` login and token refresh.
|
|
32
|
+
*/
|
|
33
|
+
/** @internal Exported for CLI env-only identity resolution (`env-snapshot.ts`). */
|
|
34
|
+
export declare function runWithTlsInsecure<T>(tlsInsecure: boolean | undefined, fn: () => Promise<T>): Promise<T>;
|
|
14
35
|
/**
|
|
15
36
|
* OAuth2 Authorization Code login flow.
|
|
16
37
|
* 1. Register client (if not already registered), OR use a provided client ID
|
|
@@ -50,6 +71,54 @@ export declare function playwrightLogin(baseUrl: string, options?: {
|
|
|
50
71
|
scope?: string;
|
|
51
72
|
tlsInsecure?: boolean;
|
|
52
73
|
}): Promise<TokenConfig>;
|
|
74
|
+
/**
|
|
75
|
+
* Parse Next.js `__NEXT_DATA__` from the OAuth2 sign-in HTML shell (CSRF + optional challenge/remember for POST /oauth2/signin).
|
|
76
|
+
* Hydra `login_challenge` may appear only in the sign-in URL; use that when `pageProps.challenge` is absent.
|
|
77
|
+
*/
|
|
78
|
+
export declare function parseSigninPageHtmlProps(html: string): {
|
|
79
|
+
challenge?: string;
|
|
80
|
+
csrftoken: string;
|
|
81
|
+
remember?: boolean;
|
|
82
|
+
/** Hex modulus, PEM, or Base64 SPKI from page (nested search + HTML regex fallback). */
|
|
83
|
+
rsaPublicKeyMaterial?: string;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
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.
|
|
88
|
+
*/
|
|
89
|
+
export declare function isStudiowebShellUnavailableError(err: unknown): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* OAuth2 Authorization Code login using HTTP **only**: `GET /oauth2/signin` (Next.js shell) and
|
|
92
|
+
* `POST /oauth2/signin` with an RSA PKCS#1 v1.5–encrypted password (same as the browser `rsa.min` / Studio
|
|
93
|
+
* `core/mediator/auth` path).
|
|
94
|
+
*
|
|
95
|
+
* `/oauth2/auth` uses `product` `adp` by default (KWeaver Studio shell); set `oauthProduct` or `KWEAVER_OAUTH_PRODUCT` for DIP (`dip`).
|
|
96
|
+
* Password ciphertext defaults to **single-line base64** (PyCrypto-style); set `KWEAVER_SIGNIN_PASSWORD_B64_RSA_MIN=1` for rsa.min-style wrapped lines.
|
|
97
|
+
*/
|
|
98
|
+
export declare function oauth2PasswordSigninLogin(baseUrl: string, options: {
|
|
99
|
+
username: string;
|
|
100
|
+
password: string;
|
|
101
|
+
port?: number;
|
|
102
|
+
scope?: string;
|
|
103
|
+
clientId?: string;
|
|
104
|
+
clientSecret?: string;
|
|
105
|
+
tlsInsecure?: boolean;
|
|
106
|
+
/**
|
|
107
|
+
* `product` query for `/oauth2/auth` (must match deployment). Default `adp`; DIP deployments often use `dip`.
|
|
108
|
+
* @default KWEAVER_OAUTH_PRODUCT env or `adp`
|
|
109
|
+
*/
|
|
110
|
+
oauthProduct?: string;
|
|
111
|
+
/**
|
|
112
|
+
* Password ciphertext: `rsa.min` uses newline every 64 chars; PyCrypto / some gateways expect a single base64 line.
|
|
113
|
+
* @default false (single-line base64, matches kweaver-core EACP-style encryption)
|
|
114
|
+
*/
|
|
115
|
+
signinPasswordBase64Plain?: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* PEM / hex / Base64-SPKI file path — overrides key from the sign-in HTML.
|
|
118
|
+
* Env: `KWEAVER_SIGNIN_RSA_PUBLIC_KEY` (same path semantics as CLI `--signin-public-key-file`).
|
|
119
|
+
*/
|
|
120
|
+
signinPublicKeyPemPath?: string;
|
|
121
|
+
}): Promise<TokenConfig>;
|
|
53
122
|
/**
|
|
54
123
|
* Log in on a headless machine using OAuth2 client credentials and a refresh token (no browser).
|
|
55
124
|
* Exchanges the refresh token for a new access token and persists ~/.kweaver/ state.
|
package/dist/auth/oauth.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { createPublicKey } from "node:crypto";
|
|
1
3
|
import { isNoAuth } from "../config/no-auth.js";
|
|
2
4
|
import { deleteClientConfig, getCurrentPlatform, loadClientConfig, loadTokenConfig, loadUserTokenConfig, resolveUserId, saveClientConfig, saveNoAuthPlatform, saveTokenConfig, setCurrentPlatform, } from "../config/store.js";
|
|
3
5
|
import { HttpError, NetworkRequestError, fetchWithRetry } from "../utils/http.js";
|
|
@@ -6,6 +8,229 @@ const TOKEN_TTL_SECONDS = 3600;
|
|
|
6
8
|
const REFRESH_THRESHOLD_SEC = 60;
|
|
7
9
|
const DEFAULT_REDIRECT_PORT = 9010;
|
|
8
10
|
const DEFAULT_SCOPE = "openid offline all";
|
|
11
|
+
/**
|
|
12
|
+
* Studioweb hardcoded LOGIN public key (PEM) — the single key used for HTTP `/oauth2/signin`.
|
|
13
|
+
* Source: kweaver-ai/kweaver `deploy/auto_cofig/auto_config.sh` `LOGIN_PUBLIC_KEY`.
|
|
14
|
+
*/
|
|
15
|
+
export const STUDIOWEB_LOGIN_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
|
|
16
|
+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyOstgbYuubBi2PUqeVj
|
|
17
|
+
GKlkwVUY6w1Y8d4k116dI2SkZI8fxcjHALv77kItO4jYLVplk9gO4HAtsisnNE2o
|
|
18
|
+
wlYIqdmyEPMwupaeFFFcg751oiTXJiYbtX7ABzU5KQYPjRSEjMq6i5qu/mL67XTk
|
|
19
|
+
hvKwrC83zme66qaKApmKupDODPb0RRkutK/zHfd1zL7sciBQ6psnNadh8pE24w8O
|
|
20
|
+
2XVy1v2bgSNkGHABgncR7seyIg81JQ3c/Axxd6GsTztjLnlvGAlmT1TphE84mi99
|
|
21
|
+
fUaGD2A1u1qdIuNc+XuisFeNcUW6fct0+x97eS2eEGRr/7qxWmO/P20sFVzXc2bF
|
|
22
|
+
1QIDAQAB
|
|
23
|
+
-----END PUBLIC KEY-----`;
|
|
24
|
+
/**
|
|
25
|
+
* Default RSA modulus (hex) for `/oauth2/signin` when `__NEXT_DATA__` has no `publicKey` / `modulus`.
|
|
26
|
+
* DIP / EACP / AnyShare-style deployments use the ISFWeb `core/auth` PUBLIC_KEY (1024-bit, exp 65537).
|
|
27
|
+
* Prefer key material from the sign-in page when present.
|
|
28
|
+
*/
|
|
29
|
+
export const DEFAULT_SIGNIN_RSA_MODULUS_HEX = "C1D9F84B95AF6B331FBA2D64D76A39CAD7529DA79DB4B3543E4DF3DF21723FEC6F7E2F6602E11037339AE0462DF6B39F94150FC256A505A8CA95BB3699E25C3FB84764D6A1DC3F483A2C1DC4F70925D85725151D0CFBF1EB5A6C4FA0E37ED32FED150C717CD82C528745CDB761D17635AC855421B3CBBEE7D405B2CA5C70CFA7";
|
|
30
|
+
/**
|
|
31
|
+
* Default PEM for HTTP `/oauth2/signin`: **the fixed `STUDIOWEB_LOGIN_PUBLIC_KEY_PEM`** (matches
|
|
32
|
+
* `kweaver-ai/kweaver/deploy/auto_cofig/auto_config.sh` `LOGIN_PUBLIC_KEY`). KWeaver platforms have
|
|
33
|
+
* standardized on this single key — no fallback list, no probing of `__NEXT_DATA__` page key, no
|
|
34
|
+
* `trying next candidate…` noise. Override with `--signin-public-key-file` /
|
|
35
|
+
* `KWEAVER_SIGNIN_RSA_PUBLIC_KEY` if a deployment ever ships a different public key.
|
|
36
|
+
*/
|
|
37
|
+
function buildHttpSigninPemCandidates(_parsedMaterial) {
|
|
38
|
+
return [STUDIOWEB_LOGIN_PUBLIC_KEY_PEM];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build an SPKI PEM from an RSA modulus (hex) and public exponent (default 65537 / 0x10001).
|
|
42
|
+
*/
|
|
43
|
+
export function rsaModulusHexToSpkiPem(modulusHex, exponent = 65537) {
|
|
44
|
+
const hex = modulusHex.replace(/\s+/g, "");
|
|
45
|
+
if (!/^[0-9a-fA-F]+$/.test(hex) || hex.length % 2 !== 0) {
|
|
46
|
+
throw new Error("RSA modulus must be an even-length hex string.");
|
|
47
|
+
}
|
|
48
|
+
const nBuf = Buffer.from(hex, "hex");
|
|
49
|
+
const eBytes = [];
|
|
50
|
+
let exp = exponent;
|
|
51
|
+
while (exp > 0) {
|
|
52
|
+
eBytes.unshift(exp & 0xff);
|
|
53
|
+
exp >>= 8;
|
|
54
|
+
}
|
|
55
|
+
const eBuf = Buffer.from(eBytes);
|
|
56
|
+
const key = createPublicKey({
|
|
57
|
+
key: {
|
|
58
|
+
kty: "RSA",
|
|
59
|
+
n: nBuf.toString("base64url"),
|
|
60
|
+
e: eBuf.toString("base64url"),
|
|
61
|
+
},
|
|
62
|
+
format: "jwk",
|
|
63
|
+
});
|
|
64
|
+
return key.export({ type: "spki", format: "pem" });
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Import SPKI DER from Base64 (no PEM headers) — same shape as af-agent `RSA.importKey(base64.b64decode(...))`.
|
|
68
|
+
*/
|
|
69
|
+
function tryDerSpkiBase64ToPem(material) {
|
|
70
|
+
const trimmed = material.replace(/\s+/g, "");
|
|
71
|
+
if (trimmed.length < 80 || !/^[A-Za-z0-9+/]+=*$/.test(trimmed)) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const buf = Buffer.from(trimmed, "base64");
|
|
76
|
+
const key = createPublicKey({ key: buf, format: "der", type: "spki" });
|
|
77
|
+
return key.export({ type: "spki", format: "pem" });
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function isLikelyRsaHexModulusString(s) {
|
|
84
|
+
const h = s.replace(/\s+/g, "");
|
|
85
|
+
return h.length >= 128 && h.length % 2 === 0 && /^[0-9a-fA-F]+$/.test(h);
|
|
86
|
+
}
|
|
87
|
+
function isLikelySpkiBase64String(s) {
|
|
88
|
+
const t = s.replace(/\s+/g, "");
|
|
89
|
+
if (t.length < 200 || !/^[A-Za-z0-9+/]+=*$/.test(t)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
return tryDerSpkiBase64ToPem(t) !== null;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* PEM from page (`BEGIN PUBLIC KEY` / `BEGIN RSA PUBLIC KEY`), hex modulus, or Base64 SPKI DER.
|
|
96
|
+
* When `material` is missing, uses the built-in modulus (`DEFAULT_SIGNIN_RSA_MODULUS_HEX`) so
|
|
97
|
+
* `--http-signin` does not require extra CLI flags. Opt out with `KWEAVER_SIGNIN_DISALLOW_BUILTIN_MODULUS=1`.
|
|
98
|
+
*/
|
|
99
|
+
function resolveSigninPublicKeyPem(material, opts) {
|
|
100
|
+
const disallowBuiltin = opts?.allowBuiltinModulus === false || process.env.KWEAVER_SIGNIN_DISALLOW_BUILTIN_MODULUS === "1";
|
|
101
|
+
if (material?.trim()) {
|
|
102
|
+
const m = material.trim();
|
|
103
|
+
if (m.includes("BEGIN PUBLIC KEY") || m.includes("BEGIN RSA PUBLIC KEY")) {
|
|
104
|
+
return m;
|
|
105
|
+
}
|
|
106
|
+
const hex = m.replace(/\s+/g, "");
|
|
107
|
+
if (/^[0-9a-fA-F]+$/.test(hex) && hex.length % 2 === 0) {
|
|
108
|
+
return rsaModulusHexToSpkiPem(hex);
|
|
109
|
+
}
|
|
110
|
+
const fromDer = tryDerSpkiBase64ToPem(m);
|
|
111
|
+
if (fromDer) {
|
|
112
|
+
return fromDer;
|
|
113
|
+
}
|
|
114
|
+
throw new Error("RSA public key material is present but could not be parsed (expected PEM, hex modulus, or Base64 SPKI).");
|
|
115
|
+
}
|
|
116
|
+
if (disallowBuiltin) {
|
|
117
|
+
throw new Error("No RSA public key in sign-in HTML and built-in modulus disabled (KWEAVER_SIGNIN_DISALLOW_BUILTIN_MODULUS=1). " +
|
|
118
|
+
"Use --signin-public-key-file or KWEAVER_SIGNIN_RSA_PUBLIC_KEY.");
|
|
119
|
+
}
|
|
120
|
+
return rsaModulusHexToSpkiPem(DEFAULT_SIGNIN_RSA_MODULUS_HEX.replace(/\s+/g, ""));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Recursively find a string that looks like PEM, hex modulus, or Base64 SPKI in `pageProps` (nested configs).
|
|
124
|
+
*/
|
|
125
|
+
function deepFindSigninRsaMaterial(obj, depth, seen) {
|
|
126
|
+
if (depth < 0 || obj === null || obj === undefined) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
if (typeof obj === "string") {
|
|
130
|
+
const t = obj.trim();
|
|
131
|
+
if (!t) {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
if (t.includes("BEGIN PUBLIC KEY") || t.includes("BEGIN RSA PUBLIC KEY")) {
|
|
135
|
+
return t;
|
|
136
|
+
}
|
|
137
|
+
if (isLikelyRsaHexModulusString(t)) {
|
|
138
|
+
return t.replace(/\s+/g, "");
|
|
139
|
+
}
|
|
140
|
+
if (isLikelySpkiBase64String(t)) {
|
|
141
|
+
return t.replace(/\s+/g, "");
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
if (typeof obj !== "object") {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
if (seen.has(obj)) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
seen.add(obj);
|
|
152
|
+
if (Array.isArray(obj)) {
|
|
153
|
+
for (const el of obj) {
|
|
154
|
+
const r = deepFindSigninRsaMaterial(el, depth - 1, seen);
|
|
155
|
+
if (r) {
|
|
156
|
+
return r;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
const rec = obj;
|
|
162
|
+
for (const k of Object.keys(rec)) {
|
|
163
|
+
const r = deepFindSigninRsaMaterial(rec[k], depth - 1, seen);
|
|
164
|
+
if (r) {
|
|
165
|
+
return r;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Regex fallback when JSON path differs (escaped quotes, minified bundles, inline scripts).
|
|
172
|
+
* Some deployments put the SPKI Base64 as a raw substring (no JSON key).
|
|
173
|
+
*/
|
|
174
|
+
function extractSigninRsaMaterialFromHtml(html) {
|
|
175
|
+
const pemPub = html.match(/-----BEGIN PUBLIC KEY-----[\s\S]*?-----END PUBLIC KEY-----/);
|
|
176
|
+
if (pemPub) {
|
|
177
|
+
return pemPub[0].trim();
|
|
178
|
+
}
|
|
179
|
+
const pemRsa = html.match(/-----BEGIN RSA PUBLIC KEY-----[\s\S]*?-----END RSA PUBLIC KEY-----/);
|
|
180
|
+
if (pemRsa) {
|
|
181
|
+
return pemRsa[0].trim();
|
|
182
|
+
}
|
|
183
|
+
const jsonPatterns = [
|
|
184
|
+
/"modulus"\s*:\s*"([0-9a-fA-F]{128,})"/,
|
|
185
|
+
/'modulus'\s*:\s*'([0-9a-fA-F]{128,})'/,
|
|
186
|
+
/"(?:publicKey|rsaPublicKey|public_key|encryptPublicKey|rsaModulus|passwordPublicKey|loginPublicKey|pwdPublicKey|encryptKey)"\s*:\s*"([A-Za-z0-9+/=\s]{200,})"/,
|
|
187
|
+
/'(?:publicKey|rsaPublicKey|public_key)'\s*:\s*'([A-Za-z0-9+/=]{200,})'/,
|
|
188
|
+
];
|
|
189
|
+
for (const re of jsonPatterns) {
|
|
190
|
+
const m = html.match(re);
|
|
191
|
+
if (m?.[1]) {
|
|
192
|
+
return m[1].replace(/\s+/g, "");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Raw SPKI Base64 blocks (2048-bit RSA SPKI often starts with MIIBIjAN…)
|
|
196
|
+
const rawSpki = html.match(/(MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA[A-Za-z0-9+/=]{80,800})/);
|
|
197
|
+
if (rawSpki) {
|
|
198
|
+
return rawSpki[1].replace(/\s+/g, "");
|
|
199
|
+
}
|
|
200
|
+
const rawSpki1024 = html.match(/(MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQ[A-Za-z0-9+/=\s]{80,500})/);
|
|
201
|
+
if (rawSpki1024) {
|
|
202
|
+
return rawSpki1024[1].replace(/\s+/g, "");
|
|
203
|
+
}
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
/** Match `rsa.min` / Studio `rsaEncrypt`: base64 with newline every 64 chars (EACP may validate format). */
|
|
207
|
+
function formatPasswordBase64LikeRsaMin(b64) {
|
|
208
|
+
return b64.replace(/(.{64})/g, "$1\n");
|
|
209
|
+
}
|
|
210
|
+
function extractRsaPublicKeyMaterialFromPageProps(pageProps) {
|
|
211
|
+
const keys = [
|
|
212
|
+
"publicKey",
|
|
213
|
+
"rsaPublicKey",
|
|
214
|
+
"public_key",
|
|
215
|
+
"modulus",
|
|
216
|
+
"encryptPublicKey",
|
|
217
|
+
"pubKey",
|
|
218
|
+
"rsaModulus",
|
|
219
|
+
"passwordPublicKey",
|
|
220
|
+
"loginPublicKey",
|
|
221
|
+
"encryptKey",
|
|
222
|
+
"pwdPublicKey",
|
|
223
|
+
"modulusHex",
|
|
224
|
+
"rsaPublicKeyHex",
|
|
225
|
+
];
|
|
226
|
+
for (const k of keys) {
|
|
227
|
+
const v = pageProps[k];
|
|
228
|
+
if (typeof v === "string" && v.trim()) {
|
|
229
|
+
return v.trim();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return deepFindSigninRsaMaterial(pageProps, 5, new Set());
|
|
233
|
+
}
|
|
9
234
|
/** Best-effort fetch of display name via EACP userinfo (ShareServer). */
|
|
10
235
|
async function fetchDisplayName(baseUrl, accessToken, tlsInsecure) {
|
|
11
236
|
try {
|
|
@@ -115,7 +340,8 @@ export function normalizeBaseUrl(value) {
|
|
|
115
340
|
* Temporarily disable TLS certificate verification for Node `fetch` (sets
|
|
116
341
|
* NODE_TLS_REJECT_UNAUTHORIZED). Used for `--insecure` login and token refresh.
|
|
117
342
|
*/
|
|
118
|
-
|
|
343
|
+
/** @internal Exported for CLI env-only identity resolution (`env-snapshot.ts`). */
|
|
344
|
+
export async function runWithTlsInsecure(tlsInsecure, fn) {
|
|
119
345
|
if (!tlsInsecure) {
|
|
120
346
|
return fn();
|
|
121
347
|
}
|
|
@@ -697,6 +923,419 @@ export async function playwrightLogin(baseUrl, options) {
|
|
|
697
923
|
return token;
|
|
698
924
|
});
|
|
699
925
|
}
|
|
926
|
+
function mergeCookieJarForSignin(existing, response) {
|
|
927
|
+
const setCookies = typeof response.headers.getSetCookie === "function"
|
|
928
|
+
? response.headers.getSetCookie()
|
|
929
|
+
: (() => {
|
|
930
|
+
const raw = response.headers.get("set-cookie");
|
|
931
|
+
return raw ? [raw] : [];
|
|
932
|
+
})();
|
|
933
|
+
const map = new Map();
|
|
934
|
+
for (const part of existing
|
|
935
|
+
.split(";")
|
|
936
|
+
.map((s) => s.trim())
|
|
937
|
+
.filter(Boolean)) {
|
|
938
|
+
const eq = part.indexOf("=");
|
|
939
|
+
if (eq > 0)
|
|
940
|
+
map.set(part.slice(0, eq), part.slice(eq + 1));
|
|
941
|
+
}
|
|
942
|
+
for (const sc of setCookies) {
|
|
943
|
+
const first = sc.split(";")[0]?.trim() ?? "";
|
|
944
|
+
const eq = first.indexOf("=");
|
|
945
|
+
if (eq > 0)
|
|
946
|
+
map.set(first.slice(0, eq), first.slice(eq + 1));
|
|
947
|
+
}
|
|
948
|
+
return [...map.entries()].map(([k, v]) => `${k}=${v}`).join("; ");
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Parse query parameters from a `Location` header value (absolute or relative URL).
|
|
952
|
+
*/
|
|
953
|
+
function parseQueryFromLocationHeader(location) {
|
|
954
|
+
const q = location.includes("?") ? location.slice(location.indexOf("?")) : "";
|
|
955
|
+
const params = new URLSearchParams(q.startsWith("?") ? q.slice(1) : q);
|
|
956
|
+
const out = {};
|
|
957
|
+
params.forEach((v, k) => {
|
|
958
|
+
out[k] = v;
|
|
959
|
+
});
|
|
960
|
+
return out;
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Parse Next.js `__NEXT_DATA__` from the OAuth2 sign-in HTML shell (CSRF + optional challenge/remember for POST /oauth2/signin).
|
|
964
|
+
* Hydra `login_challenge` may appear only in the sign-in URL; use that when `pageProps.challenge` is absent.
|
|
965
|
+
*/
|
|
966
|
+
export function parseSigninPageHtmlProps(html) {
|
|
967
|
+
const m = html.match(/<script[^>]*\bid=["']__NEXT_DATA__["'][^>]*>([\s\S]*?)<\/script>/i);
|
|
968
|
+
if (!m) {
|
|
969
|
+
throw new Error("Could not find __NEXT_DATA__ on the sign-in page.");
|
|
970
|
+
}
|
|
971
|
+
const data = JSON.parse(m[1]);
|
|
972
|
+
const pageProps = data.props?.pageProps;
|
|
973
|
+
if (!pageProps) {
|
|
974
|
+
throw new Error("Invalid __NEXT_DATA__: missing pageProps.");
|
|
975
|
+
}
|
|
976
|
+
const challenge = pageProps.challenge;
|
|
977
|
+
const csrftoken = pageProps.csrftoken ?? pageProps._csrf;
|
|
978
|
+
if (typeof csrftoken !== "string") {
|
|
979
|
+
throw new Error("Sign-in page did not expose csrftoken (expected in __NEXT_DATA__.props.pageProps).");
|
|
980
|
+
}
|
|
981
|
+
const challengeStr = typeof challenge === "string" ? challenge : undefined;
|
|
982
|
+
const rememberRaw = pageProps.remember;
|
|
983
|
+
const remember = typeof rememberRaw === "boolean"
|
|
984
|
+
? rememberRaw
|
|
985
|
+
: typeof rememberRaw === "string"
|
|
986
|
+
? rememberRaw === "true"
|
|
987
|
+
: undefined;
|
|
988
|
+
let rsaPublicKeyMaterial = extractRsaPublicKeyMaterialFromPageProps(pageProps);
|
|
989
|
+
if (!rsaPublicKeyMaterial) {
|
|
990
|
+
rsaPublicKeyMaterial = deepFindSigninRsaMaterial(data, 10, new Set());
|
|
991
|
+
}
|
|
992
|
+
if (!rsaPublicKeyMaterial) {
|
|
993
|
+
rsaPublicKeyMaterial = extractSigninRsaMaterialFromHtml(html);
|
|
994
|
+
}
|
|
995
|
+
return { challenge: challengeStr, csrftoken, remember, rsaPublicKeyMaterial };
|
|
996
|
+
}
|
|
997
|
+
async function followSigninRedirectsUntilCallback(startUrl, initialJar, state, redirectUri, base, scope) {
|
|
998
|
+
let url = startUrl;
|
|
999
|
+
let jar = initialJar;
|
|
1000
|
+
const callbackHost = new URL(redirectUri).origin;
|
|
1001
|
+
const callbackPath = new URL(redirectUri).pathname;
|
|
1002
|
+
for (let hop = 0; hop < 40; hop++) {
|
|
1003
|
+
const resp = await fetch(url, {
|
|
1004
|
+
method: "GET",
|
|
1005
|
+
headers: {
|
|
1006
|
+
Cookie: jar,
|
|
1007
|
+
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
1008
|
+
},
|
|
1009
|
+
redirect: "manual",
|
|
1010
|
+
});
|
|
1011
|
+
jar = mergeCookieJarForSignin(jar, resp);
|
|
1012
|
+
if (resp.status === 302 || resp.status === 303 || resp.status === 307 || resp.status === 308) {
|
|
1013
|
+
const loc = resp.headers.get("location");
|
|
1014
|
+
if (!loc) {
|
|
1015
|
+
throw new HttpError(resp.status, "Missing Location", "");
|
|
1016
|
+
}
|
|
1017
|
+
const next = new URL(loc, url);
|
|
1018
|
+
if (next.origin === callbackHost && next.pathname === callbackPath) {
|
|
1019
|
+
const code = next.searchParams.get("code");
|
|
1020
|
+
const st = next.searchParams.get("state");
|
|
1021
|
+
if (st !== state) {
|
|
1022
|
+
throw new Error("OAuth2 state mismatch — possible CSRF attack.");
|
|
1023
|
+
}
|
|
1024
|
+
const err = next.searchParams.get("error");
|
|
1025
|
+
if (err) {
|
|
1026
|
+
const desc = next.searchParams.get("error_description") ?? "";
|
|
1027
|
+
throw new Error(desc ? `Authorization failed: ${err} — ${desc}` : `Authorization failed: ${err}`);
|
|
1028
|
+
}
|
|
1029
|
+
if (!code) {
|
|
1030
|
+
throw new Error("Callback URL missing authorization code.");
|
|
1031
|
+
}
|
|
1032
|
+
return { code, jar };
|
|
1033
|
+
}
|
|
1034
|
+
url = next.href;
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
if (resp.status === 200) {
|
|
1038
|
+
const html = await resp.text();
|
|
1039
|
+
const consentResult = await tryAcceptConsentAfterSignin(base, url, html, jar, scope, state, redirectUri);
|
|
1040
|
+
if (consentResult) {
|
|
1041
|
+
return consentResult;
|
|
1042
|
+
}
|
|
1043
|
+
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.`);
|
|
1045
|
+
}
|
|
1046
|
+
const text = await resp.text().catch(() => "");
|
|
1047
|
+
throw new HttpError(resp.status, resp.statusText, text);
|
|
1048
|
+
}
|
|
1049
|
+
throw new Error("Too many OAuth redirects.");
|
|
1050
|
+
}
|
|
1051
|
+
async function tryAcceptConsentAfterSignin(base, pageUrl, html, jar, scope, state, redirectUri) {
|
|
1052
|
+
let data;
|
|
1053
|
+
try {
|
|
1054
|
+
const m = html.match(/<script[^>]*\bid=["']__NEXT_DATA__["'][^>]*>([\s\S]*?)<\/script>/i);
|
|
1055
|
+
if (!m) {
|
|
1056
|
+
return null;
|
|
1057
|
+
}
|
|
1058
|
+
data = JSON.parse(m[1]);
|
|
1059
|
+
}
|
|
1060
|
+
catch {
|
|
1061
|
+
return null;
|
|
1062
|
+
}
|
|
1063
|
+
const pageProps = data.props?.pageProps;
|
|
1064
|
+
if (!pageProps) {
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
const consentChallenge = pageProps.consent_challenge;
|
|
1068
|
+
if (typeof consentChallenge !== "string") {
|
|
1069
|
+
return null;
|
|
1070
|
+
}
|
|
1071
|
+
const scopes = scope.split(/\s+/).filter(Boolean);
|
|
1072
|
+
const body = new URLSearchParams();
|
|
1073
|
+
body.set("consent_challenge", consentChallenge);
|
|
1074
|
+
for (const s of scopes) {
|
|
1075
|
+
body.append("grant_scope", s);
|
|
1076
|
+
}
|
|
1077
|
+
const resp = await fetch(`${base}/oauth2/consent`, {
|
|
1078
|
+
method: "POST",
|
|
1079
|
+
headers: {
|
|
1080
|
+
Cookie: jar,
|
|
1081
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1082
|
+
Accept: "text/html,application/xhtml+xml,application/json;q=0.9,*/*;q=0.8",
|
|
1083
|
+
},
|
|
1084
|
+
body: body.toString(),
|
|
1085
|
+
redirect: "manual",
|
|
1086
|
+
});
|
|
1087
|
+
const newJar = mergeCookieJarForSignin(jar, resp);
|
|
1088
|
+
if (resp.status === 302 || resp.status === 303 || resp.status === 307) {
|
|
1089
|
+
const loc = resp.headers.get("location");
|
|
1090
|
+
if (!loc) {
|
|
1091
|
+
throw new HttpError(resp.status, "Missing Location after consent", "");
|
|
1092
|
+
}
|
|
1093
|
+
return followSigninRedirectsUntilCallback(new URL(loc, pageUrl).href, newJar, state, redirectUri, base, scope);
|
|
1094
|
+
}
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
const STUDIOWEB_SHELL_UNAVAILABLE_SNIPPETS = [
|
|
1098
|
+
"Studioweb signin endpoint not available",
|
|
1099
|
+
"Cannot reach studioweb signin endpoint",
|
|
1100
|
+
];
|
|
1101
|
+
/**
|
|
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.
|
|
1104
|
+
*/
|
|
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));
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* OAuth2 Authorization Code login using HTTP **only**: `GET /oauth2/signin` (Next.js shell) and
|
|
1111
|
+
* `POST /oauth2/signin` with an RSA PKCS#1 v1.5–encrypted password (same as the browser `rsa.min` / Studio
|
|
1112
|
+
* `core/mediator/auth` path).
|
|
1113
|
+
*
|
|
1114
|
+
* `/oauth2/auth` uses `product` `adp` by default (KWeaver Studio shell); set `oauthProduct` or `KWEAVER_OAUTH_PRODUCT` for DIP (`dip`).
|
|
1115
|
+
* Password ciphertext defaults to **single-line base64** (PyCrypto-style); set `KWEAVER_SIGNIN_PASSWORD_B64_RSA_MIN=1` for rsa.min-style wrapped lines.
|
|
1116
|
+
*/
|
|
1117
|
+
export async function oauth2PasswordSigninLogin(baseUrl, options) {
|
|
1118
|
+
return runWithTlsInsecure(options.tlsInsecure, async () => {
|
|
1119
|
+
const { publicEncrypt, constants: cryptoConstants } = await import("node:crypto");
|
|
1120
|
+
const { randomBytes } = await import("node:crypto");
|
|
1121
|
+
const base = normalizeBaseUrl(baseUrl);
|
|
1122
|
+
const port = options.port ?? DEFAULT_REDIRECT_PORT;
|
|
1123
|
+
const scope = options.scope ?? DEFAULT_SCOPE;
|
|
1124
|
+
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
1125
|
+
const state = randomBytes(12).toString("hex");
|
|
1126
|
+
const oauthProduct = options.oauthProduct?.trim() ||
|
|
1127
|
+
(typeof process.env.KWEAVER_OAUTH_PRODUCT === "string" && process.env.KWEAVER_OAUTH_PRODUCT.trim()
|
|
1128
|
+
? process.env.KWEAVER_OAUTH_PRODUCT.trim()
|
|
1129
|
+
: "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
|
+
}
|
|
1151
|
+
let client;
|
|
1152
|
+
try {
|
|
1153
|
+
client = await resolveOrRegisterClient(base, redirectUri, scope, {
|
|
1154
|
+
clientId: options.clientId,
|
|
1155
|
+
clientSecret: options.clientSecret,
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
catch (e) {
|
|
1159
|
+
if (e instanceof HttpError && e.status === 404) {
|
|
1160
|
+
process.stderr.write("OAuth2 endpoint not found (404). Saving platform in no-auth mode.\n");
|
|
1161
|
+
return saveNoAuthPlatform(base, { tlsInsecure: options.tlsInsecure });
|
|
1162
|
+
}
|
|
1163
|
+
throw e;
|
|
1164
|
+
}
|
|
1165
|
+
const usePkce = !client.clientSecret;
|
|
1166
|
+
const pkce = usePkce ? await generatePkce() : null;
|
|
1167
|
+
const authParams = new URLSearchParams({
|
|
1168
|
+
redirect_uri: redirectUri,
|
|
1169
|
+
"x-forwarded-prefix": "",
|
|
1170
|
+
client_id: client.clientId,
|
|
1171
|
+
scope,
|
|
1172
|
+
response_type: "code",
|
|
1173
|
+
state,
|
|
1174
|
+
lang: "zh-cn",
|
|
1175
|
+
product: oauthProduct,
|
|
1176
|
+
});
|
|
1177
|
+
if (pkce) {
|
|
1178
|
+
authParams.set("code_challenge", pkce.challenge);
|
|
1179
|
+
authParams.set("code_challenge_method", "S256");
|
|
1180
|
+
}
|
|
1181
|
+
const authUrl = `${base}/oauth2/auth?${authParams.toString()}`;
|
|
1182
|
+
let jar = "";
|
|
1183
|
+
const authResp = await fetch(authUrl, { method: "GET", redirect: "manual" });
|
|
1184
|
+
jar = mergeCookieJarForSignin(jar, authResp);
|
|
1185
|
+
if (authResp.status !== 302 && authResp.status !== 303 && authResp.status !== 307) {
|
|
1186
|
+
const t = await authResp.text();
|
|
1187
|
+
throw new HttpError(authResp.status, authResp.statusText, t);
|
|
1188
|
+
}
|
|
1189
|
+
const authLoc = authResp.headers.get("location");
|
|
1190
|
+
if (!authLoc) {
|
|
1191
|
+
throw new HttpError(authResp.status, "Missing Location after /oauth2/auth", "");
|
|
1192
|
+
}
|
|
1193
|
+
const signinUrl = new URL(authLoc, base);
|
|
1194
|
+
if (!signinUrl.pathname.includes("signin")) {
|
|
1195
|
+
throw new Error(`Expected redirect to a sign-in page, got: ${authLoc}`);
|
|
1196
|
+
}
|
|
1197
|
+
const signinPageResp = await fetch(signinUrl.href, {
|
|
1198
|
+
method: "GET",
|
|
1199
|
+
headers: {
|
|
1200
|
+
Cookie: jar,
|
|
1201
|
+
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
1202
|
+
},
|
|
1203
|
+
redirect: "manual",
|
|
1204
|
+
});
|
|
1205
|
+
jar = mergeCookieJarForSignin(jar, signinPageResp);
|
|
1206
|
+
if (signinPageResp.status !== 200) {
|
|
1207
|
+
const t = await signinPageResp.text();
|
|
1208
|
+
throw new HttpError(signinPageResp.status, signinPageResp.statusText, t);
|
|
1209
|
+
}
|
|
1210
|
+
const html = await signinPageResp.text();
|
|
1211
|
+
const parsed = parseSigninPageHtmlProps(html);
|
|
1212
|
+
const loginChallenge = signinUrl.searchParams.get("login_challenge")?.trim() || parsed.challenge?.trim();
|
|
1213
|
+
if (!loginChallenge) {
|
|
1214
|
+
throw new Error("Could not resolve login challenge: missing login_challenge in sign-in URL and __NEXT_DATA__.props.pageProps.challenge.");
|
|
1215
|
+
}
|
|
1216
|
+
const csrftoken = parsed.csrftoken;
|
|
1217
|
+
const remember = parsed.remember ?? false;
|
|
1218
|
+
const keyPath = options.signinPublicKeyPemPath?.trim() ||
|
|
1219
|
+
(typeof process.env.KWEAVER_SIGNIN_RSA_PUBLIC_KEY === "string"
|
|
1220
|
+
? process.env.KWEAVER_SIGNIN_RSA_PUBLIC_KEY.trim()
|
|
1221
|
+
: "");
|
|
1222
|
+
const pems = keyPath
|
|
1223
|
+
? [
|
|
1224
|
+
resolveSigninPublicKeyPem((await readFile(keyPath, "utf8")).trim(), {
|
|
1225
|
+
allowBuiltinModulus: false,
|
|
1226
|
+
}),
|
|
1227
|
+
]
|
|
1228
|
+
: buildHttpSigninPemCandidates(parsed.rsaPublicKeyMaterial);
|
|
1229
|
+
const usePlainB64 = options.signinPasswordBase64Plain === true
|
|
1230
|
+
? true
|
|
1231
|
+
: options.signinPasswordBase64Plain === false
|
|
1232
|
+
? false
|
|
1233
|
+
: 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,
|
|
1238
|
+
challenge: loginChallenge,
|
|
1239
|
+
account: options.username,
|
|
1240
|
+
password: "",
|
|
1241
|
+
vcode: { id: "", content: "" },
|
|
1242
|
+
dualfactorauthinfo: {
|
|
1243
|
+
validcode: { vcode: "" },
|
|
1244
|
+
OTP: { OTP: "" },
|
|
1245
|
+
},
|
|
1246
|
+
remember,
|
|
1247
|
+
device: {
|
|
1248
|
+
name: "",
|
|
1249
|
+
description: "",
|
|
1250
|
+
client_type: "unknown",
|
|
1251
|
+
udids: [],
|
|
1252
|
+
},
|
|
1253
|
+
};
|
|
1254
|
+
const origin = new URL(base).origin;
|
|
1255
|
+
/** Some gateways (e.g. DIP) return HTTP 200 + `{"redirect":"..."}` instead of 3xx Location. */
|
|
1256
|
+
let signinRedirectFromJson;
|
|
1257
|
+
// Single fixed RSA public key (STUDIOWEB_LOGIN_PUBLIC_KEY_PEM) unless the caller overrides via
|
|
1258
|
+
// --signin-public-key-file / KWEAVER_SIGNIN_RSA_PUBLIC_KEY. No fallback list, no candidate noise.
|
|
1259
|
+
const pem = pems[0];
|
|
1260
|
+
const encrypted = publicEncrypt({ key: pem, padding: cryptoConstants.RSA_PKCS1_PADDING }, Buffer.from(options.password, "utf8"));
|
|
1261
|
+
const rawB64 = encrypted.toString("base64");
|
|
1262
|
+
const passwordB64 = usePlainB64 ? rawB64 : formatPasswordBase64LikeRsaMin(rawB64);
|
|
1263
|
+
postBody.password = passwordB64;
|
|
1264
|
+
const postResp = await fetch(`${base}/oauth2/signin`, {
|
|
1265
|
+
method: "POST",
|
|
1266
|
+
headers: {
|
|
1267
|
+
Cookie: jar,
|
|
1268
|
+
"Content-Type": "application/json",
|
|
1269
|
+
Accept: "application/json, text/plain, */*",
|
|
1270
|
+
Origin: origin,
|
|
1271
|
+
Referer: signinUrl.href,
|
|
1272
|
+
},
|
|
1273
|
+
body: JSON.stringify(postBody),
|
|
1274
|
+
redirect: "manual",
|
|
1275
|
+
});
|
|
1276
|
+
jar = mergeCookieJarForSignin(jar, postResp);
|
|
1277
|
+
if (postResp.status !== 302 && postResp.status !== 303 && postResp.status !== 307) {
|
|
1278
|
+
const bodyText = await postResp.text();
|
|
1279
|
+
if (/RSA_private_decrypt/i.test(bodyText)) {
|
|
1280
|
+
throw new Error("HTTP sign-in: RSA ciphertext rejected by server. The built-in STUDIOWEB_LOGIN_PUBLIC_KEY_PEM " +
|
|
1281
|
+
"does not match this deployment's `/oauth2/signin` public key. Provide the correct key via " +
|
|
1282
|
+
"--signin-public-key-file <pem> or KWEAVER_SIGNIN_RSA_PUBLIC_KEY=...");
|
|
1283
|
+
}
|
|
1284
|
+
if (postResp.status === 200) {
|
|
1285
|
+
const ct = postResp.headers.get("content-type") ?? "";
|
|
1286
|
+
const looksLikeJson = ct.includes("application/json") || /^\s*\{/.test(bodyText);
|
|
1287
|
+
if (looksLikeJson) {
|
|
1288
|
+
let j;
|
|
1289
|
+
try {
|
|
1290
|
+
j = JSON.parse(bodyText);
|
|
1291
|
+
}
|
|
1292
|
+
catch {
|
|
1293
|
+
throw new Error(`Sign-in failed: ${bodyText.slice(0, 500)}`);
|
|
1294
|
+
}
|
|
1295
|
+
const redir = j.redirect;
|
|
1296
|
+
if (typeof redir === "string" && redir.trim() !== "") {
|
|
1297
|
+
signinRedirectFromJson = redir.trim();
|
|
1298
|
+
}
|
|
1299
|
+
else {
|
|
1300
|
+
const msg = typeof j.message === "string"
|
|
1301
|
+
? j.message
|
|
1302
|
+
: typeof j.error === "string"
|
|
1303
|
+
? j.error
|
|
1304
|
+
: bodyText.slice(0, 500);
|
|
1305
|
+
throw new Error(`Sign-in failed: ${msg}`);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
else {
|
|
1309
|
+
throw new Error("Sign-in POST returned 200 without redirect. Check password, CSRF, or RSA public key PEM.");
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
else {
|
|
1313
|
+
throw new HttpError(postResp.status, postResp.statusText, bodyText);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
let code;
|
|
1317
|
+
if (signinRedirectFromJson) {
|
|
1318
|
+
const out = await followSigninRedirectsUntilCallback(new URL(signinRedirectFromJson, base).href, jar, state, redirectUri, base, scope);
|
|
1319
|
+
code = out.code;
|
|
1320
|
+
}
|
|
1321
|
+
else if (postResp.status === 302 || postResp.status === 303 || postResp.status === 307) {
|
|
1322
|
+
const loc = postResp.headers.get("location");
|
|
1323
|
+
if (!loc) {
|
|
1324
|
+
throw new HttpError(postResp.status, "Missing Location after sign-in", "");
|
|
1325
|
+
}
|
|
1326
|
+
const out = await followSigninRedirectsUntilCallback(new URL(loc, base).href, jar, state, redirectUri, base, scope);
|
|
1327
|
+
code = out.code;
|
|
1328
|
+
}
|
|
1329
|
+
else {
|
|
1330
|
+
throw new Error("HTTP sign-in: exhausted RSA key candidates without redirect");
|
|
1331
|
+
}
|
|
1332
|
+
const token = await exchangeCodeForToken(base, code, client.clientId, client.clientSecret, redirectUri, pkce?.verifier, options.tlsInsecure);
|
|
1333
|
+
const copyCommand = buildCopyCommand(base, client.clientId, client.clientSecret, token.refreshToken, options.tlsInsecure);
|
|
1334
|
+
process.stderr.write("\nHTTP sign-in: copy this command for headless hosts:\n\n" + copyCommand + "\n\n");
|
|
1335
|
+
setCurrentPlatform(base);
|
|
1336
|
+
return token;
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
700
1339
|
/**
|
|
701
1340
|
* Log in on a headless machine using OAuth2 client credentials and a refresh token (no browser).
|
|
702
1341
|
* Exchanges the refresh token for a new access token and persists ~/.kweaver/ state.
|
|
@@ -952,6 +1591,13 @@ export async function withTokenRetry(fn) {
|
|
|
952
1591
|
throw error;
|
|
953
1592
|
}
|
|
954
1593
|
const platformUrl = normalizeBaseUrl(token.baseUrl);
|
|
1594
|
+
// env-sourced token: no refresh_token / OAuth client — refresh is impossible.
|
|
1595
|
+
// Surface an env-aware hint instead of telling the user to `auth login` (which writes to disk).
|
|
1596
|
+
if (process.env.KWEAVER_TOKEN && !token.refreshToken) {
|
|
1597
|
+
throw new Error(`Authentication failed (401) for ${platformUrl}. Your KWEAVER_TOKEN appears to be invalid or expired.\n` +
|
|
1598
|
+
` - Refresh the token and re-export: export KWEAVER_TOKEN=<new-token>\n` +
|
|
1599
|
+
` - Or run \`kweaver auth login ${platformUrl}\` to save a full session (with refresh_token) to ~/.kweaver/.`, { cause: error });
|
|
1600
|
+
}
|
|
955
1601
|
const envUser = process.env.KWEAVER_USER;
|
|
956
1602
|
let latest;
|
|
957
1603
|
if (envUser) {
|
package/dist/cli.js
CHANGED
|
@@ -21,7 +21,7 @@ Usage:
|
|
|
21
21
|
kweaver --version | -V
|
|
22
22
|
kweaver --help | -h
|
|
23
23
|
|
|
24
|
-
kweaver auth <platform-url> [--alias name] [--no-auth] [--no-browser] [-u user] [-p pass] [--playwright] [--insecure|-k]
|
|
24
|
+
kweaver auth <platform-url> [--alias name] [--no-auth] [--no-browser] [-u user] [-p pass] [--http-signin] [--playwright] [--insecure|-k]
|
|
25
25
|
kweaver auth login <platform-url> (alias for auth <url>)
|
|
26
26
|
kweaver auth login <url> --client-id ID --client-secret S --refresh-token T (run on host without browser)
|
|
27
27
|
kweaver auth whoami [platform-url|alias] [--json]
|
package/dist/commands/auth.js
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
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, normalizeBaseUrl, oauth2Login, playwrightLogin, refreshTokenLogin, } from "../auth/oauth.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
|
+
}
|
|
5
16
|
export async function runAuthCommand(args) {
|
|
6
17
|
const target = args[0];
|
|
7
18
|
const rest = args.slice(1);
|
|
8
19
|
if (!target || target === "--help" || target === "-h") {
|
|
9
20
|
console.log(`kweaver auth login <url> [options] Login to a platform (browser OAuth2 by default)
|
|
10
21
|
kweaver auth <url> Login (shorthand; same options as login)
|
|
11
|
-
kweaver auth whoami [url|alias]
|
|
22
|
+
kweaver auth whoami [url|alias] [--json] Show current user identity (from id_token)
|
|
12
23
|
kweaver auth export [url|alias] [--json] Export credentials; run printed command on a headless host
|
|
13
24
|
kweaver auth status [url|alias] Show current auth status
|
|
14
25
|
kweaver auth list List all platforms and users (tree view)
|
|
@@ -30,16 +41,17 @@ Login options:
|
|
|
30
41
|
--port <n> Local callback port (default: 9010). Use when 9010 is occupied.
|
|
31
42
|
--no-browser Do not open a browser; print the auth URL and prompt for the callback URL or code (stdin).
|
|
32
43
|
Use on headless servers or when automatic browser launch fails.
|
|
33
|
-
-u, --username Username (with -p
|
|
34
|
-
-p, --password Password
|
|
35
|
-
--
|
|
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.
|
|
36
48
|
--insecure, -k Skip TLS certificate verification (self-signed / dev HTTPS only)
|
|
37
49
|
--no-auth Save platform without OAuth (servers with no authentication). Same as detecting OAuth 404 during login.`);
|
|
38
50
|
return 0;
|
|
39
51
|
}
|
|
40
52
|
if (target === "login") {
|
|
41
53
|
if (rest[0] === "--help" || rest[0] === "-h") {
|
|
42
|
-
console.log(`kweaver auth login <platform-url> [--alias <name>] [--no-auth] [--no-browser] [-u user] [-p pass] [--playwright] [--refresh-token T --client-id ID --client-secret S]`);
|
|
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
55
|
return 0;
|
|
44
56
|
}
|
|
45
57
|
const url = rest[0];
|
|
@@ -69,6 +81,9 @@ Login options:
|
|
|
69
81
|
const username = readOption(args, "--username") ?? readOption(args, "-u");
|
|
70
82
|
const password = readOption(args, "--password") ?? readOption(args, "-p");
|
|
71
83
|
const usePlaywright = args.includes("--playwright");
|
|
84
|
+
const httpSignin = args.includes("--http-signin");
|
|
85
|
+
const oauthProduct = readOption(args, "--oauth-product");
|
|
86
|
+
const signinPublicKeyFile = readOption(args, "--signin-public-key-file");
|
|
72
87
|
const clientId = readOption(args, "--client-id");
|
|
73
88
|
const clientSecret = readOption(args, "--client-secret");
|
|
74
89
|
const refreshToken = readOption(args, "--refresh-token");
|
|
@@ -83,11 +98,16 @@ Login options:
|
|
|
83
98
|
const KNOWN_LOGIN_FLAGS = new Set([
|
|
84
99
|
"--alias", "--client-id", "--client-secret", "--refresh-token",
|
|
85
100
|
"--port", "--no-browser", "--username", "-u", "--password", "-p",
|
|
101
|
+
"--http-signin",
|
|
102
|
+
"--oauth-product",
|
|
103
|
+
"--signin-public-key-file",
|
|
86
104
|
"--playwright", "--insecure", "-k", "--no-auth", "--redirect-uri",
|
|
87
105
|
]);
|
|
88
106
|
const KNOWN_VALUE_FLAGS = new Set([
|
|
89
107
|
"--alias", "--client-id", "--client-secret", "--refresh-token",
|
|
90
108
|
"--port", "--username", "-u", "--password", "-p", "--redirect-uri",
|
|
109
|
+
"--oauth-product",
|
|
110
|
+
"--signin-public-key-file",
|
|
91
111
|
]);
|
|
92
112
|
for (let i = 0; i < args.length; i++) {
|
|
93
113
|
const a = args[i];
|
|
@@ -110,12 +130,24 @@ Login options:
|
|
|
110
130
|
if (noAuth && noBrowser) {
|
|
111
131
|
console.error("--no-auth does not require a browser; --no-browser is ignored.");
|
|
112
132
|
}
|
|
113
|
-
if (noAuth && (username || password || usePlaywright)) {
|
|
114
|
-
console.error("--no-auth cannot be used with Playwright login or -u/-p.");
|
|
133
|
+
if (noAuth && (username || password || usePlaywright || httpSignin)) {
|
|
134
|
+
console.error("--no-auth cannot be used with Playwright login, HTTP sign-in, or -u/-p.");
|
|
135
|
+
return 1;
|
|
136
|
+
}
|
|
137
|
+
if (noBrowser && (username || password || usePlaywright || httpSignin)) {
|
|
138
|
+
console.error("--no-browser cannot be used with Playwright login, HTTP sign-in, or -u/-p.");
|
|
115
139
|
return 1;
|
|
116
140
|
}
|
|
117
|
-
if (
|
|
118
|
-
console.error("--
|
|
141
|
+
if (httpSignin && usePlaywright) {
|
|
142
|
+
console.error("--http-signin cannot be used with --playwright.");
|
|
143
|
+
return 1;
|
|
144
|
+
}
|
|
145
|
+
if (httpSignin && refreshToken) {
|
|
146
|
+
console.error("--http-signin cannot be used with --refresh-token.");
|
|
147
|
+
return 1;
|
|
148
|
+
}
|
|
149
|
+
if (httpSignin && (!username || !password)) {
|
|
150
|
+
console.error("--http-signin requires -u/--username and -p/--password.");
|
|
119
151
|
return 1;
|
|
120
152
|
}
|
|
121
153
|
if (noBrowser && refreshToken) {
|
|
@@ -137,12 +169,67 @@ Login options:
|
|
|
137
169
|
clientId, clientSecret, refreshToken, tlsInsecure,
|
|
138
170
|
});
|
|
139
171
|
}
|
|
140
|
-
else if (username && password) {
|
|
141
|
-
console.log("Logging in (
|
|
172
|
+
else if (username && password && httpSignin) {
|
|
173
|
+
console.log("Logging in (HTTP /oauth2/signin)...");
|
|
174
|
+
token = await oauth2PasswordSigninLogin(normalizedTarget, {
|
|
175
|
+
username,
|
|
176
|
+
password,
|
|
177
|
+
tlsInsecure,
|
|
178
|
+
port: customPort,
|
|
179
|
+
clientId: clientId ?? undefined,
|
|
180
|
+
clientSecret: clientSecret ?? undefined,
|
|
181
|
+
oauthProduct: oauthProduct ?? undefined,
|
|
182
|
+
signinPublicKeyPemPath: signinPublicKeyFile ?? undefined,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
else if (username && password && usePlaywright) {
|
|
186
|
+
console.log("Logging in (headless, Playwright)...");
|
|
142
187
|
token = await playwrightLogin(normalizedTarget, {
|
|
143
|
-
username,
|
|
188
|
+
username,
|
|
189
|
+
password,
|
|
190
|
+
tlsInsecure,
|
|
191
|
+
port: customPort,
|
|
144
192
|
});
|
|
145
193
|
}
|
|
194
|
+
else if (username && password) {
|
|
195
|
+
const signinOpts = {
|
|
196
|
+
username,
|
|
197
|
+
password,
|
|
198
|
+
tlsInsecure,
|
|
199
|
+
port: customPort,
|
|
200
|
+
clientId: clientId ?? undefined,
|
|
201
|
+
clientSecret: clientSecret ?? undefined,
|
|
202
|
+
oauthProduct: oauthProduct ?? undefined,
|
|
203
|
+
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
|
+
}
|
|
146
233
|
else if (usePlaywright) {
|
|
147
234
|
console.log("Opening browser for login (Playwright)...");
|
|
148
235
|
token = await playwrightLogin(normalizedTarget, {
|
|
@@ -217,8 +304,19 @@ Login options:
|
|
|
217
304
|
const statusTarget = resolvedTarget && /^https?:\/\//.test(resolvedTarget) ? normalizeBaseUrl(resolvedTarget) : resolvedTarget ?? undefined;
|
|
218
305
|
const platform = statusTarget ?? getCurrentPlatform();
|
|
219
306
|
if (!platform) {
|
|
220
|
-
|
|
221
|
-
|
|
307
|
+
const envRaw = process.env.KWEAVER_BASE_URL?.trim();
|
|
308
|
+
const envUrl = envRaw ? normalizeBaseUrl(envRaw) : undefined;
|
|
309
|
+
const envToken = process.env.KWEAVER_TOKEN?.trim();
|
|
310
|
+
if (!envUrl || !envToken) {
|
|
311
|
+
console.error("No active platform. Run `kweaver auth login <platform-url>` first.\n" +
|
|
312
|
+
" Tip: set KWEAVER_BASE_URL and KWEAVER_TOKEN to use this command without a saved login.");
|
|
313
|
+
return 1;
|
|
314
|
+
}
|
|
315
|
+
console.log(`Config directory: ${getConfigDir()}`);
|
|
316
|
+
console.log(`Platform: ${envUrl} (KWEAVER_BASE_URL)`);
|
|
317
|
+
console.log(`Token present: yes (KWEAVER_TOKEN)`);
|
|
318
|
+
console.log(`Refresh token: n/a (env)`);
|
|
319
|
+
return 0;
|
|
222
320
|
}
|
|
223
321
|
const token = loadTokenConfig(platform);
|
|
224
322
|
if (!token) {
|
|
@@ -359,7 +457,7 @@ Login options:
|
|
|
359
457
|
return 0;
|
|
360
458
|
}
|
|
361
459
|
console.error("Usage: kweaver auth login <platform-url> [--alias <name>] [-u user] [-p pass] [--playwright]");
|
|
362
|
-
console.error(" kweaver auth whoami [platform-url|alias]");
|
|
460
|
+
console.error(" kweaver auth whoami [platform-url|alias] [--json]");
|
|
363
461
|
console.error(" kweaver auth export [platform-url|alias] [--json]");
|
|
364
462
|
console.error(" kweaver auth status [platform-url|alias]");
|
|
365
463
|
console.error(" kweaver auth list");
|
|
@@ -456,8 +554,37 @@ Options:
|
|
|
456
554
|
const resolved = positional ? resolvePlatformIdentifier(positional) : null;
|
|
457
555
|
const platform = resolved && /^https?:\/\//.test(resolved) ? normalizeBaseUrl(resolved) : resolved ?? getCurrentPlatform();
|
|
458
556
|
if (!platform) {
|
|
459
|
-
|
|
460
|
-
|
|
557
|
+
const envRaw = process.env.KWEAVER_BASE_URL?.trim();
|
|
558
|
+
const envUrl = envRaw ? normalizeBaseUrl(envRaw) : undefined;
|
|
559
|
+
const envToken = process.env.KWEAVER_TOKEN?.trim();
|
|
560
|
+
if (!envUrl || !envToken) {
|
|
561
|
+
console.error("No active platform. Run `kweaver auth login <platform-url>` first.");
|
|
562
|
+
return 1;
|
|
563
|
+
}
|
|
564
|
+
const accessToken = envToken.replace(/^Bearer\s+/i, "");
|
|
565
|
+
const payload = decodeJwtPayload(accessToken);
|
|
566
|
+
if (jsonOutput) {
|
|
567
|
+
console.log(JSON.stringify({ platform: envUrl, source: "env", ...(payload ?? {}) }, null, 2));
|
|
568
|
+
return 0;
|
|
569
|
+
}
|
|
570
|
+
console.log(`Platform: ${envUrl}`);
|
|
571
|
+
console.log(`Source: env (KWEAVER_TOKEN)`);
|
|
572
|
+
if (payload) {
|
|
573
|
+
const uname = payload.preferred_username ?? payload.name;
|
|
574
|
+
if (uname)
|
|
575
|
+
console.log(`Username: ${uname}`);
|
|
576
|
+
console.log(`User ID: ${payload.sub ?? "(unknown)"}`);
|
|
577
|
+
console.log(`Issuer: ${payload.iss ?? "(unknown)"}`);
|
|
578
|
+
if (payload.iat)
|
|
579
|
+
console.log(`Issued: ${new Date(payload.iat * 1000).toISOString()}`);
|
|
580
|
+
if (payload.exp)
|
|
581
|
+
console.log(`Expires: ${new Date(payload.exp * 1000).toISOString()}`);
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
console.log(`User info unavailable: opaque access token.`);
|
|
585
|
+
console.log(`Hint: run \`kweaver auth login ${envUrl}\` to obtain a full session.`);
|
|
586
|
+
}
|
|
587
|
+
return 0;
|
|
461
588
|
}
|
|
462
589
|
const token = loadTokenConfig(platform);
|
|
463
590
|
if (!token) {
|
package/dist/commands/config.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { listBusinessDomains } from "../api/business-domains.js";
|
|
2
|
-
import { withTokenRetry } from "../auth/oauth.js";
|
|
2
|
+
import { normalizeBaseUrl, withTokenRetry } from "../auth/oauth.js";
|
|
3
|
+
// Resolve platform URL: saved current platform > KWEAVER_BASE_URL (normalized to
|
|
4
|
+
// match what `auth login` writes, so env users share the same platforms/<key>/ dir).
|
|
5
|
+
function resolvePlatformUrl() {
|
|
6
|
+
const saved = getCurrentPlatform();
|
|
7
|
+
if (saved)
|
|
8
|
+
return saved;
|
|
9
|
+
const env = process.env.KWEAVER_BASE_URL?.trim();
|
|
10
|
+
return env ? normalizeBaseUrl(env) : undefined;
|
|
11
|
+
}
|
|
3
12
|
import { getCurrentPlatform, loadPlatformBusinessDomain, resolveBusinessDomain, savePlatformBusinessDomain, } from "../config/store.js";
|
|
4
13
|
const HELP = `kweaver config
|
|
5
14
|
|
|
@@ -20,9 +29,9 @@ export async function runConfigCommand(args) {
|
|
|
20
29
|
return 0;
|
|
21
30
|
}
|
|
22
31
|
if (sub === "show") {
|
|
23
|
-
const platform =
|
|
32
|
+
const platform = resolvePlatformUrl();
|
|
24
33
|
if (!platform) {
|
|
25
|
-
console.error("No active platform. Run `kweaver auth login <url>` first.");
|
|
34
|
+
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.");
|
|
26
35
|
return 1;
|
|
27
36
|
}
|
|
28
37
|
const bd = resolveBusinessDomain(platform);
|
|
@@ -31,7 +40,8 @@ export async function runConfigCommand(args) {
|
|
|
31
40
|
: loadPlatformBusinessDomain(platform)
|
|
32
41
|
? "config"
|
|
33
42
|
: "default";
|
|
34
|
-
|
|
43
|
+
const platformSource = getCurrentPlatform() ? "" : " (KWEAVER_BASE_URL)";
|
|
44
|
+
console.log(`Platform: ${platform}${platformSource}`);
|
|
35
45
|
console.log(`Business Domain: ${bd} (${source})`);
|
|
36
46
|
return 0;
|
|
37
47
|
}
|
|
@@ -41,19 +51,19 @@ export async function runConfigCommand(args) {
|
|
|
41
51
|
console.error("Usage: kweaver config set-bd <value>");
|
|
42
52
|
return 1;
|
|
43
53
|
}
|
|
44
|
-
const platform =
|
|
54
|
+
const platform = resolvePlatformUrl();
|
|
45
55
|
if (!platform) {
|
|
46
|
-
console.error("No active platform. Run `kweaver auth login <url>` first.");
|
|
56
|
+
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.");
|
|
47
57
|
return 1;
|
|
48
58
|
}
|
|
49
59
|
savePlatformBusinessDomain(platform, value);
|
|
50
|
-
console.log(`Business domain set to: ${value}`);
|
|
60
|
+
console.log(`Business domain set to: ${value} (${getCurrentPlatform() ? platform : `${platform} via KWEAVER_BASE_URL`})`);
|
|
51
61
|
return 0;
|
|
52
62
|
}
|
|
53
63
|
if (sub === "list-bd") {
|
|
54
|
-
const platform =
|
|
64
|
+
const platform = resolvePlatformUrl();
|
|
55
65
|
if (!platform) {
|
|
56
|
-
console.error("No active platform. Run `kweaver auth login <url>` first.");
|
|
66
|
+
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.");
|
|
57
67
|
return 1;
|
|
58
68
|
}
|
|
59
69
|
try {
|
|
@@ -281,20 +281,26 @@ async function runGetPrompt(options, args, pretty) {
|
|
|
281
281
|
async function runKnSearch(options, args, pretty) {
|
|
282
282
|
let query;
|
|
283
283
|
let onlySchema = false;
|
|
284
|
+
let knIdOverride;
|
|
284
285
|
for (let i = 0; i < args.length; i += 1) {
|
|
285
286
|
const arg = args[i];
|
|
286
287
|
if (arg === "--only-schema") {
|
|
287
288
|
onlySchema = true;
|
|
288
289
|
}
|
|
290
|
+
else if ((arg === "--kn-id" || arg === "-k") && args[i + 1]) {
|
|
291
|
+
knIdOverride = args[i + 1];
|
|
292
|
+
i += 1;
|
|
293
|
+
}
|
|
289
294
|
else if (!arg.startsWith("-") && !query) {
|
|
290
295
|
query = arg;
|
|
291
296
|
}
|
|
292
297
|
}
|
|
293
298
|
if (!query) {
|
|
294
|
-
console.error("Usage: kweaver context-loader kn-search <query> [--only-schema]");
|
|
299
|
+
console.error("Usage: kweaver context-loader kn-search <query> [--kn-id <id>] [--only-schema]");
|
|
295
300
|
return 1;
|
|
296
301
|
}
|
|
297
|
-
const
|
|
302
|
+
const effectiveOptions = knIdOverride ? { ...options, knId: knIdOverride } : options;
|
|
303
|
+
const result = await knSearch(effectiveOptions, { query, only_schema: onlySchema });
|
|
298
304
|
console.log(formatOutput(result, pretty));
|
|
299
305
|
return 0;
|
|
300
306
|
}
|
package/dist/commands/ds.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export declare function resolveFiles(pattern: string): Promise<string[]>;
|
|
|
17
17
|
export interface ImportCsvResult {
|
|
18
18
|
code: number;
|
|
19
19
|
tables: string[];
|
|
20
|
+
failed: string[];
|
|
20
21
|
tableColumns: Record<string, string[]>;
|
|
21
22
|
sampleRows: Record<string, Array<Record<string, string | null>>>;
|
|
22
23
|
}
|
package/dist/commands/ds.js
CHANGED
|
@@ -384,17 +384,17 @@ export async function runDsImportCsv(args) {
|
|
|
384
384
|
catch (error) {
|
|
385
385
|
if (error instanceof Error && error.message === "help") {
|
|
386
386
|
console.log(IMPORT_CSV_HELP);
|
|
387
|
-
return { code: 0, tables: [], tableColumns: {}, sampleRows: {} };
|
|
387
|
+
return { code: 0, tables: [], failed: [], tableColumns: {}, sampleRows: {} };
|
|
388
388
|
}
|
|
389
389
|
throw error;
|
|
390
390
|
}
|
|
391
391
|
if (!options.datasourceId) {
|
|
392
392
|
console.error("Usage: kweaver ds import-csv <ds-id> --files <glob_or_list> [options]");
|
|
393
|
-
return { code: 1, tables: [], tableColumns: {}, sampleRows: {} };
|
|
393
|
+
return { code: 1, tables: [], failed: [], tableColumns: {}, sampleRows: {} };
|
|
394
394
|
}
|
|
395
395
|
if (!options.files) {
|
|
396
396
|
console.error("Error: --files is required");
|
|
397
|
-
return { code: 1, tables: [], tableColumns: {}, sampleRows: {} };
|
|
397
|
+
return { code: 1, tables: [], failed: [], tableColumns: {}, sampleRows: {} };
|
|
398
398
|
}
|
|
399
399
|
// 1. Get credentials
|
|
400
400
|
const token = await ensureValidToken();
|
|
@@ -429,7 +429,7 @@ export async function runDsImportCsv(args) {
|
|
|
429
429
|
}
|
|
430
430
|
if (parsed.length === 0) {
|
|
431
431
|
console.error("All files were skipped — nothing to import");
|
|
432
|
-
return { code: 1, tables: [], tableColumns: {}, sampleRows: {} };
|
|
432
|
+
return { code: 1, tables: [], failed: [], tableColumns: {}, sampleRows: {} };
|
|
433
433
|
}
|
|
434
434
|
// Phase 2: Import each file in batches
|
|
435
435
|
const succeeded = [];
|
|
@@ -487,14 +487,14 @@ export async function runDsImportCsv(args) {
|
|
|
487
487
|
if (failed.length > 0) {
|
|
488
488
|
console.error(`Failed tables: ${failed.join(", ")}`);
|
|
489
489
|
}
|
|
490
|
-
|
|
491
|
-
tables: succeeded,
|
|
492
|
-
failed,
|
|
493
|
-
summary: { succeeded: succeeded.length, failed: failed.length },
|
|
494
|
-
}, null, 2));
|
|
495
|
-
return { code: failed.length > 0 ? 1 : 0, tables: succeeded, tableColumns, sampleRows };
|
|
490
|
+
return { code: failed.length > 0 ? 1 : 0, tables: succeeded, failed, tableColumns, sampleRows };
|
|
496
491
|
}
|
|
497
492
|
export async function runDsImportCsvCommand(args) {
|
|
498
493
|
const result = await runDsImportCsv(args);
|
|
494
|
+
console.log(JSON.stringify({
|
|
495
|
+
tables: result.tables,
|
|
496
|
+
failed: result.failed,
|
|
497
|
+
summary: { succeeded: result.tables.length, failed: result.failed.length },
|
|
498
|
+
}, null, 2));
|
|
499
499
|
return result.code;
|
|
500
500
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -62,3 +62,4 @@ 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";
|
package/dist/index.js
CHANGED
|
@@ -49,3 +49,5 @@ export { HttpError, NetworkRequestError, fetchTextOrThrow } from "./utils/http.j
|
|
|
49
49
|
export { NO_AUTH_TOKEN, isNoAuth, saveNoAuthPlatform, autoSelectBusinessDomain, getConfigDir, getCurrentPlatform, getActiveUser, setActiveUser, listUsers, listUserProfiles, resolveUserId, extractUserId, } from "./config/store.js";
|
|
50
50
|
// ── JWT utilities ─────────────────────────────────────────────────────────────
|
|
51
51
|
export { decodeJwtPayload, extractUserIdFromJwt } from "./config/jwt.js";
|
|
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";
|
package/package.json
CHANGED