zelari-code 0.1.2 → 0.1.3

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.
@@ -10,276 +10,71 @@ var __export = (target, all) => {
10
10
  __defProp(target, name, { get: all[name], enumerable: true });
11
11
  };
12
12
 
13
- // src/cli/oauthCallbackServer.ts
14
- import { createServer } from "node:http";
15
- function startOAuthCallbackServer(options = {}) {
16
- const host = options.host ?? "127.0.0.1";
17
- const preferredPort = options.port ?? 14523;
18
- const timeoutMs = options.timeoutMs ?? 6e4;
19
- const expectedPath = options.expectedPath ?? "/oauth/callback";
20
- const successHtml = options.successHtml ?? DEFAULT_SUCCESS_HTML;
21
- return new Promise((resolveStart, rejectStart) => {
22
- let resolved = false;
23
- let resolveCode = null;
24
- let rejectCode = null;
25
- let timeoutHandle = null;
26
- const server = createServer((req, res) => {
27
- try {
28
- if (!req.url) {
29
- res.writeHead(400, { "Content-Type": "text/plain" });
30
- res.end("Bad request");
31
- return;
32
- }
33
- const url2 = new URL(req.url, `http://${host}:${preferredPort}`);
34
- if (url2.pathname !== expectedPath) {
35
- res.writeHead(404, { "Content-Type": "text/plain" });
36
- res.end("Not found");
37
- return;
38
- }
39
- const code = url2.searchParams.get("code");
40
- const state = url2.searchParams.get("state");
41
- const errorParam = url2.searchParams.get("error");
42
- const errorDescription = url2.searchParams.get("error_description");
43
- const extras = {};
44
- for (const [key, value] of url2.searchParams.entries()) {
45
- if (key !== "code" && key !== "state" && key !== "error" && key !== "error_description") {
46
- extras[key] = value;
47
- }
48
- }
49
- const params = {
50
- ...code ? { code } : {},
51
- ...state ? { state } : {},
52
- ...errorParam ? { error: errorParam } : {},
53
- ...errorDescription ? { errorDescription } : {},
54
- extras
55
- };
56
- if (errorParam) {
57
- res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
58
- res.end(`<h1>OAuth error: ${errorParam}</h1>`);
59
- if (rejectCode && !resolved) {
60
- resolved = true;
61
- if (timeoutHandle) clearTimeout(timeoutHandle);
62
- rejectCode(new OAuthCallbackError(`OAuth provider returned error: ${errorParam}`));
63
- }
64
- return;
65
- }
66
- if (!code) {
67
- res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
68
- res.end("<h1>Missing <code>code</code> query parameter</h1>");
69
- return;
70
- }
71
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
72
- res.end(successHtml);
73
- if (resolveCode && !resolved) {
74
- resolved = true;
75
- if (timeoutHandle) clearTimeout(timeoutHandle);
76
- resolveCode(params);
77
- }
78
- setTimeout(() => {
79
- try {
80
- server.close();
81
- } catch {
82
- }
83
- }, 100);
84
- } catch (err) {
85
- try {
86
- res.writeHead(500, { "Content-Type": "text/plain" });
87
- res.end("Internal error");
88
- } catch {
89
- }
90
- if (!resolved && rejectCode) {
91
- resolved = true;
92
- if (timeoutHandle) clearTimeout(timeoutHandle);
93
- rejectCode(err instanceof Error ? err : new Error(String(err)));
94
- }
95
- }
96
- });
97
- const onError = (err) => {
98
- if (!resolved) {
99
- resolved = true;
100
- if (timeoutHandle) clearTimeout(timeoutHandle);
101
- rejectStart(err);
102
- }
103
- };
104
- server.once("error", onError);
105
- server.listen(preferredPort, host, () => {
106
- const addr = server.address();
107
- const actualPort = addr ? addr.port : preferredPort;
108
- const waitForCode = () => {
109
- return new Promise((res, rej) => {
110
- if (resolved) {
111
- rej(new OAuthCallbackClosedError());
112
- return;
113
- }
114
- resolveCode = res;
115
- rejectCode = rej;
116
- timeoutHandle = setTimeout(() => {
117
- if (!resolved) {
118
- resolved = true;
119
- try {
120
- server.close();
121
- } catch {
122
- }
123
- rej(new OAuthCallbackTimeoutError(timeoutMs));
124
- }
125
- }, timeoutMs);
126
- if (typeof timeoutHandle?.unref === "function") timeoutHandle.unref();
127
- });
128
- };
129
- const close = () => {
130
- if (timeoutHandle) clearTimeout(timeoutHandle);
131
- try {
132
- server.close();
133
- } catch {
134
- }
135
- if (!resolved && rejectCode) {
136
- resolved = true;
137
- rejectCode(new OAuthCallbackClosedError());
138
- }
139
- };
140
- resolveStart({ port: actualPort, waitForCode, close });
141
- });
142
- });
143
- }
144
- var DEFAULT_SUCCESS_HTML, OAuthCallbackTimeoutError, OAuthCallbackClosedError, OAuthCallbackError;
145
- var init_oauthCallbackServer = __esm({
146
- "src/cli/oauthCallbackServer.ts"() {
147
- "use strict";
148
- DEFAULT_SUCCESS_HTML = `
149
- <!DOCTYPE html>
150
- <html lang="en">
151
- <head>
152
- <meta charset="UTF-8">
153
- <title>zelari-code OAuth</title>
154
- </head>
155
- <body style="font-family: -apple-system, BlinkMacSystemFont, sans-serif; padding: 40px; text-align: center; color: #1a1a1a;">
156
- <h1 style="color: #00aa33;">\u2713 Authentication complete</h1>
157
- <p>You can close this tab and return to zelari-code.</p>
158
- </body>
159
- </html>
160
- `.trim();
161
- OAuthCallbackTimeoutError = class extends Error {
162
- constructor(timeoutMs) {
163
- super(`OAuth callback not received within ${timeoutMs}ms`);
164
- this.name = "OAuthCallbackTimeoutError";
165
- }
166
- };
167
- OAuthCallbackClosedError = class extends Error {
168
- constructor() {
169
- super("OAuth callback server closed before receiving a callback");
170
- this.name = "OAuthCallbackClosedError";
171
- }
172
- };
173
- OAuthCallbackError = class extends Error {
174
- constructor(message) {
175
- super(message);
176
- this.name = "OAuthCallbackError";
177
- }
178
- };
179
- }
180
- });
181
-
182
13
  // src/cli/grokOAuth.ts
183
- import nodeCrypto from "node:crypto";
184
- async function fetchGrokDiscovery(options = {}) {
14
+ async function requestDeviceCode(options) {
185
15
  const fetchImpl = options.fetchImpl ?? fetch;
186
- const discoveryUrl = options.discoveryUrl ?? `${options.issuer ?? DEFAULT_GROK_OAUTH_ISSUER}/.well-known/openid-configuration`;
187
- let response;
188
- try {
189
- response = await fetchImpl(discoveryUrl, { method: "GET", headers: { Accept: "application/json" } });
190
- } catch (err) {
191
- throw new GrokOAuthError(`OAuth discovery network error: ${err instanceof Error ? err.message : String(err)}`, "discovery_network_error");
192
- }
193
- if (!response.ok) {
194
- const text = await response.text().catch(() => "");
195
- throw new GrokOAuthError(
196
- `OAuth discovery HTTP ${response.status}: ${text.slice(0, 200)}`,
197
- `discovery_http_${response.status}`
198
- );
199
- }
200
- let body;
201
- try {
202
- body = await response.json();
203
- } catch (err) {
204
- throw new GrokOAuthError(`OAuth discovery returned invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
205
- }
206
- if (!body || typeof body !== "object") {
207
- throw new GrokOAuthError("OAuth discovery returned non-object body");
208
- }
209
- const obj = body;
210
- if (typeof obj.authorization_endpoint !== "string" || typeof obj.token_endpoint !== "string") {
211
- throw new GrokOAuthError("OAuth discovery missing authorization_endpoint or token_endpoint");
212
- }
213
- return {
214
- authorizationEndpoint: obj.authorization_endpoint,
215
- tokenEndpoint: obj.token_endpoint
216
- };
217
- }
218
- function generatePkce() {
219
- const random = getRandomValues(new Uint8Array(64));
220
- const verifier = Buffer.from(random).toString("base64url").slice(0, 128);
221
- const hash2 = createHash("sha256").update(verifier).digest();
222
- const challenge = hash2.toString("base64url").replace(/=+$/, "");
223
- return { verifier, challenge };
224
- }
225
- function buildGrokAuthorizeUrl(options) {
16
+ const endpoint = options.deviceCodeEndpoint ?? DEFAULT_DEVICE_CODE_ENDPOINT;
226
17
  const scopes = options.scopes ?? DEFAULT_GROK_OAUTH_SCOPES;
227
- const params = new URLSearchParams({
228
- client_id: options.clientId,
229
- redirect_uri: options.redirectUri,
230
- response_type: "code",
231
- scope: scopes.join(" "),
232
- state: options.state ?? crypto.randomUUID(),
233
- code_challenge: options.codeChallenge,
234
- code_challenge_method: "S256"
235
- });
236
- return `${options.authorizeEndpoint}?${params.toString()}`;
237
- }
238
- async function exchangeGrokCode(options) {
239
- const fetchImpl = options.fetchImpl ?? fetch;
240
18
  let response;
241
19
  try {
242
- response = await fetchImpl(options.tokenEndpoint, {
20
+ response = await fetchImpl(endpoint, {
243
21
  method: "POST",
244
22
  headers: {
245
23
  "Content-Type": "application/x-www-form-urlencoded",
246
24
  Accept: "application/json"
247
25
  },
248
26
  body: new URLSearchParams({
249
- grant_type: "authorization_code",
250
27
  client_id: options.clientId,
251
- code: options.code,
252
- redirect_uri: options.redirectUri,
253
- code_verifier: options.codeVerifier
28
+ scope: scopes.join(" ")
254
29
  }).toString()
255
30
  });
256
31
  } catch (err) {
257
- throw new GrokOAuthError(`Token exchange network error: ${err instanceof Error ? err.message : String(err)}`);
32
+ throw new GrokOAuthError(
33
+ `Device code request network error: ${err instanceof Error ? err.message : String(err)}`,
34
+ "device_code_network_error"
35
+ );
258
36
  }
259
37
  if (!response.ok) {
260
38
  const errText = await response.text().catch(() => "");
261
39
  throw new GrokOAuthError(
262
- `Token exchange HTTP ${response.status}: ${errText.slice(0, 200)}`,
263
- `http_${response.status}`
40
+ `Device code request HTTP ${response.status}: ${errText.slice(0, 200)}`,
41
+ `device_code_http_${response.status}`
264
42
  );
265
43
  }
266
44
  let body;
267
45
  try {
268
46
  body = await response.json();
269
47
  } catch (err) {
270
- throw new GrokOAuthError(`Token exchange returned invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
48
+ throw new GrokOAuthError(
49
+ `Device code response invalid JSON: ${err instanceof Error ? err.message : String(err)}`
50
+ );
271
51
  }
272
52
  if (!body || typeof body !== "object") {
273
- throw new GrokOAuthError("Token exchange returned non-object body");
53
+ throw new GrokOAuthError("Device code response is not an object");
274
54
  }
275
55
  const obj = body;
276
- const accessToken = obj.access_token;
277
- if (typeof accessToken !== "string" || accessToken.length === 0) {
278
- throw new GrokOAuthError("Token exchange response missing access_token", "no_access_token");
56
+ const deviceCode = obj.device_code;
57
+ const userCode = obj.user_code;
58
+ const verificationUri = obj.verification_uri;
59
+ if (typeof deviceCode !== "string" || deviceCode.length === 0) {
60
+ throw new GrokOAuthError("Device code response missing device_code", "no_device_code");
61
+ }
62
+ if (typeof userCode !== "string" || userCode.length === 0) {
63
+ throw new GrokOAuthError("Device code response missing user_code", "no_user_code");
64
+ }
65
+ if (typeof verificationUri !== "string" || verificationUri.length === 0) {
66
+ throw new GrokOAuthError("Device code response missing verification_uri", "no_verification_uri");
279
67
  }
280
- return parseTokenResponseBody(obj, accessToken, "Token exchange");
68
+ return {
69
+ deviceCode,
70
+ userCode,
71
+ verificationUri,
72
+ ...typeof obj.verification_uri_complete === "string" && obj.verification_uri_complete.length > 0 ? { verificationUriComplete: obj.verification_uri_complete } : {},
73
+ expiresIn: typeof obj.expires_in === "number" && Number.isFinite(obj.expires_in) ? obj.expires_in : 1800,
74
+ interval: typeof obj.interval === "number" && Number.isFinite(obj.interval) ? obj.interval : 5
75
+ };
281
76
  }
282
- function parseTokenResponseBody(obj, accessToken, label) {
77
+ function parseTokenResponseBody(obj, accessToken) {
283
78
  const result = { accessToken };
284
79
  if (typeof obj.expires_in === "number" && Number.isFinite(obj.expires_in)) {
285
80
  result.expiresAt = Date.now() + obj.expires_in * 1e3;
@@ -292,6 +87,116 @@ function parseTokenResponseBody(obj, accessToken, label) {
292
87
  }
293
88
  return result;
294
89
  }
90
+ async function pollForDeviceToken(options) {
91
+ const fetchImpl = options.fetchImpl ?? fetch;
92
+ const sleep = options.sleepImpl ?? defaultSleep;
93
+ const endpoint = options.tokenEndpoint ?? DEFAULT_TOKEN_ENDPOINT;
94
+ const deadline = Date.now() + options.timeoutMs;
95
+ let interval = Math.max(options.interval, 1);
96
+ while (true) {
97
+ if (Date.now() >= deadline) {
98
+ throw new GrokOAuthError("Device authorization timed out waiting for user approval", "timeout");
99
+ }
100
+ let response;
101
+ try {
102
+ response = await fetchImpl(endpoint, {
103
+ method: "POST",
104
+ headers: {
105
+ "Content-Type": "application/x-www-form-urlencoded",
106
+ Accept: "application/json"
107
+ },
108
+ body: new URLSearchParams({
109
+ grant_type: DEVICE_GRANT_TYPE,
110
+ client_id: options.clientId,
111
+ device_code: options.deviceCode
112
+ }).toString()
113
+ });
114
+ } catch (err) {
115
+ throw new GrokOAuthError(
116
+ `Token poll network error: ${err instanceof Error ? err.message : String(err)}`,
117
+ "poll_network_error"
118
+ );
119
+ }
120
+ if (response.ok) {
121
+ let body;
122
+ try {
123
+ body = await response.json();
124
+ } catch (err) {
125
+ throw new GrokOAuthError(
126
+ `Token response invalid JSON: ${err instanceof Error ? err.message : String(err)}`
127
+ );
128
+ }
129
+ if (!body || typeof body !== "object") {
130
+ throw new GrokOAuthError("Token response is not an object");
131
+ }
132
+ const obj = body;
133
+ const accessToken = obj.access_token;
134
+ if (typeof accessToken !== "string" || accessToken.length === 0) {
135
+ throw new GrokOAuthError("Token response missing access_token", "no_access_token");
136
+ }
137
+ return parseTokenResponseBody(obj, accessToken);
138
+ }
139
+ let errBody = {};
140
+ try {
141
+ const parsed = await response.json();
142
+ if (parsed && typeof parsed === "object") errBody = parsed;
143
+ } catch {
144
+ }
145
+ const errorCode = typeof errBody.error === "string" ? errBody.error : `http_${response.status}`;
146
+ switch (errorCode) {
147
+ case "authorization_pending":
148
+ await sleep(interval * 1e3);
149
+ continue;
150
+ case "slow_down":
151
+ interval += 5;
152
+ await sleep(interval * 1e3);
153
+ continue;
154
+ case "expired_token":
155
+ case "expired":
156
+ throw new GrokOAuthError("Device code expired before user authorized", "expired");
157
+ case "access_denied":
158
+ case "denied":
159
+ throw new GrokOAuthError("User denied the authorization request", "denied");
160
+ case "invalid_grant":
161
+ throw new GrokOAuthError("Device code rejected (invalid_grant)", "invalid_grant");
162
+ default:
163
+ throw new GrokOAuthError(
164
+ `Token poll error: ${errorCode}${typeof errBody.error_description === "string" ? ` \u2014 ${errBody.error_description}` : ""}`,
165
+ errorCode
166
+ );
167
+ }
168
+ }
169
+ }
170
+ async function runGrokOAuthFlow(options = {}) {
171
+ const clientId = options.clientId || process.env.GROK_OAUTH_CLIENT_ID || DEFAULT_GROK_OAUTH_CLIENT_ID;
172
+ if (!clientId || clientId.trim().length === 0) {
173
+ throw new GrokOAuthError("Missing clientId", "no_client_id");
174
+ }
175
+ const deviceAuth = await requestDeviceCode({
176
+ clientId,
177
+ scopes: options.scopes,
178
+ deviceCodeEndpoint: options.deviceCodeEndpoint,
179
+ fetchImpl: options.fetchImpl
180
+ });
181
+ if (options.onUserCode) {
182
+ await options.onUserCode(deviceAuth);
183
+ }
184
+ try {
185
+ await openBrowser(deviceAuth.verificationUriComplete ?? deviceAuth.verificationUri);
186
+ } catch {
187
+ }
188
+ const timeoutMs = options.timeoutMs ?? DEFAULT_OAUTH_TIMEOUT_MS;
189
+ const effectiveTimeout = Math.min(timeoutMs, deviceAuth.expiresIn * 1e3);
190
+ return pollForDeviceToken({
191
+ clientId,
192
+ deviceCode: deviceAuth.deviceCode,
193
+ interval: deviceAuth.interval,
194
+ timeoutMs: effectiveTimeout,
195
+ tokenEndpoint: options.tokenEndpoint,
196
+ fetchImpl: options.fetchImpl,
197
+ sleepImpl: options.sleepImpl
198
+ });
199
+ }
295
200
  async function refreshGrokToken(options) {
296
201
  if (!options.clientId || options.clientId.trim().length === 0) {
297
202
  throw new GrokOAuthError("Missing clientId", "no_client_id");
@@ -299,7 +204,7 @@ async function refreshGrokToken(options) {
299
204
  if (!options.refreshToken || options.refreshToken.trim().length === 0) {
300
205
  throw new GrokOAuthError("Missing refreshToken", "no_refresh_token");
301
206
  }
302
- const endpoint = options.tokenEndpoint ?? "https://oauth.x.ai/token";
207
+ const endpoint = options.tokenEndpoint ?? DEFAULT_TOKEN_ENDPOINT;
303
208
  const fetchImpl = options.fetchImpl ?? fetch;
304
209
  let response;
305
210
  try {
@@ -340,77 +245,7 @@ async function refreshGrokToken(options) {
340
245
  if (typeof accessToken !== "string" || accessToken.length === 0) {
341
246
  throw new GrokOAuthError("Token refresh response missing access_token", "no_access_token");
342
247
  }
343
- return parseTokenResponseBody(obj, accessToken, "Token refresh");
344
- }
345
- async function runGrokOAuthFlow(options) {
346
- const clientId = options.clientId || process.env.GROK_OAUTH_CLIENT_ID || DEFAULT_GROK_OAUTH_CLIENT_ID;
347
- if (!clientId || clientId.trim().length === 0) {
348
- throw new GrokOAuthError("Missing clientId", "no_client_id");
349
- }
350
- const callbackPort = options.callbackPort ?? DEFAULT_GROK_OAUTH_REDIRECT_PORT;
351
- const callbackPath = options.callbackPath ?? DEFAULT_GROK_OAUTH_REDIRECT_PATH;
352
- const redirectUri = `http://127.0.0.1:${callbackPort}${callbackPath}`;
353
- const discovery = await fetchGrokDiscovery({
354
- issuer: options.issuer,
355
- discoveryUrl: options.discoveryUrl,
356
- fetchImpl: options.fetchImpl
357
- });
358
- const { verifier: codeVerifier, challenge: codeChallenge } = generatePkce();
359
- const state = crypto.randomUUID();
360
- const authorizeUrl = buildGrokAuthorizeUrl({
361
- clientId,
362
- redirectUri,
363
- scopes: options.scopes,
364
- state,
365
- codeChallenge,
366
- authorizeEndpoint: discovery.authorizationEndpoint
367
- });
368
- let handle;
369
- try {
370
- handle = await startOAuthCallbackServer({
371
- port: callbackPort,
372
- timeoutMs: options.callbackTimeoutMs ?? 12e4,
373
- expectedPath: callbackPath
374
- });
375
- } catch (err) {
376
- throw new GrokOAuthError(
377
- `Failed to start OAuth callback server on port ${callbackPort}: ${err instanceof Error ? err.message : String(err)}`,
378
- "callback_bind_failed"
379
- );
380
- }
381
- try {
382
- if (options.onBrowserOpen) {
383
- await options.onBrowserOpen(authorizeUrl);
384
- } else {
385
- await openBrowser(authorizeUrl);
386
- }
387
- const callbackResult = await handle.waitForCode();
388
- if (callbackResult.state !== state) {
389
- throw new GrokOAuthError(
390
- `OAuth state mismatch \u2014 possible CSRF. expected=${state} got=${callbackResult.state ?? "(none)"}`,
391
- "state_mismatch"
392
- );
393
- }
394
- if (callbackResult.error) {
395
- throw new GrokOAuthError(
396
- `OAuth provider returned error: ${callbackResult.error}${callbackResult.errorDescription ? ` \u2014 ${callbackResult.errorDescription}` : ""}`,
397
- callbackResult.error
398
- );
399
- }
400
- if (!callbackResult.code) {
401
- throw new GrokOAuthError("OAuth callback did not include authorization code", "no_code");
402
- }
403
- return await exchangeGrokCode({
404
- clientId,
405
- code: callbackResult.code,
406
- codeVerifier,
407
- redirectUri,
408
- tokenEndpoint: discovery.tokenEndpoint,
409
- fetchImpl: options.fetchImpl
410
- });
411
- } finally {
412
- handle.close();
413
- }
248
+ return parseTokenResponseBody(obj, accessToken);
414
249
  }
415
250
  async function openBrowser(url2) {
416
251
  const { spawn: spawn3 } = await import("node:child_process");
@@ -438,12 +273,10 @@ async function openBrowser(url2) {
438
273
  }
439
274
  });
440
275
  }
441
- var getRandomValues, createHash, GrokOAuthError, DEFAULT_GROK_OAUTH_CLIENT_ID, DEFAULT_GROK_OAUTH_ISSUER, DEFAULT_GROK_OAUTH_SCOPES, DEFAULT_GROK_OAUTH_REDIRECT_PORT, DEFAULT_GROK_OAUTH_REDIRECT_PATH;
276
+ var GrokOAuthError, DEFAULT_GROK_OAUTH_CLIENT_ID, DEFAULT_GROK_OAUTH_SCOPES, DEFAULT_DEVICE_CODE_ENDPOINT, DEFAULT_TOKEN_ENDPOINT, DEVICE_GRANT_TYPE, DEFAULT_OAUTH_TIMEOUT_MS, defaultSleep;
442
277
  var init_grokOAuth = __esm({
443
278
  "src/cli/grokOAuth.ts"() {
444
279
  "use strict";
445
- init_oauthCallbackServer();
446
- ({ getRandomValues, createHash } = nodeCrypto);
447
280
  GrokOAuthError = class extends Error {
448
281
  constructor(message, code) {
449
282
  super(message);
@@ -452,7 +285,6 @@ var init_grokOAuth = __esm({
452
285
  }
453
286
  };
454
287
  DEFAULT_GROK_OAUTH_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
455
- DEFAULT_GROK_OAUTH_ISSUER = "https://auth.x.ai";
456
288
  DEFAULT_GROK_OAUTH_SCOPES = [
457
289
  "openid",
458
290
  "profile",
@@ -461,8 +293,11 @@ var init_grokOAuth = __esm({
461
293
  "grok-cli:access",
462
294
  "api:access"
463
295
  ];
464
- DEFAULT_GROK_OAUTH_REDIRECT_PORT = 56121;
465
- DEFAULT_GROK_OAUTH_REDIRECT_PATH = "/callback";
296
+ DEFAULT_DEVICE_CODE_ENDPOINT = "https://auth.x.ai/oauth2/device/code";
297
+ DEFAULT_TOKEN_ENDPOINT = "https://auth.x.ai/oauth2/token";
298
+ DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
299
+ DEFAULT_OAUTH_TIMEOUT_MS = 3e5;
300
+ defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
466
301
  }
467
302
  });
468
303
 
@@ -21219,14 +21054,27 @@ ${lines.join("\n")}`,
21219
21054
  {
21220
21055
  id: crypto.randomUUID(),
21221
21056
  role: "system",
21222
- content: "[login oauth] opening browser for SuperGrok authentication (PKCE)...",
21057
+ content: "[login oauth] requesting device code from xAI...",
21223
21058
  ts: Date.now()
21224
21059
  }
21225
21060
  ]);
21226
21061
  setBusy(true);
21227
21062
  try {
21228
21063
  const resultOAuth = await runGrokOAuthFlow({
21229
- callbackTimeoutMs: 12e4
21064
+ // Show the user_code + verification_uri as soon as xAI returns them.
21065
+ onUserCode: (info) => {
21066
+ setMessages((prev) => [
21067
+ ...prev,
21068
+ {
21069
+ id: crypto.randomUUID(),
21070
+ role: "system",
21071
+ content: `[login oauth] Open ${info.verificationUri} in your browser and enter the code:
21072
+ ${info.userCode}
21073
+ (Opening your browser automatically...)`,
21074
+ ts: Date.now()
21075
+ }
21076
+ ]);
21077
+ }
21230
21078
  });
21231
21079
  setOAuthToken("grok", {
21232
21080
  apiKey: resultOAuth.accessToken,