tokentracker-cli 0.87.0 → 0.87.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/dashboard/dist/assets/{AchievementBadge-CU9m5POo.js → AchievementBadge-C468-Rsw.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-ESzayVR5.js → AchievementsPage-CZWzSdiH.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-DUVRqw19.js → AchievementsSection-DXt6VbwH.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-CxNQcN6_.js → BadgeDetailModal-BtGvpqQm.js} +1 -1
- package/dashboard/dist/assets/{Card-DXIoSOtO.js → Card-DZ-_fXkw.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-BsuxMkNd.js → ClawdAnimated-9rXaHo1-.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-C-amBp21.js → CommandPalette-sqIvZNsH.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-ByFOQube.js → CommunityStatsModal-c8VohIqQ.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-BnUyidbn.js → ConfirmModal-Ct2HLdds.js} +1 -1
- package/dashboard/dist/assets/Controls-DPtXGUGi.js +1 -0
- package/dashboard/dist/assets/DashboardPage-C65hbl8o.js +1 -0
- package/dashboard/dist/assets/{DevicePage-B_o-jhku.js → DevicePage-Ds4GEuqY.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-BSd7Jiag.js → DialogClose-B2IAbJG4.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-DeRza-LI.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-XBpneDtQ.js → DialogTitle-BIwLH6gA.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-Dg0v4V9p.js → FadeIn-DPS7_DOB.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-BIsQD1OK.js → HeaderGithubStar-CsSgr9Hd.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-CtmRa8ne.js → HoverTooltip-CLGZW2d-.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-BUoLz5sL.js → IpCheckPage-DEM_uk69.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-BheA-lw8.js → LandingPage-BQ3YzZ68.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-DAy9SOdL.js → LeaderboardAvatar-ySznr4Ml.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-CwLqtEUN.js → LeaderboardPage-Br4JCoVE.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-BZ9yGyjB.js → LeaderboardProfileModal-Drvsb5xF.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-Cs-LTfvS.js → LeaderboardProfilePage-BD0P-zZF.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-BQle8e8N.js → LimitsPage-Mbn2mj6S.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-Bp4CqObQ.js → LocalOnlyNotice-BsKgtoVa.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-BIXfWkR2.js → LoginCard-CTQDdHcx.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-B-ubPXdr.js → LoginModal-BxCyLDEW.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-9arjWT-s.js → LoginPage-ftxnX8g1.js} +1 -1
- package/dashboard/dist/assets/PetPage-CQsfqsm8.js +1 -0
- package/dashboard/dist/assets/{PopoverPopup-DMg7XE1h.js → PopoverPopup-C16r8jQC.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-BG3oqeST.js → ProviderIcon-7Tkq9_m6.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-LQ9rfcRE.js → ResetPasswordPage-D-MPpQd1.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-GY2tRk1u.js → SegmentedControl-CYFrzu9w.js} +1 -1
- package/dashboard/dist/assets/{Select-CCngFwmz.js → Select-Dp4V9p6t.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-B9Zm8WTR.js → SelectItemText-CxhBeoxx.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-BxPvk1-W.js → ServiceStatusPage-BT6hvvLW.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-DGo6yAwP.js → SessionsPage-BopeZBiH.js} +1 -1
- package/dashboard/dist/assets/SettingsPage-D03GJJBd.js +1 -0
- package/dashboard/dist/assets/{SkillsPage-D0PB3WR5.js → SkillsPage-Cr8O6eOH.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-Chh_fPIv.js → TokenGalaxy-BoUsrUEs.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-BXacVvrF.js → ToolbarRootContext-CwRaHhMN.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-CjiCwNmO.js → TrendMonitor-BV2ugEsw.js} +1 -1
- package/dashboard/dist/assets/WidgetsPage-B7r4mvrw.js +1 -0
- package/dashboard/dist/assets/{WrappedPage-CEpBAMZR.js → WrappedPage-CFU9l22i.js} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-CzQk3FUc.css +1 -0
- package/dashboard/dist/assets/{__vite-browser-external-Dyag-P6u.js → __vite-browser-external-Db9onipF.js} +10 -3
- package/dashboard/dist/assets/{agent-logos-CLQGE0AM.js → agent-logos-DSSn_SPi.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-CU2HDjoG.js → arrow-up-right-COvrArNz.js} +1 -1
- package/dashboard/dist/assets/{check-CucGM5lh.js → check-CasSqiev.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-DYzUvcID.js → chevron-right-B1PMeNoD.js} +1 -1
- package/dashboard/dist/assets/{copy-BFeQ31Tr.js → copy-D67vycfp.js} +1 -1
- package/dashboard/dist/assets/{download-Coq_llCX.js → download-adOCFO2v.js} +1 -1
- package/dashboard/dist/assets/{external-link-BJiDBW3d.js → external-link-DVanKEuy.js} +1 -1
- package/dashboard/dist/assets/{flame-5stsnhc0.js → flame-RbiHZM9b.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-CpTJbo-2.js → format-tokens-DhMp1e1L.js} +1 -1
- package/dashboard/dist/assets/{icons-jsuvmnnO.js → icons-BC0p_aGr.js} +1 -1
- package/dashboard/dist/assets/{index-DSD3RLQ4.js → index-A41xjc83.js} +1 -1
- package/dashboard/dist/assets/{index-BU6GsTbx.js → index-CaRyTJ6K.js} +1 -1
- package/dashboard/dist/assets/{index-Bs97feNM.js → index-DrgBqR3o.js} +1 -1
- package/dashboard/dist/assets/{info-DNNUzeV_.js → info-D9LwqlnC.js} +1 -1
- package/dashboard/dist/assets/{limits-providers-DKTqNO55.js → limits-providers-CRHpXhmW.js} +1 -1
- package/dashboard/dist/assets/{link-2-DC1hnQcE.js → link-2-B5f4qFR0.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-DtInM4Io.js → loader-circle-DCmwZEdS.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-DxpsaYXp.js → maximize-2-BMalEDCZ.js} +1 -1
- package/dashboard/dist/assets/{provider-display-hkyvfEJF.js → provider-display-qMFRUZHm.js} +1 -1
- package/dashboard/dist/assets/{react-TbJoDSP4.js → react-BnaPNGj8.js} +1 -1
- package/dashboard/dist/assets/{search-C7bnqB9b.js → search-BrqBkLs2.js} +1 -1
- package/dashboard/dist/assets/{skills-api-D7glOWg7.js → skills-api-vo3iM6yD.js} +1 -1
- package/dashboard/dist/assets/{store-CCVlzuqv.js → store-BwMVXuz3.js} +1 -1
- package/dashboard/dist/assets/{terminal-uQlsmzGi.js → terminal-BiyUh_8k.js} +1 -1
- package/dashboard/dist/assets/{trash-2-B1yuX-oD.js → trash-2-DAr4sqkX.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-0Fs5nY1G.js → use-community-stats-Cg48Zphy.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-CrftAn62.js → use-limits-display-prefs-DfKtH1Zn.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-DAdPjLb5.js → use-ordered-list-CnXfrWeF.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-BIgYrbON.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-Cc52HiK0.js → use-session-efficiency-pref-CMDTCAAd.js} +1 -1
- package/dashboard/dist/assets/{use-transform-D76wHhX0.js → use-transform-B2d9Txt5.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-B7Prb8rk.js → use-usage-limits-CJbTUl9W.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-DWoM_A3s.js → useCurrency-B0hJte5f.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-Bp6ddiQo.js → useScrollLock-DuosKdSB.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-DZOYz5HT.js → useTokenFormat-DB8pKLFM.js} +1 -1
- package/dashboard/dist/assets/{zap-DyeAeO8l.js → zap-rbzC4XgP.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dashboard/dist/ip-check.html +2 -2
- package/dashboard/dist/leaderboard.html +2 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +1 -1
- package/src/commands/device-login.js +12 -7
- package/src/commands/init.js +3 -0
- package/src/commands/sync.js +115 -20
- package/src/lib/grok-context-breakdown.js +124 -19
- package/src/lib/grok-limits.js +374 -9
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/runtime-config.js +23 -3
- package/dashboard/dist/assets/DashboardPage-GyXkKYW0.js +0 -1
- package/dashboard/dist/assets/DialogDescription-BFBVGbUT.js +0 -1
- package/dashboard/dist/assets/PetPage-BzAVsvh6.js +0 -1
- package/dashboard/dist/assets/SettingsPage-ConPEiwO.js +0 -1
- package/dashboard/dist/assets/WidgetsPage-CBw9uBCO.js +0 -1
- package/dashboard/dist/assets/__vite-browser-external-hXV_ZYTG.css +0 -1
- package/dashboard/dist/assets/use-native-settings-CVqB9eoO.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-DVGYs3-k.js +0 -1
package/src/lib/grok-limits.js
CHANGED
|
@@ -4,6 +4,10 @@ const path = require("node:path");
|
|
|
4
4
|
|
|
5
5
|
const DEFAULT_BILLING_BASE_URL = "https://cli-chat-proxy.grok.com";
|
|
6
6
|
const DEFAULT_BILLING_TIMEOUT_MS = 15_000;
|
|
7
|
+
const DEFAULT_OIDC_ISSUER = "https://auth.x.ai";
|
|
8
|
+
const DEFAULT_TOKEN_ENDPOINT = "https://auth.x.ai/oauth2/token";
|
|
9
|
+
// Refresh slightly before wall-clock expiry so Limits doesn't race a just-expired JWT.
|
|
10
|
+
const ACCESS_TOKEN_EXPIRY_SKEW_MS = 60_000;
|
|
7
11
|
|
|
8
12
|
function grokAuthError() {
|
|
9
13
|
const error = new Error("Not logged in to Grok Build. Run `grok login` in Terminal to authenticate.");
|
|
@@ -11,6 +15,14 @@ function grokAuthError() {
|
|
|
11
15
|
return error;
|
|
12
16
|
}
|
|
13
17
|
|
|
18
|
+
function grokReauthError() {
|
|
19
|
+
const error = new Error(
|
|
20
|
+
"Grok session expired. Run `grok login` in Terminal to re-authenticate.",
|
|
21
|
+
);
|
|
22
|
+
error.code = "GROK_REAUTH_REQUIRED";
|
|
23
|
+
return error;
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
function grokBillingTimeoutError() {
|
|
15
27
|
const error = new Error("Grok billing request timed out.");
|
|
16
28
|
error.code = "GROK_BILLING_TIMEOUT";
|
|
@@ -174,18 +186,29 @@ function isGrokInstalled({ home, env } = {}) {
|
|
|
174
186
|
function loadGrokAuthEntry({ home, env } = {}) {
|
|
175
187
|
const authPath = path.join(resolveGrokHome({ home, env }), "auth.json");
|
|
176
188
|
if (!fs.existsSync(authPath)) return null;
|
|
189
|
+
let fallback = null;
|
|
177
190
|
try {
|
|
178
191
|
const parsed = JSON.parse(fs.readFileSync(authPath, "utf8"));
|
|
179
|
-
if (!parsed || typeof parsed !== "object") return null;
|
|
180
|
-
for (const value of Object.
|
|
181
|
-
if (!value || typeof value !== "object") continue;
|
|
192
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
193
|
+
for (const [scopeKey, value] of Object.entries(parsed)) {
|
|
194
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
182
195
|
const key = typeof value.key === "string" ? value.key.trim() : "";
|
|
183
|
-
|
|
196
|
+
const refreshToken =
|
|
197
|
+
typeof value.refresh_token === "string" ? value.refresh_token.trim() : "";
|
|
198
|
+
// Entries with an access token win outright. A refresh-token-only entry
|
|
199
|
+
// is only usable when a client id is resolvable, and must not shadow a
|
|
200
|
+
// later keyed entry — keep it as a fallback instead.
|
|
201
|
+
if (key) {
|
|
202
|
+
return { entry: value, authPath, scopeKey, authFile: parsed };
|
|
203
|
+
}
|
|
204
|
+
if (refreshToken && !fallback && resolveGrokOidcClientId(value, scopeKey)) {
|
|
205
|
+
fallback = { entry: value, authPath, scopeKey, authFile: parsed };
|
|
206
|
+
}
|
|
184
207
|
}
|
|
185
208
|
} catch (_error) {
|
|
186
209
|
return null;
|
|
187
210
|
}
|
|
188
|
-
return
|
|
211
|
+
return fallback;
|
|
189
212
|
}
|
|
190
213
|
|
|
191
214
|
function readGrokAccessToken({ home, env } = {}) {
|
|
@@ -194,6 +217,284 @@ function readGrokAccessToken({ home, env } = {}) {
|
|
|
194
217
|
return key || null;
|
|
195
218
|
}
|
|
196
219
|
|
|
220
|
+
function isGrokAccessTokenExpired(expiresAt, nowMs = Date.now(), skewMs = ACCESS_TOKEN_EXPIRY_SKEW_MS) {
|
|
221
|
+
if (expiresAt == null || expiresAt === "") return false;
|
|
222
|
+
const ts = typeof expiresAt === "number" ? expiresAt : Date.parse(String(expiresAt));
|
|
223
|
+
if (!Number.isFinite(ts)) return false;
|
|
224
|
+
const skew = Number.isFinite(skewMs) && skewMs >= 0 ? skewMs : ACCESS_TOKEN_EXPIRY_SKEW_MS;
|
|
225
|
+
return ts <= nowMs + skew;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function resolveGrokOidcClientId(entry, scopeKey) {
|
|
229
|
+
if (entry && typeof entry.oidc_client_id === "string" && entry.oidc_client_id.trim()) {
|
|
230
|
+
return entry.oidc_client_id.trim();
|
|
231
|
+
}
|
|
232
|
+
if (typeof scopeKey === "string" && scopeKey.includes("::")) {
|
|
233
|
+
const suffix = scopeKey.slice(scopeKey.lastIndexOf("::") + 2).trim();
|
|
234
|
+
if (suffix) return suffix;
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function resolveGrokOidcIssuer(entry) {
|
|
240
|
+
if (entry && typeof entry.oidc_issuer === "string" && entry.oidc_issuer.trim()) {
|
|
241
|
+
return entry.oidc_issuer.trim().replace(/\/$/, "");
|
|
242
|
+
}
|
|
243
|
+
return DEFAULT_OIDC_ISSUER;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function resolveGrokTokenEndpoint(entry, env = process.env) {
|
|
247
|
+
if (typeof env.TOKENTRACKER_GROK_TOKEN_ENDPOINT === "string" && env.TOKENTRACKER_GROK_TOKEN_ENDPOINT.trim()) {
|
|
248
|
+
return env.TOKENTRACKER_GROK_TOKEN_ENDPOINT.trim();
|
|
249
|
+
}
|
|
250
|
+
if (typeof env.GROK_OIDC_TOKEN_ENDPOINT === "string" && env.GROK_OIDC_TOKEN_ENDPOINT.trim()) {
|
|
251
|
+
return env.GROK_OIDC_TOKEN_ENDPOINT.trim();
|
|
252
|
+
}
|
|
253
|
+
const issuer = resolveGrokOidcIssuer(entry);
|
|
254
|
+
if (issuer === DEFAULT_OIDC_ISSUER) return DEFAULT_TOKEN_ENDPOINT;
|
|
255
|
+
return `${issuer}/oauth2/token`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function grokEntryRefreshToken(entry) {
|
|
259
|
+
return entry && typeof entry.refresh_token === "string" ? entry.refresh_token.trim() : "";
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function grokEntryAccessToken(entry) {
|
|
263
|
+
return entry && typeof entry.key === "string" ? entry.key.trim() : "";
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Public xAI OIDC client refresh (auth method "none").
|
|
268
|
+
* Matches Grok Build CLI's refresh against https://auth.x.ai/oauth2/token.
|
|
269
|
+
*/
|
|
270
|
+
async function refreshGrokTokens({
|
|
271
|
+
refreshToken,
|
|
272
|
+
clientId,
|
|
273
|
+
tokenEndpoint = DEFAULT_TOKEN_ENDPOINT,
|
|
274
|
+
fetchImpl = fetch,
|
|
275
|
+
timeoutMs = DEFAULT_BILLING_TIMEOUT_MS,
|
|
276
|
+
} = {}) {
|
|
277
|
+
if (typeof refreshToken !== "string" || !refreshToken.trim()) {
|
|
278
|
+
const err = new Error("Grok refresh skipped: no refresh_token in auth.json");
|
|
279
|
+
err.code = "NO_REFRESH_TOKEN";
|
|
280
|
+
throw err;
|
|
281
|
+
}
|
|
282
|
+
if (typeof clientId !== "string" || !clientId.trim()) {
|
|
283
|
+
const err = new Error("Grok refresh skipped: missing oidc_client_id");
|
|
284
|
+
err.code = "NO_CLIENT_ID";
|
|
285
|
+
throw err;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const body = new URLSearchParams({
|
|
289
|
+
grant_type: "refresh_token",
|
|
290
|
+
refresh_token: refreshToken.trim(),
|
|
291
|
+
client_id: clientId.trim(),
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Same abort deadline discipline as fetchGrokBilling — a stalled token
|
|
295
|
+
// endpoint must not block fetchGrokLimits (and its poller) indefinitely.
|
|
296
|
+
// The timer stays armed through the body reads below, not just the headers.
|
|
297
|
+
const normalizedTimeoutMs =
|
|
298
|
+
Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_BILLING_TIMEOUT_MS;
|
|
299
|
+
const controller = new AbortController();
|
|
300
|
+
const timeoutError = new Error("Grok token refresh timed out.");
|
|
301
|
+
timeoutError.code = "GROK_REFRESH_TIMEOUT";
|
|
302
|
+
const timer = setTimeout(() => controller.abort(timeoutError), normalizedTimeoutMs);
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
const res = await fetchImpl(tokenEndpoint, {
|
|
306
|
+
method: "POST",
|
|
307
|
+
headers: {
|
|
308
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
309
|
+
Accept: "application/json",
|
|
310
|
+
},
|
|
311
|
+
body: body.toString(),
|
|
312
|
+
signal: controller.signal,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
if (res.status === 400 || res.status === 401) {
|
|
316
|
+
let oauthError = null;
|
|
317
|
+
try {
|
|
318
|
+
const payload = await res.json();
|
|
319
|
+
oauthError =
|
|
320
|
+
(typeof payload?.error === "string" && payload.error) ||
|
|
321
|
+
(payload?.error && typeof payload.error === "object" && payload.error.code) ||
|
|
322
|
+
null;
|
|
323
|
+
} catch (_error) {
|
|
324
|
+
// Status remains authoritative.
|
|
325
|
+
}
|
|
326
|
+
const err = grokReauthError();
|
|
327
|
+
err.oauthError = oauthError;
|
|
328
|
+
err.status = res.status;
|
|
329
|
+
throw err;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (!res.ok) {
|
|
333
|
+
const err = new Error(`Grok token refresh failed: HTTP ${res.status}`);
|
|
334
|
+
err.code = "REFRESH_HTTP_ERROR";
|
|
335
|
+
err.status = res.status;
|
|
336
|
+
throw err;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const payload = await res.json();
|
|
340
|
+
const accessToken =
|
|
341
|
+
typeof payload?.access_token === "string" ? payload.access_token.trim() : "";
|
|
342
|
+
if (!accessToken) {
|
|
343
|
+
const err = new Error("Grok token refresh response missing access_token");
|
|
344
|
+
err.code = "REFRESH_INVALID_RESPONSE";
|
|
345
|
+
throw err;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const nextRefresh =
|
|
349
|
+
typeof payload?.refresh_token === "string" && payload.refresh_token.trim()
|
|
350
|
+
? payload.refresh_token.trim()
|
|
351
|
+
: refreshToken.trim();
|
|
352
|
+
|
|
353
|
+
let expiresAt = null;
|
|
354
|
+
const expiresIn = Number(payload?.expires_in);
|
|
355
|
+
if (Number.isFinite(expiresIn) && expiresIn > 0) {
|
|
356
|
+
expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
|
|
357
|
+
} else if (typeof payload?.expires_at === "string" && payload.expires_at.trim()) {
|
|
358
|
+
const parsed = Date.parse(payload.expires_at.trim());
|
|
359
|
+
if (Number.isFinite(parsed)) expiresAt = new Date(parsed).toISOString();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
access_token: accessToken,
|
|
364
|
+
refresh_token: nextRefresh,
|
|
365
|
+
expires_at: expiresAt,
|
|
366
|
+
token_type: typeof payload?.token_type === "string" ? payload.token_type : null,
|
|
367
|
+
};
|
|
368
|
+
} catch (error) {
|
|
369
|
+
if (controller.signal.aborted && controller.signal.reason === timeoutError) {
|
|
370
|
+
throw timeoutError;
|
|
371
|
+
}
|
|
372
|
+
throw error;
|
|
373
|
+
} finally {
|
|
374
|
+
clearTimeout(timer);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function persistGrokRefreshedAuth(authPath, authFile, scopeKey, entry, newTokens) {
|
|
379
|
+
if (!authPath || !scopeKey || !entry || !newTokens?.access_token) {
|
|
380
|
+
throw new Error("Grok auth persist requires auth path, scope, entry, and access_token");
|
|
381
|
+
}
|
|
382
|
+
const nextEntry = {
|
|
383
|
+
...entry,
|
|
384
|
+
key: newTokens.access_token,
|
|
385
|
+
refresh_token: newTokens.refresh_token || entry.refresh_token,
|
|
386
|
+
};
|
|
387
|
+
if (newTokens.expires_at) {
|
|
388
|
+
nextEntry.expires_at = newTokens.expires_at;
|
|
389
|
+
} else {
|
|
390
|
+
// Unknown new lifetime: drop the stale timestamp, otherwise the next poll
|
|
391
|
+
// sees an already-expired expires_at next to a fresh key and refreshes
|
|
392
|
+
// again on every cycle (burning a rotation each time).
|
|
393
|
+
delete nextEntry.expires_at;
|
|
394
|
+
}
|
|
395
|
+
const merged = {
|
|
396
|
+
...authFile,
|
|
397
|
+
[scopeKey]: nextEntry,
|
|
398
|
+
};
|
|
399
|
+
const tmp = `${authPath}.tmp.${process.pid}.${Date.now()}`;
|
|
400
|
+
try {
|
|
401
|
+
await fs.promises.writeFile(tmp, `${JSON.stringify(merged, null, 2)}\n`, { mode: 0o600 });
|
|
402
|
+
await fs.promises.rename(tmp, authPath);
|
|
403
|
+
} catch (error) {
|
|
404
|
+
// Never leave a tmp file holding fresh tokens behind.
|
|
405
|
+
await fs.promises.rm(tmp, { force: true }).catch(() => {});
|
|
406
|
+
throw error;
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
await fs.promises.chmod(authPath, 0o600);
|
|
410
|
+
} catch (_error) {
|
|
411
|
+
// Best-effort; some filesystems reject chmod on rename targets.
|
|
412
|
+
}
|
|
413
|
+
return { entry: nextEntry, authFile: merged, authPath, scopeKey };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Return a usable access token, refreshing via OIDC when expired or forced.
|
|
418
|
+
* Does not throw for missing install; throws GROK_REAUTH_REQUIRED when refresh is rejected.
|
|
419
|
+
*/
|
|
420
|
+
async function resolveGrokAccessToken({
|
|
421
|
+
home,
|
|
422
|
+
env = process.env,
|
|
423
|
+
fetchImpl = fetch,
|
|
424
|
+
forceRefresh = false,
|
|
425
|
+
nowMs = Date.now(),
|
|
426
|
+
} = {}) {
|
|
427
|
+
const loaded = loadGrokAuthEntry({ home, env });
|
|
428
|
+
if (!loaded) {
|
|
429
|
+
return {
|
|
430
|
+
accessToken: null,
|
|
431
|
+
configured: false,
|
|
432
|
+
canRefresh: false,
|
|
433
|
+
refreshed: false,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const accessToken = grokEntryAccessToken(loaded.entry);
|
|
438
|
+
const refreshToken = grokEntryRefreshToken(loaded.entry);
|
|
439
|
+
const expired = isGrokAccessTokenExpired(loaded.entry.expires_at, nowMs);
|
|
440
|
+
const canRefresh = Boolean(refreshToken && resolveGrokOidcClientId(loaded.entry, loaded.scopeKey));
|
|
441
|
+
const needsRefresh = forceRefresh || !accessToken || expired;
|
|
442
|
+
|
|
443
|
+
if (!needsRefresh) {
|
|
444
|
+
return {
|
|
445
|
+
accessToken,
|
|
446
|
+
configured: true,
|
|
447
|
+
canRefresh,
|
|
448
|
+
refreshed: false,
|
|
449
|
+
loaded,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (!canRefresh) {
|
|
454
|
+
if (accessToken) {
|
|
455
|
+
// Stale key still present — let billing decide (may 401).
|
|
456
|
+
return {
|
|
457
|
+
accessToken,
|
|
458
|
+
configured: true,
|
|
459
|
+
canRefresh: false,
|
|
460
|
+
refreshed: false,
|
|
461
|
+
loaded,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
return {
|
|
465
|
+
accessToken: null,
|
|
466
|
+
configured: true,
|
|
467
|
+
canRefresh: false,
|
|
468
|
+
refreshed: false,
|
|
469
|
+
loaded,
|
|
470
|
+
error: grokAuthError(),
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const clientId = resolveGrokOidcClientId(loaded.entry, loaded.scopeKey);
|
|
475
|
+
const tokens = await refreshGrokTokens({
|
|
476
|
+
refreshToken,
|
|
477
|
+
clientId,
|
|
478
|
+
tokenEndpoint: resolveGrokTokenEndpoint(loaded.entry, env),
|
|
479
|
+
fetchImpl,
|
|
480
|
+
});
|
|
481
|
+
const persisted = await persistGrokRefreshedAuth(
|
|
482
|
+
loaded.authPath,
|
|
483
|
+
loaded.authFile,
|
|
484
|
+
loaded.scopeKey,
|
|
485
|
+
loaded.entry,
|
|
486
|
+
tokens,
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
return {
|
|
490
|
+
accessToken: tokens.access_token,
|
|
491
|
+
configured: true,
|
|
492
|
+
canRefresh: true,
|
|
493
|
+
refreshed: true,
|
|
494
|
+
loaded: persisted,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
197
498
|
/**
|
|
198
499
|
* Parse either:
|
|
199
500
|
* - Unified billing (`?format=credits`): weekly/monthly period + creditUsagePercent
|
|
@@ -234,6 +535,20 @@ function normalizeGrokBillingResponse(body) {
|
|
|
234
535
|
if (!periodType) periodType = "monthly";
|
|
235
536
|
}
|
|
236
537
|
|
|
538
|
+
// Unified billing omits creditUsagePercent (and productUsage) entirely when
|
|
539
|
+
// nothing was used this period. A valid currentPeriod window with the usage
|
|
540
|
+
// fields absent means 0%, not an unparseable response. Present-but-malformed
|
|
541
|
+
// fields still fall through to the parse error below.
|
|
542
|
+
if (
|
|
543
|
+
usedPercent === null &&
|
|
544
|
+
currentPeriod &&
|
|
545
|
+
(periodStart || resetAt) &&
|
|
546
|
+
config.creditUsagePercent === undefined &&
|
|
547
|
+
config.productUsage === undefined
|
|
548
|
+
) {
|
|
549
|
+
usedPercent = 0;
|
|
550
|
+
}
|
|
551
|
+
|
|
237
552
|
const onDemandCap = grokValNumber(config.onDemandCap);
|
|
238
553
|
const onDemandUsed = grokValNumber(config.onDemandUsed);
|
|
239
554
|
|
|
@@ -324,16 +639,57 @@ async function fetchGrokBilling(
|
|
|
324
639
|
return legacyResult.body;
|
|
325
640
|
}
|
|
326
641
|
|
|
327
|
-
async function fetchGrokLimits({ home, env, fetchImpl = fetch, timeoutMs } = {}) {
|
|
642
|
+
async function fetchGrokLimits({ home, env, fetchImpl = fetch, timeoutMs, nowMs } = {}) {
|
|
328
643
|
if (!isGrokInstalled({ home, env })) {
|
|
329
644
|
return { configured: false };
|
|
330
645
|
}
|
|
331
|
-
|
|
332
|
-
|
|
646
|
+
|
|
647
|
+
let resolved;
|
|
648
|
+
try {
|
|
649
|
+
resolved = await resolveGrokAccessToken({ home, env, fetchImpl, nowMs });
|
|
650
|
+
} catch (error) {
|
|
651
|
+
return {
|
|
652
|
+
configured: true,
|
|
653
|
+
error: error?.message || "Unknown error",
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (!resolved.configured) {
|
|
333
658
|
return { configured: false };
|
|
334
659
|
}
|
|
660
|
+
if (!resolved.accessToken) {
|
|
661
|
+
return {
|
|
662
|
+
configured: true,
|
|
663
|
+
error: resolved.error?.message || grokAuthError().message,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
335
667
|
try {
|
|
336
|
-
|
|
668
|
+
let body;
|
|
669
|
+
try {
|
|
670
|
+
body = await fetchGrokBilling(resolved.accessToken, { fetchImpl, env, timeoutMs });
|
|
671
|
+
} catch (error) {
|
|
672
|
+
// Access token rejected — refresh once when a refresh_token is available.
|
|
673
|
+
if (
|
|
674
|
+
error?.code === "GROK_AUTH_REQUIRED" &&
|
|
675
|
+
resolved.canRefresh &&
|
|
676
|
+
!resolved.refreshed
|
|
677
|
+
) {
|
|
678
|
+
const retry = await resolveGrokAccessToken({
|
|
679
|
+
home,
|
|
680
|
+
env,
|
|
681
|
+
fetchImpl,
|
|
682
|
+
forceRefresh: true,
|
|
683
|
+
nowMs,
|
|
684
|
+
});
|
|
685
|
+
if (!retry.accessToken) {
|
|
686
|
+
throw retry.error || grokReauthError();
|
|
687
|
+
}
|
|
688
|
+
body = await fetchGrokBilling(retry.accessToken, { fetchImpl, env, timeoutMs });
|
|
689
|
+
} else {
|
|
690
|
+
throw error;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
337
693
|
return {
|
|
338
694
|
configured: true,
|
|
339
695
|
error: null,
|
|
@@ -350,9 +706,18 @@ async function fetchGrokLimits({ home, env, fetchImpl = fetch, timeoutMs } = {})
|
|
|
350
706
|
module.exports = {
|
|
351
707
|
resolveGrokHome,
|
|
352
708
|
resolveGrokBillingBaseUrl,
|
|
709
|
+
DEFAULT_TOKEN_ENDPOINT,
|
|
710
|
+
ACCESS_TOKEN_EXPIRY_SKEW_MS,
|
|
353
711
|
isGrokInstalled,
|
|
354
712
|
loadGrokAuthEntry,
|
|
355
713
|
readGrokAccessToken,
|
|
714
|
+
isGrokAccessTokenExpired,
|
|
715
|
+
resolveGrokOidcClientId,
|
|
716
|
+
resolveGrokOidcIssuer,
|
|
717
|
+
resolveGrokTokenEndpoint,
|
|
718
|
+
refreshGrokTokens,
|
|
719
|
+
persistGrokRefreshedAuth,
|
|
720
|
+
resolveGrokAccessToken,
|
|
356
721
|
normalizeGrokPeriodType,
|
|
357
722
|
inferGrokPeriodTypeFromDates,
|
|
358
723
|
sumProductUsagePercent,
|