@demicodes/provider-grok-build 0.3.2 → 0.4.1
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/dist/index.d.mts +39 -3
- package/dist/index.mjs +125 -15
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import { Provider, ProviderAuthState, ProviderCredentials, ProviderModelList, ProviderQuota, ProviderQuotaProbeResult } from "@demicodes/provider";
|
|
2
2
|
import { FileCredentialPool } from "@demicodes/provider/credentials-pool";
|
|
3
3
|
//#region src/auth.d.ts
|
|
4
|
+
/** One credential entry as written by the Grok CLI (`~/.grok/auth.json`). */
|
|
5
|
+
interface GrokAuthEntry {
|
|
6
|
+
key?: string;
|
|
7
|
+
auth_mode?: string;
|
|
8
|
+
refresh_token?: string;
|
|
9
|
+
expires_at?: string;
|
|
10
|
+
oidc_issuer?: string;
|
|
11
|
+
oidc_client_id?: string;
|
|
12
|
+
email?: string;
|
|
13
|
+
first_name?: string;
|
|
14
|
+
user_id?: string;
|
|
15
|
+
team_id?: string;
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
4
18
|
interface GrokResolvedAuth {
|
|
5
19
|
accessToken: string;
|
|
6
20
|
refreshToken: string | null;
|
|
@@ -117,9 +131,31 @@ declare function openGrokCredentialPool(options?: {
|
|
|
117
131
|
}): FileCredentialPool;
|
|
118
132
|
declare function createGrokBuildCredentials(pool: FileCredentialPool, authStore: GrokAuthStore, options?: {
|
|
119
133
|
grokHome?: string;
|
|
120
|
-
loginCommand?: string;
|
|
121
|
-
loginArgs?: string[];
|
|
122
134
|
quota?: ProviderQuota | null;
|
|
135
|
+
/** Injectable fetch for the device-code login flow (tests). */
|
|
136
|
+
loginFetch?: typeof fetch;
|
|
123
137
|
}): ProviderCredentials;
|
|
124
138
|
//#endregion
|
|
125
|
-
|
|
139
|
+
//#region src/device-login.d.ts
|
|
140
|
+
interface GrokDeviceLoginPending {
|
|
141
|
+
verificationUrl: string;
|
|
142
|
+
userCode: string;
|
|
143
|
+
expiresAt: string;
|
|
144
|
+
}
|
|
145
|
+
interface GrokDeviceLoginOptions {
|
|
146
|
+
signal?: AbortSignal;
|
|
147
|
+
/** Fires once with the URL + one-time code the user needs. */
|
|
148
|
+
onPending?: (pending: GrokDeviceLoginPending) => void;
|
|
149
|
+
fetch?: typeof fetch;
|
|
150
|
+
issuer?: string;
|
|
151
|
+
clientId?: string;
|
|
152
|
+
scope?: string;
|
|
153
|
+
}
|
|
154
|
+
interface GrokDeviceLoginResult {
|
|
155
|
+
entryKey: string;
|
|
156
|
+
entry: GrokAuthEntry;
|
|
157
|
+
}
|
|
158
|
+
/** Runs the full device flow and returns a vendor-shaped auth.json entry keyed like the Grok CLI. */
|
|
159
|
+
declare function runGrokDeviceLogin(options?: GrokDeviceLoginOptions): Promise<GrokDeviceLoginResult>;
|
|
160
|
+
//#endregion
|
|
161
|
+
export { type GrokBuildFetch, type GrokBuildProviderOptions, type GrokBuildQuotaOptions, type GrokDeviceLoginOptions, type GrokDeviceLoginPending, type GrokDeviceLoginResult, PoolAwareGrokAuthStore, createGrokBuildCredentials, createGrokBuildProvider, createGrokBuildQuota, grokBuildAuthStatus, grokBuildFallbackModels, listGrokBuildModels, mapGrokQuotaProbe, observeGrokRateLimitHeaders, openGrokCredentialPool, parseGrokBuildProviderConfig, runGrokDeviceLogin };
|
package/dist/index.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { homedir } from "node:os";
|
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import process from "node:process";
|
|
8
8
|
import { zeroUsage } from "@demicodes/core";
|
|
9
|
-
import { FileCredentialPool, credentialIdFromIdentity
|
|
9
|
+
import { FileCredentialPool, credentialIdFromIdentity } from "@demicodes/provider/credentials-pool";
|
|
10
10
|
import { readFileSync } from "node:fs";
|
|
11
11
|
//#region src/auth.ts
|
|
12
12
|
/** Grok auth.json stores the access token under `key`; redact it alongside the standard fields. */
|
|
@@ -599,6 +599,112 @@ function grokUsage(usage) {
|
|
|
599
599
|
};
|
|
600
600
|
}
|
|
601
601
|
//#endregion
|
|
602
|
+
//#region src/device-login.ts
|
|
603
|
+
const GROK_ISSUER = "https://auth.x.ai";
|
|
604
|
+
const GROK_CLI_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
|
|
605
|
+
const GROK_LOGIN_SCOPE = "openid profile email offline_access grok-cli:access";
|
|
606
|
+
const GROK_LOGIN_FALLBACK_INTERVAL_S = 5;
|
|
607
|
+
const GROK_LOGIN_FALLBACK_EXPIRES_S = 900;
|
|
608
|
+
async function postForm(fetchImpl, url, params, signal) {
|
|
609
|
+
return fetchImpl(url, {
|
|
610
|
+
method: "POST",
|
|
611
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
612
|
+
body: new URLSearchParams(params).toString(),
|
|
613
|
+
signal
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
async function jsonBody(response, what) {
|
|
617
|
+
const body = await response.json().catch(() => null);
|
|
618
|
+
if (!isRecord(body)) throw new GrokAuthError("auth_invalid", `${what} response is not a JSON object`);
|
|
619
|
+
return body;
|
|
620
|
+
}
|
|
621
|
+
async function requestDeviceCode(fetchImpl, issuer, clientId, scope, signal) {
|
|
622
|
+
const response = await postForm(fetchImpl, `${issuer}/oauth2/device/code`, {
|
|
623
|
+
client_id: clientId,
|
|
624
|
+
scope
|
|
625
|
+
}, signal);
|
|
626
|
+
if (!response.ok) throw new GrokAuthError("auth_invalid", `Grok device code request failed with HTTP ${response.status}`);
|
|
627
|
+
const body = await jsonBody(response, "Grok device code");
|
|
628
|
+
const deviceCode = nonEmptyString(body.device_code);
|
|
629
|
+
const userCode = nonEmptyString(body.user_code);
|
|
630
|
+
const verificationUrl = nonEmptyString(body.verification_uri_complete) ?? nonEmptyString(body.verification_uri);
|
|
631
|
+
if (!deviceCode || !userCode || !verificationUrl) throw new GrokAuthError("auth_invalid", "Grok device code response is missing device_code, user_code, or verification_uri");
|
|
632
|
+
const interval = Number(body.interval);
|
|
633
|
+
const expiresIn = Number(body.expires_in);
|
|
634
|
+
return {
|
|
635
|
+
deviceCode,
|
|
636
|
+
userCode,
|
|
637
|
+
verificationUrl,
|
|
638
|
+
intervalSeconds: Number.isFinite(interval) && interval >= 0 ? interval : GROK_LOGIN_FALLBACK_INTERVAL_S,
|
|
639
|
+
expiresAt: Date.now() + (Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : GROK_LOGIN_FALLBACK_EXPIRES_S) * 1e3
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
async function pollForTokens(fetchImpl, issuer, clientId, device, signal) {
|
|
643
|
+
let intervalSeconds = device.intervalSeconds;
|
|
644
|
+
for (;;) {
|
|
645
|
+
signal?.throwIfAborted();
|
|
646
|
+
const response = await postForm(fetchImpl, `${issuer}/oauth2/token`, {
|
|
647
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
648
|
+
device_code: device.deviceCode,
|
|
649
|
+
client_id: clientId
|
|
650
|
+
}, signal);
|
|
651
|
+
const body = await jsonBody(response, "Grok device token");
|
|
652
|
+
if (response.ok) {
|
|
653
|
+
const accessToken = nonEmptyString(body.access_token);
|
|
654
|
+
if (!accessToken) throw new GrokAuthError("auth_invalid", "Grok token response is missing access_token");
|
|
655
|
+
const expiresIn = Number(body.expires_in);
|
|
656
|
+
return {
|
|
657
|
+
accessToken,
|
|
658
|
+
refreshToken: nonEmptyString(body.refresh_token) ?? null,
|
|
659
|
+
expiresIn: Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : null
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
const error = nonEmptyString(body.error);
|
|
663
|
+
if (error === "slow_down") intervalSeconds += 5;
|
|
664
|
+
else if (error !== "authorization_pending") throw new GrokAuthError("auth_invalid", `Grok device login failed: ${error ?? `HTTP ${response.status}`}`);
|
|
665
|
+
if (Date.now() >= device.expiresAt) throw new GrokAuthError("auth_invalid", "Grok device login timed out before the user confirmed");
|
|
666
|
+
await delay(intervalSeconds * 1e3);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
async function fetchUserinfo(fetchImpl, issuer, accessToken, signal) {
|
|
670
|
+
const response = await fetchImpl(`${issuer}/oauth2/userinfo`, {
|
|
671
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
672
|
+
signal
|
|
673
|
+
});
|
|
674
|
+
if (!response.ok) return {};
|
|
675
|
+
const body = await response.json().catch(() => null);
|
|
676
|
+
return isRecord(body) ? body : {};
|
|
677
|
+
}
|
|
678
|
+
/** Runs the full device flow and returns a vendor-shaped auth.json entry keyed like the Grok CLI. */
|
|
679
|
+
async function runGrokDeviceLogin(options = {}) {
|
|
680
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
681
|
+
const issuer = (options.issuer ?? GROK_ISSUER).replace(/\/+$/, "");
|
|
682
|
+
const clientId = options.clientId ?? GROK_CLI_CLIENT_ID;
|
|
683
|
+
const device = await requestDeviceCode(fetchImpl, issuer, clientId, options.scope ?? GROK_LOGIN_SCOPE, options.signal);
|
|
684
|
+
options.onPending?.({
|
|
685
|
+
verificationUrl: device.verificationUrl,
|
|
686
|
+
userCode: device.userCode,
|
|
687
|
+
expiresAt: new Date(device.expiresAt).toISOString()
|
|
688
|
+
});
|
|
689
|
+
const tokens = await pollForTokens(fetchImpl, issuer, clientId, device, options.signal);
|
|
690
|
+
const userinfo = await fetchUserinfo(fetchImpl, issuer, tokens.accessToken, options.signal);
|
|
691
|
+
const entry = {
|
|
692
|
+
key: tokens.accessToken,
|
|
693
|
+
auth_mode: "oidc",
|
|
694
|
+
...tokens.refreshToken ? { refresh_token: tokens.refreshToken } : {},
|
|
695
|
+
...tokens.expiresIn ? { expires_at: new Date(Date.now() + tokens.expiresIn * 1e3).toISOString() } : {},
|
|
696
|
+
oidc_issuer: issuer,
|
|
697
|
+
oidc_client_id: clientId,
|
|
698
|
+
...nonEmptyString(userinfo.email) ? { email: nonEmptyString(userinfo.email) } : {},
|
|
699
|
+
...nonEmptyString(userinfo.given_name) ? { first_name: nonEmptyString(userinfo.given_name) } : {},
|
|
700
|
+
...nonEmptyString(userinfo.sub) ? { user_id: nonEmptyString(userinfo.sub) } : {}
|
|
701
|
+
};
|
|
702
|
+
return {
|
|
703
|
+
entryKey: `${issuer}::${clientId}`,
|
|
704
|
+
entry
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
//#endregion
|
|
602
708
|
//#region src/credentials.ts
|
|
603
709
|
var PoolAwareGrokAuthStore = class {
|
|
604
710
|
pool;
|
|
@@ -642,8 +748,6 @@ function openGrokCredentialPool(options = {}) {
|
|
|
642
748
|
}
|
|
643
749
|
function createGrokBuildCredentials(pool, authStore, options = {}) {
|
|
644
750
|
const vendorHome = options.grokHome ?? defaultGrokHome();
|
|
645
|
-
const loginCommand = options.loginCommand ?? "grok";
|
|
646
|
-
const loginArgs = options.loginArgs ?? ["login"];
|
|
647
751
|
const capability = () => ({
|
|
648
752
|
mode: "supported",
|
|
649
753
|
canBeginLogin: true,
|
|
@@ -710,17 +814,23 @@ function createGrokBuildCredentials(pool, authStore, options = {}) {
|
|
|
710
814
|
getActive,
|
|
711
815
|
setActive,
|
|
712
816
|
beginLogin: async (loginOptions) => {
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
}
|
|
817
|
+
try {
|
|
818
|
+
const { entryKey, entry } = await runGrokDeviceLogin({
|
|
819
|
+
signal: loginOptions?.signal,
|
|
820
|
+
onPending: loginOptions?.onPending,
|
|
821
|
+
fetch: options.loginFetch
|
|
822
|
+
});
|
|
823
|
+
return {
|
|
824
|
+
status: "completed",
|
|
825
|
+
credentialId: (await importEntry(entryKey, entry, "login:device")).id
|
|
826
|
+
};
|
|
827
|
+
} catch (error) {
|
|
828
|
+
if (loginOptions?.signal?.aborted) return { status: "cancelled" };
|
|
829
|
+
return {
|
|
830
|
+
status: "failed",
|
|
831
|
+
message: errorMessage(error)
|
|
832
|
+
};
|
|
833
|
+
}
|
|
724
834
|
},
|
|
725
835
|
importDefault: async () => {
|
|
726
836
|
const authFile = join(vendorHome, "auth.json");
|
|
@@ -1214,4 +1324,4 @@ function chatCompletionsUrl(baseUrl) {
|
|
|
1214
1324
|
return normalized.endsWith("/chat/completions") ? normalized : `${normalized}/chat/completions`;
|
|
1215
1325
|
}
|
|
1216
1326
|
//#endregion
|
|
1217
|
-
export { PoolAwareGrokAuthStore, createGrokBuildCredentials, createGrokBuildProvider, createGrokBuildQuota, grokBuildAuthStatus, grokBuildFallbackModels, listGrokBuildModels, mapGrokQuotaProbe, observeGrokRateLimitHeaders, openGrokCredentialPool, parseGrokBuildProviderConfig };
|
|
1327
|
+
export { PoolAwareGrokAuthStore, createGrokBuildCredentials, createGrokBuildProvider, createGrokBuildQuota, grokBuildAuthStatus, grokBuildFallbackModels, listGrokBuildModels, mapGrokQuotaProbe, observeGrokRateLimitHeaders, openGrokCredentialPool, parseGrokBuildProviderConfig, runGrokDeviceLogin };
|
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.
|
|
4
|
+
"version": "0.4.1",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@demicodes/core": "^0.3.2",
|
|
15
|
-
"@demicodes/provider": "^0.3
|
|
16
|
-
"@demicodes/utils": "^0.
|
|
15
|
+
"@demicodes/provider": "^0.4.3",
|
|
16
|
+
"@demicodes/utils": "^0.4.0"
|
|
17
17
|
},
|
|
18
18
|
"license": "Apache-2.0",
|
|
19
19
|
"main": "./dist/index.mjs",
|