@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 +1 -1
- package/dist/index.d.mts +12 -1
- package/dist/index.mjs +237 -94
- package/package.json +4 -4
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 →
|
|
113
|
+
* - GET /v1/billing?format=credits → creditUsagePercent / 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 =
|
|
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
|
|
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
|
|
605
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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 :
|
|
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
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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
|
|
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
|
|
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 →
|
|
1166
|
+
* - GET /v1/billing?format=credits → creditUsagePercent / 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
|
|
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
|
|
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.
|
|
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.
|
|
15
|
-
"@demicodes/provider": "^0.17.
|
|
16
|
-
"@demicodes/utils": "^0.17.
|
|
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",
|