@kweaver-ai/kweaver-sdk 0.6.3 → 0.6.5
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 +29 -4
- package/README.zh.md +7 -3
- package/dist/api/dataflow.d.ts +1 -1
- package/dist/api/dataflow.js +4 -1
- package/dist/api/toolboxes.d.ts +47 -0
- package/dist/api/toolboxes.js +90 -0
- package/dist/auth/oauth.d.ts +69 -0
- package/dist/auth/oauth.js +647 -1
- package/dist/cli.js +20 -1
- package/dist/commands/auth.js +145 -18
- package/dist/commands/bkn-ops.d.ts +1 -0
- package/dist/commands/bkn-ops.js +8 -1
- package/dist/commands/call.d.ts +10 -0
- package/dist/commands/call.js +61 -5
- 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 +11 -11
- package/dist/commands/import-csv.d.ts +1 -1
- package/dist/commands/import-csv.js +3 -1
- package/dist/commands/tool.d.ts +16 -0
- package/dist/commands/tool.js +208 -0
- package/dist/commands/toolbox.d.ts +14 -0
- package/dist/commands/toolbox.js +256 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/package.json +1 -1
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) {
|