@gajae-code/ai 0.2.5 → 0.3.0
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/CHANGELOG.md +6 -0
- package/README.md +4 -2
- package/dist/types/utils/oauth/types.d.ts +1 -1
- package/dist/types/utils/oauth/xai.d.ts +23 -0
- package/package.json +2 -2
- package/src/auth-storage.ts +15 -0
- package/src/cli.ts +1 -0
- package/src/utils/oauth/index.ts +10 -0
- package/src/utils/oauth/types.ts +1 -0
- package/src/utils/oauth/xai.ts +210 -0
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -1014,6 +1014,7 @@ Several providers support OAuth authentication (some also support static API key
|
|
|
1014
1014
|
- **Google Gemini CLI** (Gemini 2.0/2.5 via Google Cloud Code Assist; free tier or paid subscription)
|
|
1015
1015
|
- **Antigravity** (Free Gemini 3, Anthropic model, GPT-OSS via Google Cloud)
|
|
1016
1016
|
- **Qwen Portal** (Qwen OAuth token or API key)
|
|
1017
|
+
- **xAI** (Grok OAuth login via xAI account)
|
|
1017
1018
|
|
|
1018
1019
|
For paid Cloud Code Assist subscriptions, set `GOOGLE_CLOUD_PROJECT` or `GOOGLE_CLOUD_PROJECT_ID` to your project ID.
|
|
1019
1020
|
|
|
@@ -1063,12 +1064,13 @@ The quickest way to authenticate:
|
|
|
1063
1064
|
bunx @gajae-code/ai login # interactive provider selection
|
|
1064
1065
|
bunx @gajae-code/ai login anthropic # login to specific provider
|
|
1065
1066
|
bunx @gajae-code/ai login vllm # store vLLM API key (or placeholder for local no-auth)
|
|
1067
|
+
bunx @gajae-code/ai login xai # sign in with xAI/Grok OAuth
|
|
1066
1068
|
bunx @gajae-code/ai list # list available providers
|
|
1067
1069
|
```
|
|
1068
1070
|
|
|
1069
|
-
Credentials are saved to `agent.db` in the agent directory. `/login qianfan` opens the Qianfan console and stores the pasted API key.
|
|
1071
|
+
Credentials are saved to `agent.db` in the agent directory. `/login qianfan` opens the Qianfan console and stores the pasted API key; `/login xai` opens xAI/Grok OAuth login and stores refreshable OAuth credentials.
|
|
1070
1072
|
|
|
1071
|
-
`login` supports OAuth providers (Anthropic, OpenAI code provider, GitHub Copilot, Gemini CLI, Antigravity) and API-key onboarding flows.
|
|
1073
|
+
`login` supports OAuth providers (Anthropic, OpenAI code provider, GitHub Copilot, Gemini CLI, Antigravity, xAI) and API-key onboarding flows.
|
|
1072
1074
|
|
|
1073
1075
|
For the current API-key onboarding flows, the library covers Together, Moonshot, Qianfan, NVIDIA, NanoGPT, Hugging Face, Venice, Xiaomi, vLLM, LiteLLM, Cloudflare AI Gateway, Qwen Portal, and Ollama Cloud. Ollama remains the local runtime integration; set `OLLAMA_API_KEY` only when your local or self-hosted deployment enforces bearer auth.
|
|
1074
1076
|
|
|
@@ -7,7 +7,7 @@ export type OAuthCredentials = {
|
|
|
7
7
|
email?: string;
|
|
8
8
|
accountId?: string;
|
|
9
9
|
};
|
|
10
|
-
export type OAuthProvider = "alibaba-coding-plan" | "anthropic" | "cerebras" | "cloudflare-ai-gateway" | "cursor" | "deepseek" | "fireworks" | "firepass" | "github-copilot" | "google-gemini-cli" | "google-antigravity" | "gitlab-duo" | "huggingface" | "kimi-code" | "kilo" | "kagi" | "litellm" | "lm-studio" | "minimax-code" | "minimax-code-cn" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "openai-codex" | "openai-codex-device" | "opencode-go" | "opencode-zen" | "parallel" | "perplexity" | "qianfan" | "qwen-portal" | "synthetic" | "tavily" | "together" | "venice" | "vercel-ai-gateway" | "vllm" | "xiaomi" | "zenmux" | "zai";
|
|
10
|
+
export type OAuthProvider = "alibaba-coding-plan" | "anthropic" | "cerebras" | "cloudflare-ai-gateway" | "cursor" | "deepseek" | "fireworks" | "firepass" | "github-copilot" | "google-gemini-cli" | "google-antigravity" | "gitlab-duo" | "huggingface" | "kimi-code" | "kilo" | "kagi" | "litellm" | "lm-studio" | "minimax-code" | "minimax-code-cn" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "openai-codex" | "openai-codex-device" | "opencode-go" | "opencode-zen" | "parallel" | "perplexity" | "qianfan" | "qwen-portal" | "synthetic" | "tavily" | "together" | "venice" | "vercel-ai-gateway" | "vllm" | "xai" | "xiaomi" | "zenmux" | "zai";
|
|
11
11
|
export type OAuthProviderId = OAuthProvider | (string & {});
|
|
12
12
|
export type OAuthPrompt = {
|
|
13
13
|
message: string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** xAI OAuth flow (Grok account login). */
|
|
2
|
+
import { OAuthCallbackFlow } from "./callback-server";
|
|
3
|
+
import type { OAuthController, OAuthCredentials } from "./types";
|
|
4
|
+
export declare const XAI_OAUTH_DISCOVERY_URL = "https://auth.x.ai/.well-known/openid-configuration";
|
|
5
|
+
export declare const XAI_OAUTH_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
|
|
6
|
+
export declare const XAI_OAUTH_SCOPE = "openid profile email offline_access grok-cli:access api:access";
|
|
7
|
+
interface XaiDiscovery {
|
|
8
|
+
authorizationEndpoint: string;
|
|
9
|
+
tokenEndpoint: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function discoverXaiOAuthEndpoints(signal?: AbortSignal): Promise<XaiDiscovery>;
|
|
12
|
+
export declare class XaiOAuthFlow extends OAuthCallbackFlow {
|
|
13
|
+
#private;
|
|
14
|
+
constructor(ctrl: OAuthController);
|
|
15
|
+
generateAuthUrl(state: string, redirectUri: string): Promise<{
|
|
16
|
+
url: string;
|
|
17
|
+
instructions?: string;
|
|
18
|
+
}>;
|
|
19
|
+
exchangeToken(code: string, _state: string, redirectUri: string): Promise<OAuthCredentials>;
|
|
20
|
+
}
|
|
21
|
+
export declare function loginXai(ctrl: OAuthController): Promise<OAuthCredentials>;
|
|
22
|
+
export declare function refreshXaiToken(refreshToken: string, signal?: AbortSignal): Promise<OAuthCredentials>;
|
|
23
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/ai",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://gaebal-gajae.dev",
|
|
7
7
|
"author": "Yeachan-Heo",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@anthropic-ai/sdk": "^0.94.0",
|
|
45
45
|
"@bufbuild/protobuf": "^2.12.0",
|
|
46
|
-
"@gajae-code/utils": "0.
|
|
46
|
+
"@gajae-code/utils": "0.3.0",
|
|
47
47
|
"openai": "^6.36.0",
|
|
48
48
|
"partial-json": "^0.1.7",
|
|
49
49
|
"zod": "4.4.3"
|
package/src/auth-storage.ts
CHANGED
|
@@ -1484,6 +1484,14 @@ export class AuthStorage {
|
|
|
1484
1484
|
await saveApiKeyCredential(apiKey);
|
|
1485
1485
|
return;
|
|
1486
1486
|
}
|
|
1487
|
+
case "xai": {
|
|
1488
|
+
const { loginXai } = await import("./utils/oauth/xai");
|
|
1489
|
+
credentials = await loginXai({
|
|
1490
|
+
...ctrl,
|
|
1491
|
+
onManualCodeInput: ctrl.onManualCodeInput ?? manualCodeInput,
|
|
1492
|
+
});
|
|
1493
|
+
break;
|
|
1494
|
+
}
|
|
1487
1495
|
case "fireworks": {
|
|
1488
1496
|
const { loginFireworks } = await import("./utils/oauth/fireworks");
|
|
1489
1497
|
const apiKey = await loginFireworks(ctrl);
|
|
@@ -1637,6 +1645,13 @@ export class AuthStorage {
|
|
|
1637
1645
|
}
|
|
1638
1646
|
}
|
|
1639
1647
|
const newCredential: OAuthCredential = { type: "oauth", ...credentials };
|
|
1648
|
+
if (provider === "xai") {
|
|
1649
|
+
const existingOAuthCredentials = this.#getCredentialsForProvider(provider).filter(
|
|
1650
|
+
(credential): credential is OAuthCredential => credential.type === "oauth",
|
|
1651
|
+
);
|
|
1652
|
+
await this.set(provider, [...existingOAuthCredentials, newCredential]);
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1640
1655
|
await this.#upsertOAuthCredential(provider, newCredential);
|
|
1641
1656
|
}
|
|
1642
1657
|
|
package/src/cli.ts
CHANGED
package/src/utils/oauth/index.ts
CHANGED
|
@@ -60,6 +60,11 @@ const builtInOAuthProviders: OAuthProviderInfo[] = [
|
|
|
60
60
|
name: "DeepSeek",
|
|
61
61
|
available: true,
|
|
62
62
|
},
|
|
63
|
+
{
|
|
64
|
+
id: "xai",
|
|
65
|
+
name: "xAI",
|
|
66
|
+
available: true,
|
|
67
|
+
},
|
|
63
68
|
{
|
|
64
69
|
id: "fireworks",
|
|
65
70
|
name: "Fireworks",
|
|
@@ -310,6 +315,11 @@ export async function refreshOAuthToken(
|
|
|
310
315
|
newCredentials = await refreshCursorToken(credentials.refresh);
|
|
311
316
|
break;
|
|
312
317
|
}
|
|
318
|
+
case "xai": {
|
|
319
|
+
const { refreshXaiToken } = await import("./xai");
|
|
320
|
+
newCredentials = await refreshXaiToken(credentials.refresh);
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
313
323
|
case "kilo":
|
|
314
324
|
case "perplexity":
|
|
315
325
|
case "huggingface":
|
package/src/utils/oauth/types.ts
CHANGED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/** xAI OAuth flow (Grok account login). */
|
|
2
|
+
import { OAuthCallbackFlow, type OAuthCallbackFlowOptions } from "./callback-server";
|
|
3
|
+
import { generatePKCE } from "./pkce";
|
|
4
|
+
import type { OAuthController, OAuthCredentials } from "./types";
|
|
5
|
+
|
|
6
|
+
const XAI_OAUTH_ISSUER = "https://auth.x.ai";
|
|
7
|
+
export const XAI_OAUTH_DISCOVERY_URL = `${XAI_OAUTH_ISSUER}/.well-known/openid-configuration`;
|
|
8
|
+
export const XAI_OAUTH_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
|
|
9
|
+
export const XAI_OAUTH_SCOPE = "openid profile email offline_access grok-cli:access api:access";
|
|
10
|
+
const XAI_OAUTH_CALLBACK_PORT = 56121;
|
|
11
|
+
const XAI_OAUTH_CALLBACK_PATH = "/callback";
|
|
12
|
+
const XAI_OAUTH_REFRESH_SKEW_MS = 2 * 60 * 1000;
|
|
13
|
+
const TOKEN_REQUEST_TIMEOUT_MS = 30_000;
|
|
14
|
+
|
|
15
|
+
interface XaiDiscovery {
|
|
16
|
+
authorizationEndpoint: string;
|
|
17
|
+
tokenEndpoint: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface XaiDiscoveryPayload {
|
|
21
|
+
authorization_endpoint?: unknown;
|
|
22
|
+
token_endpoint?: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface XaiTokenPayload {
|
|
26
|
+
access_token?: unknown;
|
|
27
|
+
refresh_token?: unknown;
|
|
28
|
+
expires_in?: unknown;
|
|
29
|
+
id_token?: unknown;
|
|
30
|
+
token_type?: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface XaiJwtPayload {
|
|
34
|
+
sub?: unknown;
|
|
35
|
+
email?: unknown;
|
|
36
|
+
[key: string]: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function requestSignal(signal: AbortSignal | undefined): AbortSignal {
|
|
40
|
+
const timeoutSignal = AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS);
|
|
41
|
+
return signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function validateXaiEndpoint(rawUrl: string): string {
|
|
45
|
+
const parsed = new URL(rawUrl);
|
|
46
|
+
const host = parsed.hostname.toLowerCase();
|
|
47
|
+
if (parsed.protocol !== "https:" || (host !== "x.ai" && !host.endsWith(".x.ai"))) {
|
|
48
|
+
throw new Error(`xAI OAuth discovery returned an unexpected endpoint: ${rawUrl}`);
|
|
49
|
+
}
|
|
50
|
+
return parsed.toString();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function discoverXaiOAuthEndpoints(signal?: AbortSignal): Promise<XaiDiscovery> {
|
|
54
|
+
const response = await fetch(XAI_OAUTH_DISCOVERY_URL, {
|
|
55
|
+
headers: { Accept: "application/json" },
|
|
56
|
+
signal: requestSignal(signal),
|
|
57
|
+
});
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
throw new Error(`xAI OAuth discovery failed: ${response.status} ${await response.text()}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const payload = (await response.json()) as XaiDiscoveryPayload;
|
|
63
|
+
if (typeof payload.authorization_endpoint !== "string" || typeof payload.token_endpoint !== "string") {
|
|
64
|
+
throw new Error("xAI OAuth discovery response missing authorization/token endpoints");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
authorizationEndpoint: validateXaiEndpoint(payload.authorization_endpoint),
|
|
69
|
+
tokenEndpoint: validateXaiEndpoint(payload.token_endpoint),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function decodeJwtPayload(token: string): XaiJwtPayload | undefined {
|
|
74
|
+
const parts = token.split(".");
|
|
75
|
+
const payload = parts[1];
|
|
76
|
+
if (parts.length !== 3 || !payload) return undefined;
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) as XaiJwtPayload;
|
|
79
|
+
} catch {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getTokenIdentity(accessToken: string, idToken: string | undefined): { accountId?: string; email?: string } {
|
|
85
|
+
const payload = (idToken ? decodeJwtPayload(idToken) : undefined) ?? decodeJwtPayload(accessToken);
|
|
86
|
+
const accountId = typeof payload?.sub === "string" && payload.sub.length > 0 ? payload.sub : undefined;
|
|
87
|
+
const email =
|
|
88
|
+
typeof payload?.email === "string" && payload.email.length > 0 ? payload.email.toLowerCase() : undefined;
|
|
89
|
+
return { accountId, email };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function postXaiToken(
|
|
93
|
+
tokenEndpoint: string,
|
|
94
|
+
body: Record<string, string>,
|
|
95
|
+
signal?: AbortSignal,
|
|
96
|
+
): Promise<XaiTokenPayload> {
|
|
97
|
+
const response = await fetch(tokenEndpoint, {
|
|
98
|
+
method: "POST",
|
|
99
|
+
headers: {
|
|
100
|
+
Accept: "application/json",
|
|
101
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
102
|
+
},
|
|
103
|
+
body: new URLSearchParams(body).toString(),
|
|
104
|
+
signal: requestSignal(signal),
|
|
105
|
+
});
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
throw new Error(`xAI token request failed: ${response.status} ${await response.text()}`);
|
|
108
|
+
}
|
|
109
|
+
return (await response.json()) as XaiTokenPayload;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function credentialsFromTokenPayload(payload: XaiTokenPayload, refreshFallback = ""): OAuthCredentials {
|
|
113
|
+
if (typeof payload.access_token !== "string" || payload.access_token.length === 0) {
|
|
114
|
+
throw new Error("xAI token response did not include an access token");
|
|
115
|
+
}
|
|
116
|
+
const refresh =
|
|
117
|
+
typeof payload.refresh_token === "string" && payload.refresh_token.length > 0
|
|
118
|
+
? payload.refresh_token
|
|
119
|
+
: refreshFallback;
|
|
120
|
+
if (!refresh) {
|
|
121
|
+
throw new Error("xAI token response did not include a refresh token");
|
|
122
|
+
}
|
|
123
|
+
const expiresIn =
|
|
124
|
+
typeof payload.expires_in === "number" && Number.isFinite(payload.expires_in) ? payload.expires_in : 3600;
|
|
125
|
+
const idToken = typeof payload.id_token === "string" ? payload.id_token : undefined;
|
|
126
|
+
const { accountId, email } = getTokenIdentity(payload.access_token, idToken);
|
|
127
|
+
return {
|
|
128
|
+
refresh,
|
|
129
|
+
access: payload.access_token,
|
|
130
|
+
expires: Date.now() + expiresIn * 1000 - XAI_OAUTH_REFRESH_SKEW_MS,
|
|
131
|
+
accountId,
|
|
132
|
+
email,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export class XaiOAuthFlow extends OAuthCallbackFlow {
|
|
137
|
+
#verifier = "";
|
|
138
|
+
#discovery: XaiDiscovery | undefined;
|
|
139
|
+
|
|
140
|
+
constructor(ctrl: OAuthController) {
|
|
141
|
+
super(ctrl, {
|
|
142
|
+
preferredPort: XAI_OAUTH_CALLBACK_PORT,
|
|
143
|
+
callbackPath: XAI_OAUTH_CALLBACK_PATH,
|
|
144
|
+
callbackHostname: "127.0.0.1",
|
|
145
|
+
callbackBindHostname: "127.0.0.1",
|
|
146
|
+
redirectUri: `http://127.0.0.1:${XAI_OAUTH_CALLBACK_PORT}${XAI_OAUTH_CALLBACK_PATH}`,
|
|
147
|
+
} satisfies OAuthCallbackFlowOptions);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }> {
|
|
151
|
+
const pkce = await generatePKCE();
|
|
152
|
+
this.#verifier = pkce.verifier;
|
|
153
|
+
this.#discovery = await discoverXaiOAuthEndpoints(this.ctrl.signal);
|
|
154
|
+
const params = new URLSearchParams({
|
|
155
|
+
response_type: "code",
|
|
156
|
+
client_id: XAI_OAUTH_CLIENT_ID,
|
|
157
|
+
redirect_uri: redirectUri,
|
|
158
|
+
scope: XAI_OAUTH_SCOPE,
|
|
159
|
+
code_challenge: pkce.challenge,
|
|
160
|
+
code_challenge_method: "S256",
|
|
161
|
+
state,
|
|
162
|
+
nonce: crypto.randomUUID(),
|
|
163
|
+
});
|
|
164
|
+
return {
|
|
165
|
+
url: `${this.#discovery.authorizationEndpoint}?${params.toString()}`,
|
|
166
|
+
instructions:
|
|
167
|
+
"Complete xAI/Grok login in your browser. If the browser cannot reach this machine, paste the final redirect URL or authorization code when prompted.",
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async exchangeToken(code: string, _state: string, redirectUri: string): Promise<OAuthCredentials> {
|
|
172
|
+
if (!this.#verifier) {
|
|
173
|
+
throw new Error("xAI OAuth PKCE verifier was not initialized");
|
|
174
|
+
}
|
|
175
|
+
const discovery = this.#discovery ?? (await discoverXaiOAuthEndpoints(this.ctrl.signal));
|
|
176
|
+
const tokenPayload = await postXaiToken(
|
|
177
|
+
discovery.tokenEndpoint,
|
|
178
|
+
{
|
|
179
|
+
grant_type: "authorization_code",
|
|
180
|
+
client_id: XAI_OAUTH_CLIENT_ID,
|
|
181
|
+
code,
|
|
182
|
+
redirect_uri: redirectUri,
|
|
183
|
+
code_verifier: this.#verifier,
|
|
184
|
+
},
|
|
185
|
+
this.ctrl.signal,
|
|
186
|
+
);
|
|
187
|
+
return credentialsFromTokenPayload(tokenPayload);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function loginXai(ctrl: OAuthController): Promise<OAuthCredentials> {
|
|
192
|
+
return new XaiOAuthFlow(ctrl).login();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function refreshXaiToken(refreshToken: string, signal?: AbortSignal): Promise<OAuthCredentials> {
|
|
196
|
+
if (!refreshToken) {
|
|
197
|
+
throw new Error("xAI credentials are expired and do not include a refresh token");
|
|
198
|
+
}
|
|
199
|
+
const discovery = await discoverXaiOAuthEndpoints(signal);
|
|
200
|
+
const tokenPayload = await postXaiToken(
|
|
201
|
+
discovery.tokenEndpoint,
|
|
202
|
+
{
|
|
203
|
+
grant_type: "refresh_token",
|
|
204
|
+
client_id: XAI_OAUTH_CLIENT_ID,
|
|
205
|
+
refresh_token: refreshToken,
|
|
206
|
+
},
|
|
207
|
+
signal,
|
|
208
|
+
);
|
|
209
|
+
return credentialsFromTokenPayload(tokenPayload, refreshToken);
|
|
210
|
+
}
|