@kweaver-ai/kweaver-sdk 0.4.10 → 0.4.11

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.
@@ -8,82 +8,137 @@ const DEFAULT_SCOPE = "openid offline all";
8
8
  export function normalizeBaseUrl(value) {
9
9
  return value.replace(/\/+$/, "");
10
10
  }
11
+ /**
12
+ * Temporarily disable TLS certificate verification for Node `fetch` (sets
13
+ * NODE_TLS_REJECT_UNAUTHORIZED). Used for `--insecure` login and token refresh.
14
+ */
15
+ async function runWithTlsInsecure(tlsInsecure, fn) {
16
+ if (!tlsInsecure) {
17
+ return fn();
18
+ }
19
+ const prev = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
20
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
21
+ try {
22
+ return await fn();
23
+ }
24
+ finally {
25
+ if (prev === undefined) {
26
+ delete process.env.NODE_TLS_REJECT_UNAUTHORIZED;
27
+ }
28
+ else {
29
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = prev;
30
+ }
31
+ }
32
+ }
33
+ /** Generate a PKCE code_verifier and code_challenge (S256). */
34
+ async function generatePkce() {
35
+ const { randomBytes, createHash } = await import("node:crypto");
36
+ const verifier = randomBytes(48).toString("base64url");
37
+ const challenge = createHash("sha256").update(verifier).digest("base64url");
38
+ return { verifier, challenge };
39
+ }
11
40
  /**
12
41
  * OAuth2 Authorization Code login flow.
13
- * 1. Register client (if not already registered)
42
+ * 1. Register client (if not already registered), OR use a provided client ID
14
43
  * 2. Open browser to /oauth2/auth
15
44
  * 3. Receive authorization code via local HTTP callback
16
45
  * 4. Exchange code for access_token + refresh_token
17
46
  * 5. Save token.json + client.json to ~/.kweaver/
18
47
  */
19
48
  export async function oauth2Login(baseUrl, options) {
20
- const { createServer } = await import("node:http");
21
- const { randomBytes } = await import("node:crypto");
22
- const base = normalizeBaseUrl(baseUrl);
23
- const port = options?.port ?? DEFAULT_REDIRECT_PORT;
24
- const scope = options?.scope ?? DEFAULT_SCOPE;
25
- const redirectUri = `http://127.0.0.1:${port}/callback`;
26
- // Step 1: Ensure registered client
27
- let client = loadClientConfig(base);
28
- if (!client?.clientId) {
29
- client = await registerOAuth2Client(base, redirectUri, scope);
30
- saveClientConfig(base, client);
31
- }
32
- // Step 2: Generate CSRF state
33
- const state = randomBytes(12).toString("hex");
34
- // Step 3: Build authorization URL
35
- const authParams = new URLSearchParams({
36
- redirect_uri: redirectUri,
37
- "x-forwarded-prefix": "",
38
- client_id: client.clientId,
39
- scope,
40
- response_type: "code",
41
- state,
42
- lang: "zh-cn",
43
- product: "adp",
44
- });
45
- const authUrl = `${base}/oauth2/auth?${authParams.toString()}`;
46
- // Step 4: Start local callback server, wait for code
47
- const code = await new Promise((resolve, reject) => {
48
- const timeoutId = setTimeout(() => {
49
- server.close();
50
- reject(new Error("OAuth2 login timed out (120s). No authorization code received."));
51
- }, 120_000);
52
- const server = createServer((req, res) => {
53
- const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
54
- if (url.pathname === "/callback") {
55
- const receivedState = url.searchParams.get("state");
56
- const receivedCode = url.searchParams.get("code");
57
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
58
- res.end("<html><body><h2>Login successful. You can close this tab.</h2></body></html>");
59
- clearTimeout(timeoutId);
49
+ return runWithTlsInsecure(options?.tlsInsecure, async () => {
50
+ const { createServer } = await import("node:http");
51
+ const { randomBytes } = await import("node:crypto");
52
+ const base = normalizeBaseUrl(baseUrl);
53
+ const port = options?.port ?? DEFAULT_REDIRECT_PORT;
54
+ const scope = options?.scope ?? DEFAULT_SCOPE;
55
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
56
+ // Step 1: Determine client use provided client ID or fall back to dynamic registration
57
+ let client = loadClientConfig(base);
58
+ if (options?.clientId) {
59
+ // Use the platform's existing client (e.g. the web app client).
60
+ // Persist it so future logins reuse it without re-registering.
61
+ client = {
62
+ baseUrl: base,
63
+ clientId: options.clientId,
64
+ clientSecret: options.clientSecret ?? "",
65
+ redirectUri,
66
+ logoutRedirectUri: redirectUri.replace("/callback", "/successful-logout"),
67
+ scope,
68
+ lang: "zh-cn",
69
+ product: "adp",
70
+ xForwardedPrefix: "",
71
+ };
72
+ saveClientConfig(base, client);
73
+ }
74
+ else if (!client?.clientId) {
75
+ client = await registerOAuth2Client(base, redirectUri, scope);
76
+ saveClientConfig(base, client);
77
+ }
78
+ // Use PKCE when no client secret is available (public client / platform client).
79
+ const usePkce = !client.clientSecret;
80
+ const pkce = usePkce ? await generatePkce() : null;
81
+ // Step 2: Generate CSRF state
82
+ const state = randomBytes(12).toString("hex");
83
+ // Step 3: Build authorization URL
84
+ const authParams = new URLSearchParams({
85
+ redirect_uri: redirectUri,
86
+ "x-forwarded-prefix": "",
87
+ client_id: client.clientId,
88
+ scope,
89
+ response_type: "code",
90
+ state,
91
+ lang: "zh-cn",
92
+ product: "adp",
93
+ });
94
+ if (pkce) {
95
+ authParams.set("code_challenge", pkce.challenge);
96
+ authParams.set("code_challenge_method", "S256");
97
+ }
98
+ const authUrl = `${base}/oauth2/auth?${authParams.toString()}`;
99
+ // Step 4: Start local callback server, wait for code
100
+ const code = await new Promise((resolve, reject) => {
101
+ const timeoutId = setTimeout(() => {
60
102
  server.close();
61
- if (receivedState !== state) {
62
- reject(new Error("OAuth2 state mismatch — possible CSRF attack."));
63
- }
64
- else if (!receivedCode) {
65
- reject(new Error("No authorization code received in callback."));
103
+ reject(new Error("OAuth2 login timed out (120s). No authorization code received."));
104
+ }, 120_000);
105
+ const server = createServer((req, res) => {
106
+ const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
107
+ if (url.pathname === "/callback") {
108
+ const receivedState = url.searchParams.get("state");
109
+ const receivedCode = url.searchParams.get("code");
110
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
111
+ res.end("<html><body><h2>Login successful. You can close this tab.</h2></body></html>");
112
+ clearTimeout(timeoutId);
113
+ server.close();
114
+ if (receivedState !== state) {
115
+ reject(new Error("OAuth2 state mismatch — possible CSRF attack."));
116
+ }
117
+ else if (!receivedCode) {
118
+ reject(new Error("No authorization code received in callback."));
119
+ }
120
+ else {
121
+ resolve(receivedCode);
122
+ }
66
123
  }
67
124
  else {
68
- resolve(receivedCode);
125
+ res.writeHead(404);
126
+ res.end();
69
127
  }
70
- }
71
- else {
72
- res.writeHead(404);
73
- res.end();
74
- }
75
- });
76
- server.listen(port, "127.0.0.1", () => {
77
- // Step 5: Open browser (uses spawn with proper Windows quoting)
78
- import("../utils/browser.js").then(({ openBrowser }) => {
79
- openBrowser(authUrl);
128
+ });
129
+ server.listen(port, "127.0.0.1", () => {
130
+ // Step 5: Open browser (uses spawn with proper Windows quoting)
131
+ import("../utils/browser.js").then(({ openBrowser }) => {
132
+ openBrowser(authUrl);
133
+ });
134
+ process.stderr.write(`If the wrong browser opens, copy this URL to your correct browser:\n ${authUrl}\n`);
80
135
  });
81
136
  });
137
+ // Step 6: Exchange code for tokens
138
+ const token = await exchangeCodeForToken(base, code, client.clientId, client.clientSecret, redirectUri, pkce?.verifier, options?.tlsInsecure);
139
+ setCurrentPlatform(base);
140
+ return token;
82
141
  });
83
- // Step 6: Exchange code for tokens
84
- const token = await exchangeCodeForToken(base, code, client.clientId, client.clientSecret, redirectUri);
85
- setCurrentPlatform(base);
86
- return token;
87
142
  }
88
143
  async function registerOAuth2Client(baseUrl, redirectUri, scope) {
89
144
  const logoutUri = redirectUri.replace("/callback", "/successful-logout");
@@ -123,20 +178,31 @@ async function registerOAuth2Client(baseUrl, redirectUri, scope) {
123
178
  xForwardedPrefix: "",
124
179
  };
125
180
  }
126
- async function exchangeCodeForToken(baseUrl, code, clientId, clientSecret, redirectUri) {
127
- const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
181
+ async function exchangeCodeForToken(baseUrl, code, clientId, clientSecret, redirectUri, codeVerifier, tlsInsecure) {
182
+ const params = {
183
+ grant_type: "authorization_code",
184
+ code,
185
+ redirect_uri: redirectUri,
186
+ };
187
+ if (codeVerifier) {
188
+ params.code_verifier = codeVerifier;
189
+ }
190
+ const headers = {
191
+ "Content-Type": "application/x-www-form-urlencoded",
192
+ Accept: "application/json",
193
+ };
194
+ if (clientSecret) {
195
+ // Confidential client: use HTTP Basic auth
196
+ headers.Authorization = `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`;
197
+ }
198
+ else {
199
+ // Public client (PKCE): send client_id in body
200
+ params.client_id = clientId;
201
+ }
128
202
  const response = await fetch(`${baseUrl}/oauth2/token`, {
129
203
  method: "POST",
130
- headers: {
131
- Authorization: `Basic ${credentials}`,
132
- "Content-Type": "application/x-www-form-urlencoded",
133
- Accept: "application/json",
134
- },
135
- body: new URLSearchParams({
136
- grant_type: "authorization_code",
137
- code,
138
- redirect_uri: redirectUri,
139
- }).toString(),
204
+ headers,
205
+ body: new URLSearchParams(params).toString(),
140
206
  });
141
207
  const text = await response.text();
142
208
  if (!response.ok) {
@@ -155,6 +221,7 @@ async function exchangeCodeForToken(baseUrl, code, clientId, clientSecret, redir
155
221
  refreshToken: data.refresh_token ?? "",
156
222
  idToken: data.id_token ?? "",
157
223
  obtainedAt: now.toISOString(),
224
+ ...(tlsInsecure ? { tlsInsecure: true } : {}),
158
225
  };
159
226
  saveTokenConfig(token);
160
227
  return token;
@@ -171,105 +238,107 @@ async function exchangeCodeForToken(baseUrl, code, clientId, clientSecret, redir
171
238
  * window for manual login (same UX as the old cookie-based flow).
172
239
  */
173
240
  export async function playwrightLogin(baseUrl, options) {
174
- const { createServer } = await import("node:http");
175
- const { randomBytes } = await import("node:crypto");
176
- let chromium;
177
- try {
178
- const modName = "playwright";
179
- const pw = await import(/* webpackIgnore: true */ modName);
180
- chromium = pw.chromium;
181
- }
182
- catch {
183
- throw new Error("Playwright is not installed. Run:\n npm install playwright && npx playwright install chromium");
184
- }
185
- const base = normalizeBaseUrl(baseUrl);
186
- const port = options?.port ?? DEFAULT_REDIRECT_PORT;
187
- const scope = options?.scope ?? DEFAULT_SCOPE;
188
- const redirectUri = `http://127.0.0.1:${port}/callback`;
189
- const hasCredentials = !!(options?.username && options?.password);
190
- // Step 1: Ensure registered OAuth2 client
191
- let client = loadClientConfig(base);
192
- if (!client?.clientId) {
193
- client = await registerOAuth2Client(base, redirectUri, scope);
194
- saveClientConfig(base, client);
195
- }
196
- // Step 2: Generate CSRF state
197
- const state = randomBytes(12).toString("hex");
198
- // Step 3: Build authorization URL
199
- const authParams = new URLSearchParams({
200
- redirect_uri: redirectUri,
201
- "x-forwarded-prefix": "",
202
- client_id: client.clientId,
203
- scope,
204
- response_type: "code",
205
- state,
206
- lang: "zh-cn",
207
- product: "adp",
208
- });
209
- const authUrl = `${base}/oauth2/auth?${authParams.toString()}`;
210
- // Step 4: Start local callback server to capture the authorization code
211
- const code = await new Promise((resolve, reject) => {
212
- const TIMEOUT_MS = hasCredentials ? 30_000 : 120_000;
213
- const timeoutId = setTimeout(() => {
214
- server.close();
215
- browser?.close();
216
- reject(new Error(`OAuth2 login timed out (${TIMEOUT_MS / 1000}s). No authorization code received.`));
217
- }, TIMEOUT_MS);
218
- const server = createServer((req, res) => {
219
- const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
220
- if (url.pathname === "/callback") {
221
- const receivedState = url.searchParams.get("state");
222
- const receivedCode = url.searchParams.get("code");
223
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
224
- res.end("<html><body><h2>Login successful. You can close this tab.</h2></body></html>");
225
- clearTimeout(timeoutId);
241
+ return runWithTlsInsecure(options?.tlsInsecure, async () => {
242
+ const { createServer } = await import("node:http");
243
+ const { randomBytes } = await import("node:crypto");
244
+ let chromium;
245
+ try {
246
+ const modName = "playwright";
247
+ const pw = await import(/* webpackIgnore: true */ modName);
248
+ chromium = pw.chromium;
249
+ }
250
+ catch {
251
+ throw new Error("Playwright is not installed. Run:\n npm install playwright && npx playwright install chromium");
252
+ }
253
+ const base = normalizeBaseUrl(baseUrl);
254
+ const port = options?.port ?? DEFAULT_REDIRECT_PORT;
255
+ const scope = options?.scope ?? DEFAULT_SCOPE;
256
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
257
+ const hasCredentials = !!(options?.username && options?.password);
258
+ // Step 1: Ensure registered OAuth2 client
259
+ let client = loadClientConfig(base);
260
+ if (!client?.clientId) {
261
+ client = await registerOAuth2Client(base, redirectUri, scope);
262
+ saveClientConfig(base, client);
263
+ }
264
+ // Step 2: Generate CSRF state
265
+ const state = randomBytes(12).toString("hex");
266
+ // Step 3: Build authorization URL
267
+ const authParams = new URLSearchParams({
268
+ redirect_uri: redirectUri,
269
+ "x-forwarded-prefix": "",
270
+ client_id: client.clientId,
271
+ scope,
272
+ response_type: "code",
273
+ state,
274
+ lang: "zh-cn",
275
+ product: "adp",
276
+ });
277
+ const authUrl = `${base}/oauth2/auth?${authParams.toString()}`;
278
+ // Step 4: Start local callback server to capture the authorization code
279
+ const code = await new Promise((resolve, reject) => {
280
+ const TIMEOUT_MS = hasCredentials ? 30_000 : 120_000;
281
+ const timeoutId = setTimeout(() => {
226
282
  server.close();
227
283
  browser?.close();
228
- if (receivedState !== state) {
229
- reject(new Error("OAuth2 state mismatch — possible CSRF attack."));
230
- }
231
- else if (!receivedCode) {
232
- reject(new Error("No authorization code received in callback."));
284
+ reject(new Error(`OAuth2 login timed out (${TIMEOUT_MS / 1000}s). No authorization code received.`));
285
+ }, TIMEOUT_MS);
286
+ const server = createServer((req, res) => {
287
+ const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
288
+ if (url.pathname === "/callback") {
289
+ const receivedState = url.searchParams.get("state");
290
+ const receivedCode = url.searchParams.get("code");
291
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
292
+ res.end("<html><body><h2>Login successful. You can close this tab.</h2></body></html>");
293
+ clearTimeout(timeoutId);
294
+ server.close();
295
+ browser?.close();
296
+ if (receivedState !== state) {
297
+ reject(new Error("OAuth2 state mismatch — possible CSRF attack."));
298
+ }
299
+ else if (!receivedCode) {
300
+ reject(new Error("No authorization code received in callback."));
301
+ }
302
+ else {
303
+ resolve(receivedCode);
304
+ }
233
305
  }
234
306
  else {
235
- resolve(receivedCode);
307
+ res.writeHead(404);
308
+ res.end();
236
309
  }
237
- }
238
- else {
239
- res.writeHead(404);
240
- res.end();
241
- }
242
- });
243
- let browser;
244
- server.listen(port, "127.0.0.1", async () => {
245
- try {
246
- browser = await chromium.launch({ headless: hasCredentials });
247
- const context = await browser.newContext();
248
- const page = await context.newPage();
249
- // Navigate to OAuth2 auth URL — redirects to signin page
250
- await page.goto(authUrl, { waitUntil: "networkidle", timeout: 30_000 });
251
- if (hasCredentials) {
252
- // Auto-fill credentials
253
- await page.waitForSelector('input[name="account"]', { timeout: 10_000 });
254
- await page.fill('input[name="account"]', options.username);
255
- await page.fill('input[name="password"]', options.password);
256
- await page.click("button.ant-btn-primary");
310
+ });
311
+ let browser;
312
+ server.listen(port, "127.0.0.1", async () => {
313
+ try {
314
+ browser = await chromium.launch({ headless: hasCredentials });
315
+ const context = await browser.newContext({ ignoreHTTPSErrors: !!options?.tlsInsecure });
316
+ const page = await context.newPage();
317
+ // Navigate to OAuth2 auth URL — redirects to signin page
318
+ await page.goto(authUrl, { waitUntil: "networkidle", timeout: 30_000 });
319
+ if (hasCredentials) {
320
+ // Auto-fill credentials
321
+ await page.waitForSelector('input[name="account"]', { timeout: 10_000 });
322
+ await page.fill('input[name="account"]', options.username);
323
+ await page.fill('input[name="password"]', options.password);
324
+ await page.click("button.ant-btn-primary");
325
+ }
326
+ // else: visible browser user logs in manually
327
+ // The OAuth2 callback will fire when login completes, resolving the promise above
257
328
  }
258
- // else: visible browser — user logs in manually
259
- // The OAuth2 callback will fire when login completes, resolving the promise above
260
- }
261
- catch (err) {
262
- clearTimeout(timeoutId);
263
- server.close();
264
- browser?.close();
265
- reject(err);
266
- }
329
+ catch (err) {
330
+ clearTimeout(timeoutId);
331
+ server.close();
332
+ browser?.close();
333
+ reject(err);
334
+ }
335
+ });
267
336
  });
337
+ // Step 5: Exchange authorization code for tokens (includes refresh_token)
338
+ const token = await exchangeCodeForToken(base, code, client.clientId, client.clientSecret, redirectUri, undefined, options?.tlsInsecure);
339
+ setCurrentPlatform(base);
340
+ return token;
268
341
  });
269
- // Step 5: Exchange authorization code for tokens (includes refresh_token)
270
- const token = await exchangeCodeForToken(base, code, client.clientId, client.clientSecret, redirectUri);
271
- setCurrentPlatform(base);
272
- return token;
273
342
  }
274
343
  function tokenNeedsRefresh(token) {
275
344
  if (!token.expiresAt) {
@@ -306,7 +375,7 @@ export async function refreshAccessToken(token) {
306
375
  });
307
376
  let response;
308
377
  try {
309
- response = await fetch(url, {
378
+ response = await runWithTlsInsecure(token.tlsInsecure, () => fetch(url, {
310
379
  method: "POST",
311
380
  headers: {
312
381
  Authorization: `Basic ${credentials}`,
@@ -314,7 +383,7 @@ export async function refreshAccessToken(token) {
314
383
  Accept: "application/json",
315
384
  },
316
385
  body: body.toString(),
317
- });
386
+ }));
318
387
  }
319
388
  catch (cause) {
320
389
  const hint = cause instanceof Error ? cause.message : String(cause);
@@ -346,6 +415,7 @@ export async function refreshAccessToken(token) {
346
415
  refreshToken: data.refresh_token ?? refreshToken,
347
416
  idToken: data.id_token ?? token.idToken ?? "",
348
417
  obtainedAt: now.toISOString(),
418
+ ...(token.tlsInsecure ? { tlsInsecure: true } : {}),
349
419
  };
350
420
  saveTokenConfig(newToken);
351
421
  return newToken;
@@ -486,6 +556,10 @@ export function formatHttpError(error) {
486
556
  ].join("\n").trim();
487
557
  }
488
558
  if (error instanceof Error) {
559
+ const cause = "cause" in error && error.cause instanceof Error ? error.cause.message : "";
560
+ if (cause && error.message === "fetch failed") {
561
+ return `${error.message}: ${cause}\nHint: use --insecure (-k) to skip TLS verification for self-signed certificates.`;
562
+ }
489
563
  return error.message;
490
564
  }
491
565
  return String(error);
package/dist/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { applyTlsEnvFromSavedTokens } from "./config/tls-env.js";
1
2
  import { runAgentCommand } from "./commands/agent.js";
2
3
  import { runAuthCommand } from "./commands/auth.js";
3
4
  import { runKnCommand } from "./commands/bkn.js";
@@ -14,7 +15,7 @@ Usage:
14
15
  kweaver --version | -V
15
16
  kweaver --help | -h
16
17
 
17
- kweaver auth <platform-url> [--alias name] [-u user] [-p pass] [--playwright]
18
+ kweaver auth <platform-url> [--alias name] [-u user] [-p pass] [--playwright] [--insecure|-k]
18
19
  kweaver auth login <platform-url> (alias for auth <url>)
19
20
  kweaver auth status [platform-url|alias]
20
21
  kweaver auth list
@@ -97,6 +98,7 @@ Commands:
97
98
  help Show this message`);
98
99
  }
99
100
  export async function run(argv) {
101
+ applyTlsEnvFromSavedTokens();
100
102
  const [command, ...rest] = argv;
101
103
  if (command === "--version" || command === "-V" || command === "version") {
102
104
  const { createRequire } = await import("node:module");
package/dist/client.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { applyTlsEnvFromSavedTokens } from "./config/tls-env.js";
1
2
  import { getCurrentPlatform, loadTokenConfig, } from "./config/store.js";
2
3
  import { ensureValidToken } from "./auth/oauth.js";
3
4
  import { AgentsResource } from "./resources/agents.js";
@@ -111,6 +112,7 @@ export class KWeaverClient {
111
112
  * ```
112
113
  */
113
114
  static async connect(opts = {}) {
115
+ applyTlsEnvFromSavedTokens();
114
116
  // Try with current token first
115
117
  let token = await ensureValidToken();
116
118
  const client = new KWeaverClient({
@@ -4,19 +4,24 @@ export async function runAuthCommand(args) {
4
4
  const target = args[0];
5
5
  const rest = args.slice(1);
6
6
  if (!target || target === "--help" || target === "-h") {
7
- console.log(`kweaver auth login <url> [--alias <name>] [-u user] [-p pass] [--playwright]
8
- kweaver auth <url> Login (shorthand; same options as login)
9
- kweaver auth status [url|alias]
10
- kweaver auth list List saved platforms
11
- kweaver auth use <url|alias> Switch active platform
12
- kweaver auth logout [url|alias] Logout (clear local token)
13
- kweaver auth delete <url|alias> Delete saved credentials
7
+ console.log(`kweaver auth login <url> [options] Login to a platform (browser OAuth2 by default)
8
+ kweaver auth <url> Login (shorthand; same options as login)
9
+ kweaver auth status [url|alias] Show current auth status
10
+ kweaver auth list List saved platforms
11
+ kweaver auth use <url|alias> Switch active platform
12
+ kweaver auth logout [url|alias] Logout (clear local token)
13
+ kweaver auth delete <url|alias> Delete saved credentials
14
14
 
15
- Login options (browser OAuth2 by default; use -u/-p for headless Playwright):
16
- --alias <name> Short name for this platform (use with auth use / status / logout)
17
- -u, --username Username (with -p triggers Playwright login)
18
- -p, --password Password
19
- --playwright Force browser (Playwright) login even without -u/-p`);
15
+ Login options:
16
+ --alias <name> Save platform with a short alias (use with use / status / logout)
17
+ --client-id <id> Use an existing OAuth2 client ID instead of registering a new one.
18
+ Use the platform's web app client ID to get the same permissions
19
+ as the browser. Find it in DevTools: /oauth2/auth?client_id=<id>
20
+ --client-secret <s> Client secret (omit for public/PKCE clients)
21
+ -u, --username Username (with -p triggers Playwright headless login)
22
+ -p, --password Password
23
+ --playwright Force Playwright browser login even without -u/-p
24
+ --insecure, -k Skip TLS certificate verification (self-signed / dev HTTPS only)`);
20
25
  return 0;
21
26
  }
22
27
  if (target === "login") {
@@ -38,21 +43,33 @@ Login options (browser OAuth2 by default; use -u/-p for headless Playwright):
38
43
  const username = readOption(args, "--username") ?? readOption(args, "-u");
39
44
  const password = readOption(args, "--password") ?? readOption(args, "-p");
40
45
  const usePlaywright = args.includes("--playwright");
46
+ const clientId = readOption(args, "--client-id");
47
+ const clientSecret = readOption(args, "--client-secret");
48
+ const tlsInsecure = args.includes("--insecure") || args.includes("-k");
41
49
  let token;
42
50
  if (username && password) {
43
51
  // Headless Playwright login with credentials
44
52
  console.log("Logging in (headless)...");
45
- token = await playwrightLogin(normalizedTarget, { username, password });
53
+ token = await playwrightLogin(normalizedTarget, { username, password, tlsInsecure });
46
54
  }
47
55
  else if (usePlaywright) {
48
56
  // Explicit Playwright fallback
49
57
  console.log("Opening browser for login (Playwright)...");
50
- token = await playwrightLogin(normalizedTarget);
58
+ token = await playwrightLogin(normalizedTarget, { tlsInsecure });
51
59
  }
52
60
  else {
53
61
  // Default: OAuth2 authorization code flow (supports refresh_token)
54
- console.log("Opening browser for OAuth2 login...");
55
- token = await oauth2Login(normalizedTarget);
62
+ if (clientId) {
63
+ console.log(`Opening browser for OAuth2 login (client: ${clientId})...`);
64
+ }
65
+ else {
66
+ console.log("Opening browser for OAuth2 login...");
67
+ }
68
+ token = await oauth2Login(normalizedTarget, {
69
+ clientId: clientId ?? undefined,
70
+ clientSecret: clientSecret ?? undefined,
71
+ tlsInsecure,
72
+ });
56
73
  }
57
74
  if (alias) {
58
75
  setPlatformAlias(normalizedTarget, alias);
@@ -106,6 +123,9 @@ Login options (browser OAuth2 by default; use -u/-p for headless Playwright):
106
123
  `Token present: yes`,
107
124
  ];
108
125
  lines.push(`Refresh token: ${token.refreshToken ? "yes (auto-refresh enabled)" : "no"}`);
126
+ if (token.tlsInsecure) {
127
+ lines.push(`TLS: certificate verification disabled (saved; dev only)`);
128
+ }
109
129
  if (token.expiresAt) {
110
130
  const expiry = new Date(token.expiresAt);
111
131
  const remainingMs = expiry.getTime() - Date.now();