@oh-my-pi/pi-ai 16.3.13 → 16.3.15

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.
@@ -1,31 +1,22 @@
1
- // Ported from NousResearch/hermes-agent (MIT) — hermes_cli/auth.py xAI sections (L93-111, L2979-3160, L5286-5469).
1
+ // Device authorization and token refresh adapted from NousResearch/hermes-agent (MIT).
2
2
 
3
3
  /**
4
- * xAI Grok (SuperGrok or X Premium+) OAuth flow.
4
+ * xAI Grok OAuth device authorization flow.
5
5
  *
6
- * Manual-code PKCE flow using `127.0.0.1:56121/callback` as the allowlisted
7
- * redirect URI. One token unlocks Grok-4.x
8
- * chat, Grok Imagine image generation, and Grok Voice TTS via subsequent
9
- * commits. Endpoint discovery is hardened against MITM via
10
- * {@link validateXAIEndpoint}: any non-HTTPS or non-`x.ai`/`*.x.ai` host is
11
- * rejected on every call site, not just the first.
6
+ * Requests an RFC 8628 device code, opens xAI's verification page, and polls
7
+ * the discovered token endpoint until the user approves the login.
12
8
  */
13
9
 
14
10
  import * as AIError from "../../error";
15
11
  import type { FetchImpl } from "../../types";
16
- import { OAuthCallbackFlow, type OAuthCallbackFlowOptions } from "./callback-server";
17
- import { generatePKCE } from "./pkce";
12
+ import { type OAuthDeviceCodePollResult, pollOAuthDeviceCodeFlow } from "./device-code";
18
13
  import type { OAuthController, OAuthCredentials } from "./types";
19
14
 
20
- // Hermes hermes_cli/auth.py L93-111
21
15
  const XAI_OAUTH_ISSUER = "https://auth.x.ai";
22
16
  const XAI_OAUTH_DISCOVERY_URL = `${XAI_OAUTH_ISSUER}/.well-known/openid-configuration`;
17
+ const XAI_OAUTH_DEVICE_CODE_URL = `${XAI_OAUTH_ISSUER}/oauth2/device/code`;
23
18
  const XAI_OAUTH_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
24
19
  const XAI_OAUTH_SCOPE = "openid profile email offline_access grok-cli:access api:access";
25
- const XAI_OAUTH_REDIRECT_HOST = "127.0.0.1";
26
- const XAI_OAUTH_REDIRECT_PORT = 56121;
27
- const XAI_OAUTH_REDIRECT_PATH = "/callback";
28
- const XAI_OAUTH_DOCS_URL = "https://hermes-agent.nousresearch.com/docs/guides/xai-grok-oauth";
29
20
 
30
21
  // Mirrors the 5-min skew used by anthropic.ts:160 — keeps every provider on the
31
22
  // same conservative client-side expiry window.
@@ -35,18 +26,27 @@ const DISCOVERY_TIMEOUT_MS = 15_000;
35
26
  const TOKEN_REQUEST_TIMEOUT_MS = 20_000;
36
27
 
37
28
  interface XAIOAuthDiscovery {
38
- authorization_endpoint: string;
39
29
  token_endpoint: string;
40
30
  }
41
31
 
32
+ interface XAIDeviceAuthorization {
33
+ deviceCode: string;
34
+ userCode: string;
35
+ verificationUriComplete: string;
36
+ expiresInSeconds: number;
37
+ intervalSeconds: number;
38
+ }
39
+
40
+ function isRecord(value: unknown): value is Record<string, unknown> {
41
+ return typeof value === "object" && value !== null;
42
+ }
43
+
42
44
  /**
43
- * Validate an xAI OIDC discovery endpoint against scheme + host.
45
+ * Validate an xAI OIDC endpoint against its scheme and host.
44
46
  *
45
- * Hermes `_xai_validate_oauth_endpoint` L2997-3035. The discovery response is
46
- * long-lived and cached in {@link OAuthCredentials}; a single MITM during
47
- * initial login could substitute a malicious `token_endpoint` that would then
48
- * receive every future refresh_token. Rejecting non-HTTPS or non-`x.ai` /
49
- * `*.x.ai` hosts pins the cached endpoint to the xAI auth origin.
47
+ * The discovery response is long-lived and its token endpoint receives every
48
+ * future refresh token. Rejecting non-HTTPS or non-`x.ai` / `*.x.ai` hosts
49
+ * pins that endpoint to the xAI auth origin.
50
50
  *
51
51
  * @throws Error with message `Invalid xAI <field>: <url>` when the URL fails
52
52
  * either scheme or host validation.
@@ -68,11 +68,7 @@ export function validateXAIEndpoint(url: string, field: string): string {
68
68
  return url;
69
69
  }
70
70
 
71
- /**
72
- * Fetch xAI's OIDC discovery document and validate both endpoints.
73
- *
74
- * Hermes `_xai_oauth_discovery` L3038-3084.
75
- */
71
+ /** Fetch xAI's OIDC discovery document and validate the token endpoint. */
76
72
  async function xaiOAuthDiscovery(
77
73
  timeoutMs: number = DISCOVERY_TIMEOUT_MS,
78
74
  fetchOverride?: FetchImpl,
@@ -111,37 +107,29 @@ async function xaiOAuthDiscovery(
111
107
  { kind: "validation", provider: "xai", cause: error },
112
108
  );
113
109
  }
114
- if (!payload || typeof payload !== "object") {
110
+ if (!isRecord(payload)) {
115
111
  throw new AIError.OAuthError("xAI OIDC discovery response was not a JSON object.", {
116
112
  kind: "validation",
117
113
  provider: "xai",
118
114
  });
119
115
  }
120
- const obj = payload as Record<string, unknown>;
121
- const authorizationEndpoint =
122
- typeof obj.authorization_endpoint === "string" ? obj.authorization_endpoint.trim() : "";
123
- const tokenEndpoint = typeof obj.token_endpoint === "string" ? obj.token_endpoint.trim() : "";
124
- if (!authorizationEndpoint || !tokenEndpoint) {
125
- throw new AIError.OAuthError("xAI OIDC discovery response was missing required endpoints.", {
116
+ const tokenEndpoint = typeof payload.token_endpoint === "string" ? payload.token_endpoint.trim() : "";
117
+ if (!tokenEndpoint) {
118
+ throw new AIError.OAuthError("xAI OIDC discovery response was missing token_endpoint.", {
126
119
  kind: "validation",
127
120
  provider: "xai",
128
121
  });
129
122
  }
130
- validateXAIEndpoint(authorizationEndpoint, "authorization_endpoint");
131
123
  validateXAIEndpoint(tokenEndpoint, "token_endpoint");
132
- return {
133
- authorization_endpoint: authorizationEndpoint,
134
- token_endpoint: tokenEndpoint,
135
- };
124
+ return { token_endpoint: tokenEndpoint };
136
125
  }
137
126
 
138
127
  /**
139
128
  * Check whether a JWT access token is at or past its `exp` claim (with an
140
129
  * optional refresh-skew margin).
141
130
  *
142
- * Hermes `_xai_access_token_is_expiring` L2979-2994. Returns `false` for any
143
- * malformed input — this is a refresh-trigger check, not a validation, so
144
- * non-JWTs ("no token in cache") must NOT trigger a spurious refresh.
131
+ * Returns `false` for malformed input because this is a refresh-trigger check,
132
+ * not token validation.
145
133
  */
146
134
  export function isXAIAccessTokenExpiring(jwt: string, skewSeconds: number = 0): boolean {
147
135
  try {
@@ -151,7 +139,8 @@ export function isXAIAccessTokenExpiring(jwt: string, skewSeconds: number = 0):
151
139
  const payloadPart = parts[1];
152
140
  if (!payloadPart) return false;
153
141
  const decoded = Buffer.from(payloadPart, "base64url").toString("utf8");
154
- const payload = JSON.parse(decoded) as { exp?: unknown };
142
+ const payload: unknown = JSON.parse(decoded);
143
+ if (!isRecord(payload)) return false;
155
144
  const exp = payload.exp;
156
145
  if (typeof exp !== "number" || !Number.isFinite(exp)) return false;
157
146
  const now = Math.floor(Date.now() / 1000);
@@ -162,161 +151,232 @@ export function isXAIAccessTokenExpiring(jwt: string, skewSeconds: number = 0):
162
151
  }
163
152
  }
164
153
 
165
- interface BuildXAIAuthorizeUrlOptions {
166
- authorizationEndpoint: string;
167
- redirectUri: string;
168
- codeChallenge: string;
169
- state: string;
170
- nonce: string;
171
- }
172
-
173
- /**
174
- * Build the xAI authorization URL.
175
- *
176
- * Hermes `_xai_oauth_build_authorize_url` L5286-5312. `plan=generic` opts the
177
- * consent screen into xAI's generic OAuth plan tier; without it,
178
- * `accounts.x.ai` rejects loopback OAuth from non-allowlisted clients.
179
- * `referrer=oh-my-pi` lets xAI attribute oh-my-pi-originated logins in their
180
- * OAuth server logs (Hermes uses `referrer=hermes-agent`; oh-my-pi mirrors the
181
- * pattern with its own attribution string).
182
- */
183
- function buildXAIAuthorizeUrl(opts: BuildXAIAuthorizeUrlOptions): string {
184
- const params = new URLSearchParams({
185
- response_type: "code",
186
- client_id: XAI_OAUTH_CLIENT_ID,
187
- redirect_uri: opts.redirectUri,
188
- scope: XAI_OAUTH_SCOPE,
189
- code_challenge: opts.codeChallenge,
190
- code_challenge_method: "S256",
191
- state: opts.state,
192
- nonce: opts.nonce,
193
- plan: "generic",
194
- referrer: "oh-my-pi",
195
- });
196
- return `${opts.authorizationEndpoint}?${params.toString()}`;
197
- }
198
-
199
- /**
200
- * xAI Grok OAuth code flow (Hermes `_xai_oauth_loopback_login` L5315-5469).
201
- */
202
- export class XAIOAuthFlow extends OAuthCallbackFlow {
203
- #verifier: string = "";
204
- #fetch: FetchImpl;
205
-
206
- constructor(ctrl: OAuthController) {
207
- super(ctrl, {
208
- preferredPort: XAI_OAUTH_REDIRECT_PORT,
209
- callbackPath: XAI_OAUTH_REDIRECT_PATH,
210
- callbackHostname: XAI_OAUTH_REDIRECT_HOST,
211
- redirectUri: `http://${XAI_OAUTH_REDIRECT_HOST}:${XAI_OAUTH_REDIRECT_PORT}${XAI_OAUTH_REDIRECT_PATH}`,
212
- manualInputOnly: true,
213
- } satisfies OAuthCallbackFlowOptions);
214
- this.#fetch = ctrl.fetch ?? fetch;
154
+ function parseXAIDeviceAuthorization(payload: unknown): XAIDeviceAuthorization {
155
+ if (!isRecord(payload)) {
156
+ throw new AIError.OAuthError("xAI device-code response was not a JSON object.", {
157
+ kind: "validation",
158
+ provider: "xai",
159
+ });
215
160
  }
216
161
 
217
- async generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }> {
218
- const pkce = await generatePKCE();
219
- this.#verifier = pkce.verifier;
220
- const nonce = crypto.randomUUID().replace(/-/g, "");
221
-
222
- const discovery = await xaiOAuthDiscovery(DISCOVERY_TIMEOUT_MS, this.#fetch);
223
- const url = buildXAIAuthorizeUrl({
224
- authorizationEndpoint: discovery.authorization_endpoint,
225
- redirectUri,
226
- codeChallenge: pkce.challenge,
227
- state,
228
- nonce,
162
+ const deviceCode = typeof payload.device_code === "string" ? payload.device_code.trim() : "";
163
+ const userCode = typeof payload.user_code === "string" ? payload.user_code.trim() : "";
164
+ const verificationUri = typeof payload.verification_uri === "string" ? payload.verification_uri.trim() : "";
165
+ const verificationUriComplete =
166
+ typeof payload.verification_uri_complete === "string" ? payload.verification_uri_complete.trim() : "";
167
+ const expiresInSeconds = payload.expires_in;
168
+ const intervalSeconds = payload.interval;
169
+ if (
170
+ !deviceCode ||
171
+ !userCode ||
172
+ !verificationUri ||
173
+ !verificationUriComplete ||
174
+ typeof expiresInSeconds !== "number" ||
175
+ !Number.isFinite(expiresInSeconds) ||
176
+ expiresInSeconds <= 0 ||
177
+ typeof intervalSeconds !== "number" ||
178
+ !Number.isFinite(intervalSeconds) ||
179
+ intervalSeconds <= 0
180
+ ) {
181
+ throw new AIError.OAuthError("xAI device-code response missing or invalid required fields.", {
182
+ kind: "validation",
183
+ provider: "xai",
229
184
  });
230
-
231
- return {
232
- url,
233
- instructions: `Complete login in your browser for xAI Grok (SuperGrok or X Premium+). Docs: ${XAI_OAUTH_DOCS_URL}`,
234
- };
235
185
  }
236
186
 
237
- async exchangeToken(code: string, _state: string, redirectUri: string): Promise<OAuthCredentials> {
238
- const discovery = await xaiOAuthDiscovery(DISCOVERY_TIMEOUT_MS, this.#fetch);
239
- const tokenEndpoint = validateXAIEndpoint(discovery.token_endpoint, "token_endpoint");
187
+ validateXAIEndpoint(verificationUri, "verification_uri");
188
+ validateXAIEndpoint(verificationUriComplete, "verification_uri_complete");
189
+ return {
190
+ deviceCode,
191
+ userCode,
192
+ verificationUriComplete,
193
+ expiresInSeconds,
194
+ intervalSeconds,
195
+ };
196
+ }
240
197
 
241
- const body = new URLSearchParams({
242
- grant_type: "authorization_code",
243
- client_id: XAI_OAUTH_CLIENT_ID,
244
- code,
245
- redirect_uri: redirectUri,
246
- code_verifier: this.#verifier,
198
+ function parseXAITokenResponse(payload: unknown, label: string, refreshTokenFallback?: string): OAuthCredentials {
199
+ if (!isRecord(payload)) {
200
+ throw new AIError.OAuthError(`${label} was not a JSON object`, {
201
+ kind: "validation",
202
+ provider: "xai",
247
203
  });
204
+ }
205
+ const accessToken = typeof payload.access_token === "string" ? payload.access_token : "";
206
+ const responseRefreshToken = typeof payload.refresh_token === "string" ? payload.refresh_token : "";
207
+ const refreshToken = responseRefreshToken || refreshTokenFallback || "";
208
+ const expiresInSeconds = payload.expires_in;
209
+ if (!accessToken) {
210
+ throw new AIError.OAuthError(`${label} missing access_token`, {
211
+ kind: "validation",
212
+ provider: "xai",
213
+ });
214
+ }
215
+ if (!refreshToken) {
216
+ throw new AIError.OAuthError(`${label} missing refresh_token`, {
217
+ kind: "validation",
218
+ provider: "xai",
219
+ });
220
+ }
221
+ if (typeof expiresInSeconds !== "number" || !Number.isFinite(expiresInSeconds)) {
222
+ throw new AIError.OAuthError(`${label} missing expires_in`, {
223
+ kind: "validation",
224
+ provider: "xai",
225
+ });
226
+ }
227
+ return {
228
+ access: accessToken,
229
+ refresh: refreshToken,
230
+ expires: Date.now() + expiresInSeconds * 1000 - ACCESS_TOKEN_CLIENT_SKEW_MS,
231
+ };
232
+ }
248
233
 
249
- const response = await this.#fetch(tokenEndpoint, {
234
+ async function requestXAIDeviceAuthorization(
235
+ fetchImpl: FetchImpl,
236
+ signal?: AbortSignal,
237
+ ): Promise<XAIDeviceAuthorization> {
238
+ let response: Response;
239
+ try {
240
+ const timeoutSignal = AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS);
241
+ response = await fetchImpl(XAI_OAUTH_DEVICE_CODE_URL, {
250
242
  method: "POST",
251
243
  headers: {
252
244
  "Content-Type": "application/x-www-form-urlencoded",
253
245
  Accept: "application/json",
254
246
  },
255
- body,
256
- signal: AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS),
247
+ body: new URLSearchParams({
248
+ client_id: XAI_OAUTH_CLIENT_ID,
249
+ scope: XAI_OAUTH_SCOPE,
250
+ }),
251
+ signal: signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal,
257
252
  });
253
+ } catch (error) {
254
+ if (signal?.aborted) throw new AIError.LoginCancelledError();
255
+ throw new AIError.OAuthError(
256
+ `xAI device-code request failed: ${error instanceof Error ? error.message : String(error)}`,
257
+ { kind: "device-auth", provider: "xai", cause: error },
258
+ );
259
+ }
258
260
 
259
- if (!response.ok) {
260
- let detail = "";
261
- try {
262
- detail = (await response.text()).trim();
263
- } catch {
264
- // Ignore body-read failures; the status code is the diagnostic.
265
- }
266
- throw new AIError.OAuthError(`xAI token exchange failed: ${response.status}${detail ? ` ${detail}` : ""}`, {
267
- kind: "token-exchange",
268
- provider: "xai",
269
- status: response.status,
270
- });
271
- }
272
-
273
- let tokenData: { access_token?: unknown; refresh_token?: unknown; expires_in?: unknown };
261
+ if (!response.ok) {
262
+ let detail = "";
274
263
  try {
275
- tokenData = (await response.json()) as typeof tokenData;
276
- } catch (error) {
277
- throw new AIError.OAuthError(
278
- `xAI token exchange returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
279
- { kind: "validation", provider: "xai", cause: error },
280
- );
264
+ detail = (await response.text()).trim();
265
+ } catch {
266
+ // Ignore body-read failures; the status code is the diagnostic.
281
267
  }
268
+ throw new AIError.OAuthError(`xAI device-code request failed: ${response.status}${detail ? ` ${detail}` : ""}`, {
269
+ kind: "device-auth",
270
+ provider: "xai",
271
+ status: response.status,
272
+ });
273
+ }
282
274
 
283
- if (typeof tokenData.access_token !== "string" || !tokenData.access_token) {
284
- throw new AIError.OAuthError("xAI token exchange response missing access_token", {
285
- kind: "validation",
286
- provider: "xai",
287
- });
288
- }
289
- if (typeof tokenData.refresh_token !== "string" || !tokenData.refresh_token) {
290
- throw new AIError.OAuthError("xAI token exchange response missing refresh_token", {
291
- kind: "validation",
292
- provider: "xai",
293
- });
294
- }
295
- if (typeof tokenData.expires_in !== "number" || !Number.isFinite(tokenData.expires_in)) {
296
- throw new AIError.OAuthError("xAI token exchange response missing expires_in", {
297
- kind: "validation",
298
- provider: "xai",
299
- });
300
- }
275
+ let payload: unknown;
276
+ try {
277
+ payload = await response.json();
278
+ } catch (error) {
279
+ throw new AIError.OAuthError(
280
+ `xAI device-code response returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
281
+ { kind: "validation", provider: "xai", cause: error },
282
+ );
283
+ }
284
+ return parseXAIDeviceAuthorization(payload);
285
+ }
286
+
287
+ async function pollXAIDeviceToken(
288
+ tokenEndpoint: string,
289
+ deviceCode: string,
290
+ fetchImpl: FetchImpl,
291
+ signal?: AbortSignal,
292
+ ): Promise<OAuthDeviceCodePollResult<OAuthCredentials>> {
293
+ let response: Response;
294
+ try {
295
+ const timeoutSignal = AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS);
296
+ response = await fetchImpl(tokenEndpoint, {
297
+ method: "POST",
298
+ headers: {
299
+ "Content-Type": "application/x-www-form-urlencoded",
300
+ Accept: "application/json",
301
+ },
302
+ body: new URLSearchParams({
303
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
304
+ client_id: XAI_OAUTH_CLIENT_ID,
305
+ device_code: deviceCode,
306
+ }),
307
+ signal: signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal,
308
+ });
309
+ } catch (error) {
310
+ if (signal?.aborted) throw new AIError.LoginCancelledError();
311
+ throw new AIError.OAuthError(
312
+ `xAI device-code token polling failed: ${error instanceof Error ? error.message : String(error)}`,
313
+ { kind: "polling", provider: "xai", cause: error },
314
+ );
315
+ }
301
316
 
317
+ let payload: unknown;
318
+ try {
319
+ payload = await response.json();
320
+ } catch (error) {
321
+ throw new AIError.OAuthError(
322
+ `xAI device-code token polling returned invalid JSON: ${
323
+ error instanceof Error ? error.message : String(error)
324
+ }`,
325
+ { kind: "polling", provider: "xai", status: response.status, cause: error },
326
+ );
327
+ }
328
+
329
+ if (response.ok) {
302
330
  return {
303
- access: tokenData.access_token,
304
- refresh: tokenData.refresh_token,
305
- expires: Date.now() + tokenData.expires_in * 1000 - ACCESS_TOKEN_CLIENT_SKEW_MS,
331
+ status: "complete",
332
+ value: parseXAITokenResponse(payload, "xAI device-code token response"),
306
333
  };
307
334
  }
335
+ if (!isRecord(payload)) {
336
+ throw new AIError.OAuthError(`xAI device-code token polling failed: ${response.status}`, {
337
+ kind: "polling",
338
+ provider: "xai",
339
+ status: response.status,
340
+ });
341
+ }
342
+
343
+ const errorCode = typeof payload.error === "string" ? payload.error : "";
344
+ if (errorCode === "authorization_pending") return { status: "pending" };
345
+ if (errorCode === "slow_down") return { status: "slow_down" };
346
+
347
+ const errorDescription = typeof payload.error_description === "string" ? payload.error_description : "";
348
+ const detail = errorDescription || errorCode || String(response.status);
349
+ throw new AIError.OAuthError(`xAI device-code token polling failed: ${detail}`, {
350
+ kind: "polling",
351
+ provider: "xai",
352
+ status: response.status,
353
+ });
308
354
  }
309
355
 
356
+ /** Log in to xAI Grok with the RFC 8628 device authorization grant. */
310
357
  export async function loginXAIOAuth(ctrl: OAuthController): Promise<OAuthCredentials> {
311
- return new XAIOAuthFlow(ctrl).login();
358
+ const fetchImpl = ctrl.fetch ?? fetch;
359
+ const discovery = await xaiOAuthDiscovery(DISCOVERY_TIMEOUT_MS, fetchImpl);
360
+ const device = await requestXAIDeviceAuthorization(fetchImpl, ctrl.signal);
361
+ ctrl.onAuth?.({
362
+ url: device.verificationUriComplete,
363
+ instructions: `Enter code: ${device.userCode}`,
364
+ });
365
+ ctrl.onProgress?.("Waiting for xAI device authorization...");
366
+
367
+ return pollOAuthDeviceCodeFlow({
368
+ poll: () => pollXAIDeviceToken(discovery.token_endpoint, device.deviceCode, fetchImpl, ctrl.signal),
369
+ intervalSeconds: device.intervalSeconds,
370
+ expiresInSeconds: device.expiresInSeconds,
371
+ signal: ctrl.signal,
372
+ });
312
373
  }
313
374
 
314
375
  /**
315
376
  * Refresh an xAI OAuth access token using a stored refresh_token.
316
377
  *
317
- * Hermes `refresh_xai_oauth_pure` L3087-3160. Re-runs OIDC discovery and
318
- * re-validates the cached `token_endpoint` on the refresh hot path so a
319
- * cached-but-poisoned endpoint cannot silently leak a refresh_token.
378
+ * Re-runs OIDC discovery and re-validates the token endpoint before sending
379
+ * the stored refresh token.
320
380
  */
321
381
  export async function refreshXAIOAuthToken(refreshToken: string, fetchOverride?: FetchImpl): Promise<OAuthCredentials> {
322
382
  const fetchImpl = fetchOverride ?? fetch;
@@ -357,34 +417,14 @@ export async function refreshXAIOAuthToken(refreshToken: string, fetchOverride?:
357
417
  });
358
418
  }
359
419
 
360
- let data: { access_token?: unknown; refresh_token?: unknown; expires_in?: unknown };
420
+ let payload: unknown;
361
421
  try {
362
- data = (await response.json()) as typeof data;
422
+ payload = await response.json();
363
423
  } catch (error) {
364
424
  throw new AIError.OAuthError(
365
425
  `xAI token refresh returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
366
426
  { kind: "validation", provider: "xai", cause: error },
367
427
  );
368
428
  }
369
-
370
- if (typeof data.access_token !== "string" || !data.access_token) {
371
- throw new AIError.OAuthError("xAI token refresh response missing access_token", {
372
- kind: "validation",
373
- provider: "xai",
374
- });
375
- }
376
- if (typeof data.expires_in !== "number" || !Number.isFinite(data.expires_in)) {
377
- throw new AIError.OAuthError("xAI token refresh response missing expires_in", {
378
- kind: "validation",
379
- provider: "xai",
380
- });
381
- }
382
-
383
- const newRefresh = typeof data.refresh_token === "string" && data.refresh_token ? data.refresh_token : refreshToken;
384
-
385
- return {
386
- access: data.access_token,
387
- refresh: newRefresh,
388
- expires: Date.now() + data.expires_in * 1000 - ACCESS_TOKEN_CLIENT_SKEW_MS,
389
- };
429
+ return parseXAITokenResponse(payload, "xAI token refresh response", refreshToken);
390
430
  }
@@ -14,5 +14,4 @@ export const xaiOauthProvider = {
14
14
  const { refreshXAIOAuthToken } = await import("./oauth/xai-oauth");
15
15
  return refreshXAIOAuthToken(credentials.refresh);
16
16
  },
17
- pasteCodeFlow: true,
18
17
  } as const satisfies ProviderDefinition;
package/src/types.ts CHANGED
@@ -388,9 +388,9 @@ export interface StreamOptions {
388
388
  */
389
389
  sessionId?: string;
390
390
  /**
391
- * Optional prompt-cache identity. When set, OpenAI Responses-compatible
392
- * providers use this for `prompt_cache_key` while keeping `sessionId` for
393
- * provider routing / conversation headers.
391
+ * Optional prompt-cache identity. OpenAI-family providers use this for
392
+ * `prompt_cache_key` payloads and cache-affinity headers such as
393
+ * `x-grok-conv-id`; when omitted, they fall back to `sessionId`.
394
394
  */
395
395
  promptCacheKey?: string;
396
396
  /**