zelari-code 0.1.2 → 0.1.3
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 +4 -3
- package/dist/cli/app.js +18 -3
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/grokOAuth.js +215 -216
- package/dist/cli/grokOAuth.js.map +1 -1
- package/dist/cli/main.bundled.js +167 -319
- package/dist/cli/main.bundled.js.map +4 -4
- package/package.json +1 -1
package/dist/cli/main.bundled.js
CHANGED
|
@@ -10,276 +10,71 @@ var __export = (target, all) => {
|
|
|
10
10
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
// src/cli/oauthCallbackServer.ts
|
|
14
|
-
import { createServer } from "node:http";
|
|
15
|
-
function startOAuthCallbackServer(options = {}) {
|
|
16
|
-
const host = options.host ?? "127.0.0.1";
|
|
17
|
-
const preferredPort = options.port ?? 14523;
|
|
18
|
-
const timeoutMs = options.timeoutMs ?? 6e4;
|
|
19
|
-
const expectedPath = options.expectedPath ?? "/oauth/callback";
|
|
20
|
-
const successHtml = options.successHtml ?? DEFAULT_SUCCESS_HTML;
|
|
21
|
-
return new Promise((resolveStart, rejectStart) => {
|
|
22
|
-
let resolved = false;
|
|
23
|
-
let resolveCode = null;
|
|
24
|
-
let rejectCode = null;
|
|
25
|
-
let timeoutHandle = null;
|
|
26
|
-
const server = createServer((req, res) => {
|
|
27
|
-
try {
|
|
28
|
-
if (!req.url) {
|
|
29
|
-
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
30
|
-
res.end("Bad request");
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
const url2 = new URL(req.url, `http://${host}:${preferredPort}`);
|
|
34
|
-
if (url2.pathname !== expectedPath) {
|
|
35
|
-
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
36
|
-
res.end("Not found");
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
const code = url2.searchParams.get("code");
|
|
40
|
-
const state = url2.searchParams.get("state");
|
|
41
|
-
const errorParam = url2.searchParams.get("error");
|
|
42
|
-
const errorDescription = url2.searchParams.get("error_description");
|
|
43
|
-
const extras = {};
|
|
44
|
-
for (const [key, value] of url2.searchParams.entries()) {
|
|
45
|
-
if (key !== "code" && key !== "state" && key !== "error" && key !== "error_description") {
|
|
46
|
-
extras[key] = value;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
const params = {
|
|
50
|
-
...code ? { code } : {},
|
|
51
|
-
...state ? { state } : {},
|
|
52
|
-
...errorParam ? { error: errorParam } : {},
|
|
53
|
-
...errorDescription ? { errorDescription } : {},
|
|
54
|
-
extras
|
|
55
|
-
};
|
|
56
|
-
if (errorParam) {
|
|
57
|
-
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
58
|
-
res.end(`<h1>OAuth error: ${errorParam}</h1>`);
|
|
59
|
-
if (rejectCode && !resolved) {
|
|
60
|
-
resolved = true;
|
|
61
|
-
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
62
|
-
rejectCode(new OAuthCallbackError(`OAuth provider returned error: ${errorParam}`));
|
|
63
|
-
}
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
if (!code) {
|
|
67
|
-
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
68
|
-
res.end("<h1>Missing <code>code</code> query parameter</h1>");
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
72
|
-
res.end(successHtml);
|
|
73
|
-
if (resolveCode && !resolved) {
|
|
74
|
-
resolved = true;
|
|
75
|
-
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
76
|
-
resolveCode(params);
|
|
77
|
-
}
|
|
78
|
-
setTimeout(() => {
|
|
79
|
-
try {
|
|
80
|
-
server.close();
|
|
81
|
-
} catch {
|
|
82
|
-
}
|
|
83
|
-
}, 100);
|
|
84
|
-
} catch (err) {
|
|
85
|
-
try {
|
|
86
|
-
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
87
|
-
res.end("Internal error");
|
|
88
|
-
} catch {
|
|
89
|
-
}
|
|
90
|
-
if (!resolved && rejectCode) {
|
|
91
|
-
resolved = true;
|
|
92
|
-
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
93
|
-
rejectCode(err instanceof Error ? err : new Error(String(err)));
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
const onError = (err) => {
|
|
98
|
-
if (!resolved) {
|
|
99
|
-
resolved = true;
|
|
100
|
-
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
101
|
-
rejectStart(err);
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
server.once("error", onError);
|
|
105
|
-
server.listen(preferredPort, host, () => {
|
|
106
|
-
const addr = server.address();
|
|
107
|
-
const actualPort = addr ? addr.port : preferredPort;
|
|
108
|
-
const waitForCode = () => {
|
|
109
|
-
return new Promise((res, rej) => {
|
|
110
|
-
if (resolved) {
|
|
111
|
-
rej(new OAuthCallbackClosedError());
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
resolveCode = res;
|
|
115
|
-
rejectCode = rej;
|
|
116
|
-
timeoutHandle = setTimeout(() => {
|
|
117
|
-
if (!resolved) {
|
|
118
|
-
resolved = true;
|
|
119
|
-
try {
|
|
120
|
-
server.close();
|
|
121
|
-
} catch {
|
|
122
|
-
}
|
|
123
|
-
rej(new OAuthCallbackTimeoutError(timeoutMs));
|
|
124
|
-
}
|
|
125
|
-
}, timeoutMs);
|
|
126
|
-
if (typeof timeoutHandle?.unref === "function") timeoutHandle.unref();
|
|
127
|
-
});
|
|
128
|
-
};
|
|
129
|
-
const close = () => {
|
|
130
|
-
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
131
|
-
try {
|
|
132
|
-
server.close();
|
|
133
|
-
} catch {
|
|
134
|
-
}
|
|
135
|
-
if (!resolved && rejectCode) {
|
|
136
|
-
resolved = true;
|
|
137
|
-
rejectCode(new OAuthCallbackClosedError());
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
resolveStart({ port: actualPort, waitForCode, close });
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
var DEFAULT_SUCCESS_HTML, OAuthCallbackTimeoutError, OAuthCallbackClosedError, OAuthCallbackError;
|
|
145
|
-
var init_oauthCallbackServer = __esm({
|
|
146
|
-
"src/cli/oauthCallbackServer.ts"() {
|
|
147
|
-
"use strict";
|
|
148
|
-
DEFAULT_SUCCESS_HTML = `
|
|
149
|
-
<!DOCTYPE html>
|
|
150
|
-
<html lang="en">
|
|
151
|
-
<head>
|
|
152
|
-
<meta charset="UTF-8">
|
|
153
|
-
<title>zelari-code OAuth</title>
|
|
154
|
-
</head>
|
|
155
|
-
<body style="font-family: -apple-system, BlinkMacSystemFont, sans-serif; padding: 40px; text-align: center; color: #1a1a1a;">
|
|
156
|
-
<h1 style="color: #00aa33;">\u2713 Authentication complete</h1>
|
|
157
|
-
<p>You can close this tab and return to zelari-code.</p>
|
|
158
|
-
</body>
|
|
159
|
-
</html>
|
|
160
|
-
`.trim();
|
|
161
|
-
OAuthCallbackTimeoutError = class extends Error {
|
|
162
|
-
constructor(timeoutMs) {
|
|
163
|
-
super(`OAuth callback not received within ${timeoutMs}ms`);
|
|
164
|
-
this.name = "OAuthCallbackTimeoutError";
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
OAuthCallbackClosedError = class extends Error {
|
|
168
|
-
constructor() {
|
|
169
|
-
super("OAuth callback server closed before receiving a callback");
|
|
170
|
-
this.name = "OAuthCallbackClosedError";
|
|
171
|
-
}
|
|
172
|
-
};
|
|
173
|
-
OAuthCallbackError = class extends Error {
|
|
174
|
-
constructor(message) {
|
|
175
|
-
super(message);
|
|
176
|
-
this.name = "OAuthCallbackError";
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
|
|
182
13
|
// src/cli/grokOAuth.ts
|
|
183
|
-
|
|
184
|
-
async function fetchGrokDiscovery(options = {}) {
|
|
14
|
+
async function requestDeviceCode(options) {
|
|
185
15
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
186
|
-
const
|
|
187
|
-
let response;
|
|
188
|
-
try {
|
|
189
|
-
response = await fetchImpl(discoveryUrl, { method: "GET", headers: { Accept: "application/json" } });
|
|
190
|
-
} catch (err) {
|
|
191
|
-
throw new GrokOAuthError(`OAuth discovery network error: ${err instanceof Error ? err.message : String(err)}`, "discovery_network_error");
|
|
192
|
-
}
|
|
193
|
-
if (!response.ok) {
|
|
194
|
-
const text = await response.text().catch(() => "");
|
|
195
|
-
throw new GrokOAuthError(
|
|
196
|
-
`OAuth discovery HTTP ${response.status}: ${text.slice(0, 200)}`,
|
|
197
|
-
`discovery_http_${response.status}`
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
let body;
|
|
201
|
-
try {
|
|
202
|
-
body = await response.json();
|
|
203
|
-
} catch (err) {
|
|
204
|
-
throw new GrokOAuthError(`OAuth discovery returned invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
205
|
-
}
|
|
206
|
-
if (!body || typeof body !== "object") {
|
|
207
|
-
throw new GrokOAuthError("OAuth discovery returned non-object body");
|
|
208
|
-
}
|
|
209
|
-
const obj = body;
|
|
210
|
-
if (typeof obj.authorization_endpoint !== "string" || typeof obj.token_endpoint !== "string") {
|
|
211
|
-
throw new GrokOAuthError("OAuth discovery missing authorization_endpoint or token_endpoint");
|
|
212
|
-
}
|
|
213
|
-
return {
|
|
214
|
-
authorizationEndpoint: obj.authorization_endpoint,
|
|
215
|
-
tokenEndpoint: obj.token_endpoint
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
function generatePkce() {
|
|
219
|
-
const random = getRandomValues(new Uint8Array(64));
|
|
220
|
-
const verifier = Buffer.from(random).toString("base64url").slice(0, 128);
|
|
221
|
-
const hash2 = createHash("sha256").update(verifier).digest();
|
|
222
|
-
const challenge = hash2.toString("base64url").replace(/=+$/, "");
|
|
223
|
-
return { verifier, challenge };
|
|
224
|
-
}
|
|
225
|
-
function buildGrokAuthorizeUrl(options) {
|
|
16
|
+
const endpoint = options.deviceCodeEndpoint ?? DEFAULT_DEVICE_CODE_ENDPOINT;
|
|
226
17
|
const scopes = options.scopes ?? DEFAULT_GROK_OAUTH_SCOPES;
|
|
227
|
-
const params = new URLSearchParams({
|
|
228
|
-
client_id: options.clientId,
|
|
229
|
-
redirect_uri: options.redirectUri,
|
|
230
|
-
response_type: "code",
|
|
231
|
-
scope: scopes.join(" "),
|
|
232
|
-
state: options.state ?? crypto.randomUUID(),
|
|
233
|
-
code_challenge: options.codeChallenge,
|
|
234
|
-
code_challenge_method: "S256"
|
|
235
|
-
});
|
|
236
|
-
return `${options.authorizeEndpoint}?${params.toString()}`;
|
|
237
|
-
}
|
|
238
|
-
async function exchangeGrokCode(options) {
|
|
239
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
240
18
|
let response;
|
|
241
19
|
try {
|
|
242
|
-
response = await fetchImpl(
|
|
20
|
+
response = await fetchImpl(endpoint, {
|
|
243
21
|
method: "POST",
|
|
244
22
|
headers: {
|
|
245
23
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
246
24
|
Accept: "application/json"
|
|
247
25
|
},
|
|
248
26
|
body: new URLSearchParams({
|
|
249
|
-
grant_type: "authorization_code",
|
|
250
27
|
client_id: options.clientId,
|
|
251
|
-
|
|
252
|
-
redirect_uri: options.redirectUri,
|
|
253
|
-
code_verifier: options.codeVerifier
|
|
28
|
+
scope: scopes.join(" ")
|
|
254
29
|
}).toString()
|
|
255
30
|
});
|
|
256
31
|
} catch (err) {
|
|
257
|
-
throw new GrokOAuthError(
|
|
32
|
+
throw new GrokOAuthError(
|
|
33
|
+
`Device code request network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
34
|
+
"device_code_network_error"
|
|
35
|
+
);
|
|
258
36
|
}
|
|
259
37
|
if (!response.ok) {
|
|
260
38
|
const errText = await response.text().catch(() => "");
|
|
261
39
|
throw new GrokOAuthError(
|
|
262
|
-
`
|
|
263
|
-
`
|
|
40
|
+
`Device code request HTTP ${response.status}: ${errText.slice(0, 200)}`,
|
|
41
|
+
`device_code_http_${response.status}`
|
|
264
42
|
);
|
|
265
43
|
}
|
|
266
44
|
let body;
|
|
267
45
|
try {
|
|
268
46
|
body = await response.json();
|
|
269
47
|
} catch (err) {
|
|
270
|
-
throw new GrokOAuthError(
|
|
48
|
+
throw new GrokOAuthError(
|
|
49
|
+
`Device code response invalid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
50
|
+
);
|
|
271
51
|
}
|
|
272
52
|
if (!body || typeof body !== "object") {
|
|
273
|
-
throw new GrokOAuthError("
|
|
53
|
+
throw new GrokOAuthError("Device code response is not an object");
|
|
274
54
|
}
|
|
275
55
|
const obj = body;
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
56
|
+
const deviceCode = obj.device_code;
|
|
57
|
+
const userCode = obj.user_code;
|
|
58
|
+
const verificationUri = obj.verification_uri;
|
|
59
|
+
if (typeof deviceCode !== "string" || deviceCode.length === 0) {
|
|
60
|
+
throw new GrokOAuthError("Device code response missing device_code", "no_device_code");
|
|
61
|
+
}
|
|
62
|
+
if (typeof userCode !== "string" || userCode.length === 0) {
|
|
63
|
+
throw new GrokOAuthError("Device code response missing user_code", "no_user_code");
|
|
64
|
+
}
|
|
65
|
+
if (typeof verificationUri !== "string" || verificationUri.length === 0) {
|
|
66
|
+
throw new GrokOAuthError("Device code response missing verification_uri", "no_verification_uri");
|
|
279
67
|
}
|
|
280
|
-
return
|
|
68
|
+
return {
|
|
69
|
+
deviceCode,
|
|
70
|
+
userCode,
|
|
71
|
+
verificationUri,
|
|
72
|
+
...typeof obj.verification_uri_complete === "string" && obj.verification_uri_complete.length > 0 ? { verificationUriComplete: obj.verification_uri_complete } : {},
|
|
73
|
+
expiresIn: typeof obj.expires_in === "number" && Number.isFinite(obj.expires_in) ? obj.expires_in : 1800,
|
|
74
|
+
interval: typeof obj.interval === "number" && Number.isFinite(obj.interval) ? obj.interval : 5
|
|
75
|
+
};
|
|
281
76
|
}
|
|
282
|
-
function parseTokenResponseBody(obj, accessToken
|
|
77
|
+
function parseTokenResponseBody(obj, accessToken) {
|
|
283
78
|
const result = { accessToken };
|
|
284
79
|
if (typeof obj.expires_in === "number" && Number.isFinite(obj.expires_in)) {
|
|
285
80
|
result.expiresAt = Date.now() + obj.expires_in * 1e3;
|
|
@@ -292,6 +87,116 @@ function parseTokenResponseBody(obj, accessToken, label) {
|
|
|
292
87
|
}
|
|
293
88
|
return result;
|
|
294
89
|
}
|
|
90
|
+
async function pollForDeviceToken(options) {
|
|
91
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
92
|
+
const sleep = options.sleepImpl ?? defaultSleep;
|
|
93
|
+
const endpoint = options.tokenEndpoint ?? DEFAULT_TOKEN_ENDPOINT;
|
|
94
|
+
const deadline = Date.now() + options.timeoutMs;
|
|
95
|
+
let interval = Math.max(options.interval, 1);
|
|
96
|
+
while (true) {
|
|
97
|
+
if (Date.now() >= deadline) {
|
|
98
|
+
throw new GrokOAuthError("Device authorization timed out waiting for user approval", "timeout");
|
|
99
|
+
}
|
|
100
|
+
let response;
|
|
101
|
+
try {
|
|
102
|
+
response = await fetchImpl(endpoint, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: {
|
|
105
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
106
|
+
Accept: "application/json"
|
|
107
|
+
},
|
|
108
|
+
body: new URLSearchParams({
|
|
109
|
+
grant_type: DEVICE_GRANT_TYPE,
|
|
110
|
+
client_id: options.clientId,
|
|
111
|
+
device_code: options.deviceCode
|
|
112
|
+
}).toString()
|
|
113
|
+
});
|
|
114
|
+
} catch (err) {
|
|
115
|
+
throw new GrokOAuthError(
|
|
116
|
+
`Token poll network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
117
|
+
"poll_network_error"
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (response.ok) {
|
|
121
|
+
let body;
|
|
122
|
+
try {
|
|
123
|
+
body = await response.json();
|
|
124
|
+
} catch (err) {
|
|
125
|
+
throw new GrokOAuthError(
|
|
126
|
+
`Token response invalid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
if (!body || typeof body !== "object") {
|
|
130
|
+
throw new GrokOAuthError("Token response is not an object");
|
|
131
|
+
}
|
|
132
|
+
const obj = body;
|
|
133
|
+
const accessToken = obj.access_token;
|
|
134
|
+
if (typeof accessToken !== "string" || accessToken.length === 0) {
|
|
135
|
+
throw new GrokOAuthError("Token response missing access_token", "no_access_token");
|
|
136
|
+
}
|
|
137
|
+
return parseTokenResponseBody(obj, accessToken);
|
|
138
|
+
}
|
|
139
|
+
let errBody = {};
|
|
140
|
+
try {
|
|
141
|
+
const parsed = await response.json();
|
|
142
|
+
if (parsed && typeof parsed === "object") errBody = parsed;
|
|
143
|
+
} catch {
|
|
144
|
+
}
|
|
145
|
+
const errorCode = typeof errBody.error === "string" ? errBody.error : `http_${response.status}`;
|
|
146
|
+
switch (errorCode) {
|
|
147
|
+
case "authorization_pending":
|
|
148
|
+
await sleep(interval * 1e3);
|
|
149
|
+
continue;
|
|
150
|
+
case "slow_down":
|
|
151
|
+
interval += 5;
|
|
152
|
+
await sleep(interval * 1e3);
|
|
153
|
+
continue;
|
|
154
|
+
case "expired_token":
|
|
155
|
+
case "expired":
|
|
156
|
+
throw new GrokOAuthError("Device code expired before user authorized", "expired");
|
|
157
|
+
case "access_denied":
|
|
158
|
+
case "denied":
|
|
159
|
+
throw new GrokOAuthError("User denied the authorization request", "denied");
|
|
160
|
+
case "invalid_grant":
|
|
161
|
+
throw new GrokOAuthError("Device code rejected (invalid_grant)", "invalid_grant");
|
|
162
|
+
default:
|
|
163
|
+
throw new GrokOAuthError(
|
|
164
|
+
`Token poll error: ${errorCode}${typeof errBody.error_description === "string" ? ` \u2014 ${errBody.error_description}` : ""}`,
|
|
165
|
+
errorCode
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function runGrokOAuthFlow(options = {}) {
|
|
171
|
+
const clientId = options.clientId || process.env.GROK_OAUTH_CLIENT_ID || DEFAULT_GROK_OAUTH_CLIENT_ID;
|
|
172
|
+
if (!clientId || clientId.trim().length === 0) {
|
|
173
|
+
throw new GrokOAuthError("Missing clientId", "no_client_id");
|
|
174
|
+
}
|
|
175
|
+
const deviceAuth = await requestDeviceCode({
|
|
176
|
+
clientId,
|
|
177
|
+
scopes: options.scopes,
|
|
178
|
+
deviceCodeEndpoint: options.deviceCodeEndpoint,
|
|
179
|
+
fetchImpl: options.fetchImpl
|
|
180
|
+
});
|
|
181
|
+
if (options.onUserCode) {
|
|
182
|
+
await options.onUserCode(deviceAuth);
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
await openBrowser(deviceAuth.verificationUriComplete ?? deviceAuth.verificationUri);
|
|
186
|
+
} catch {
|
|
187
|
+
}
|
|
188
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_OAUTH_TIMEOUT_MS;
|
|
189
|
+
const effectiveTimeout = Math.min(timeoutMs, deviceAuth.expiresIn * 1e3);
|
|
190
|
+
return pollForDeviceToken({
|
|
191
|
+
clientId,
|
|
192
|
+
deviceCode: deviceAuth.deviceCode,
|
|
193
|
+
interval: deviceAuth.interval,
|
|
194
|
+
timeoutMs: effectiveTimeout,
|
|
195
|
+
tokenEndpoint: options.tokenEndpoint,
|
|
196
|
+
fetchImpl: options.fetchImpl,
|
|
197
|
+
sleepImpl: options.sleepImpl
|
|
198
|
+
});
|
|
199
|
+
}
|
|
295
200
|
async function refreshGrokToken(options) {
|
|
296
201
|
if (!options.clientId || options.clientId.trim().length === 0) {
|
|
297
202
|
throw new GrokOAuthError("Missing clientId", "no_client_id");
|
|
@@ -299,7 +204,7 @@ async function refreshGrokToken(options) {
|
|
|
299
204
|
if (!options.refreshToken || options.refreshToken.trim().length === 0) {
|
|
300
205
|
throw new GrokOAuthError("Missing refreshToken", "no_refresh_token");
|
|
301
206
|
}
|
|
302
|
-
const endpoint = options.tokenEndpoint ??
|
|
207
|
+
const endpoint = options.tokenEndpoint ?? DEFAULT_TOKEN_ENDPOINT;
|
|
303
208
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
304
209
|
let response;
|
|
305
210
|
try {
|
|
@@ -340,77 +245,7 @@ async function refreshGrokToken(options) {
|
|
|
340
245
|
if (typeof accessToken !== "string" || accessToken.length === 0) {
|
|
341
246
|
throw new GrokOAuthError("Token refresh response missing access_token", "no_access_token");
|
|
342
247
|
}
|
|
343
|
-
return parseTokenResponseBody(obj, accessToken
|
|
344
|
-
}
|
|
345
|
-
async function runGrokOAuthFlow(options) {
|
|
346
|
-
const clientId = options.clientId || process.env.GROK_OAUTH_CLIENT_ID || DEFAULT_GROK_OAUTH_CLIENT_ID;
|
|
347
|
-
if (!clientId || clientId.trim().length === 0) {
|
|
348
|
-
throw new GrokOAuthError("Missing clientId", "no_client_id");
|
|
349
|
-
}
|
|
350
|
-
const callbackPort = options.callbackPort ?? DEFAULT_GROK_OAUTH_REDIRECT_PORT;
|
|
351
|
-
const callbackPath = options.callbackPath ?? DEFAULT_GROK_OAUTH_REDIRECT_PATH;
|
|
352
|
-
const redirectUri = `http://127.0.0.1:${callbackPort}${callbackPath}`;
|
|
353
|
-
const discovery = await fetchGrokDiscovery({
|
|
354
|
-
issuer: options.issuer,
|
|
355
|
-
discoveryUrl: options.discoveryUrl,
|
|
356
|
-
fetchImpl: options.fetchImpl
|
|
357
|
-
});
|
|
358
|
-
const { verifier: codeVerifier, challenge: codeChallenge } = generatePkce();
|
|
359
|
-
const state = crypto.randomUUID();
|
|
360
|
-
const authorizeUrl = buildGrokAuthorizeUrl({
|
|
361
|
-
clientId,
|
|
362
|
-
redirectUri,
|
|
363
|
-
scopes: options.scopes,
|
|
364
|
-
state,
|
|
365
|
-
codeChallenge,
|
|
366
|
-
authorizeEndpoint: discovery.authorizationEndpoint
|
|
367
|
-
});
|
|
368
|
-
let handle;
|
|
369
|
-
try {
|
|
370
|
-
handle = await startOAuthCallbackServer({
|
|
371
|
-
port: callbackPort,
|
|
372
|
-
timeoutMs: options.callbackTimeoutMs ?? 12e4,
|
|
373
|
-
expectedPath: callbackPath
|
|
374
|
-
});
|
|
375
|
-
} catch (err) {
|
|
376
|
-
throw new GrokOAuthError(
|
|
377
|
-
`Failed to start OAuth callback server on port ${callbackPort}: ${err instanceof Error ? err.message : String(err)}`,
|
|
378
|
-
"callback_bind_failed"
|
|
379
|
-
);
|
|
380
|
-
}
|
|
381
|
-
try {
|
|
382
|
-
if (options.onBrowserOpen) {
|
|
383
|
-
await options.onBrowserOpen(authorizeUrl);
|
|
384
|
-
} else {
|
|
385
|
-
await openBrowser(authorizeUrl);
|
|
386
|
-
}
|
|
387
|
-
const callbackResult = await handle.waitForCode();
|
|
388
|
-
if (callbackResult.state !== state) {
|
|
389
|
-
throw new GrokOAuthError(
|
|
390
|
-
`OAuth state mismatch \u2014 possible CSRF. expected=${state} got=${callbackResult.state ?? "(none)"}`,
|
|
391
|
-
"state_mismatch"
|
|
392
|
-
);
|
|
393
|
-
}
|
|
394
|
-
if (callbackResult.error) {
|
|
395
|
-
throw new GrokOAuthError(
|
|
396
|
-
`OAuth provider returned error: ${callbackResult.error}${callbackResult.errorDescription ? ` \u2014 ${callbackResult.errorDescription}` : ""}`,
|
|
397
|
-
callbackResult.error
|
|
398
|
-
);
|
|
399
|
-
}
|
|
400
|
-
if (!callbackResult.code) {
|
|
401
|
-
throw new GrokOAuthError("OAuth callback did not include authorization code", "no_code");
|
|
402
|
-
}
|
|
403
|
-
return await exchangeGrokCode({
|
|
404
|
-
clientId,
|
|
405
|
-
code: callbackResult.code,
|
|
406
|
-
codeVerifier,
|
|
407
|
-
redirectUri,
|
|
408
|
-
tokenEndpoint: discovery.tokenEndpoint,
|
|
409
|
-
fetchImpl: options.fetchImpl
|
|
410
|
-
});
|
|
411
|
-
} finally {
|
|
412
|
-
handle.close();
|
|
413
|
-
}
|
|
248
|
+
return parseTokenResponseBody(obj, accessToken);
|
|
414
249
|
}
|
|
415
250
|
async function openBrowser(url2) {
|
|
416
251
|
const { spawn: spawn3 } = await import("node:child_process");
|
|
@@ -438,12 +273,10 @@ async function openBrowser(url2) {
|
|
|
438
273
|
}
|
|
439
274
|
});
|
|
440
275
|
}
|
|
441
|
-
var
|
|
276
|
+
var GrokOAuthError, DEFAULT_GROK_OAUTH_CLIENT_ID, DEFAULT_GROK_OAUTH_SCOPES, DEFAULT_DEVICE_CODE_ENDPOINT, DEFAULT_TOKEN_ENDPOINT, DEVICE_GRANT_TYPE, DEFAULT_OAUTH_TIMEOUT_MS, defaultSleep;
|
|
442
277
|
var init_grokOAuth = __esm({
|
|
443
278
|
"src/cli/grokOAuth.ts"() {
|
|
444
279
|
"use strict";
|
|
445
|
-
init_oauthCallbackServer();
|
|
446
|
-
({ getRandomValues, createHash } = nodeCrypto);
|
|
447
280
|
GrokOAuthError = class extends Error {
|
|
448
281
|
constructor(message, code) {
|
|
449
282
|
super(message);
|
|
@@ -452,7 +285,6 @@ var init_grokOAuth = __esm({
|
|
|
452
285
|
}
|
|
453
286
|
};
|
|
454
287
|
DEFAULT_GROK_OAUTH_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
|
|
455
|
-
DEFAULT_GROK_OAUTH_ISSUER = "https://auth.x.ai";
|
|
456
288
|
DEFAULT_GROK_OAUTH_SCOPES = [
|
|
457
289
|
"openid",
|
|
458
290
|
"profile",
|
|
@@ -461,8 +293,11 @@ var init_grokOAuth = __esm({
|
|
|
461
293
|
"grok-cli:access",
|
|
462
294
|
"api:access"
|
|
463
295
|
];
|
|
464
|
-
|
|
465
|
-
|
|
296
|
+
DEFAULT_DEVICE_CODE_ENDPOINT = "https://auth.x.ai/oauth2/device/code";
|
|
297
|
+
DEFAULT_TOKEN_ENDPOINT = "https://auth.x.ai/oauth2/token";
|
|
298
|
+
DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
299
|
+
DEFAULT_OAUTH_TIMEOUT_MS = 3e5;
|
|
300
|
+
defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
466
301
|
}
|
|
467
302
|
});
|
|
468
303
|
|
|
@@ -21219,14 +21054,27 @@ ${lines.join("\n")}`,
|
|
|
21219
21054
|
{
|
|
21220
21055
|
id: crypto.randomUUID(),
|
|
21221
21056
|
role: "system",
|
|
21222
|
-
content: "[login oauth]
|
|
21057
|
+
content: "[login oauth] requesting device code from xAI...",
|
|
21223
21058
|
ts: Date.now()
|
|
21224
21059
|
}
|
|
21225
21060
|
]);
|
|
21226
21061
|
setBusy(true);
|
|
21227
21062
|
try {
|
|
21228
21063
|
const resultOAuth = await runGrokOAuthFlow({
|
|
21229
|
-
|
|
21064
|
+
// Show the user_code + verification_uri as soon as xAI returns them.
|
|
21065
|
+
onUserCode: (info) => {
|
|
21066
|
+
setMessages((prev) => [
|
|
21067
|
+
...prev,
|
|
21068
|
+
{
|
|
21069
|
+
id: crypto.randomUUID(),
|
|
21070
|
+
role: "system",
|
|
21071
|
+
content: `[login oauth] Open ${info.verificationUri} in your browser and enter the code:
|
|
21072
|
+
${info.userCode}
|
|
21073
|
+
(Opening your browser automatically...)`,
|
|
21074
|
+
ts: Date.now()
|
|
21075
|
+
}
|
|
21076
|
+
]);
|
|
21077
|
+
}
|
|
21230
21078
|
});
|
|
21231
21079
|
setOAuthToken("grok", {
|
|
21232
21080
|
apiKey: resultOAuth.accessToken,
|