@kweaver-ai/kweaver-sdk 0.5.1 → 0.6.0
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 +25 -2
- package/README.zh.md +24 -1
- package/dist/api/agent-chat.d.ts +8 -2
- package/dist/api/agent-chat.js +150 -44
- package/dist/api/agent-list.d.ts +35 -0
- package/dist/api/agent-list.js +95 -21
- package/dist/api/bkn-backend.d.ts +60 -0
- package/dist/api/bkn-backend.js +103 -10
- package/dist/api/business-domains.js +9 -5
- package/dist/api/context-loader.js +4 -1
- package/dist/api/conversations.d.ts +6 -3
- package/dist/api/conversations.js +29 -35
- package/dist/api/dataflow.js +1 -10
- package/dist/api/dataflow2.d.ts +95 -0
- package/dist/api/dataflow2.js +80 -0
- package/dist/api/datasources.js +1 -10
- package/dist/api/dataviews.js +1 -10
- package/dist/api/headers.d.ts +11 -0
- package/dist/api/headers.js +30 -0
- package/dist/api/knowledge-networks.d.ts +41 -0
- package/dist/api/knowledge-networks.js +69 -22
- package/dist/api/ontology-query.d.ts +14 -1
- package/dist/api/ontology-query.js +63 -49
- package/dist/api/semantic-search.js +2 -12
- package/dist/api/skills.d.ts +141 -0
- package/dist/api/skills.js +208 -0
- package/dist/api/vega.d.ts +54 -7
- package/dist/api/vega.js +112 -25
- package/dist/auth/oauth.d.ts +5 -1
- package/dist/auth/oauth.js +351 -95
- package/dist/cli.js +49 -5
- package/dist/client.d.ts +12 -0
- package/dist/client.js +52 -8
- package/dist/commands/agent.d.ts +33 -1
- package/dist/commands/agent.js +721 -49
- package/dist/commands/auth.js +226 -55
- package/dist/commands/bkn-ops.d.ts +77 -0
- package/dist/commands/bkn-ops.js +1056 -0
- package/dist/commands/bkn-query.d.ts +14 -0
- package/dist/commands/bkn-query.js +370 -0
- package/dist/commands/bkn-schema.d.ts +135 -0
- package/dist/commands/bkn-schema.js +1483 -0
- package/dist/commands/bkn-utils.d.ts +36 -0
- package/dist/commands/bkn-utils.js +102 -0
- package/dist/commands/bkn.d.ts +7 -113
- package/dist/commands/bkn.js +175 -2429
- package/dist/commands/call.js +8 -5
- package/dist/commands/dataflow.d.ts +1 -0
- package/dist/commands/dataflow.js +251 -0
- package/dist/commands/dataview.d.ts +7 -0
- package/dist/commands/dataview.js +38 -2
- package/dist/commands/ds.d.ts +1 -0
- package/dist/commands/ds.js +8 -1
- package/dist/commands/explore-bkn.d.ts +79 -0
- package/dist/commands/explore-bkn.js +273 -0
- package/dist/commands/explore-chat.d.ts +3 -0
- package/dist/commands/explore-chat.js +193 -0
- package/dist/commands/explore-vega.d.ts +3 -0
- package/dist/commands/explore-vega.js +71 -0
- package/dist/commands/explore.d.ts +9 -0
- package/dist/commands/explore.js +258 -0
- package/dist/commands/import-csv.d.ts +2 -0
- package/dist/commands/import-csv.js +3 -2
- package/dist/commands/skill.d.ts +26 -0
- package/dist/commands/skill.js +524 -0
- package/dist/commands/vega.js +372 -117
- package/dist/config/jwt.d.ts +6 -0
- package/dist/config/jwt.js +21 -0
- package/dist/config/no-auth.d.ts +3 -0
- package/dist/config/no-auth.js +5 -0
- package/dist/config/store.d.ts +45 -5
- package/dist/config/store.js +385 -30
- package/dist/index.d.ts +6 -1
- package/dist/index.js +5 -1
- package/dist/kweaver.d.ts +5 -0
- package/dist/kweaver.js +32 -2
- package/dist/resources/bkn.d.ts +4 -0
- package/dist/resources/bkn.js +6 -3
- package/dist/resources/conversations.d.ts +5 -2
- package/dist/resources/conversations.js +17 -3
- package/dist/resources/knowledge-networks.js +3 -8
- package/dist/resources/skills.d.ts +47 -0
- package/dist/resources/skills.js +47 -0
- package/dist/resources/vega.d.ts +11 -6
- package/dist/resources/vega.js +37 -10
- package/dist/templates/explorer/app.js +136 -0
- package/dist/templates/explorer/bkn.js +747 -0
- package/dist/templates/explorer/chat.js +980 -0
- package/dist/templates/explorer/dashboard.js +82 -0
- package/dist/templates/explorer/index.html +35 -0
- package/dist/templates/explorer/style.css +2440 -0
- package/dist/templates/explorer/vega.js +291 -0
- package/dist/utils/http.d.ts +3 -0
- package/dist/utils/http.js +37 -1
- package/package.json +9 -5
package/dist/auth/oauth.js
CHANGED
|
@@ -1,10 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { isNoAuth } from "../config/no-auth.js";
|
|
2
|
+
import { deleteClientConfig, getCurrentPlatform, loadClientConfig, loadTokenConfig, loadUserTokenConfig, resolveUserId, saveClientConfig, saveNoAuthPlatform, saveTokenConfig, setCurrentPlatform, } from "../config/store.js";
|
|
3
|
+
import { HttpError, NetworkRequestError, fetchWithRetry } from "../utils/http.js";
|
|
3
4
|
const TOKEN_TTL_SECONDS = 3600;
|
|
4
5
|
/** Seconds before access token expiry to trigger refresh (matches Python ConfigAuth). */
|
|
5
6
|
const REFRESH_THRESHOLD_SEC = 60;
|
|
6
7
|
const DEFAULT_REDIRECT_PORT = 9010;
|
|
7
8
|
const DEFAULT_SCOPE = "openid offline all";
|
|
9
|
+
/** Best-effort fetch of display name via EACP userinfo (ShareServer). */
|
|
10
|
+
async function fetchDisplayName(baseUrl, accessToken, tlsInsecure) {
|
|
11
|
+
try {
|
|
12
|
+
const res = await runWithTlsInsecure(tlsInsecure, () => fetch(`${baseUrl}/api/eacp/v1/user/get`, {
|
|
13
|
+
headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json" },
|
|
14
|
+
}));
|
|
15
|
+
if (!res.ok)
|
|
16
|
+
return null;
|
|
17
|
+
const info = (await res.json());
|
|
18
|
+
if (typeof info.account === "string")
|
|
19
|
+
return info.account;
|
|
20
|
+
if (typeof info.name === "string")
|
|
21
|
+
return info.name;
|
|
22
|
+
if (typeof info.mail === "string")
|
|
23
|
+
return info.mail;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
/* Non-critical — displayName will be absent. */
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
8
30
|
/** POSIX shell single-quote escaping for copy-paste commands. */
|
|
9
31
|
export function shellQuoteForShell(value) {
|
|
10
32
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -118,11 +140,132 @@ async function generatePkce() {
|
|
|
118
140
|
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
119
141
|
return { verifier, challenge };
|
|
120
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Pre-flight check: verify that a cached OAuth2 client is still recognised
|
|
145
|
+
* by the server. Fetches the authorization endpoint with `redirect: "manual"`
|
|
146
|
+
* and inspects the Location header. Returns false when Hydra redirects to an
|
|
147
|
+
* error page containing `invalid_client` or similar indicators.
|
|
148
|
+
*/
|
|
149
|
+
async function isClientStillValid(baseUrl, clientId, redirectUri) {
|
|
150
|
+
try {
|
|
151
|
+
const params = new URLSearchParams({
|
|
152
|
+
client_id: clientId,
|
|
153
|
+
response_type: "code",
|
|
154
|
+
scope: "openid",
|
|
155
|
+
redirect_uri: redirectUri,
|
|
156
|
+
state: "preflight",
|
|
157
|
+
});
|
|
158
|
+
const resp = await fetch(`${baseUrl}/oauth2/auth?${params}`, {
|
|
159
|
+
redirect: "manual",
|
|
160
|
+
});
|
|
161
|
+
if (resp.status === 302) {
|
|
162
|
+
const location = resp.headers.get("location") ?? "";
|
|
163
|
+
if (location.includes("error=invalid_client") ||
|
|
164
|
+
location.includes("error=error")) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
// Non-redirect — Hydra might serve an error page directly
|
|
170
|
+
if (resp.status >= 400)
|
|
171
|
+
return false;
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Network error — cannot pre-validate; let the real flow proceed
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Resolve a cached client or register a new one. When a cached client fails
|
|
181
|
+
* pre-flight validation (stale registration after server reset), the local
|
|
182
|
+
* client.json is deleted and a fresh registration is performed.
|
|
183
|
+
*/
|
|
184
|
+
async function resolveOrRegisterClient(baseUrl, redirectUri, scope, options) {
|
|
185
|
+
if (options?.clientId) {
|
|
186
|
+
const client = {
|
|
187
|
+
baseUrl,
|
|
188
|
+
clientId: options.clientId,
|
|
189
|
+
clientSecret: options.clientSecret ?? "",
|
|
190
|
+
redirectUri,
|
|
191
|
+
logoutRedirectUri: redirectUri.replace("/callback", "/successful-logout"),
|
|
192
|
+
scope,
|
|
193
|
+
lang: "zh-cn",
|
|
194
|
+
product: "adp",
|
|
195
|
+
xForwardedPrefix: "",
|
|
196
|
+
};
|
|
197
|
+
saveClientConfig(baseUrl, client);
|
|
198
|
+
return client;
|
|
199
|
+
}
|
|
200
|
+
let client = loadClientConfig(baseUrl);
|
|
201
|
+
if (client?.clientId) {
|
|
202
|
+
const valid = await isClientStillValid(baseUrl, client.clientId, redirectUri);
|
|
203
|
+
if (valid)
|
|
204
|
+
return client;
|
|
205
|
+
process.stderr.write("Cached OAuth2 client is no longer valid on the server. Re-registering…\n");
|
|
206
|
+
deleteClientConfig(baseUrl);
|
|
207
|
+
client = null;
|
|
208
|
+
}
|
|
209
|
+
const registered = await registerOAuth2Client(baseUrl, redirectUri, scope);
|
|
210
|
+
saveClientConfig(baseUrl, registered);
|
|
211
|
+
return registered;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Parse a redirect URI to extract host, port, and pathname.
|
|
215
|
+
* Returns null if the URI is not a valid HTTP(S) URL.
|
|
216
|
+
*/
|
|
217
|
+
function parseRedirectUri(uri) {
|
|
218
|
+
try {
|
|
219
|
+
const parsed = new URL(uri);
|
|
220
|
+
const host = parsed.hostname;
|
|
221
|
+
const port = parsed.port ? Number(parsed.port) : (parsed.protocol === "https:" ? 443 : 80);
|
|
222
|
+
const isLocalhost = host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
223
|
+
return { host, port, pathname: parsed.pathname, isLocalhost };
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Manual code flow for non-localhost redirect URIs.
|
|
231
|
+
* Prints the auth URL, then reads the full callback URL from stdin
|
|
232
|
+
* to extract the authorization code.
|
|
233
|
+
*/
|
|
234
|
+
async function waitForManualCode(authUrl, state) {
|
|
235
|
+
const { createInterface } = await import("node:readline");
|
|
236
|
+
process.stderr.write("\nSince the redirect URI is not localhost, you need to complete login manually.\n" +
|
|
237
|
+
"1. Open this URL in your browser:\n\n" +
|
|
238
|
+
` ${authUrl}\n\n` +
|
|
239
|
+
"2. After login, the browser will redirect to your callback URL.\n" +
|
|
240
|
+
"3. Copy the full callback URL and paste it here:\n\n");
|
|
241
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
242
|
+
const callbackUrl = await new Promise((resolve) => {
|
|
243
|
+
rl.question("Callback URL> ", (answer) => {
|
|
244
|
+
rl.close();
|
|
245
|
+
resolve(answer.trim());
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
const parsed = new URL(callbackUrl);
|
|
249
|
+
const receivedState = parsed.searchParams.get("state");
|
|
250
|
+
if (receivedState !== state) {
|
|
251
|
+
throw new Error("OAuth2 state mismatch — possible CSRF attack.");
|
|
252
|
+
}
|
|
253
|
+
const error = parsed.searchParams.get("error");
|
|
254
|
+
if (error) {
|
|
255
|
+
const desc = parsed.searchParams.get("error_description") ?? "";
|
|
256
|
+
throw new Error(desc ? `Authorization failed: ${error} — ${desc}` : `Authorization failed: ${error}`);
|
|
257
|
+
}
|
|
258
|
+
const code = parsed.searchParams.get("code");
|
|
259
|
+
if (!code) {
|
|
260
|
+
throw new Error("No authorization code found in the callback URL.");
|
|
261
|
+
}
|
|
262
|
+
return code;
|
|
263
|
+
}
|
|
121
264
|
/**
|
|
122
265
|
* OAuth2 Authorization Code login flow.
|
|
123
266
|
* 1. Register client (if not already registered), OR use a provided client ID
|
|
124
267
|
* 2. Open browser to /oauth2/auth
|
|
125
|
-
* 3. Receive authorization code via local HTTP callback
|
|
268
|
+
* 3. Receive authorization code via local HTTP callback (or manual paste for non-localhost)
|
|
126
269
|
* 4. Exchange code for access_token + refresh_token
|
|
127
270
|
* 5. Save token.json + client.json to ~/.kweaver/
|
|
128
271
|
*/
|
|
@@ -133,28 +276,23 @@ export async function oauth2Login(baseUrl, options) {
|
|
|
133
276
|
const base = normalizeBaseUrl(baseUrl);
|
|
134
277
|
const port = options?.port ?? DEFAULT_REDIRECT_PORT;
|
|
135
278
|
const scope = options?.scope ?? DEFAULT_SCOPE;
|
|
136
|
-
|
|
279
|
+
// Determine redirect URI: explicit option > port-based default
|
|
280
|
+
const redirectUri = options?.redirectUri ?? `http://127.0.0.1:${port}/callback`;
|
|
281
|
+
const parsedRedirect = parseRedirectUri(redirectUri);
|
|
282
|
+
const isLocalRedirect = parsedRedirect?.isLocalhost ?? true;
|
|
283
|
+
const listenPort = parsedRedirect?.port ?? port;
|
|
284
|
+
const callbackPathname = parsedRedirect?.pathname ?? "/callback";
|
|
137
285
|
// Step 1: Determine client — use provided client ID or fall back to dynamic registration
|
|
138
|
-
let client
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// Persist it so future logins reuse it without re-registering.
|
|
142
|
-
client = {
|
|
143
|
-
baseUrl: base,
|
|
144
|
-
clientId: options.clientId,
|
|
145
|
-
clientSecret: options.clientSecret ?? "",
|
|
146
|
-
redirectUri,
|
|
147
|
-
logoutRedirectUri: redirectUri.replace("/callback", "/successful-logout"),
|
|
148
|
-
scope,
|
|
149
|
-
lang: "zh-cn",
|
|
150
|
-
product: "adp",
|
|
151
|
-
xForwardedPrefix: "",
|
|
152
|
-
};
|
|
153
|
-
saveClientConfig(base, client);
|
|
286
|
+
let client;
|
|
287
|
+
try {
|
|
288
|
+
client = await resolveOrRegisterClient(base, redirectUri, scope, options);
|
|
154
289
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
290
|
+
catch (e) {
|
|
291
|
+
if (e instanceof HttpError && e.status === 404) {
|
|
292
|
+
process.stderr.write("OAuth2 endpoint not found (404). Saving platform in no-auth mode.\n");
|
|
293
|
+
return saveNoAuthPlatform(base, { tlsInsecure: options?.tlsInsecure });
|
|
294
|
+
}
|
|
295
|
+
throw e;
|
|
158
296
|
}
|
|
159
297
|
// Use PKCE when no client secret is available (public client / platform client).
|
|
160
298
|
const usePkce = !client.clientSecret;
|
|
@@ -177,71 +315,91 @@ export async function oauth2Login(baseUrl, options) {
|
|
|
177
315
|
authParams.set("code_challenge_method", "S256");
|
|
178
316
|
}
|
|
179
317
|
const authUrl = `${base}/oauth2/auth?${authParams.toString()}`;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
server
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
318
|
+
let token;
|
|
319
|
+
if (isLocalRedirect) {
|
|
320
|
+
// Step 4a: Local callback — start HTTP server to receive the authorization code
|
|
321
|
+
token = await new Promise((resolve, reject) => {
|
|
322
|
+
let server;
|
|
323
|
+
const timeoutId = setTimeout(() => {
|
|
324
|
+
server?.close();
|
|
325
|
+
reject(new Error("OAuth2 login timed out (120s). No authorization code received."));
|
|
326
|
+
}, 120_000);
|
|
327
|
+
server = createServer((req, res) => {
|
|
328
|
+
void (async () => {
|
|
329
|
+
try {
|
|
330
|
+
const url = new URL(req.url ?? "/", `http://127.0.0.1:${listenPort}`);
|
|
331
|
+
if (url.pathname !== callbackPathname) {
|
|
332
|
+
res.writeHead(404);
|
|
333
|
+
res.end();
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const receivedState = url.searchParams.get("state");
|
|
337
|
+
const code = url.searchParams.get("code");
|
|
338
|
+
const callbackError = url.searchParams.get("error");
|
|
339
|
+
const callbackErrorDesc = url.searchParams.get("error_description");
|
|
340
|
+
if (receivedState !== state) {
|
|
341
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
342
|
+
res.end(buildCallbackExchangeErrorHtml("OAuth2 state mismatch — possible CSRF attack."));
|
|
343
|
+
clearTimeout(timeoutId);
|
|
344
|
+
server.close();
|
|
345
|
+
reject(new Error("OAuth2 state mismatch — possible CSRF attack."));
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (callbackError) {
|
|
349
|
+
const msg = callbackErrorDesc
|
|
350
|
+
? `Authorization failed: ${callbackError} — ${callbackErrorDesc}`
|
|
351
|
+
: `Authorization failed: ${callbackError}`;
|
|
352
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
353
|
+
res.end(buildCallbackExchangeErrorHtml(msg));
|
|
354
|
+
clearTimeout(timeoutId);
|
|
355
|
+
server.close();
|
|
356
|
+
reject(new Error(msg));
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (!code) {
|
|
360
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
361
|
+
res.end(buildCallbackExchangeErrorHtml("No authorization code received in callback."));
|
|
362
|
+
clearTimeout(timeoutId);
|
|
363
|
+
server.close();
|
|
364
|
+
reject(new Error("No authorization code received in callback."));
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const exchanged = await exchangeCodeForToken(base, code, client.clientId, client.clientSecret, redirectUri, pkce?.verifier, options?.tlsInsecure);
|
|
368
|
+
const copyCommand = buildCopyCommand(base, client.clientId, client.clientSecret, exchanged.refreshToken, options?.tlsInsecure);
|
|
369
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
370
|
+
res.end(buildCallbackHtml(copyCommand));
|
|
201
371
|
clearTimeout(timeoutId);
|
|
202
372
|
server.close();
|
|
203
|
-
|
|
204
|
-
return;
|
|
373
|
+
resolve(exchanged);
|
|
205
374
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
375
|
+
catch (err) {
|
|
376
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
377
|
+
try {
|
|
378
|
+
res.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
|
|
379
|
+
res.end(buildCallbackExchangeErrorHtml(message));
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
/* response may already be sent */
|
|
383
|
+
}
|
|
209
384
|
clearTimeout(timeoutId);
|
|
210
385
|
server.close();
|
|
211
|
-
reject(
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
const exchanged = await exchangeCodeForToken(base, receivedCode, client.clientId, client.clientSecret, redirectUri, pkce?.verifier, options?.tlsInsecure);
|
|
215
|
-
const copyCommand = buildCopyCommand(base, client.clientId, client.clientSecret, exchanged.refreshToken, options?.tlsInsecure);
|
|
216
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
217
|
-
res.end(buildCallbackHtml(copyCommand));
|
|
218
|
-
clearTimeout(timeoutId);
|
|
219
|
-
server.close();
|
|
220
|
-
resolve(exchanged);
|
|
221
|
-
}
|
|
222
|
-
catch (err) {
|
|
223
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
224
|
-
try {
|
|
225
|
-
res.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
|
|
226
|
-
res.end(buildCallbackExchangeErrorHtml(message));
|
|
227
|
-
}
|
|
228
|
-
catch {
|
|
229
|
-
/* response may already be sent */
|
|
386
|
+
reject(err instanceof Error ? err : new Error(message));
|
|
230
387
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
// Step 5: Open browser (uses spawn with proper Windows quoting)
|
|
239
|
-
import("../utils/browser.js").then(({ openBrowser }) => {
|
|
240
|
-
openBrowser(authUrl);
|
|
388
|
+
})();
|
|
389
|
+
});
|
|
390
|
+
server.listen(listenPort, "127.0.0.1", () => {
|
|
391
|
+
import("../utils/browser.js").then(({ openBrowser }) => {
|
|
392
|
+
openBrowser(authUrl);
|
|
393
|
+
});
|
|
394
|
+
process.stderr.write(`If the wrong browser opens, copy this URL to your correct browser:\n ${authUrl}\n`);
|
|
241
395
|
});
|
|
242
|
-
process.stderr.write(`If the wrong browser opens, copy this URL to your correct browser:\n ${authUrl}\n`);
|
|
243
396
|
});
|
|
244
|
-
}
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
// Step 4b: Non-localhost redirect — manual code entry flow
|
|
400
|
+
const code = await waitForManualCode(authUrl, state);
|
|
401
|
+
token = await exchangeCodeForToken(base, code, client.clientId, client.clientSecret, redirectUri, pkce?.verifier, options?.tlsInsecure);
|
|
402
|
+
}
|
|
245
403
|
setCurrentPlatform(base);
|
|
246
404
|
return token;
|
|
247
405
|
});
|
|
@@ -329,6 +487,9 @@ async function exchangeCodeForToken(baseUrl, code, clientId, clientSecret, redir
|
|
|
329
487
|
obtainedAt: now.toISOString(),
|
|
330
488
|
...(tlsInsecure ? { tlsInsecure: true } : {}),
|
|
331
489
|
};
|
|
490
|
+
const displayName = await fetchDisplayName(baseUrl, data.access_token, tlsInsecure);
|
|
491
|
+
if (displayName)
|
|
492
|
+
token.displayName = displayName;
|
|
332
493
|
saveTokenConfig(token);
|
|
333
494
|
return token;
|
|
334
495
|
}
|
|
@@ -359,13 +520,22 @@ export async function playwrightLogin(baseUrl, options) {
|
|
|
359
520
|
const base = normalizeBaseUrl(baseUrl);
|
|
360
521
|
const port = options?.port ?? DEFAULT_REDIRECT_PORT;
|
|
361
522
|
const scope = options?.scope ?? DEFAULT_SCOPE;
|
|
362
|
-
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
523
|
+
const redirectUri = options?.redirectUri ?? `http://127.0.0.1:${port}/callback`;
|
|
524
|
+
const parsedRedirect = parseRedirectUri(redirectUri);
|
|
525
|
+
const listenPort = parsedRedirect?.port ?? port;
|
|
526
|
+
const callbackPathname = parsedRedirect?.pathname ?? "/callback";
|
|
363
527
|
const hasCredentials = !!(options?.username && options?.password);
|
|
364
|
-
// Step 1: Ensure registered OAuth2 client
|
|
365
|
-
let client
|
|
366
|
-
|
|
367
|
-
client = await
|
|
368
|
-
|
|
528
|
+
// Step 1: Ensure registered OAuth2 client (with stale-client auto-recovery)
|
|
529
|
+
let client;
|
|
530
|
+
try {
|
|
531
|
+
client = await resolveOrRegisterClient(base, redirectUri, scope);
|
|
532
|
+
}
|
|
533
|
+
catch (e) {
|
|
534
|
+
if (e instanceof HttpError && e.status === 404) {
|
|
535
|
+
process.stderr.write("OAuth2 endpoint not found (404). Saving platform in no-auth mode.\n");
|
|
536
|
+
return saveNoAuthPlatform(base, { tlsInsecure: options?.tlsInsecure });
|
|
537
|
+
}
|
|
538
|
+
throw e;
|
|
369
539
|
}
|
|
370
540
|
// Step 2: Generate CSRF state
|
|
371
541
|
const state = randomBytes(12).toString("hex");
|
|
@@ -394,14 +564,16 @@ export async function playwrightLogin(baseUrl, options) {
|
|
|
394
564
|
server = createServer((req, res) => {
|
|
395
565
|
void (async () => {
|
|
396
566
|
try {
|
|
397
|
-
const url = new URL(req.url ?? "/", `http://127.0.0.1:${
|
|
398
|
-
if (url.pathname !==
|
|
567
|
+
const url = new URL(req.url ?? "/", `http://127.0.0.1:${listenPort}`);
|
|
568
|
+
if (url.pathname !== callbackPathname) {
|
|
399
569
|
res.writeHead(404);
|
|
400
570
|
res.end();
|
|
401
571
|
return;
|
|
402
572
|
}
|
|
403
573
|
const receivedState = url.searchParams.get("state");
|
|
404
574
|
const receivedCode = url.searchParams.get("code");
|
|
575
|
+
const callbackError = url.searchParams.get("error");
|
|
576
|
+
const callbackErrorDesc = url.searchParams.get("error_description");
|
|
405
577
|
if (receivedState !== state) {
|
|
406
578
|
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
407
579
|
res.end(buildCallbackExchangeErrorHtml("OAuth2 state mismatch — possible CSRF attack."));
|
|
@@ -411,6 +583,18 @@ export async function playwrightLogin(baseUrl, options) {
|
|
|
411
583
|
reject(new Error("OAuth2 state mismatch — possible CSRF attack."));
|
|
412
584
|
return;
|
|
413
585
|
}
|
|
586
|
+
if (callbackError) {
|
|
587
|
+
const msg = callbackErrorDesc
|
|
588
|
+
? `Authorization failed: ${callbackError} — ${callbackErrorDesc}`
|
|
589
|
+
: `Authorization failed: ${callbackError}`;
|
|
590
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
591
|
+
res.end(buildCallbackExchangeErrorHtml(msg));
|
|
592
|
+
clearTimeout(timeoutId);
|
|
593
|
+
server.close();
|
|
594
|
+
browser?.close();
|
|
595
|
+
reject(new Error(msg));
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
414
598
|
if (!receivedCode) {
|
|
415
599
|
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
416
600
|
res.end(buildCallbackExchangeErrorHtml("No authorization code received in callback."));
|
|
@@ -445,7 +629,7 @@ export async function playwrightLogin(baseUrl, options) {
|
|
|
445
629
|
}
|
|
446
630
|
})();
|
|
447
631
|
});
|
|
448
|
-
server.listen(
|
|
632
|
+
server.listen(listenPort, "127.0.0.1", async () => {
|
|
449
633
|
try {
|
|
450
634
|
browser = await chromium.launch({ headless: hasCredentials });
|
|
451
635
|
const context = await browser.newContext({ ignoreHTTPSErrors: !!options?.tlsInsecure });
|
|
@@ -513,6 +697,9 @@ export async function refreshTokenLogin(baseUrl, options) {
|
|
|
513
697
|
return token;
|
|
514
698
|
}
|
|
515
699
|
function tokenNeedsRefresh(token) {
|
|
700
|
+
if (isNoAuth(token.accessToken)) {
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
516
703
|
if (!token.expiresAt) {
|
|
517
704
|
return false;
|
|
518
705
|
}
|
|
@@ -529,6 +716,9 @@ function tokenNeedsRefresh(token) {
|
|
|
529
716
|
*/
|
|
530
717
|
export async function refreshAccessToken(token) {
|
|
531
718
|
const baseUrl = normalizeBaseUrl(token.baseUrl);
|
|
719
|
+
if (isNoAuth(token.accessToken)) {
|
|
720
|
+
throw new Error(`Cannot refresh no-auth session for ${baseUrl}.`);
|
|
721
|
+
}
|
|
532
722
|
const refreshToken = token.refreshToken?.trim();
|
|
533
723
|
if (!refreshToken) {
|
|
534
724
|
throw new Error(`Token expired and no refresh_token available for ${baseUrl}. Run \`kweaver auth login ${baseUrl}\` again.`);
|
|
@@ -547,7 +737,7 @@ export async function refreshAccessToken(token) {
|
|
|
547
737
|
});
|
|
548
738
|
let response;
|
|
549
739
|
try {
|
|
550
|
-
response = await runWithTlsInsecure(token.tlsInsecure, () =>
|
|
740
|
+
response = await runWithTlsInsecure(token.tlsInsecure, () => fetchWithRetry(url, {
|
|
551
741
|
method: "POST",
|
|
552
742
|
headers: {
|
|
553
743
|
Authorization: `Basic ${credentials}`,
|
|
@@ -577,6 +767,10 @@ export async function refreshAccessToken(token) {
|
|
|
577
767
|
}
|
|
578
768
|
const now = new Date();
|
|
579
769
|
const expiresIn = typeof data.expires_in === "number" ? data.expires_in : 3600;
|
|
770
|
+
let displayName = token.displayName;
|
|
771
|
+
if (!displayName) {
|
|
772
|
+
displayName = (await fetchDisplayName(baseUrl, data.access_token, token.tlsInsecure)) ?? undefined;
|
|
773
|
+
}
|
|
580
774
|
const newToken = {
|
|
581
775
|
baseUrl,
|
|
582
776
|
accessToken: data.access_token,
|
|
@@ -588,6 +782,7 @@ export async function refreshAccessToken(token) {
|
|
|
588
782
|
idToken: data.id_token ?? token.idToken ?? "",
|
|
589
783
|
obtainedAt: now.toISOString(),
|
|
590
784
|
...(token.tlsInsecure ? { tlsInsecure: true } : {}),
|
|
785
|
+
...(displayName ? { displayName } : {}),
|
|
591
786
|
};
|
|
592
787
|
saveTokenConfig(newToken);
|
|
593
788
|
return newToken;
|
|
@@ -609,19 +804,48 @@ export async function ensureValidToken(opts) {
|
|
|
609
804
|
return {
|
|
610
805
|
baseUrl: normalizeBaseUrl(envBaseUrl),
|
|
611
806
|
accessToken: rawToken,
|
|
612
|
-
tokenType: "bearer",
|
|
807
|
+
tokenType: isNoAuth(rawToken) ? "none" : "bearer",
|
|
613
808
|
scope: "",
|
|
614
809
|
obtainedAt: new Date().toISOString(),
|
|
615
810
|
};
|
|
616
811
|
}
|
|
812
|
+
if (!opts?.forceRefresh && envToken && !envBaseUrl) {
|
|
813
|
+
const currentPlatformForEnv = getCurrentPlatform();
|
|
814
|
+
if (currentPlatformForEnv) {
|
|
815
|
+
const rawToken = envToken.replace(/^Bearer\s+/i, "");
|
|
816
|
+
return {
|
|
817
|
+
baseUrl: normalizeBaseUrl(currentPlatformForEnv),
|
|
818
|
+
accessToken: rawToken,
|
|
819
|
+
tokenType: isNoAuth(rawToken) ? "none" : "bearer",
|
|
820
|
+
scope: "",
|
|
821
|
+
obtainedAt: new Date().toISOString(),
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
}
|
|
617
825
|
const currentPlatform = getCurrentPlatform();
|
|
618
826
|
if (!currentPlatform) {
|
|
619
827
|
throw new Error("No active platform selected. Run `kweaver auth login <platform-url>` first.");
|
|
620
828
|
}
|
|
621
|
-
|
|
829
|
+
// KWEAVER_USER: load a specific user's token without switching active user
|
|
830
|
+
const envUser = process.env.KWEAVER_USER;
|
|
831
|
+
let token;
|
|
832
|
+
if (envUser) {
|
|
833
|
+
const userId = resolveUserId(currentPlatform, envUser);
|
|
834
|
+
if (!userId) {
|
|
835
|
+
throw new Error(`User '${envUser}' not found for ${currentPlatform}. ` +
|
|
836
|
+
"Run `kweaver auth users` to see available users.");
|
|
837
|
+
}
|
|
838
|
+
token = loadUserTokenConfig(currentPlatform, userId);
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
token = loadTokenConfig(currentPlatform);
|
|
842
|
+
}
|
|
622
843
|
if (!token) {
|
|
623
844
|
throw new Error(`No saved token for ${currentPlatform}. Run \`kweaver auth login ${currentPlatform}\` first.`);
|
|
624
845
|
}
|
|
846
|
+
if (isNoAuth(token.accessToken)) {
|
|
847
|
+
return token;
|
|
848
|
+
}
|
|
625
849
|
if (opts?.forceRefresh) {
|
|
626
850
|
return refreshAccessToken(token);
|
|
627
851
|
}
|
|
@@ -652,10 +876,21 @@ export async function with401RefreshRetry(fn) {
|
|
|
652
876
|
throw error;
|
|
653
877
|
}
|
|
654
878
|
const platformUrl = normalizeBaseUrl(currentPlatform);
|
|
655
|
-
const
|
|
879
|
+
const envUser = process.env.KWEAVER_USER;
|
|
880
|
+
let latest;
|
|
881
|
+
if (envUser) {
|
|
882
|
+
const userId = resolveUserId(platformUrl, envUser);
|
|
883
|
+
latest = userId ? loadUserTokenConfig(platformUrl, userId) : null;
|
|
884
|
+
}
|
|
885
|
+
else {
|
|
886
|
+
latest = loadTokenConfig(platformUrl);
|
|
887
|
+
}
|
|
656
888
|
if (!latest) {
|
|
657
889
|
throw error;
|
|
658
890
|
}
|
|
891
|
+
if (isNoAuth(latest.accessToken)) {
|
|
892
|
+
throw error;
|
|
893
|
+
}
|
|
659
894
|
try {
|
|
660
895
|
await refreshAccessToken(latest);
|
|
661
896
|
}
|
|
@@ -680,8 +915,21 @@ export async function withTokenRetry(fn) {
|
|
|
680
915
|
}
|
|
681
916
|
catch (error) {
|
|
682
917
|
if (error instanceof HttpError && error.status === 401) {
|
|
918
|
+
if (isNoAuth(token.accessToken)) {
|
|
919
|
+
throw error;
|
|
920
|
+
}
|
|
683
921
|
const platformUrl = normalizeBaseUrl(token.baseUrl);
|
|
684
|
-
const
|
|
922
|
+
const envUser = process.env.KWEAVER_USER;
|
|
923
|
+
let latest;
|
|
924
|
+
if (envUser) {
|
|
925
|
+
const userId = resolveUserId(platformUrl, envUser);
|
|
926
|
+
latest = userId ? loadUserTokenConfig(platformUrl, userId) : null;
|
|
927
|
+
}
|
|
928
|
+
else {
|
|
929
|
+
latest = loadTokenConfig(platformUrl);
|
|
930
|
+
}
|
|
931
|
+
if (!latest)
|
|
932
|
+
latest = token;
|
|
685
933
|
try {
|
|
686
934
|
const refreshed = await refreshAccessToken(latest);
|
|
687
935
|
return await fn(refreshed);
|
|
@@ -719,6 +967,11 @@ function formatOAuthErrorBody(body) {
|
|
|
719
967
|
}
|
|
720
968
|
return lines.join("\n");
|
|
721
969
|
}
|
|
970
|
+
function isTlsVerificationDisabledForProcess() {
|
|
971
|
+
return (process.env.NODE_TLS_REJECT_UNAUTHORIZED === "0" ||
|
|
972
|
+
process.env.KWEAVER_TLS_INSECURE === "1" ||
|
|
973
|
+
process.env.KWEAVER_TLS_INSECURE === "true");
|
|
974
|
+
}
|
|
722
975
|
export function formatHttpError(error) {
|
|
723
976
|
if (error instanceof HttpError) {
|
|
724
977
|
const oauthMessage = formatOAuthErrorBody(error.body);
|
|
@@ -739,7 +992,10 @@ export function formatHttpError(error) {
|
|
|
739
992
|
if (error instanceof Error) {
|
|
740
993
|
const cause = "cause" in error && error.cause instanceof Error ? error.cause.message : "";
|
|
741
994
|
if (cause && error.message === "fetch failed") {
|
|
742
|
-
|
|
995
|
+
const hint = isTlsVerificationDisabledForProcess()
|
|
996
|
+
? "Hint: TLS verification is already disabled for this process. Check network reachability, TLS termination, or proxy stability."
|
|
997
|
+
: "Hint: use --insecure (-k) to skip TLS verification for self-signed certificates.";
|
|
998
|
+
return `${error.message}: ${cause}\n${hint}`;
|
|
743
999
|
}
|
|
744
1000
|
return error.message;
|
|
745
1001
|
}
|