@demicodes/provider-grok-build 0.17.1 → 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,7 +22,7 @@ See [docs/provider-global-credentials.md](../../docs/provider-global-credentials
22
22
 
23
23
  ## Quota
24
24
 
25
- - **probe** (cost: `free`): `/v1/billing` + `/v1/user?include=subscription` on
25
+ - **probe** (cost: `free`): `/v1/billing?format=credits` + `/v1/user?include=subscription` on
26
26
  cli-chat-proxy.
27
27
  - **observe**: short-window ratelimit headers on chat responses (separate from monthly).
28
28
 
package/dist/index.d.mts CHANGED
@@ -11,8 +11,12 @@ interface GrokAuthEntry {
11
11
  oidc_client_id?: string;
12
12
  email?: string;
13
13
  first_name?: string;
14
+ last_name?: string;
14
15
  user_id?: string;
15
16
  team_id?: string;
17
+ principal_type?: string;
18
+ principal_id?: string;
19
+ organization_id?: string;
16
20
  [key: string]: unknown;
17
21
  }
18
22
  interface GrokResolvedAuth {
@@ -20,6 +24,9 @@ interface GrokResolvedAuth {
20
24
  refreshToken: string | null;
21
25
  expiresAt: Date | null;
22
26
  email: string | null;
27
+ userId: string | null;
28
+ principalType: string | null;
29
+ principalId: string | null;
23
30
  issuer: string | null;
24
31
  clientId: string | null;
25
32
  entryKey: string;
@@ -55,6 +62,8 @@ type GrokTokenRefresh = (input: {
55
62
  refreshToken: string;
56
63
  clientId: string;
57
64
  tokenEndpoint: string;
65
+ principalType?: string | null;
66
+ principalId?: string | null;
58
67
  }, signal?: AbortSignal) => Promise<GrokRefreshTokenResponse>;
59
68
  declare function grokBuildAuthStatus(options?: FileGrokAuthStoreOptions): Promise<ProviderAuthState>;
60
69
  //#endregion
@@ -101,7 +110,7 @@ interface GrokBuildQuotaOptions {
101
110
  /**
102
111
  * Active probe against cli-chat-proxy:
103
112
  * - GET /v1/user?include=subscription → tier
104
- * - GET /v1/billing → monthly used/limit
113
+ * - GET /v1/billing?format=creditscreditUsagePercent / currentPeriod
105
114
  *
106
115
  * Optional observation of short-window x-ratelimit-* headers from chat responses
107
116
  * (separate windows from monthly subscription quota).
@@ -137,6 +146,7 @@ declare function createGrokBuildCredentials(pool: FileCredentialPool, authStore:
137
146
  }): ProviderCredentials;
138
147
  //#endregion
139
148
  //#region src/device-login.d.ts
149
+ type GrokLoginSurface = 'ui' | 'cli' | 'headless';
140
150
  interface GrokDeviceLoginPending {
141
151
  verificationUrl: string;
142
152
  userCode: string;
@@ -150,6 +160,7 @@ interface GrokDeviceLoginOptions {
150
160
  issuer?: string;
151
161
  clientId?: string;
152
162
  scope?: string;
163
+ surface?: GrokLoginSurface;
153
164
  }
154
165
  interface GrokDeviceLoginResult {
155
166
  entryKey: string;
package/dist/index.mjs CHANGED
@@ -1,13 +1,13 @@
1
1
  import { delay, errorCode, errorMessage, isAbortError, isRecord, nonEmptyString, normalizeBaseUrl, numberOrNull, numberOrZero, parseJsonObject, parseJsonOrString, stringOrNull } from "@demicodes/utils";
2
- import { createProviderQuota, defineProvider, httpRequestFailedEvent, normalizeErrorCode, numberHeader, providerErrorFromUnknown, redactCredentialText, severityFromUsedPercent, toolResultContentToText, usedPercentFromRatio } from "@demicodes/provider";
2
+ import { clampUsedPercent, createProviderQuota, defineProvider, httpRequestFailedEvent, normalizeErrorCode, numberHeader, providerErrorFromUnknown, redactCredentialText, severityFromUsedPercent, toolResultContentToText, usedPercentFromRatio } from "@demicodes/provider";
3
3
  import { Buffer } from "node:buffer";
4
4
  import { chmod, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
5
5
  import { homedir } from "node:os";
6
6
  import { dirname, join } from "node:path";
7
- import process from "node:process";
7
+ import process$1 from "node:process";
8
8
  import { zeroUsage } from "@demicodes/core";
9
- import { FileCredentialPool, credentialIdFromIdentity } from "@demicodes/provider/credentials-pool";
10
9
  import { readFileSync } from "node:fs";
10
+ import { FileCredentialPool, credentialIdFromIdentity } from "@demicodes/provider/credentials-pool";
11
11
  //#region src/auth.ts
12
12
  /** Grok auth.json stores the access token under `key`; redact it alongside the standard fields. */
13
13
  const SECRET_FIELD_PATTERNS = ["\\bkey\\b"];
@@ -68,18 +68,18 @@ var FileGrokAuthStore = class {
68
68
  if ((options.forceRefresh === true || expiresWithin(expiresAt, this.now(), REFRESH_EXPIRY_SKEW_MS)) && refreshToken && clientId) return this.refreshAndResolve(accessToken, entryKey, {
69
69
  refreshToken,
70
70
  clientId,
71
- tokenEndpoint: tokenEndpointForIssuer(issuer)
71
+ tokenEndpoint: tokenEndpointForIssuer(issuer),
72
+ principalType: stringOrNull(entry.principal_type),
73
+ principalId: stringOrNull(entry.principal_id)
72
74
  });
73
- return {
74
- accessToken,
75
+ return resolvedAuthFromEntry(entry, accessToken, {
75
76
  refreshToken,
76
77
  expiresAt,
77
- email: stringOrNull(entry.email),
78
78
  issuer,
79
79
  clientId,
80
80
  entryKey,
81
81
  authFile: this.authFile
82
- };
82
+ });
83
83
  }
84
84
  async refreshAndResolve(staleAccessToken, entryKey, refreshInput) {
85
85
  return this.withAuthFileLock(async () => {
@@ -92,21 +92,21 @@ var FileGrokAuthStore = class {
92
92
  const latestAccessToken = nonEmptyString(latestEntry.key);
93
93
  if (latestAccessToken && latestAccessToken !== staleAccessToken) {
94
94
  const latestExpiresAt = parseExpiresAt(latestEntry.expires_at) ?? parseJwtExpiration(latestAccessToken);
95
- if (!expiresWithin(latestExpiresAt, this.now(), REFRESH_EXPIRY_SKEW_MS)) return {
96
- accessToken: latestAccessToken,
95
+ if (!expiresWithin(latestExpiresAt, this.now(), REFRESH_EXPIRY_SKEW_MS)) return resolvedAuthFromEntry(latestEntry, latestAccessToken, {
97
96
  refreshToken: nonEmptyString(latestEntry.refresh_token) ?? null,
98
97
  expiresAt: latestExpiresAt,
99
- email: stringOrNull(latestEntry.email),
100
98
  issuer,
101
99
  clientId,
102
100
  entryKey,
103
101
  authFile: this.authFile
104
- };
102
+ });
105
103
  }
106
104
  const response = await this.refreshImpl({
107
105
  refreshToken,
108
106
  clientId,
109
- tokenEndpoint: tokenEndpointForIssuer(issuer) || refreshInput.tokenEndpoint
107
+ tokenEndpoint: tokenEndpointForIssuer(issuer) || refreshInput.tokenEndpoint,
108
+ principalType: stringOrNull(latestEntry.principal_type) ?? refreshInput.principalType,
109
+ principalId: stringOrNull(latestEntry.principal_id) ?? refreshInput.principalId
110
110
  });
111
111
  const accessToken = nonEmptyString(response.access_token);
112
112
  if (!accessToken) throw new GrokAuthError("auth_refresh_failed", "Grok token refresh returned no access_token");
@@ -122,16 +122,14 @@ var FileGrokAuthStore = class {
122
122
  [entryKey]: nextEntry
123
123
  };
124
124
  await writeAuthJsonAtomic(this.authFile, nextFile);
125
- return {
126
- accessToken,
125
+ return resolvedAuthFromEntry(nextEntry, accessToken, {
127
126
  refreshToken: nonEmptyString(nextEntry.refresh_token) ?? null,
128
127
  expiresAt,
129
- email: stringOrNull(nextEntry.email),
130
128
  issuer,
131
129
  clientId,
132
130
  entryKey,
133
131
  authFile: this.authFile
134
- };
132
+ });
135
133
  });
136
134
  }
137
135
  async readAuthFile() {
@@ -164,7 +162,7 @@ var FileGrokAuthStore = class {
164
162
  await delay(this.lockRetryDelayMs);
165
163
  }
166
164
  try {
167
- await writeFile(lockFile, `${process.pid}:${Math.floor(this.now().getTime() / 1e3)}`, { mode: 384 });
165
+ await writeFile(lockFile, `${process$1.pid}:${Math.floor(this.now().getTime() / 1e3)}`, { mode: 384 });
168
166
  return await fn();
169
167
  } finally {
170
168
  const ownedIdentity = await handle.stat().then(toFileIdentity).catch(() => null);
@@ -182,7 +180,7 @@ var GrokAuthError = class extends Error {
182
180
  }
183
181
  };
184
182
  function defaultGrokHome() {
185
- const fromEnv = process.env.GROK_HOME;
183
+ const fromEnv = process$1.env.GROK_HOME;
186
184
  return fromEnv && fromEnv.trim() ? fromEnv : join(homedir(), ".grok");
187
185
  }
188
186
  function selectAuthEntry(file) {
@@ -220,6 +218,8 @@ async function refreshGrokOidcToken(input, signal) {
220
218
  refresh_token: input.refreshToken,
221
219
  client_id: input.clientId
222
220
  });
221
+ if (nonEmptyString(input.principalType)) body.set("principal_type", input.principalType);
222
+ if (nonEmptyString(input.principalId)) body.set("principal_id", input.principalId);
223
223
  const response = await fetch(input.tokenEndpoint, {
224
224
  method: "POST",
225
225
  headers: {
@@ -232,8 +232,20 @@ async function refreshGrokOidcToken(input, signal) {
232
232
  if (!response.ok) throw new GrokAuthError("auth_refresh_failed", `Grok token refresh failed with HTTP ${response.status}`);
233
233
  return await response.json();
234
234
  }
235
+ function peekGrokAccessTokenPrincipal(accessToken) {
236
+ const payload = decodeGrokJwtPayload(accessToken);
237
+ if (!payload) return null;
238
+ const principalType = nonEmptyString(payload.principal_type) ?? nonEmptyString(payload.principalType);
239
+ const principalId = nonEmptyString(payload.principal_id) ?? nonEmptyString(payload.principalId);
240
+ if (!principalType || !principalId) return null;
241
+ return {
242
+ principalType,
243
+ principalId,
244
+ teamId: nonEmptyString(payload.team_id) ?? null
245
+ };
246
+ }
235
247
  function parseJwtExpiration(jwt) {
236
- const exp = decodeJwtPayload(jwt)?.exp;
248
+ const exp = decodeGrokJwtPayload(jwt)?.exp;
237
249
  return typeof exp === "number" ? /* @__PURE__ */ new Date(exp * 1e3) : null;
238
250
  }
239
251
  function tokenEndpointForIssuer(issuer) {
@@ -261,12 +273,12 @@ function expiresWithin(expiresAt, now, skewMs) {
261
273
  }
262
274
  async function writeAuthJsonAtomic(authFile, auth) {
263
275
  await mkdir(dirname(authFile), { recursive: true });
264
- const temp = `${authFile}.${process.pid}.${Date.now()}.tmp`;
276
+ const temp = `${authFile}.${process$1.pid}.${Date.now()}.tmp`;
265
277
  await writeFile(temp, `${JSON.stringify(auth, null, 2)}\n`, { mode: 384 });
266
278
  await chmod(temp, 384);
267
279
  await rename(temp, authFile);
268
280
  }
269
- function decodeJwtPayload(jwt) {
281
+ function decodeGrokJwtPayload(jwt) {
270
282
  const parts = jwt.split(".");
271
283
  if (parts.length !== 3 || !parts[1]) return null;
272
284
  try {
@@ -277,6 +289,25 @@ function decodeJwtPayload(jwt) {
277
289
  return null;
278
290
  }
279
291
  }
292
+ function resolveGrokUserId(entry, accessToken) {
293
+ return stringOrNull(entry.user_id) ?? peekGrokAccessTokenPrincipal(accessToken)?.principalId ?? stringOrNull(decodeGrokJwtPayload(accessToken)?.sub);
294
+ }
295
+ function resolvedAuthFromEntry(entry, accessToken, rest) {
296
+ const peeked = peekGrokAccessTokenPrincipal(accessToken);
297
+ return {
298
+ accessToken,
299
+ refreshToken: rest.refreshToken,
300
+ expiresAt: rest.expiresAt,
301
+ email: stringOrNull(entry.email),
302
+ userId: resolveGrokUserId(entry, accessToken),
303
+ principalType: stringOrNull(entry.principal_type) ?? peeked?.principalType ?? null,
304
+ principalId: stringOrNull(entry.principal_id) ?? peeked?.principalId ?? null,
305
+ issuer: rest.issuer,
306
+ clientId: rest.clientId,
307
+ entryKey: rest.entryKey,
308
+ authFile: rest.authFile
309
+ };
310
+ }
280
311
  /** Grok CLI lock format is `pid:unix_seconds`. A valid live PID always owns its lock. */
281
312
  async function isAbandonedGrokAuthLock(lockFile, now, maxAgeMs = 3e4) {
282
313
  try {
@@ -311,7 +342,7 @@ async function removeLockFileIfSame(lockFile, expected) {
311
342
  }
312
343
  function isProcessAlive(pid) {
313
344
  try {
314
- process.kill(pid, 0);
345
+ process$1.kill(pid, 0);
315
346
  return true;
316
347
  } catch (error) {
317
348
  if (errorCode(error) === "EPERM") return true;
@@ -596,16 +627,75 @@ function grokUsage(usage) {
596
627
  };
597
628
  }
598
629
  //#endregion
630
+ //#region src/headers.ts
631
+ const DEFAULT_GROK_BUILD_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
632
+ const GROK_CLI_TOKEN_AUTH = "xai-grok-cli";
633
+ const GROK_AUTHENTICATE_RESPONSE = "authenticate-response";
634
+ const GROK_CLIENT_IDENTIFIER = "grok-shell";
635
+ const GROK_CLIENT_MODE = "interactive";
636
+ /** Official Grok CLI crate version, used when no local `~/.grok/version.json` exists. */
637
+ const DEFAULT_GROK_CLIENT_VERSION = "1.0.5";
638
+ function buildGrokBuildHeaders(auth, request, options) {
639
+ const headers = new Headers(options?.extra);
640
+ headers.set("authorization", `Bearer ${auth.accessToken}`);
641
+ headers.set("X-XAI-Token-Auth", GROK_CLI_TOKEN_AUTH);
642
+ headers.set("x-authenticateresponse", GROK_AUTHENTICATE_RESPONSE);
643
+ headers.set("x-grok-client-version", resolveGrokClientVersion(options?.clientVersion, options?.grokHome));
644
+ headers.set("x-grok-client-identifier", nonEmptyString(process.env.GROK_CLIENT_NAME) ?? GROK_CLIENT_IDENTIFIER);
645
+ headers.set("x-grok-client-mode", GROK_CLIENT_MODE);
646
+ if (auth.userId) {
647
+ headers.set("x-userid", auth.userId);
648
+ headers.set("x-grok-user-id", auth.userId);
649
+ }
650
+ if (auth.email) headers.set("x-email", auth.email);
651
+ if (request?.modelId) headers.set("x-grok-model-override", request.modelId);
652
+ if (request?.sessionId) {
653
+ headers.set("x-grok-session-id", request.sessionId);
654
+ headers.set("x-grok-conv-id", request.sessionId);
655
+ }
656
+ if (request?.requestId) headers.set("x-grok-req-id", request.requestId);
657
+ if (request?.turnId) headers.set("x-grok-turn-idx", request.turnId);
658
+ return headers;
659
+ }
660
+ function resolveGrokClientVersion(explicit, grokHome) {
661
+ const fromExplicit = nonEmptyString(explicit);
662
+ if (fromExplicit) return fromExplicit;
663
+ return readGrokCliVersion(grokHome ?? defaultGrokHome()) ?? DEFAULT_GROK_CLIENT_VERSION;
664
+ }
665
+ function readGrokCliVersion(grokHome) {
666
+ try {
667
+ const parsed = JSON.parse(readFileSync(join(grokHome, "version.json"), "utf8"));
668
+ if (!isRecord(parsed)) return null;
669
+ return nonEmptyString(parsed.version) ?? null;
670
+ } catch {
671
+ return null;
672
+ }
673
+ }
674
+ //#endregion
599
675
  //#region src/device-login.ts
600
676
  const GROK_ISSUER = "https://auth.x.ai";
601
677
  const GROK_CLI_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
602
- const GROK_LOGIN_SCOPE = "openid profile email offline_access grok-cli:access";
678
+ const GROK_LOGIN_SCOPE = "openid profile email offline_access grok-cli:access api:access conversations:read conversations:write workspaces:read workspaces:write";
679
+ const GROK_LOGIN_REFERRER = "grok-build";
603
680
  const GROK_LOGIN_FALLBACK_INTERVAL_S = 5;
604
- const GROK_LOGIN_FALLBACK_EXPIRES_S = 900;
605
- async function postForm(fetchImpl, url, params, signal) {
681
+ const GROK_LOGIN_MIN_EXPIRES_S = 600;
682
+ const TEAM_PRINCIPAL = "Team";
683
+ const ORGANIZATION_PRINCIPAL = "Organization";
684
+ function resolveLoginSurface(options) {
685
+ if (options.surface) return options.surface;
686
+ return options.onPending ? "ui" : "headless";
687
+ }
688
+ function oauthHeaders(surface) {
689
+ return {
690
+ "content-type": "application/x-www-form-urlencoded",
691
+ "x-grok-client-version": resolveGrokClientVersion(),
692
+ "x-grok-client-surface": surface
693
+ };
694
+ }
695
+ async function postForm(fetchImpl, url, params, surface, signal) {
606
696
  return fetchImpl(url, {
607
697
  method: "POST",
608
- headers: { "content-type": "application/x-www-form-urlencoded" },
698
+ headers: oauthHeaders(surface),
609
699
  body: new URLSearchParams(params).toString(),
610
700
  signal
611
701
  });
@@ -615,17 +705,37 @@ async function jsonBody(response, what) {
615
705
  if (!isRecord(body)) throw new GrokAuthError("auth_invalid", `${what} response is not a JSON object`);
616
706
  return body;
617
707
  }
618
- async function requestDeviceCode(fetchImpl, issuer, clientId, scope, signal) {
708
+ function assertUserCode(userCode) {
709
+ if (![...userCode].every((ch) => /[A-Za-z0-9-]/.test(ch))) throw new GrokAuthError("auth_invalid", "Grok device code response has an invalid user_code");
710
+ }
711
+ function assertVerificationUri(uri) {
712
+ if ([...uri].some((ch) => ch.charCodeAt(0) < 32)) throw new GrokAuthError("auth_invalid", "Grok device code response has an invalid verification URI");
713
+ let parsed;
714
+ try {
715
+ parsed = new URL(uri);
716
+ } catch {
717
+ throw new GrokAuthError("auth_invalid", "Grok device code response has an invalid verification URI");
718
+ }
719
+ if (parsed.protocol === "https:") return;
720
+ if (parsed.protocol === "http:" && (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1")) return;
721
+ throw new GrokAuthError("auth_invalid", "Grok device code response has an unsupported verification URI scheme");
722
+ }
723
+ async function requestDeviceCode(fetchImpl, issuer, clientId, scope, surface, signal) {
619
724
  const response = await postForm(fetchImpl, `${issuer}/oauth2/device/code`, {
620
725
  client_id: clientId,
621
- scope
622
- }, signal);
726
+ scope,
727
+ referrer: GROK_LOGIN_REFERRER
728
+ }, surface, signal);
623
729
  if (!response.ok) throw new GrokAuthError("auth_invalid", `Grok device code request failed with HTTP ${response.status}`);
624
730
  const body = await jsonBody(response, "Grok device code");
625
731
  const deviceCode = nonEmptyString(body.device_code);
626
732
  const userCode = nonEmptyString(body.user_code);
627
733
  const verificationUrl = nonEmptyString(body.verification_uri_complete) ?? nonEmptyString(body.verification_uri);
734
+ const verificationUri = nonEmptyString(body.verification_uri);
628
735
  if (!deviceCode || !userCode || !verificationUrl) throw new GrokAuthError("auth_invalid", "Grok device code response is missing device_code, user_code, or verification_uri");
736
+ assertUserCode(userCode);
737
+ if (verificationUri) assertVerificationUri(verificationUri);
738
+ if (nonEmptyString(body.verification_uri_complete)) assertVerificationUri(nonEmptyString(body.verification_uri_complete));
629
739
  const interval = Number(body.interval);
630
740
  const expiresIn = Number(body.expires_in);
631
741
  return {
@@ -633,18 +743,20 @@ async function requestDeviceCode(fetchImpl, issuer, clientId, scope, signal) {
633
743
  userCode,
634
744
  verificationUrl,
635
745
  intervalSeconds: Number.isFinite(interval) && interval >= 0 ? interval : GROK_LOGIN_FALLBACK_INTERVAL_S,
636
- expiresAt: Date.now() + (Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : GROK_LOGIN_FALLBACK_EXPIRES_S) * 1e3
746
+ expiresAt: Date.now() + Math.max(Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : 0, GROK_LOGIN_MIN_EXPIRES_S) * 1e3
637
747
  };
638
748
  }
639
- async function pollForTokens(fetchImpl, issuer, clientId, device, signal) {
640
- let intervalSeconds = device.intervalSeconds;
749
+ async function pollForTokens(fetchImpl, issuer, clientId, device, surface, signal) {
750
+ let intervalSeconds = Math.max(device.intervalSeconds, 1);
641
751
  for (;;) {
642
752
  signal?.throwIfAborted();
753
+ await delay(intervalSeconds * 1e3);
754
+ if (Date.now() >= device.expiresAt) throw new GrokAuthError("auth_invalid", "Grok device login timed out before the user confirmed");
643
755
  const response = await postForm(fetchImpl, `${issuer}/oauth2/token`, {
644
756
  grant_type: "urn:ietf:params:oauth:grant-type:device_code",
645
757
  device_code: device.deviceCode,
646
758
  client_id: clientId
647
- }, signal);
759
+ }, surface, signal);
648
760
  const body = await jsonBody(response, "Grok device token");
649
761
  if (response.ok) {
650
762
  const accessToken = nonEmptyString(body.access_token);
@@ -653,49 +765,103 @@ async function pollForTokens(fetchImpl, issuer, clientId, device, signal) {
653
765
  return {
654
766
  accessToken,
655
767
  refreshToken: nonEmptyString(body.refresh_token) ?? null,
656
- expiresIn: Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : null
768
+ expiresIn: Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : null,
769
+ idToken: nonEmptyString(body.id_token) ?? null
657
770
  };
658
771
  }
659
772
  const error = nonEmptyString(body.error);
660
773
  if (error === "slow_down") intervalSeconds += 5;
661
774
  else if (error !== "authorization_pending") throw new GrokAuthError("auth_invalid", `Grok device login failed: ${error ?? `HTTP ${response.status}`}`);
662
- if (Date.now() >= device.expiresAt) throw new GrokAuthError("auth_invalid", "Grok device login timed out before the user confirmed");
663
- await delay(intervalSeconds * 1e3);
664
775
  }
665
776
  }
666
- async function fetchUserinfo(fetchImpl, issuer, accessToken, signal) {
667
- const response = await fetchImpl(`${issuer}/oauth2/userinfo`, {
668
- headers: { authorization: `Bearer ${accessToken}` },
669
- signal
670
- });
671
- if (!response.ok) return {};
672
- const body = await response.json().catch(() => null);
673
- return isRecord(body) ? body : {};
777
+ async function fetchUserEnrichment(fetchImpl, accessToken, signal) {
778
+ try {
779
+ const response = await fetchImpl(`${DEFAULT_GROK_BUILD_BASE_URL}/user`, {
780
+ headers: {
781
+ authorization: `Bearer ${accessToken}`,
782
+ "X-XAI-Token-Auth": GROK_CLI_TOKEN_AUTH,
783
+ "x-grok-client-version": resolveGrokClientVersion(),
784
+ "x-grok-client-mode": "interactive"
785
+ },
786
+ signal
787
+ });
788
+ if (!response.ok) return null;
789
+ const body = await response.json().catch(() => null);
790
+ if (!isRecord(body)) return null;
791
+ if (!nonEmptyString(body.userId) && !nonEmptyString(body.user_id)) return null;
792
+ return body;
793
+ } catch {
794
+ return null;
795
+ }
796
+ }
797
+ function applyUserInfoEnrichment(entry, user) {
798
+ const userId = nonEmptyString(user.userId) ?? nonEmptyString(user.user_id);
799
+ if (userId) entry.user_id = userId;
800
+ const firstName = nonEmptyString(user.firstName) ?? nonEmptyString(user.first_name);
801
+ if (firstName) entry.first_name = firstName;
802
+ const lastName = nonEmptyString(user.lastName) ?? nonEmptyString(user.last_name);
803
+ if (lastName) entry.last_name = lastName;
804
+ const principalType = nonEmptyString(user.principalType) ?? nonEmptyString(user.principal_type);
805
+ if (principalType) entry.principal_type = principalType;
806
+ const principalId = nonEmptyString(user.principalId) ?? nonEmptyString(user.principal_id);
807
+ if (principalId) entry.principal_id = principalId;
808
+ const teamId = nonEmptyString(user.teamId) ?? nonEmptyString(user.team_id);
809
+ if (teamId) entry.team_id = teamId;
810
+ const organizationId = nonEmptyString(user.organizationId) ?? nonEmptyString(user.organization_id);
811
+ if (organizationId) entry.organization_id = organizationId;
812
+ const email = nonEmptyString(user.email);
813
+ if (email) entry.email = email;
814
+ }
815
+ async function assembleAuthEntry(fetchImpl, issuer, clientId, tokens, signal) {
816
+ const idClaims = tokens.idToken ? decodeGrokJwtPayload(tokens.idToken) : null;
817
+ let userId = nonEmptyString(idClaims?.sub) ?? "";
818
+ let email = nonEmptyString(idClaims?.email) ?? null;
819
+ const peeked = peekGrokAccessTokenPrincipal(tokens.accessToken);
820
+ const principalType = peeked?.principalType ?? null;
821
+ const principalId = peeked?.principalId ?? null;
822
+ let teamId = peeked?.teamId ?? null;
823
+ let organizationId = null;
824
+ if (principalType === TEAM_PRINCIPAL && principalId) {
825
+ userId = principalId;
826
+ email = null;
827
+ teamId = principalId;
828
+ } else if (principalType === ORGANIZATION_PRINCIPAL && principalId) {
829
+ userId = principalId;
830
+ email = null;
831
+ organizationId = principalId;
832
+ }
833
+ const entry = {
834
+ key: tokens.accessToken,
835
+ auth_mode: "oidc",
836
+ ...tokens.refreshToken ? { refresh_token: tokens.refreshToken } : {},
837
+ ...tokens.expiresIn ? { expires_at: new Date(Date.now() + tokens.expiresIn * 1e3).toISOString() } : {},
838
+ oidc_issuer: issuer,
839
+ oidc_client_id: clientId,
840
+ ...email ? { email } : {},
841
+ ...userId ? { user_id: userId } : {},
842
+ ...principalType ? { principal_type: principalType } : {},
843
+ ...principalId ? { principal_id: principalId } : {},
844
+ ...teamId ? { team_id: teamId } : {},
845
+ ...organizationId ? { organization_id: organizationId } : {}
846
+ };
847
+ const enriched = await fetchUserEnrichment(fetchImpl, tokens.accessToken, signal);
848
+ if (enriched) applyUserInfoEnrichment(entry, enriched);
849
+ return entry;
674
850
  }
675
851
  /** Runs the full device flow and returns a vendor-shaped auth.json entry keyed like the Grok CLI. */
676
852
  async function runGrokDeviceLogin(options = {}) {
677
853
  const fetchImpl = options.fetch ?? fetch;
678
854
  const issuer = (options.issuer ?? GROK_ISSUER).replace(/\/+$/, "");
679
855
  const clientId = options.clientId ?? GROK_CLI_CLIENT_ID;
680
- const device = await requestDeviceCode(fetchImpl, issuer, clientId, options.scope ?? GROK_LOGIN_SCOPE, options.signal);
856
+ const scope = options.scope ?? GROK_LOGIN_SCOPE;
857
+ const surface = resolveLoginSurface(options);
858
+ const device = await requestDeviceCode(fetchImpl, issuer, clientId, scope, surface, options.signal);
681
859
  options.onPending?.({
682
860
  verificationUrl: device.verificationUrl,
683
861
  userCode: device.userCode,
684
862
  expiresAt: new Date(device.expiresAt).toISOString()
685
863
  });
686
- const tokens = await pollForTokens(fetchImpl, issuer, clientId, device, options.signal);
687
- const userinfo = await fetchUserinfo(fetchImpl, issuer, tokens.accessToken, options.signal);
688
- const entry = {
689
- key: tokens.accessToken,
690
- auth_mode: "oidc",
691
- ...tokens.refreshToken ? { refresh_token: tokens.refreshToken } : {},
692
- ...tokens.expiresIn ? { expires_at: new Date(Date.now() + tokens.expiresIn * 1e3).toISOString() } : {},
693
- oidc_issuer: issuer,
694
- oidc_client_id: clientId,
695
- ...nonEmptyString(userinfo.email) ? { email: nonEmptyString(userinfo.email) } : {},
696
- ...nonEmptyString(userinfo.given_name) ? { first_name: nonEmptyString(userinfo.given_name) } : {},
697
- ...nonEmptyString(userinfo.sub) ? { user_id: nonEmptyString(userinfo.sub) } : {}
698
- };
864
+ const entry = await assembleAuthEntry(fetchImpl, issuer, clientId, await pollForTokens(fetchImpl, issuer, clientId, device, surface, options.signal), options.signal);
699
865
  return {
700
866
  entryKey: `${issuer}::${clientId}`,
701
867
  entry
@@ -815,7 +981,8 @@ function createGrokBuildCredentials(pool, authStore, options = {}) {
815
981
  const { entryKey, entry } = await runGrokDeviceLogin({
816
982
  signal: loginOptions?.signal,
817
983
  onPending: loginOptions?.onPending,
818
- fetch: options.loginFetch
984
+ fetch: options.loginFetch,
985
+ surface: "ui"
819
986
  });
820
987
  return {
821
988
  status: "completed",
@@ -869,33 +1036,6 @@ function createGrokBuildCredentials(pool, authStore, options = {}) {
869
1036
  }
870
1037
  };
871
1038
  }
872
- const GROK_CLI_TOKEN_AUTH = "xai-grok-cli";
873
- const GROK_CLIENT_SURFACE = "grok-build";
874
- function buildGrokBuildHeaders(auth, request, options) {
875
- const headers = new Headers(options?.extra);
876
- headers.set("authorization", `Bearer ${auth.accessToken}`);
877
- headers.set("X-XAI-Token-Auth", GROK_CLI_TOKEN_AUTH);
878
- headers.set("x-grok-client-surface", GROK_CLIENT_SURFACE);
879
- headers.set("x-grok-client-version", resolveGrokClientVersion(options?.clientVersion, options?.grokHome));
880
- if (request?.modelId) headers.set("x-grok-model-override", request.modelId);
881
- if (request?.sessionId) headers.set("x-grok-session-id", request.sessionId);
882
- if (request?.requestId) headers.set("x-grok-req-id", request.requestId);
883
- return headers;
884
- }
885
- function resolveGrokClientVersion(explicit, grokHome) {
886
- const fromExplicit = nonEmptyString(explicit);
887
- if (fromExplicit) return fromExplicit;
888
- return readGrokCliVersion(grokHome ?? defaultGrokHome()) ?? "0.1.202";
889
- }
890
- function readGrokCliVersion(grokHome) {
891
- try {
892
- const parsed = JSON.parse(readFileSync(join(grokHome, "version.json"), "utf8"));
893
- if (!isRecord(parsed)) return null;
894
- return nonEmptyString(parsed.version) ?? null;
895
- } catch {
896
- return null;
897
- }
898
- }
899
1039
  //#endregion
900
1040
  //#region src/models.ts
901
1041
  const FALLBACK_SOURCE_FETCHED_AT = "1970-01-01T00:00:00.000Z";
@@ -1023,7 +1163,7 @@ function positiveOrDefault(value, fallback) {
1023
1163
  /**
1024
1164
  * Active probe against cli-chat-proxy:
1025
1165
  * - GET /v1/user?include=subscription → tier
1026
- * - GET /v1/billing → monthly used/limit
1166
+ * - GET /v1/billing?format=creditscreditUsagePercent / currentPeriod
1027
1167
  *
1028
1168
  * Optional observation of short-window x-ratelimit-* headers from chat responses
1029
1169
  * (separate windows from monthly subscription quota).
@@ -1046,7 +1186,7 @@ function createGrokBuildQuota(options = {}) {
1046
1186
  const [user, billing] = await Promise.all([fetchJson(fetchImpl, `${baseUrl}/user?include=subscription`, auth, {
1047
1187
  grokHome,
1048
1188
  clientVersion
1049
- }, signal), fetchJson(fetchImpl, `${baseUrl}/billing`, auth, {
1189
+ }, signal), fetchJson(fetchImpl, `${baseUrl}/billing?format=credits`, auth, {
1050
1190
  grokHome,
1051
1191
  clientVersion
1052
1192
  }, signal)]);
@@ -1059,18 +1199,21 @@ function mapGrokQuotaProbe(user, billing, auth) {
1059
1199
  const userRecord = isRecord(user) ? user : {};
1060
1200
  const billingRecord = isRecord(billing) ? billing : {};
1061
1201
  const config = isRecord(billingRecord.config) ? billingRecord.config : {};
1202
+ const period = isRecord(config.currentPeriod) ? config.currentPeriod : null;
1203
+ const isWeekly = stringOrNull(period?.type) === "USAGE_PERIOD_TYPE_WEEKLY";
1204
+ const resetsAt = stringOrNull(period?.end) ?? stringOrNull(config.billingPeriodEnd);
1062
1205
  const monthlyLimit = moneyVal(config.monthlyLimit);
1063
1206
  const used = moneyVal(config.used);
1064
1207
  const onDemandCap = moneyVal(config.onDemandCap);
1065
- const usedPercent = usedPercentFromRatio(used, monthlyLimit);
1208
+ const usedPercent = clampUsedPercent(config.creditUsagePercent) ?? usedPercentFromRatio(used, monthlyLimit);
1066
1209
  const windows = [{
1067
- id: "monthly",
1068
- label: "Monthly credits",
1210
+ id: isWeekly ? "weekly" : "monthly",
1211
+ label: isWeekly ? "Weekly credits" : "Monthly credits",
1069
1212
  usedPercent,
1070
1213
  used,
1071
1214
  limit: monthlyLimit,
1072
1215
  unit: "credits",
1073
- resetsAt: stringOrNull(config.billingPeriodEnd),
1216
+ resetsAt,
1074
1217
  severity: severityFromUsedPercent(usedPercent)
1075
1218
  }];
1076
1219
  if (onDemandCap != null && onDemandCap > 0) windows.push({
@@ -1080,7 +1223,7 @@ function mapGrokQuotaProbe(user, billing, auth) {
1080
1223
  used: null,
1081
1224
  limit: onDemandCap,
1082
1225
  unit: "credits",
1083
- resetsAt: stringOrNull(config.billingPeriodEnd)
1226
+ resetsAt
1084
1227
  });
1085
1228
  const tier = stringOrNull(userRecord.subscriptionTier);
1086
1229
  return {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@demicodes/provider-grok-build",
3
3
  "description": "Grok Build provider adapter for Demi (reuses ~/.grok OAuth session).",
4
- "version": "0.17.1",
4
+ "version": "0.17.2",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "exports": {
@@ -11,9 +11,9 @@
11
11
  }
12
12
  },
13
13
  "dependencies": {
14
- "@demicodes/core": "^0.17.1",
15
- "@demicodes/provider": "^0.17.1",
16
- "@demicodes/utils": "^0.17.1"
14
+ "@demicodes/core": "^0.17.2",
15
+ "@demicodes/provider": "^0.17.2",
16
+ "@demicodes/utils": "^0.17.2"
17
17
  },
18
18
  "license": "Apache-2.0",
19
19
  "main": "./dist/index.mjs",