@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.
Files changed (95) hide show
  1. package/README.md +25 -2
  2. package/README.zh.md +24 -1
  3. package/dist/api/agent-chat.d.ts +8 -2
  4. package/dist/api/agent-chat.js +150 -44
  5. package/dist/api/agent-list.d.ts +35 -0
  6. package/dist/api/agent-list.js +95 -21
  7. package/dist/api/bkn-backend.d.ts +60 -0
  8. package/dist/api/bkn-backend.js +103 -10
  9. package/dist/api/business-domains.js +9 -5
  10. package/dist/api/context-loader.js +4 -1
  11. package/dist/api/conversations.d.ts +6 -3
  12. package/dist/api/conversations.js +29 -35
  13. package/dist/api/dataflow.js +1 -10
  14. package/dist/api/dataflow2.d.ts +95 -0
  15. package/dist/api/dataflow2.js +80 -0
  16. package/dist/api/datasources.js +1 -10
  17. package/dist/api/dataviews.js +1 -10
  18. package/dist/api/headers.d.ts +11 -0
  19. package/dist/api/headers.js +30 -0
  20. package/dist/api/knowledge-networks.d.ts +41 -0
  21. package/dist/api/knowledge-networks.js +69 -22
  22. package/dist/api/ontology-query.d.ts +14 -1
  23. package/dist/api/ontology-query.js +63 -49
  24. package/dist/api/semantic-search.js +2 -12
  25. package/dist/api/skills.d.ts +141 -0
  26. package/dist/api/skills.js +208 -0
  27. package/dist/api/vega.d.ts +54 -7
  28. package/dist/api/vega.js +112 -25
  29. package/dist/auth/oauth.d.ts +5 -1
  30. package/dist/auth/oauth.js +351 -95
  31. package/dist/cli.js +49 -5
  32. package/dist/client.d.ts +12 -0
  33. package/dist/client.js +52 -8
  34. package/dist/commands/agent.d.ts +33 -1
  35. package/dist/commands/agent.js +721 -49
  36. package/dist/commands/auth.js +226 -55
  37. package/dist/commands/bkn-ops.d.ts +77 -0
  38. package/dist/commands/bkn-ops.js +1056 -0
  39. package/dist/commands/bkn-query.d.ts +14 -0
  40. package/dist/commands/bkn-query.js +370 -0
  41. package/dist/commands/bkn-schema.d.ts +135 -0
  42. package/dist/commands/bkn-schema.js +1483 -0
  43. package/dist/commands/bkn-utils.d.ts +36 -0
  44. package/dist/commands/bkn-utils.js +102 -0
  45. package/dist/commands/bkn.d.ts +7 -113
  46. package/dist/commands/bkn.js +175 -2429
  47. package/dist/commands/call.js +8 -5
  48. package/dist/commands/dataflow.d.ts +1 -0
  49. package/dist/commands/dataflow.js +251 -0
  50. package/dist/commands/dataview.d.ts +7 -0
  51. package/dist/commands/dataview.js +38 -2
  52. package/dist/commands/ds.d.ts +1 -0
  53. package/dist/commands/ds.js +8 -1
  54. package/dist/commands/explore-bkn.d.ts +79 -0
  55. package/dist/commands/explore-bkn.js +273 -0
  56. package/dist/commands/explore-chat.d.ts +3 -0
  57. package/dist/commands/explore-chat.js +193 -0
  58. package/dist/commands/explore-vega.d.ts +3 -0
  59. package/dist/commands/explore-vega.js +71 -0
  60. package/dist/commands/explore.d.ts +9 -0
  61. package/dist/commands/explore.js +258 -0
  62. package/dist/commands/import-csv.d.ts +2 -0
  63. package/dist/commands/import-csv.js +3 -2
  64. package/dist/commands/skill.d.ts +26 -0
  65. package/dist/commands/skill.js +524 -0
  66. package/dist/commands/vega.js +372 -117
  67. package/dist/config/jwt.d.ts +6 -0
  68. package/dist/config/jwt.js +21 -0
  69. package/dist/config/no-auth.d.ts +3 -0
  70. package/dist/config/no-auth.js +5 -0
  71. package/dist/config/store.d.ts +45 -5
  72. package/dist/config/store.js +385 -30
  73. package/dist/index.d.ts +6 -1
  74. package/dist/index.js +5 -1
  75. package/dist/kweaver.d.ts +5 -0
  76. package/dist/kweaver.js +32 -2
  77. package/dist/resources/bkn.d.ts +4 -0
  78. package/dist/resources/bkn.js +6 -3
  79. package/dist/resources/conversations.d.ts +5 -2
  80. package/dist/resources/conversations.js +17 -3
  81. package/dist/resources/knowledge-networks.js +3 -8
  82. package/dist/resources/skills.d.ts +47 -0
  83. package/dist/resources/skills.js +47 -0
  84. package/dist/resources/vega.d.ts +11 -6
  85. package/dist/resources/vega.js +37 -10
  86. package/dist/templates/explorer/app.js +136 -0
  87. package/dist/templates/explorer/bkn.js +747 -0
  88. package/dist/templates/explorer/chat.js +980 -0
  89. package/dist/templates/explorer/dashboard.js +82 -0
  90. package/dist/templates/explorer/index.html +35 -0
  91. package/dist/templates/explorer/style.css +2440 -0
  92. package/dist/templates/explorer/vega.js +291 -0
  93. package/dist/utils/http.d.ts +3 -0
  94. package/dist/utils/http.js +37 -1
  95. package/package.json +9 -5
@@ -1,10 +1,32 @@
1
- import { getCurrentPlatform, loadClientConfig, loadTokenConfig, saveClientConfig, saveTokenConfig, setCurrentPlatform, } from "../config/store.js";
2
- import { HttpError, NetworkRequestError } from "../utils/http.js";
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
- const redirectUri = `http://127.0.0.1:${port}/callback`;
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 = loadClientConfig(base);
139
- if (options?.clientId) {
140
- // Use the platform's existing client (e.g. the web app client).
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
- else if (!client?.clientId) {
156
- client = await registerOAuth2Client(base, redirectUri, scope);
157
- saveClientConfig(base, client);
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
- // Step 4: Start local callback server; exchange code inside handler, then show credentials HTML
181
- const token = await new Promise((resolve, reject) => {
182
- let server;
183
- const timeoutId = setTimeout(() => {
184
- server?.close();
185
- reject(new Error("OAuth2 login timed out (120s). No authorization code received."));
186
- }, 120_000);
187
- server = createServer((req, res) => {
188
- void (async () => {
189
- try {
190
- const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
191
- if (url.pathname !== "/callback") {
192
- res.writeHead(404);
193
- res.end();
194
- return;
195
- }
196
- const receivedState = url.searchParams.get("state");
197
- const receivedCode = url.searchParams.get("code");
198
- if (receivedState !== state) {
199
- res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
200
- res.end(buildCallbackExchangeErrorHtml("OAuth2 state mismatch — possible CSRF attack."));
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
- reject(new Error("OAuth2 state mismatch — possible CSRF attack."));
204
- return;
373
+ resolve(exchanged);
205
374
  }
206
- if (!receivedCode) {
207
- res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
208
- res.end(buildCallbackExchangeErrorHtml("No authorization code received in callback."));
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(new Error("No authorization code received in callback."));
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
- clearTimeout(timeoutId);
232
- server.close();
233
- reject(err instanceof Error ? err : new Error(message));
234
- }
235
- })();
236
- });
237
- server.listen(port, "127.0.0.1", () => {
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 = loadClientConfig(base);
366
- if (!client?.clientId) {
367
- client = await registerOAuth2Client(base, redirectUri, scope);
368
- saveClientConfig(base, client);
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:${port}`);
398
- if (url.pathname !== "/callback") {
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(port, "127.0.0.1", async () => {
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, () => fetch(url, {
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
- let token = loadTokenConfig(currentPlatform);
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 latest = loadTokenConfig(platformUrl);
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 latest = loadTokenConfig(platformUrl) ?? token;
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
- return `${error.message}: ${cause}\nHint: use --insecure (-k) to skip TLS verification for self-signed certificates.`;
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
  }