@kenkaiiii/gg-core 5.94.0 → 5.95.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/README.md +11 -11
- package/dist/{chunk-JLEBGMG4.js → chunk-J7W5HS6X.js} +622 -1105
- package/dist/chunk-J7W5HS6X.js.map +1 -0
- package/dist/index.cjs +816 -1310
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +165 -157
- package/dist/index.d.ts +165 -157
- package/dist/index.js +153 -160
- package/dist/index.js.map +1 -1
- package/dist/model-registry.cjs +57 -75
- package/dist/model-registry.cjs.map +1 -1
- package/dist/model-registry.d.cts +11 -15
- package/dist/model-registry.d.ts +11 -15
- package/dist/model-registry.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-JLEBGMG4.js.map +0 -1
|
@@ -2,244 +2,49 @@ import {
|
|
|
2
2
|
getAppPaths
|
|
3
3
|
} from "./chunk-GXOFTHM5.js";
|
|
4
4
|
|
|
5
|
-
// src/
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// src/oauth/request-timeout.ts
|
|
13
|
-
var OAUTH_REQUEST_TIMEOUT_MS = 3e4;
|
|
14
|
-
function oauthRequestSignal() {
|
|
15
|
-
return AbortSignal.timeout(OAUTH_REQUEST_TIMEOUT_MS);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// src/oauth/kimi.ts
|
|
19
|
-
var CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098";
|
|
20
|
-
var DEFAULT_OAUTH_HOST = "https://auth.kimi.com";
|
|
21
|
-
var DEFAULT_CODING_BASE_URL = "https://api.kimi.com/coding/v1";
|
|
22
|
-
var KIMI_PLATFORM = "kimi_code_cli";
|
|
23
|
-
var DEFAULT_KIMI_VERSION = "1.0.11";
|
|
24
|
-
var DEVICE_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
25
|
-
function oauthHost() {
|
|
26
|
-
const host = process.env.KIMI_CODE_OAUTH_HOST ?? process.env.KIMI_OAUTH_HOST ?? DEFAULT_OAUTH_HOST;
|
|
27
|
-
return host.replace(/\/+$/, "");
|
|
28
|
-
}
|
|
29
|
-
function kimiCodeBaseUrl() {
|
|
30
|
-
return (process.env.KIMI_CODE_BASE_URL ?? DEFAULT_CODING_BASE_URL).replace(/\/+$/, "");
|
|
31
|
-
}
|
|
32
|
-
function kimiVersion() {
|
|
33
|
-
const v = process.env.KIMI_CODE_VERSION ?? DEFAULT_KIMI_VERSION;
|
|
34
|
-
return asciiHeader(v, DEFAULT_KIMI_VERSION);
|
|
35
|
-
}
|
|
36
|
-
function asciiHeader(value, fallback = "unknown") {
|
|
37
|
-
const cleaned = value.replace(/[^\u0020-\u007E]/g, "").trim();
|
|
38
|
-
return cleaned.length > 0 ? cleaned : fallback;
|
|
39
|
-
}
|
|
40
|
-
function macOsProductVersion() {
|
|
5
|
+
// src/kimi-region.ts
|
|
6
|
+
import { GG_CODER_USER_AGENT } from "@kenkaiiii/gg-ai";
|
|
7
|
+
var KIMI_CODE_DEFAULT_BASE_URL = "https://api.kimi.com/coding/v1";
|
|
8
|
+
var KIMI_CODE_OVERSEAS_BASE_URL = "https://api.kimi.ai/coding/v1";
|
|
9
|
+
var KIMI_CODE_BASE_URLS = [KIMI_CODE_DEFAULT_BASE_URL, KIMI_CODE_OVERSEAS_BASE_URL];
|
|
10
|
+
async function probeHost(baseUrl, apiKey, fetchImpl, timeoutMs) {
|
|
41
11
|
try {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
function deviceModel() {
|
|
52
|
-
const os = type();
|
|
53
|
-
const version = release();
|
|
54
|
-
const osArch = arch();
|
|
55
|
-
if (os === "Darwin") return `macOS ${macOsProductVersion() ?? version} ${osArch}`;
|
|
56
|
-
if (os === "Windows_NT") return `Windows ${version} ${osArch}`;
|
|
57
|
-
return `${os} ${version} ${osArch}`.trim();
|
|
58
|
-
}
|
|
59
|
-
function deviceId() {
|
|
60
|
-
const idPath = path.join(getAppPaths().agentDir, "kimi_device_id");
|
|
61
|
-
if (existsSync(idPath)) {
|
|
62
|
-
try {
|
|
63
|
-
const text = readFileSync(idPath, "utf-8").trim();
|
|
64
|
-
if (text.length > 0) return text;
|
|
65
|
-
} catch {
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
const id = randomUUID();
|
|
69
|
-
try {
|
|
70
|
-
mkdirSync(getAppPaths().agentDir, { recursive: true, mode: 448 });
|
|
71
|
-
writeFileSync(idPath, id, { encoding: "utf-8", mode: 384 });
|
|
12
|
+
const res = await fetchImpl(`${baseUrl}/models`, {
|
|
13
|
+
headers: { Authorization: `Bearer ${apiKey}`, "User-Agent": GG_CODER_USER_AGENT },
|
|
14
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
15
|
+
});
|
|
16
|
+
if (res.ok) return "authorized";
|
|
17
|
+
if (res.status === 401 || res.status === 403) return "unauthorized";
|
|
18
|
+
return "inconclusive";
|
|
72
19
|
} catch {
|
|
20
|
+
return "inconclusive";
|
|
73
21
|
}
|
|
74
|
-
return id;
|
|
75
|
-
}
|
|
76
|
-
function deviceHeaders() {
|
|
77
|
-
return {
|
|
78
|
-
"X-Msh-Platform": KIMI_PLATFORM,
|
|
79
|
-
"X-Msh-Version": kimiVersion(),
|
|
80
|
-
"X-Msh-Device-Name": asciiHeader(hostname()),
|
|
81
|
-
"X-Msh-Device-Model": asciiHeader(deviceModel()),
|
|
82
|
-
"X-Msh-Os-Version": asciiHeader(release()),
|
|
83
|
-
"X-Msh-Device-Id": deviceId()
|
|
84
|
-
};
|
|
85
22
|
}
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
23
|
+
async function detectKimiCodeBaseUrl(apiKey, options = {}) {
|
|
24
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
25
|
+
const timeoutMs = options.timeoutMs ?? 5e3;
|
|
26
|
+
const results = await Promise.all(
|
|
27
|
+
KIMI_CODE_BASE_URLS.map((url) => probeHost(url, apiKey, fetchImpl, timeoutMs))
|
|
28
|
+
);
|
|
29
|
+
const index = results.indexOf("authorized");
|
|
30
|
+
return index === -1 ? void 0 : KIMI_CODE_BASE_URLS[index];
|
|
91
31
|
}
|
|
92
32
|
function isKimiCodingEndpoint(baseUrl) {
|
|
93
33
|
if (typeof baseUrl !== "string" || baseUrl.length === 0) return false;
|
|
94
|
-
|
|
95
|
-
return normalized === kimiCodeBaseUrl() || /(^|\.)kimi\.com/i.test(normalized);
|
|
96
|
-
}
|
|
97
|
-
async function postForm(endpoint, params) {
|
|
98
|
-
const response = await fetch(`${oauthHost()}${endpoint}`, {
|
|
99
|
-
method: "POST",
|
|
100
|
-
headers: {
|
|
101
|
-
...deviceHeaders(),
|
|
102
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
103
|
-
Accept: "application/json"
|
|
104
|
-
},
|
|
105
|
-
body: new URLSearchParams(params).toString(),
|
|
106
|
-
signal: oauthRequestSignal()
|
|
107
|
-
});
|
|
108
|
-
let data = {};
|
|
109
|
-
try {
|
|
110
|
-
const parsed = await response.json();
|
|
111
|
-
if (parsed && typeof parsed === "object") data = parsed;
|
|
112
|
-
} catch {
|
|
113
|
-
}
|
|
114
|
-
return { status: response.status, data };
|
|
115
|
-
}
|
|
116
|
-
function errorDetail(data) {
|
|
117
|
-
const desc = data.error_description ?? data.message ?? data.error;
|
|
118
|
-
return typeof desc === "string" && desc.length > 0 ? desc : "unknown error";
|
|
119
|
-
}
|
|
120
|
-
function credsFromTokenResponse(data, opts) {
|
|
121
|
-
const accessToken = data.access_token;
|
|
122
|
-
const responseRefreshToken = data.refresh_token;
|
|
123
|
-
const expiresIn = Number(data.expires_in);
|
|
124
|
-
if (typeof accessToken !== "string" || accessToken.length === 0) {
|
|
125
|
-
throw new Error("Kimi OAuth response missing access_token.");
|
|
126
|
-
}
|
|
127
|
-
const refreshToken = typeof responseRefreshToken === "string" && responseRefreshToken.length > 0 ? responseRefreshToken : opts?.fallbackRefreshToken ?? "";
|
|
128
|
-
if (refreshToken.length === 0) {
|
|
129
|
-
throw new Error("Kimi OAuth response missing refresh_token.");
|
|
130
|
-
}
|
|
131
|
-
if (!Number.isFinite(expiresIn) || expiresIn <= 0) {
|
|
132
|
-
throw new Error("Kimi OAuth response missing or invalid expires_in.");
|
|
133
|
-
}
|
|
134
|
-
return {
|
|
135
|
-
accessToken,
|
|
136
|
-
refreshToken,
|
|
137
|
-
expiresAt: Date.now() + expiresIn * 1e3,
|
|
138
|
-
expiresIn,
|
|
139
|
-
baseUrl: kimiCodeBaseUrl()
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
async function requestDeviceAuthorization() {
|
|
143
|
-
const { status, data } = await postForm("/api/oauth/device_authorization", {
|
|
144
|
-
client_id: CLIENT_ID
|
|
145
|
-
});
|
|
146
|
-
if (status !== 200) {
|
|
147
|
-
throw new Error(`Kimi device authorization failed (${status}): ${errorDetail(data)}`);
|
|
148
|
-
}
|
|
149
|
-
const userCode = data.user_code;
|
|
150
|
-
const deviceCode = data.device_code;
|
|
151
|
-
const verificationUriComplete = data.verification_uri_complete;
|
|
152
|
-
if (typeof userCode !== "string" || typeof deviceCode !== "string") {
|
|
153
|
-
throw new Error("Kimi device authorization response missing user_code/device_code.");
|
|
154
|
-
}
|
|
155
|
-
return {
|
|
156
|
-
userCode,
|
|
157
|
-
deviceCode,
|
|
158
|
-
verificationUri: typeof data.verification_uri === "string" ? data.verification_uri : "",
|
|
159
|
-
verificationUriComplete: typeof verificationUriComplete === "string" ? verificationUriComplete : "",
|
|
160
|
-
interval: Number(data.interval ?? 5) || 5
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
async function pollDeviceToken(deviceCode) {
|
|
164
|
-
const { status, data } = await postForm("/api/oauth/token", {
|
|
165
|
-
client_id: CLIENT_ID,
|
|
166
|
-
device_code: deviceCode,
|
|
167
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
168
|
-
});
|
|
169
|
-
if (status === 200 && typeof data.access_token === "string") {
|
|
170
|
-
return { kind: "success", creds: credsFromTokenResponse(data) };
|
|
171
|
-
}
|
|
172
|
-
if (status >= 500) {
|
|
173
|
-
throw new Error(`Kimi token polling server error (${status}): ${errorDetail(data)}`);
|
|
174
|
-
}
|
|
175
|
-
const errorCode = typeof data.error === "string" ? data.error : "unknown_error";
|
|
176
|
-
switch (errorCode) {
|
|
177
|
-
case "authorization_pending":
|
|
178
|
-
return { kind: "pending" };
|
|
179
|
-
case "slow_down":
|
|
180
|
-
return { kind: "slow_down" };
|
|
181
|
-
case "expired_token":
|
|
182
|
-
return { kind: "expired" };
|
|
183
|
-
case "access_denied":
|
|
184
|
-
return { kind: "denied" };
|
|
185
|
-
default:
|
|
186
|
-
throw new Error(`Kimi token polling failed (${status}): ${errorDetail(data)}`);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
function sleep(ms) {
|
|
190
|
-
return new Promise((resolve) => {
|
|
191
|
-
setTimeout(resolve, ms);
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
async function loginKimi(callbacks) {
|
|
195
|
-
const auth = await requestDeviceAuthorization();
|
|
196
|
-
callbacks.onStatus(
|
|
197
|
-
`Visit ${auth.verificationUri || auth.verificationUriComplete} and enter code: ${auth.userCode}`
|
|
198
|
-
);
|
|
199
|
-
callbacks.onOpenUrl(auth.verificationUriComplete || auth.verificationUri);
|
|
200
|
-
callbacks.onStatus("Waiting for you to authorize in the browser...");
|
|
201
|
-
const deadline = Date.now() + DEVICE_TIMEOUT_MS;
|
|
202
|
-
let interval = Math.max(auth.interval, 1);
|
|
203
|
-
while (Date.now() < deadline) {
|
|
204
|
-
await sleep(interval * 1e3);
|
|
205
|
-
const result = await pollDeviceToken(auth.deviceCode);
|
|
206
|
-
if (result.kind === "success") return result.creds;
|
|
207
|
-
if (result.kind === "denied") {
|
|
208
|
-
throw new Error("Kimi authorization was denied.");
|
|
209
|
-
}
|
|
210
|
-
if (result.kind === "expired") {
|
|
211
|
-
throw new Error("Kimi device code expired. Please run login again.");
|
|
212
|
-
}
|
|
213
|
-
if (result.kind === "slow_down") {
|
|
214
|
-
interval += 5;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
throw new Error("Kimi login timed out. Please run login again.");
|
|
218
|
-
}
|
|
219
|
-
async function refreshKimiToken(refreshToken) {
|
|
220
|
-
const { status, data } = await postForm("/api/oauth/token", {
|
|
221
|
-
client_id: CLIENT_ID,
|
|
222
|
-
grant_type: "refresh_token",
|
|
223
|
-
refresh_token: refreshToken
|
|
224
|
-
});
|
|
225
|
-
if (status === 200 && typeof data.access_token === "string") {
|
|
226
|
-
return credsFromTokenResponse(data, { fallbackRefreshToken: refreshToken });
|
|
227
|
-
}
|
|
228
|
-
const errorCode = typeof data.error === "string" ? data.error : "";
|
|
229
|
-
throw new Error(`Kimi token refresh failed (${status}): ${errorCode || errorDetail(data)}`);
|
|
34
|
+
return /^https:\/\/api\.kimi\.(com|ai)\/coding(\/|$)/i.test(baseUrl);
|
|
230
35
|
}
|
|
231
36
|
|
|
232
37
|
// src/auth-storage.ts
|
|
233
|
-
import
|
|
234
|
-
import { readFileSync
|
|
235
|
-
import
|
|
38
|
+
import fs6 from "fs/promises";
|
|
39
|
+
import { readFileSync } from "fs";
|
|
40
|
+
import crypto4 from "crypto";
|
|
236
41
|
|
|
237
42
|
// src/claude-code/runtime.ts
|
|
238
43
|
import { spawn } from "child_process";
|
|
239
44
|
import crypto2 from "crypto";
|
|
240
45
|
import fs2 from "fs";
|
|
241
46
|
import fsp2 from "fs/promises";
|
|
242
|
-
import
|
|
47
|
+
import path from "path";
|
|
243
48
|
|
|
244
49
|
// src/claude-code/tar.ts
|
|
245
50
|
import fs from "fs";
|
|
@@ -320,7 +125,7 @@ async function extractFileFromTarGz(tgzPath, entryName, destPath) {
|
|
|
320
125
|
pos += BLOCK;
|
|
321
126
|
if (header.every((b) => b === 0)) continue;
|
|
322
127
|
const size = readSize(header, 124);
|
|
323
|
-
const
|
|
128
|
+
const type = String.fromCharCode(header[156] || 48);
|
|
324
129
|
let name = readString(header, 0, 100);
|
|
325
130
|
if (readString(header, 257, 6) === "ustar") {
|
|
326
131
|
const prefix = readString(header, 345, 155);
|
|
@@ -329,12 +134,12 @@ async function extractFileFromTarGz(tgzPath, entryName, destPath) {
|
|
|
329
134
|
remaining = size;
|
|
330
135
|
padding = (BLOCK - size % BLOCK) % BLOCK;
|
|
331
136
|
meta = [];
|
|
332
|
-
if (
|
|
333
|
-
else if (
|
|
137
|
+
if (type === "x") kind = "pax";
|
|
138
|
+
else if (type === "L") kind = "longname";
|
|
334
139
|
else {
|
|
335
140
|
const path4 = overridePath ?? name;
|
|
336
141
|
overridePath = void 0;
|
|
337
|
-
if ((
|
|
142
|
+
if ((type === "0" || type === "\0" || type === "7") && path4 === entryName) {
|
|
338
143
|
kind = "target";
|
|
339
144
|
out = await fsp.open(destPath, "w", 493);
|
|
340
145
|
if (size === 0) {
|
|
@@ -424,10 +229,10 @@ function isMusl() {
|
|
|
424
229
|
return report != null && report.header?.glibcVersionRuntime === void 0;
|
|
425
230
|
}
|
|
426
231
|
function claudeCodePlatformKey(info = { platform: process.platform, arch: process.arch, musl: isMusl() }) {
|
|
427
|
-
const
|
|
428
|
-
if (!
|
|
429
|
-
if (info.platform === "darwin" || info.platform === "win32") return `${info.platform}-${
|
|
430
|
-
if (info.platform === "linux") return `linux-${
|
|
232
|
+
const arch = info.arch === "arm64" || info.arch === "x64" ? info.arch : null;
|
|
233
|
+
if (!arch) return null;
|
|
234
|
+
if (info.platform === "darwin" || info.platform === "win32") return `${info.platform}-${arch}`;
|
|
235
|
+
if (info.platform === "linux") return `linux-${arch}${info.musl ? "-musl" : ""}`;
|
|
431
236
|
return null;
|
|
432
237
|
}
|
|
433
238
|
function resolveAsset(opts) {
|
|
@@ -445,11 +250,11 @@ function claudeCodeHome(opts = {}) {
|
|
|
445
250
|
return opts.home ?? getAppPaths().claudeCodeDir;
|
|
446
251
|
}
|
|
447
252
|
function claudeCodeConfigDir(opts = {}) {
|
|
448
|
-
return opts.home ?
|
|
253
|
+
return opts.home ? path.join(opts.home, "config") : getAppPaths().claudeCodeConfigDir;
|
|
449
254
|
}
|
|
450
255
|
function claudeCodeBinaryPath(opts = {}) {
|
|
451
256
|
const { manifest, asset } = resolveAsset(opts);
|
|
452
|
-
return
|
|
257
|
+
return path.join(claudeCodeHome(opts), manifest.claudeCodeVersion, asset.bin);
|
|
453
258
|
}
|
|
454
259
|
function resolveClaudeCodeBinary(opts = {}) {
|
|
455
260
|
let binary;
|
|
@@ -479,12 +284,12 @@ async function ensureClaudeCodeBinary(opts = {}) {
|
|
|
479
284
|
async function installClaudeCode(opts) {
|
|
480
285
|
const { manifest, asset } = resolveAsset(opts);
|
|
481
286
|
const home = claudeCodeHome(opts);
|
|
482
|
-
const versionDir =
|
|
483
|
-
const binary =
|
|
287
|
+
const versionDir = path.join(home, manifest.claudeCodeVersion);
|
|
288
|
+
const binary = path.join(versionDir, asset.bin);
|
|
484
289
|
await fsp2.mkdir(home, { recursive: true });
|
|
485
290
|
const tag = `${process.pid}-${crypto2.randomBytes(4).toString("hex")}`;
|
|
486
|
-
const tarball =
|
|
487
|
-
const staging =
|
|
291
|
+
const tarball = path.join(home, `.download-${tag}.tgz`);
|
|
292
|
+
const staging = path.join(home, `.staging-${tag}`);
|
|
488
293
|
try {
|
|
489
294
|
await download(asset, tarball, opts);
|
|
490
295
|
opts.onProgress?.({ phase: "extract" });
|
|
@@ -492,10 +297,10 @@ async function installClaudeCode(opts) {
|
|
|
492
297
|
const found = await extractFileFromTarGz(
|
|
493
298
|
tarball,
|
|
494
299
|
`package/${asset.bin}`,
|
|
495
|
-
|
|
300
|
+
path.join(staging, asset.bin)
|
|
496
301
|
);
|
|
497
302
|
if (!found) throw new Error(`The Claude Code download has no ${asset.bin} binary.`);
|
|
498
|
-
await fsp2.chmod(
|
|
303
|
+
await fsp2.chmod(path.join(staging, asset.bin), 493);
|
|
499
304
|
try {
|
|
500
305
|
await fsp2.rename(staging, versionDir);
|
|
501
306
|
} catch (err) {
|
|
@@ -555,7 +360,7 @@ async function removeOldVersions(home, keep) {
|
|
|
555
360
|
for (const entry of entries) {
|
|
556
361
|
if (!entry.isDirectory() || entry.name === keep || entry.name === "config") continue;
|
|
557
362
|
if (!/^\d+\.\d+\.\d+/.test(entry.name)) continue;
|
|
558
|
-
await fsp2.rm(
|
|
363
|
+
await fsp2.rm(path.join(home, entry.name), { recursive: true, force: true }).catch(() => {
|
|
559
364
|
});
|
|
560
365
|
}
|
|
561
366
|
}
|
|
@@ -657,8 +462,8 @@ async function loginClaudeCode(callbacks, opts = {}) {
|
|
|
657
462
|
const home = claudeCodeHome(opts);
|
|
658
463
|
await fsp2.mkdir(claudeCodeConfigDir(opts), { recursive: true });
|
|
659
464
|
const tag = `${process.pid}-${crypto2.randomBytes(4).toString("hex")}`;
|
|
660
|
-
const hookFile =
|
|
661
|
-
const urlFile =
|
|
465
|
+
const hookFile = path.join(home, `.open-url-${tag}.cjs`);
|
|
466
|
+
const urlFile = path.join(home, `.open-url-${tag}.txt`);
|
|
662
467
|
await fsp2.writeFile(hookFile, OPEN_URL_HOOK);
|
|
663
468
|
const env = {
|
|
664
469
|
...claudeCodeEnv(opts),
|
|
@@ -739,6 +544,8 @@ async function loginClaudeCode(callbacks, opts = {}) {
|
|
|
739
544
|
// src/oauth/openai.ts
|
|
740
545
|
import http from "http";
|
|
741
546
|
import crypto3 from "crypto";
|
|
547
|
+
import fs3 from "fs/promises";
|
|
548
|
+
import path2 from "path";
|
|
742
549
|
|
|
743
550
|
// src/oauth/pkce.ts
|
|
744
551
|
function base64urlEncode(bytes) {
|
|
@@ -758,100 +565,233 @@ async function generatePKCE() {
|
|
|
758
565
|
return { verifier, challenge };
|
|
759
566
|
}
|
|
760
567
|
|
|
568
|
+
// src/oauth/request-timeout.ts
|
|
569
|
+
var OAUTH_REQUEST_TIMEOUT_MS = 3e4;
|
|
570
|
+
function oauthRequestSignal() {
|
|
571
|
+
return AbortSignal.timeout(OAUTH_REQUEST_TIMEOUT_MS);
|
|
572
|
+
}
|
|
573
|
+
|
|
761
574
|
// src/oauth/openai.ts
|
|
762
|
-
var
|
|
763
|
-
var AUTHORIZE_URL =
|
|
764
|
-
var TOKEN_URL =
|
|
765
|
-
var
|
|
766
|
-
var
|
|
767
|
-
var
|
|
575
|
+
var ISSUER = "https://auth.openai.com";
|
|
576
|
+
var AUTHORIZE_URL = `${ISSUER}/api/accounts/authorize`;
|
|
577
|
+
var TOKEN_URL = `${ISSUER}/api/accounts/oauth/token`;
|
|
578
|
+
var REVOKE_URL = `${ISSUER}/api/accounts/oauth/revoke`;
|
|
579
|
+
var JWKS_URL = `${ISSUER}/.well-known/jwks.json`;
|
|
580
|
+
var REGISTRATION_CLIENT_ID = "dynamic_agent_client";
|
|
581
|
+
var AGENT_NAME = "GG Coder";
|
|
582
|
+
var RESOURCE = "https://api.openai.com/v1";
|
|
583
|
+
var CHATGPT_PLAN_SCOPE = "chatgpt.tokens.use.direct";
|
|
584
|
+
var SCOPES = `openid profile email offline_access resource.invoke ${CHATGPT_PLAN_SCOPE}`;
|
|
585
|
+
var CHATGPT_PLAN_BASE_URL = "https://api.openai.com/v1";
|
|
586
|
+
var CHATGPT_USAGE_URL = "https://chatgpt.com/settings/usage";
|
|
587
|
+
var CALLBACK_PATH = "/auth/callback";
|
|
588
|
+
var PREFERRED_PORT = 1455;
|
|
768
589
|
var CALLBACK_TIMEOUT_MS = 12e4;
|
|
769
590
|
var MAX_PASTE_ATTEMPTS = 3;
|
|
770
|
-
var
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
const
|
|
787
|
-
const creds = await exchangeOpenAICode(code, verifier);
|
|
788
|
-
const accountId = getAccountId(creds.accessToken);
|
|
789
|
-
if (!accountId) {
|
|
790
|
-
throw new Error("Failed to extract accountId from OpenAI token.");
|
|
791
|
-
}
|
|
792
|
-
creds.accountId = accountId;
|
|
793
|
-
return creds;
|
|
794
|
-
}
|
|
795
|
-
function parseAuthorizationInput(input) {
|
|
796
|
-
const value = input.trim();
|
|
797
|
-
if (!value) return {};
|
|
591
|
+
var ID_TOKEN_CLOCK_SKEW_S = 60;
|
|
592
|
+
var CHATGPT_PLAN_NOT_ENABLED_MESSAGE = "ChatGPT plan use isn't enabled for GG Coder. Allow Use your ChatGPT plan, or add an OpenAI API key.";
|
|
593
|
+
var ChatGPTPlanNotEnabledError = class extends Error {
|
|
594
|
+
constructor() {
|
|
595
|
+
super(CHATGPT_PLAN_NOT_ENABLED_MESSAGE);
|
|
596
|
+
this.name = "ChatGPTPlanNotEnabledError";
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
function defaultHostIdFile() {
|
|
600
|
+
return path2.join(getAppPaths().agentDir, "openai-host-id");
|
|
601
|
+
}
|
|
602
|
+
function defaultRegistrationFile() {
|
|
603
|
+
return path2.join(getAppPaths().agentDir, "openai-chatgpt-client.json");
|
|
604
|
+
}
|
|
605
|
+
async function writePrivateFile(file, content) {
|
|
606
|
+
await fs3.mkdir(path2.dirname(file), { recursive: true });
|
|
607
|
+
const tmp = `${file}.${process.pid}.${crypto3.randomUUID().slice(0, 8)}.tmp`;
|
|
798
608
|
try {
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
};
|
|
804
|
-
|
|
609
|
+
await fs3.writeFile(tmp, content, { encoding: "utf-8", mode: 384 });
|
|
610
|
+
await fs3.rename(tmp, file);
|
|
611
|
+
} catch (err) {
|
|
612
|
+
await fs3.unlink(tmp).catch(() => {
|
|
613
|
+
});
|
|
614
|
+
throw err;
|
|
805
615
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
616
|
+
}
|
|
617
|
+
var HOST_ID_PATTERN = /^urn:uuid:[0-9a-f-]{36}$/i;
|
|
618
|
+
async function getChatGPTHostId(file = defaultHostIdFile()) {
|
|
619
|
+
const existing = await fs3.readFile(file, "utf-8").catch(() => "");
|
|
620
|
+
if (HOST_ID_PATTERN.test(existing.trim())) return existing.trim();
|
|
621
|
+
await writePrivateFile(file, `urn:uuid:${crypto3.randomUUID()}`);
|
|
622
|
+
return (await fs3.readFile(file, "utf-8")).trim();
|
|
623
|
+
}
|
|
624
|
+
async function readRegistration(file) {
|
|
625
|
+
try {
|
|
626
|
+
const value = JSON.parse(await fs3.readFile(file, "utf-8"));
|
|
627
|
+
if (typeof value.clientId === "string" && typeof value.subject === "string") {
|
|
628
|
+
return { clientId: value.clientId, subject: value.subject };
|
|
629
|
+
}
|
|
630
|
+
} catch {
|
|
809
631
|
}
|
|
810
|
-
|
|
811
|
-
|
|
632
|
+
return void 0;
|
|
633
|
+
}
|
|
634
|
+
async function loginChatGPT(callbacks, options = {}) {
|
|
635
|
+
const hostId = await getChatGPTHostId(options.hostIdFile);
|
|
636
|
+
const registrationFile = options.registrationFile ?? defaultRegistrationFile();
|
|
637
|
+
const registration = await readRegistration(registrationFile);
|
|
638
|
+
const { verifier, challenge } = await generatePKCE();
|
|
639
|
+
const state = crypto3.randomBytes(16).toString("hex");
|
|
640
|
+
const nonce = crypto3.randomBytes(16).toString("hex");
|
|
641
|
+
const listener = await startCallbackListener(state, Boolean(registration));
|
|
642
|
+
try {
|
|
643
|
+
const redirectUri = `http://127.0.0.1:${listener.port}${CALLBACK_PATH}`;
|
|
644
|
+
const url = new URL(AUTHORIZE_URL);
|
|
645
|
+
url.searchParams.set("client_id", registration?.clientId ?? REGISTRATION_CLIENT_ID);
|
|
646
|
+
if (!registration) url.searchParams.set("agent_name_hint", AGENT_NAME);
|
|
647
|
+
url.searchParams.set("ext_agent_host_id", hostId);
|
|
648
|
+
if (registration && options.previous?.idToken) {
|
|
649
|
+
url.searchParams.set("id_token_hint", options.previous.idToken);
|
|
650
|
+
}
|
|
651
|
+
url.searchParams.set("response_type", "code");
|
|
652
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
653
|
+
url.searchParams.set("scope", SCOPES);
|
|
654
|
+
url.searchParams.set("resource", RESOURCE);
|
|
655
|
+
url.searchParams.set("state", state);
|
|
656
|
+
url.searchParams.set("nonce", nonce);
|
|
657
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
658
|
+
url.searchParams.set("code_challenge", challenge);
|
|
659
|
+
const callback = await acquireCallback(url.toString(), state, listener, callbacks);
|
|
660
|
+
if (callback.get("error")) {
|
|
661
|
+
if (callback.get("error") === "access_denied") throw new ChatGPTPlanNotEnabledError();
|
|
662
|
+
throw new Error(
|
|
663
|
+
`ChatGPT sign-in failed: ${callback.get("error_description") ?? callback.get("error")}`
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
const code = callback.get("code");
|
|
667
|
+
if (!code) throw new Error("ChatGPT sign-in returned no authorization code.");
|
|
668
|
+
const issued = callback.get("client_id") ?? void 0;
|
|
669
|
+
if (registration && issued && issued !== registration.clientId) {
|
|
670
|
+
throw new Error("ChatGPT returned a different app registration. Start the sign-in again.");
|
|
671
|
+
}
|
|
672
|
+
const clientId = registration?.clientId ?? issued;
|
|
673
|
+
if (!clientId || clientId === REGISTRATION_CLIENT_ID) {
|
|
674
|
+
throw new Error(
|
|
675
|
+
"ChatGPT didn't finish registering GG Coder. Start the sign-in again, and paste the full callback URL if you're on another machine."
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
const tokens = await exchangeCode({ code, verifier, clientId, redirectUri });
|
|
679
|
+
const claims = await verifyIdToken(tokens.idToken, { clientId, nonce });
|
|
680
|
+
const subject = String(claims.sub ?? "");
|
|
681
|
+
if (!subject) throw new Error("ChatGPT ID token has no subject.");
|
|
682
|
+
if (registration && registration.subject !== subject) {
|
|
683
|
+
await fs3.unlink(registrationFile).catch(() => {
|
|
684
|
+
});
|
|
685
|
+
await revokeChatGPTSignIn({ refreshToken: tokens.refreshToken, clientId });
|
|
686
|
+
throw new Error(
|
|
687
|
+
"That was a different ChatGPT account. Continue with ChatGPT again to connect it."
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
if (!tokens.scopes.includes(CHATGPT_PLAN_SCOPE)) {
|
|
691
|
+
await revokeChatGPTSignIn({ refreshToken: tokens.refreshToken, clientId });
|
|
692
|
+
throw new ChatGPTPlanNotEnabledError();
|
|
693
|
+
}
|
|
694
|
+
if (!registration) {
|
|
695
|
+
await writePrivateFile(registrationFile, JSON.stringify({ clientId, subject }));
|
|
696
|
+
}
|
|
812
697
|
return {
|
|
813
|
-
|
|
814
|
-
|
|
698
|
+
accessToken: tokens.accessToken,
|
|
699
|
+
refreshToken: tokens.refreshToken,
|
|
700
|
+
expiresAt: Date.now() + tokens.expiresIn * 1e3,
|
|
701
|
+
expiresIn: tokens.expiresIn,
|
|
702
|
+
accountId: subject,
|
|
703
|
+
clientId,
|
|
704
|
+
idToken: tokens.idToken,
|
|
705
|
+
scopes: tokens.scopes,
|
|
706
|
+
baseUrl: CHATGPT_PLAN_BASE_URL
|
|
815
707
|
};
|
|
708
|
+
} finally {
|
|
709
|
+
listener.close();
|
|
816
710
|
}
|
|
817
|
-
if (/\s/.test(value) || value.length < MIN_RAW_CODE_LENGTH) return {};
|
|
818
|
-
return { code: value };
|
|
819
711
|
}
|
|
820
|
-
function
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
}
|
|
712
|
+
async function fetchChatGPTPlanModels(accessToken, fetchFn = fetch) {
|
|
713
|
+
const response = await fetchFn(`${CHATGPT_PLAN_BASE_URL}/models`, {
|
|
714
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
715
|
+
signal: oauthRequestSignal()
|
|
716
|
+
});
|
|
717
|
+
if (!response.ok) throw new Error(`ChatGPT model list failed (${response.status})`);
|
|
718
|
+
const data = await response.json();
|
|
719
|
+
return (data.models ?? []).filter((m) => typeof m.slug === "string" && (m.visibility ?? "list") === "list").map((m) => m.slug);
|
|
829
720
|
}
|
|
830
|
-
function
|
|
831
|
-
|
|
832
|
-
const
|
|
833
|
-
|
|
834
|
-
|
|
721
|
+
async function startCallbackListener(expectedState, anyPort) {
|
|
722
|
+
let settle;
|
|
723
|
+
const result = new Promise((resolve, reject) => {
|
|
724
|
+
settle = { resolve, reject };
|
|
725
|
+
});
|
|
726
|
+
result.catch(() => {
|
|
727
|
+
});
|
|
728
|
+
const server = http.createServer((req, res) => {
|
|
729
|
+
const url = new URL(req.url || "", "http://127.0.0.1");
|
|
730
|
+
if (url.pathname !== CALLBACK_PATH) {
|
|
731
|
+
res.statusCode = 404;
|
|
732
|
+
res.end("Not found");
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
if (url.searchParams.get("state") !== expectedState) {
|
|
736
|
+
res.statusCode = 400;
|
|
737
|
+
res.end("State mismatch");
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
741
|
+
res.end(
|
|
742
|
+
"<html><body><h1>You're signed in to GG Coder.</h1><p>You can close this tab.</p></body></html>"
|
|
743
|
+
);
|
|
744
|
+
settle?.resolve(url.searchParams);
|
|
745
|
+
server.close();
|
|
746
|
+
});
|
|
747
|
+
const listen = (port2) => new Promise((resolve, reject) => {
|
|
748
|
+
const onError = (err) => reject(err);
|
|
749
|
+
server.once("error", onError);
|
|
750
|
+
server.listen(port2, "127.0.0.1", () => {
|
|
751
|
+
server.off("error", onError);
|
|
752
|
+
resolve(server.address().port);
|
|
753
|
+
});
|
|
754
|
+
});
|
|
755
|
+
let port;
|
|
756
|
+
for (const candidate of anyPort ? [PREFERRED_PORT, 0] : [PREFERRED_PORT]) {
|
|
757
|
+
try {
|
|
758
|
+
port = await listen(candidate);
|
|
759
|
+
break;
|
|
760
|
+
} catch {
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (port === void 0) {
|
|
764
|
+
settle?.reject(new Error("Could not start the local sign-in listener."));
|
|
765
|
+
return { port: PREFERRED_PORT, result, close: () => {
|
|
766
|
+
} };
|
|
767
|
+
}
|
|
768
|
+
const timeout = setTimeout(() => server.close(), CALLBACK_TIMEOUT_MS);
|
|
769
|
+
timeout.unref();
|
|
770
|
+
server.on("close", () => {
|
|
771
|
+
clearTimeout(timeout);
|
|
772
|
+
settle?.reject(new Error("Sign-in listener closed without a callback."));
|
|
773
|
+
});
|
|
774
|
+
return { port, result, close: () => server.close() };
|
|
835
775
|
}
|
|
836
|
-
async function
|
|
776
|
+
async function acquireCallback(authUrl, expectedState, listener, callbacks) {
|
|
837
777
|
callbacks.onOpenUrl(authUrl);
|
|
838
|
-
callbacks.onStatus("Waiting for browser
|
|
778
|
+
callbacks.onStatus("Waiting for ChatGPT sign-in in your browser...");
|
|
839
779
|
const done = new AbortController();
|
|
840
780
|
try {
|
|
841
781
|
return await new Promise((resolve, reject) => {
|
|
842
|
-
let
|
|
782
|
+
let listenerFailed = false;
|
|
843
783
|
let pasteError;
|
|
844
784
|
const failIfExhausted = () => {
|
|
845
|
-
if (!
|
|
785
|
+
if (!listenerFailed || pasteError === void 0) return;
|
|
846
786
|
reject(
|
|
847
|
-
pasteError instanceof Error ? pasteError : new Error("Could not
|
|
787
|
+
pasteError instanceof Error ? pasteError : new Error("Could not complete the sign-in.")
|
|
848
788
|
);
|
|
849
789
|
};
|
|
850
|
-
|
|
851
|
-
|
|
790
|
+
listener.result.then(resolve, () => {
|
|
791
|
+
listenerFailed = true;
|
|
852
792
|
failIfExhausted();
|
|
853
793
|
});
|
|
854
|
-
|
|
794
|
+
promptForCallback(expectedState, callbacks, done.signal).then(resolve, (err) => {
|
|
855
795
|
if (err instanceof FatalLoginError) {
|
|
856
796
|
reject(err);
|
|
857
797
|
return;
|
|
@@ -862,686 +802,158 @@ async function acquireAuthorizationCode(authUrl, expectedState, callbacks) {
|
|
|
862
802
|
});
|
|
863
803
|
} finally {
|
|
864
804
|
done.abort();
|
|
805
|
+
listener.close();
|
|
865
806
|
}
|
|
866
807
|
}
|
|
867
808
|
var FatalLoginError = class extends Error {
|
|
868
809
|
};
|
|
869
|
-
|
|
870
|
-
|
|
810
|
+
function parseCallbackInput(input) {
|
|
811
|
+
const value = input.trim();
|
|
812
|
+
if (!value) return void 0;
|
|
813
|
+
try {
|
|
814
|
+
return new URL(value).searchParams;
|
|
815
|
+
} catch {
|
|
816
|
+
}
|
|
817
|
+
if (/[?&]?(code|error)=/.test(value)) {
|
|
818
|
+
return new URLSearchParams(value.replace(/^.*\?/, ""));
|
|
819
|
+
}
|
|
820
|
+
return void 0;
|
|
821
|
+
}
|
|
822
|
+
async function promptForCallback(expectedState, callbacks, signal) {
|
|
823
|
+
let message = "If the browser is on another machine (SSH/headless), paste the full callback URL here:";
|
|
871
824
|
for (let attempt = 0; attempt < MAX_PASTE_ATTEMPTS; attempt++) {
|
|
872
|
-
if (signal.aborted) throw new Error("
|
|
825
|
+
if (signal.aborted) throw new Error("Callback already received.");
|
|
873
826
|
const raw = await callbacks.onPromptCode(message, signal);
|
|
874
|
-
if (signal.aborted) throw new Error("
|
|
875
|
-
const
|
|
876
|
-
if (
|
|
877
|
-
|
|
827
|
+
if (signal.aborted) throw new Error("Callback already received.");
|
|
828
|
+
const params = parseCallbackInput(raw);
|
|
829
|
+
if (params && (params.get("code") || params.get("error"))) {
|
|
830
|
+
if (params.get("state") !== expectedState) {
|
|
831
|
+
throw new FatalLoginError("Sign-in state mismatch \u2014 start the sign-in again.");
|
|
832
|
+
}
|
|
833
|
+
return params;
|
|
878
834
|
}
|
|
879
|
-
|
|
880
|
-
message = "That didn't contain an authorization code. Paste the full callback URL:";
|
|
835
|
+
message = "That wasn't a callback URL. Paste the full URL from the browser's address bar:";
|
|
881
836
|
}
|
|
882
|
-
throw new Error("No
|
|
837
|
+
throw new Error("No callback URL found in input.");
|
|
883
838
|
}
|
|
884
|
-
async function
|
|
885
|
-
return
|
|
886
|
-
let receivedCode = null;
|
|
887
|
-
const server = http.createServer((req, res) => {
|
|
888
|
-
const url = new URL(req.url || "", "http://localhost");
|
|
889
|
-
if (url.pathname !== "/auth/callback") {
|
|
890
|
-
res.statusCode = 404;
|
|
891
|
-
res.end("Not found");
|
|
892
|
-
return;
|
|
893
|
-
}
|
|
894
|
-
if (url.searchParams.get("state") !== expectedState) {
|
|
895
|
-
res.statusCode = 400;
|
|
896
|
-
res.end("State mismatch");
|
|
897
|
-
return;
|
|
898
|
-
}
|
|
899
|
-
receivedCode = url.searchParams.get("code");
|
|
900
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
901
|
-
res.end("<html><body><h1>Login successful!</h1><p>You can close this tab.</p></body></html>");
|
|
902
|
-
server.close();
|
|
903
|
-
});
|
|
904
|
-
server.on("error", (err) => {
|
|
905
|
-
reject(err);
|
|
906
|
-
});
|
|
907
|
-
server.listen(1455, "127.0.0.1");
|
|
908
|
-
const timeout = setTimeout(() => {
|
|
909
|
-
if (!receivedCode) server.close();
|
|
910
|
-
}, CALLBACK_TIMEOUT_MS);
|
|
911
|
-
timeout.unref();
|
|
912
|
-
const onAbort = () => {
|
|
913
|
-
server.close();
|
|
914
|
-
};
|
|
915
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
916
|
-
server.on("close", () => {
|
|
917
|
-
clearTimeout(timeout);
|
|
918
|
-
signal.removeEventListener("abort", onAbort);
|
|
919
|
-
if (receivedCode) {
|
|
920
|
-
resolve(receivedCode);
|
|
921
|
-
} else {
|
|
922
|
-
reject(new Error("Server closed without receiving code"));
|
|
923
|
-
}
|
|
924
|
-
});
|
|
925
|
-
});
|
|
926
|
-
}
|
|
927
|
-
async function exchangeOpenAICode(code, verifier) {
|
|
928
|
-
const response = await fetch(TOKEN_URL, {
|
|
839
|
+
async function postForm(url, form) {
|
|
840
|
+
return fetch(url, {
|
|
929
841
|
method: "POST",
|
|
930
842
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
931
|
-
body: new URLSearchParams(
|
|
932
|
-
grant_type: "authorization_code",
|
|
933
|
-
client_id: CLIENT_ID2,
|
|
934
|
-
code,
|
|
935
|
-
redirect_uri: REDIRECT_URI,
|
|
936
|
-
code_verifier: verifier
|
|
937
|
-
}),
|
|
843
|
+
body: new URLSearchParams(form),
|
|
938
844
|
signal: oauthRequestSignal()
|
|
939
845
|
});
|
|
940
|
-
if (!response.ok) {
|
|
941
|
-
const text = await response.text();
|
|
942
|
-
throw new Error(`OpenAI token exchange failed (${response.status}): ${text}`);
|
|
943
|
-
}
|
|
944
|
-
const data = await response.json();
|
|
945
|
-
return {
|
|
946
|
-
accessToken: data.access_token,
|
|
947
|
-
refreshToken: data.refresh_token,
|
|
948
|
-
expiresAt: Date.now() + data.expires_in * 1e3
|
|
949
|
-
};
|
|
950
846
|
}
|
|
951
|
-
async function
|
|
952
|
-
const response = await
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
}),
|
|
960
|
-
signal: oauthRequestSignal()
|
|
847
|
+
async function exchangeCode(input) {
|
|
848
|
+
const response = await postForm(TOKEN_URL, {
|
|
849
|
+
grant_type: "authorization_code",
|
|
850
|
+
client_id: input.clientId,
|
|
851
|
+
code: input.code,
|
|
852
|
+
code_verifier: input.verifier,
|
|
853
|
+
redirect_uri: input.redirectUri,
|
|
854
|
+
resource: RESOURCE
|
|
961
855
|
});
|
|
962
856
|
if (!response.ok) {
|
|
963
|
-
const text = await response.text();
|
|
964
|
-
throw new Error(`
|
|
857
|
+
const text = await response.text().catch(() => "");
|
|
858
|
+
throw new Error(`ChatGPT token exchange failed (${response.status}): ${text.slice(0, 300)}`);
|
|
965
859
|
}
|
|
966
860
|
const data = await response.json();
|
|
967
|
-
|
|
861
|
+
if (!data.access_token || !data.refresh_token || !data.id_token) {
|
|
862
|
+
throw new Error("ChatGPT token response is missing tokens.");
|
|
863
|
+
}
|
|
864
|
+
return {
|
|
968
865
|
accessToken: data.access_token,
|
|
969
866
|
refreshToken: data.refresh_token,
|
|
970
|
-
|
|
867
|
+
idToken: data.id_token,
|
|
868
|
+
expiresIn: data.expires_in ?? 3600,
|
|
869
|
+
scopes: (data.scope ?? "").split(/\s+/).filter(Boolean)
|
|
971
870
|
};
|
|
972
|
-
const accountId = getAccountId(creds.accessToken);
|
|
973
|
-
if (accountId) {
|
|
974
|
-
creds.accountId = accountId;
|
|
975
|
-
}
|
|
976
|
-
return creds;
|
|
977
871
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
import crypto4 from "crypto";
|
|
982
|
-
var CLIENT_ID_ENV = "GGCODER_GEMINI_OAUTH_CLIENT_ID";
|
|
983
|
-
var CLIENT_SECRET_ENV = "GGCODER_GEMINI_OAUTH_CLIENT_SECRET";
|
|
984
|
-
var DEFAULT_CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com";
|
|
985
|
-
var DEFAULT_CLIENT_SECRET = "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";
|
|
986
|
-
var AUTHORIZE_URL2 = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
987
|
-
var TOKEN_URL2 = "https://oauth2.googleapis.com/token";
|
|
988
|
-
var CODE_ASSIST_BASE_URL = "https://cloudcode-pa.googleapis.com";
|
|
989
|
-
var CODE_ASSIST_API_VERSION = "v1internal";
|
|
990
|
-
var CODE_ASSIST_POST_RETRIES = 3;
|
|
991
|
-
var CODE_ASSIST_POST_RETRY_DELAY_MS = 100;
|
|
992
|
-
var SCOPE2 = [
|
|
993
|
-
"https://www.googleapis.com/auth/cloud-platform",
|
|
994
|
-
"https://www.googleapis.com/auth/userinfo.email",
|
|
995
|
-
"https://www.googleapis.com/auth/userinfo.profile"
|
|
996
|
-
].join(" ");
|
|
997
|
-
var USER_TIER_FREE = "free-tier";
|
|
998
|
-
var USER_TIER_LEGACY = "legacy-tier";
|
|
999
|
-
var USER_TIER_STANDARD = "standard-tier";
|
|
1000
|
-
var VALIDATION_REQUIRED_REASON = "VALIDATION_REQUIRED";
|
|
1001
|
-
var VPC_SC_REASON = "SECURITY_POLICY_VIOLATED";
|
|
1002
|
-
var CodeAssistHttpError = class extends Error {
|
|
1003
|
-
status;
|
|
1004
|
-
body;
|
|
1005
|
-
constructor(label, status, body) {
|
|
1006
|
-
super(`Gemini Code Assist ${label} failed (${status}): ${body}`);
|
|
1007
|
-
this.name = "CodeAssistHttpError";
|
|
1008
|
-
this.status = status;
|
|
1009
|
-
this.body = body;
|
|
1010
|
-
}
|
|
1011
|
-
};
|
|
1012
|
-
async function loginGemini(callbacks) {
|
|
1013
|
-
const { clientId: clientId2, clientSecret } = getGeminiOAuthClientCredentials();
|
|
1014
|
-
const { verifier, challenge } = await generatePKCE();
|
|
1015
|
-
const state = crypto4.randomBytes(32).toString("hex");
|
|
1016
|
-
const redirectUri = await getLoopbackRedirectUri();
|
|
1017
|
-
const url = new URL(AUTHORIZE_URL2);
|
|
1018
|
-
url.searchParams.set("response_type", "code");
|
|
1019
|
-
url.searchParams.set("client_id", clientId2);
|
|
1020
|
-
url.searchParams.set("redirect_uri", redirectUri);
|
|
1021
|
-
url.searchParams.set("scope", SCOPE2);
|
|
1022
|
-
url.searchParams.set("access_type", "offline");
|
|
1023
|
-
url.searchParams.set("prompt", "consent");
|
|
1024
|
-
url.searchParams.set("code_challenge", challenge);
|
|
1025
|
-
url.searchParams.set("code_challenge_method", "S256");
|
|
1026
|
-
url.searchParams.set("state", state);
|
|
1027
|
-
let code;
|
|
1028
|
-
try {
|
|
1029
|
-
code = await loginWithServer(url.toString(), redirectUri, state, callbacks);
|
|
1030
|
-
} catch {
|
|
1031
|
-
callbacks.onOpenUrl(url.toString());
|
|
1032
|
-
const raw = await callbacks.onPromptCode(
|
|
1033
|
-
"Could not start local server. Paste the callback URL or code from the browser:"
|
|
1034
|
-
);
|
|
1035
|
-
const parsed = parseAuthorizationInput2(raw);
|
|
1036
|
-
if (!parsed.code) {
|
|
1037
|
-
throw new Error("No authorization code found in input.");
|
|
1038
|
-
}
|
|
1039
|
-
if (parsed.state && parsed.state !== state) {
|
|
1040
|
-
throw new Error("Invalid state. Please try again.");
|
|
1041
|
-
}
|
|
1042
|
-
code = parsed.code;
|
|
872
|
+
async function refreshChatGPTToken(creds) {
|
|
873
|
+
if (!creds.clientId) {
|
|
874
|
+
throw new Error("ChatGPT token refresh failed (401): no issued client ID saved");
|
|
1043
875
|
}
|
|
1044
|
-
const
|
|
1045
|
-
callbacks.onStatus("Setting up Gemini Code Assist access...");
|
|
1046
|
-
const projectId = await setupCodeAssistProject(creds.accessToken, callbacks);
|
|
1047
|
-
return {
|
|
1048
|
-
...creds,
|
|
1049
|
-
projectId
|
|
1050
|
-
};
|
|
1051
|
-
}
|
|
1052
|
-
async function refreshGeminiToken(refreshToken) {
|
|
1053
|
-
const { clientId: clientId2, clientSecret } = getGeminiOAuthClientCredentials();
|
|
1054
|
-
const data = await postTokenRequest({
|
|
876
|
+
const response = await postForm(TOKEN_URL, {
|
|
1055
877
|
grant_type: "refresh_token",
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
});
|
|
1060
|
-
return {
|
|
1061
|
-
accessToken: data.access_token,
|
|
1062
|
-
refreshToken: data.refresh_token ?? refreshToken,
|
|
1063
|
-
expiresAt: Date.now() + data.expires_in * 1e3 - 5 * 60 * 1e3
|
|
1064
|
-
};
|
|
1065
|
-
}
|
|
1066
|
-
function getGeminiOAuthClientCredentials() {
|
|
1067
|
-
const clientId2 = process.env[CLIENT_ID_ENV]?.trim() || DEFAULT_CLIENT_ID;
|
|
1068
|
-
const clientSecret = process.env[CLIENT_SECRET_ENV]?.trim() || DEFAULT_CLIENT_SECRET;
|
|
1069
|
-
return { clientId: clientId2, clientSecret };
|
|
1070
|
-
}
|
|
1071
|
-
async function getLoopbackRedirectUri() {
|
|
1072
|
-
return new Promise((resolve, reject) => {
|
|
1073
|
-
const server = http2.createServer();
|
|
1074
|
-
server.listen(0, "127.0.0.1", () => {
|
|
1075
|
-
const addr = server.address();
|
|
1076
|
-
server.close(() => {
|
|
1077
|
-
if (addr && typeof addr === "object") {
|
|
1078
|
-
resolve(`http://127.0.0.1:${addr.port}/oauth2callback`);
|
|
1079
|
-
} else {
|
|
1080
|
-
reject(new Error("Failed to allocate OAuth callback port."));
|
|
1081
|
-
}
|
|
1082
|
-
});
|
|
1083
|
-
});
|
|
1084
|
-
server.on("error", reject);
|
|
878
|
+
client_id: creds.clientId,
|
|
879
|
+
refresh_token: creds.refreshToken,
|
|
880
|
+
resource: RESOURCE
|
|
1085
881
|
});
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
if (!value) return {};
|
|
1090
|
-
try {
|
|
1091
|
-
const url = new URL(value);
|
|
1092
|
-
return {
|
|
1093
|
-
code: url.searchParams.get("code") ?? void 0,
|
|
1094
|
-
state: url.searchParams.get("state") ?? void 0
|
|
1095
|
-
};
|
|
1096
|
-
} catch {
|
|
1097
|
-
}
|
|
1098
|
-
if (value.includes("code=")) {
|
|
1099
|
-
const params = new URLSearchParams(value);
|
|
1100
|
-
return {
|
|
1101
|
-
code: params.get("code") ?? void 0,
|
|
1102
|
-
state: params.get("state") ?? void 0
|
|
1103
|
-
};
|
|
882
|
+
if (!response.ok) {
|
|
883
|
+
const text = await response.text().catch(() => "");
|
|
884
|
+
throw new Error(`ChatGPT token refresh failed (${response.status}): ${text.slice(0, 300)}`);
|
|
1104
885
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
const redirect = new URL(redirectUri);
|
|
1109
|
-
const port = Number(redirect.port);
|
|
1110
|
-
return new Promise((resolve, reject) => {
|
|
1111
|
-
let receivedCode = null;
|
|
1112
|
-
const server = http2.createServer((req, res) => {
|
|
1113
|
-
const url = new URL(req.url || "", redirect.origin);
|
|
1114
|
-
if (url.pathname !== redirect.pathname) {
|
|
1115
|
-
res.statusCode = 404;
|
|
1116
|
-
res.end("Not found");
|
|
1117
|
-
return;
|
|
1118
|
-
}
|
|
1119
|
-
if (url.searchParams.get("state") !== expectedState) {
|
|
1120
|
-
res.statusCode = 400;
|
|
1121
|
-
res.end("State mismatch");
|
|
1122
|
-
return;
|
|
1123
|
-
}
|
|
1124
|
-
receivedCode = url.searchParams.get("code");
|
|
1125
|
-
res.writeHead(200, { "Content-Type": "text/html", Connection: "close" });
|
|
1126
|
-
res.end("<html><body><h1>Login successful!</h1><p>You can close this tab.</p></body></html>");
|
|
1127
|
-
server.close();
|
|
1128
|
-
});
|
|
1129
|
-
server.on("error", (err) => reject(err));
|
|
1130
|
-
server.listen(port, "127.0.0.1", () => {
|
|
1131
|
-
callbacks.onOpenUrl(authUrl);
|
|
1132
|
-
callbacks.onStatus("Waiting for browser callback...");
|
|
1133
|
-
});
|
|
1134
|
-
const timeout = setTimeout(() => {
|
|
1135
|
-
if (!receivedCode) server.close();
|
|
1136
|
-
}, 12e4);
|
|
1137
|
-
timeout.unref();
|
|
1138
|
-
server.on("close", () => {
|
|
1139
|
-
clearTimeout(timeout);
|
|
1140
|
-
if (receivedCode) {
|
|
1141
|
-
resolve(receivedCode);
|
|
1142
|
-
} else {
|
|
1143
|
-
reject(new Error("Server closed without receiving code."));
|
|
1144
|
-
}
|
|
1145
|
-
});
|
|
1146
|
-
});
|
|
1147
|
-
}
|
|
1148
|
-
async function exchangeGeminiCode(code, verifier, redirectUri, clientId2, clientSecret) {
|
|
1149
|
-
const data = await postTokenRequest({
|
|
1150
|
-
grant_type: "authorization_code",
|
|
1151
|
-
client_id: clientId2,
|
|
1152
|
-
client_secret: clientSecret,
|
|
1153
|
-
code,
|
|
1154
|
-
redirect_uri: redirectUri,
|
|
1155
|
-
code_verifier: verifier
|
|
1156
|
-
});
|
|
1157
|
-
if (!data.refresh_token) {
|
|
1158
|
-
throw new Error("Gemini OAuth did not return a refresh token. Please try login again.");
|
|
886
|
+
const data = await response.json();
|
|
887
|
+
if (!data.access_token || !data.refresh_token) {
|
|
888
|
+
throw new Error("ChatGPT token refresh returned no tokens.");
|
|
1159
889
|
}
|
|
890
|
+
const expiresIn = data.expires_in ?? 3600;
|
|
1160
891
|
return {
|
|
1161
892
|
accessToken: data.access_token,
|
|
1162
893
|
refreshToken: data.refresh_token,
|
|
1163
|
-
expiresAt: Date.now() +
|
|
1164
|
-
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
const response = await fetch(TOKEN_URL2, {
|
|
1168
|
-
method: "POST",
|
|
1169
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1170
|
-
body: new URLSearchParams(body),
|
|
1171
|
-
signal: oauthRequestSignal()
|
|
1172
|
-
});
|
|
1173
|
-
if (!response.ok) {
|
|
1174
|
-
const text = await response.text();
|
|
1175
|
-
throw new Error(`Gemini token request failed (${response.status}): ${text}`);
|
|
1176
|
-
}
|
|
1177
|
-
return await response.json();
|
|
1178
|
-
}
|
|
1179
|
-
async function setupCodeAssistProject(accessToken, callbacks) {
|
|
1180
|
-
const envProject = process.env.GOOGLE_CLOUD_PROJECT ?? process.env.GOOGLE_CLOUD_PROJECT_ID;
|
|
1181
|
-
if (envProject && /^\d+$/.test(envProject)) {
|
|
1182
|
-
throw new Error("GOOGLE_CLOUD_PROJECT must be a project ID, not a numeric project number.");
|
|
1183
|
-
}
|
|
1184
|
-
const coreMetadata = {
|
|
1185
|
-
ideType: "IDE_UNSPECIFIED",
|
|
1186
|
-
platform: "PLATFORM_UNSPECIFIED",
|
|
1187
|
-
pluginType: "GEMINI"
|
|
1188
|
-
};
|
|
1189
|
-
const projectMetadata = {
|
|
1190
|
-
...coreMetadata,
|
|
1191
|
-
...envProject ? { duetProject: envProject } : {}
|
|
1192
|
-
};
|
|
1193
|
-
let loadRes;
|
|
1194
|
-
while (true) {
|
|
1195
|
-
loadRes = await loadCodeAssist(accessToken, envProject, projectMetadata);
|
|
1196
|
-
const validation = getValidationRequiredTier(loadRes);
|
|
1197
|
-
if (!validation) break;
|
|
1198
|
-
callbacks.onStatus(
|
|
1199
|
-
`Gemini Code Assist requires account validation${validation.reasonMessage ? `: ${validation.reasonMessage}` : ""}`
|
|
1200
|
-
);
|
|
1201
|
-
callbacks.onOpenUrl(validation.validationUrl);
|
|
1202
|
-
const answer = await callbacks.onPromptCode(
|
|
1203
|
-
"Complete validation in the browser, then press Enter to retry (or type cancel):"
|
|
1204
|
-
);
|
|
1205
|
-
if (answer.trim().toLowerCase() === "cancel") {
|
|
1206
|
-
throw new Error("Gemini Code Assist account validation was cancelled.");
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
if (loadRes.currentTier) {
|
|
1210
|
-
const project2 = loadRes.cloudaicompanionProject ?? envProject;
|
|
1211
|
-
if (!project2) throwProjectError(loadRes);
|
|
1212
|
-
return project2;
|
|
1213
|
-
}
|
|
1214
|
-
const tier = getOnboardTier(loadRes);
|
|
1215
|
-
const onboardReq = tier.id === USER_TIER_FREE ? {
|
|
1216
|
-
tierId: tier.id,
|
|
1217
|
-
cloudaicompanionProject: void 0,
|
|
1218
|
-
metadata: coreMetadata
|
|
1219
|
-
} : {
|
|
1220
|
-
tierId: tier.id,
|
|
1221
|
-
cloudaicompanionProject: envProject,
|
|
1222
|
-
metadata: projectMetadata
|
|
894
|
+
expiresAt: Date.now() + expiresIn * 1e3,
|
|
895
|
+
expiresIn,
|
|
896
|
+
...data.id_token ? { idToken: data.id_token } : {},
|
|
897
|
+
...data.scope ? { scopes: data.scope.split(/\s+/).filter(Boolean) } : {}
|
|
1223
898
|
};
|
|
1224
|
-
let operation = await codeAssistPost(
|
|
1225
|
-
accessToken,
|
|
1226
|
-
"onboardUser",
|
|
1227
|
-
onboardReq
|
|
1228
|
-
);
|
|
1229
|
-
while (!operation.done && operation.name) {
|
|
1230
|
-
await new Promise((resolve) => setTimeout(resolve, 5e3));
|
|
1231
|
-
operation = await codeAssistGet(accessToken, operation.name);
|
|
1232
|
-
}
|
|
1233
|
-
const project = operation.response?.cloudaicompanionProject?.id ?? envProject;
|
|
1234
|
-
if (!project) throwProjectError(loadRes);
|
|
1235
|
-
return project;
|
|
1236
899
|
}
|
|
1237
|
-
async function
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
metadata
|
|
1242
|
-
});
|
|
1243
|
-
} catch (err) {
|
|
1244
|
-
if (err instanceof CodeAssistHttpError && isVpcScAffectedError(err)) {
|
|
1245
|
-
return { currentTier: { id: USER_TIER_STANDARD } };
|
|
1246
|
-
}
|
|
1247
|
-
if (err instanceof CodeAssistHttpError && err.status === 403 && envProject === "cloudshell-gca") {
|
|
1248
|
-
throw new Error(
|
|
1249
|
-
"Access to the default Cloud Shell Gemini project was denied.\nPlease set your own Google Cloud project by running:\ngcloud config set project [PROJECT_ID]\nor setting export GOOGLE_CLOUD_PROJECT=...",
|
|
1250
|
-
{ cause: err }
|
|
1251
|
-
);
|
|
1252
|
-
}
|
|
1253
|
-
throw err;
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
function getValidationRequiredTier(response) {
|
|
1257
|
-
return response.ineligibleTiers?.find(
|
|
1258
|
-
(tier) => tier.reasonCode === VALIDATION_REQUIRED_REASON && typeof tier.validationUrl === "string"
|
|
1259
|
-
);
|
|
1260
|
-
}
|
|
1261
|
-
function getOnboardTier(response) {
|
|
1262
|
-
const defaultTier = response.allowedTiers?.find((tier) => tier.isDefault);
|
|
1263
|
-
return defaultTier ?? { id: USER_TIER_LEGACY, name: "" };
|
|
1264
|
-
}
|
|
1265
|
-
function throwProjectError(response) {
|
|
1266
|
-
const reasons = response.ineligibleTiers?.map((tier) => tier.reasonMessage ?? tier.tierName).filter((reason) => Boolean(reason));
|
|
1267
|
-
if (reasons && reasons.length > 0) {
|
|
1268
|
-
throw new Error(`Gemini Code Assist setup failed: ${reasons.join(", ")}`);
|
|
1269
|
-
}
|
|
1270
|
-
throw new Error(
|
|
1271
|
-
"Gemini requires a Google Cloud project for this account. Set GOOGLE_CLOUD_PROJECT and try again."
|
|
1272
|
-
);
|
|
1273
|
-
}
|
|
1274
|
-
async function codeAssistPost(accessToken, method, body) {
|
|
1275
|
-
let lastError;
|
|
1276
|
-
for (let attempt = 0; attempt <= CODE_ASSIST_POST_RETRIES; attempt++) {
|
|
900
|
+
async function revokeChatGPTSignIn(creds, options = {}) {
|
|
901
|
+
if (!creds.clientId || !creds.refreshToken) return false;
|
|
902
|
+
const attempts = options.attempts ?? 3;
|
|
903
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
1277
904
|
try {
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
905
|
+
const response = await postForm(REVOKE_URL, {
|
|
906
|
+
token: creds.refreshToken,
|
|
907
|
+
token_type_hint: "refresh_token",
|
|
908
|
+
client_id: creds.clientId
|
|
1281
909
|
});
|
|
1282
|
-
|
|
1283
|
-
if (
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
910
|
+
if (response.ok) return true;
|
|
911
|
+
if (response.status < 500) return false;
|
|
912
|
+
} catch {
|
|
913
|
+
}
|
|
914
|
+
if (attempt < attempts - 1) {
|
|
915
|
+
await new Promise((r) => setTimeout(r, (options.backoffMs ?? 500) * 2 ** attempt));
|
|
1287
916
|
}
|
|
1288
|
-
await new Promise((resolve) => setTimeout(resolve, CODE_ASSIST_POST_RETRY_DELAY_MS));
|
|
1289
|
-
}
|
|
1290
|
-
throw lastError ?? new Error(`Gemini Code Assist ${method} failed.`);
|
|
1291
|
-
}
|
|
1292
|
-
async function codeAssistGet(accessToken, operationName) {
|
|
1293
|
-
return codeAssistRequest(getCodeAssistOperationUrl(operationName), accessToken, "operation", {
|
|
1294
|
-
method: "GET"
|
|
1295
|
-
});
|
|
1296
|
-
}
|
|
1297
|
-
async function codeAssistRequest(url, accessToken, label, init) {
|
|
1298
|
-
const response = await fetch(url, {
|
|
1299
|
-
...init,
|
|
1300
|
-
headers: codeAssistHeaders(accessToken),
|
|
1301
|
-
signal: oauthRequestSignal()
|
|
1302
|
-
});
|
|
1303
|
-
if (!response.ok) {
|
|
1304
|
-
const text = await response.text();
|
|
1305
|
-
throw new CodeAssistHttpError(label, response.status, text);
|
|
1306
|
-
}
|
|
1307
|
-
return await response.json();
|
|
1308
|
-
}
|
|
1309
|
-
function getCodeAssistBaseUrl() {
|
|
1310
|
-
const endpoint = process.env.CODE_ASSIST_ENDPOINT ?? CODE_ASSIST_BASE_URL;
|
|
1311
|
-
const version = process.env.CODE_ASSIST_API_VERSION || CODE_ASSIST_API_VERSION;
|
|
1312
|
-
return `${endpoint}/${version}`;
|
|
1313
|
-
}
|
|
1314
|
-
function getCodeAssistMethodUrl(method) {
|
|
1315
|
-
return `${getCodeAssistBaseUrl()}:${method}`;
|
|
1316
|
-
}
|
|
1317
|
-
function getCodeAssistOperationUrl(operationName) {
|
|
1318
|
-
return `${getCodeAssistBaseUrl()}/${operationName}`;
|
|
1319
|
-
}
|
|
1320
|
-
function shouldRetryCodeAssistStatus(status) {
|
|
1321
|
-
return status === 429 || status === 499 || status >= 500 && status <= 599;
|
|
1322
|
-
}
|
|
1323
|
-
function isVpcScAffectedError(error) {
|
|
1324
|
-
try {
|
|
1325
|
-
const parsed = JSON.parse(error.body);
|
|
1326
|
-
if (!parsed || typeof parsed !== "object" || !("error" in parsed)) return false;
|
|
1327
|
-
const details = parsed.error?.details;
|
|
1328
|
-
return Array.isArray(details) ? details.some(
|
|
1329
|
-
(detail) => detail != null && typeof detail === "object" && "reason" in detail && detail.reason === VPC_SC_REASON
|
|
1330
|
-
) : false;
|
|
1331
|
-
} catch {
|
|
1332
|
-
return false;
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
function codeAssistHeaders(accessToken) {
|
|
1336
|
-
return {
|
|
1337
|
-
Authorization: `Bearer ${accessToken}`,
|
|
1338
|
-
"Content-Type": "application/json",
|
|
1339
|
-
"User-Agent": "google-gemini-cli",
|
|
1340
|
-
"X-Goog-Api-Client": "gemini-cli/0.0.0"
|
|
1341
|
-
};
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
// src/oauth/xai.ts
|
|
1345
|
-
var DEFAULT_CLIENT_ID2 = "b1a00492-073a-47ea-816f-4c329264a828";
|
|
1346
|
-
var DEFAULT_ISSUER = "https://auth.x.ai";
|
|
1347
|
-
var DEFAULT_CLI_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
|
|
1348
|
-
var OAUTH_SCOPE = "openid profile email offline_access grok-cli:access api:access";
|
|
1349
|
-
var DEFAULT_GROK_CLI_VERSION = "1.0.67";
|
|
1350
|
-
var DEVICE_TIMEOUT_FALLBACK_MS = 10 * 60 * 1e3;
|
|
1351
|
-
var DEFAULT_TOKEN_LIFETIME_SECONDS = 3600;
|
|
1352
|
-
function issuer() {
|
|
1353
|
-
return (process.env.XAI_OAUTH_ISSUER ?? DEFAULT_ISSUER).replace(/\/+$/, "");
|
|
1354
|
-
}
|
|
1355
|
-
function clientId() {
|
|
1356
|
-
return process.env.XAI_OAUTH_CLIENT_ID ?? DEFAULT_CLIENT_ID2;
|
|
1357
|
-
}
|
|
1358
|
-
function grokCliBaseUrl() {
|
|
1359
|
-
return (process.env.XAI_CLI_BASE_URL ?? DEFAULT_CLI_BASE_URL).replace(/\/+$/, "");
|
|
1360
|
-
}
|
|
1361
|
-
function grokCliVersion() {
|
|
1362
|
-
const raw = process.env.GROK_CLI_VERSION ?? DEFAULT_GROK_CLI_VERSION;
|
|
1363
|
-
const cleaned = raw.replace(/[^\u0020-\u007E]/g, "").trim();
|
|
1364
|
-
return cleaned.length > 0 ? cleaned : DEFAULT_GROK_CLI_VERSION;
|
|
1365
|
-
}
|
|
1366
|
-
function grokCliHeaders(modelId) {
|
|
1367
|
-
return {
|
|
1368
|
-
"X-XAI-Token-Auth": "xai-grok-cli",
|
|
1369
|
-
"x-grok-client-version": grokCliVersion(),
|
|
1370
|
-
"x-grok-client-identifier": "ggcoder",
|
|
1371
|
-
...modelId ? { "x-grok-model-override": modelId } : {}
|
|
1372
|
-
};
|
|
1373
|
-
}
|
|
1374
|
-
function isGrokCliEndpoint(baseUrl) {
|
|
1375
|
-
if (typeof baseUrl !== "string" || baseUrl.length === 0) return false;
|
|
1376
|
-
const normalized = baseUrl.replace(/\/+$/, "");
|
|
1377
|
-
return normalized === grokCliBaseUrl() || /(^|\.)grok\.com/i.test(normalized);
|
|
1378
|
-
}
|
|
1379
|
-
async function postForm2(endpoint, params) {
|
|
1380
|
-
const response = await fetch(`${issuer()}${endpoint}`, {
|
|
1381
|
-
method: "POST",
|
|
1382
|
-
headers: {
|
|
1383
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
1384
|
-
Accept: "application/json",
|
|
1385
|
-
"User-Agent": `grok-cli/${grokCliVersion()}`
|
|
1386
|
-
},
|
|
1387
|
-
body: new URLSearchParams(params).toString(),
|
|
1388
|
-
signal: oauthRequestSignal()
|
|
1389
|
-
});
|
|
1390
|
-
let data = {};
|
|
1391
|
-
try {
|
|
1392
|
-
const parsed = await response.json();
|
|
1393
|
-
if (parsed && typeof parsed === "object") data = parsed;
|
|
1394
|
-
} catch {
|
|
1395
|
-
}
|
|
1396
|
-
return { status: response.status, data };
|
|
1397
|
-
}
|
|
1398
|
-
function errorDetail2(data) {
|
|
1399
|
-
const desc = data.error_description ?? data.message ?? data.error;
|
|
1400
|
-
return typeof desc === "string" && desc.length > 0 ? desc : "unknown error";
|
|
1401
|
-
}
|
|
1402
|
-
function jwtExpirySeconds(token) {
|
|
1403
|
-
const payload = token.split(".")[1];
|
|
1404
|
-
if (!payload) return void 0;
|
|
1405
|
-
try {
|
|
1406
|
-
const json = Buffer.from(payload.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString(
|
|
1407
|
-
"utf-8"
|
|
1408
|
-
);
|
|
1409
|
-
const claims = JSON.parse(json);
|
|
1410
|
-
if (!claims || typeof claims !== "object") return void 0;
|
|
1411
|
-
const exp = claims.exp;
|
|
1412
|
-
return typeof exp === "number" && Number.isFinite(exp) && exp > 0 ? exp : void 0;
|
|
1413
|
-
} catch {
|
|
1414
|
-
return void 0;
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
function credsFromTokenResponse2(data, opts) {
|
|
1418
|
-
const accessToken = data.access_token;
|
|
1419
|
-
const responseRefreshToken = data.refresh_token;
|
|
1420
|
-
if (typeof accessToken !== "string" || accessToken.length === 0) {
|
|
1421
|
-
throw new Error("Grok OAuth response missing access_token.");
|
|
1422
|
-
}
|
|
1423
|
-
const refreshToken = typeof responseRefreshToken === "string" && responseRefreshToken.length > 0 ? responseRefreshToken : opts?.fallbackRefreshToken ?? "";
|
|
1424
|
-
if (refreshToken.length === 0) {
|
|
1425
|
-
throw new Error(
|
|
1426
|
-
"Grok OAuth response missing refresh_token \u2014 the offline_access scope was not granted."
|
|
1427
|
-
);
|
|
1428
|
-
}
|
|
1429
|
-
const responseExpiresIn = Number(data.expires_in);
|
|
1430
|
-
let expiresIn;
|
|
1431
|
-
if (Number.isFinite(responseExpiresIn) && responseExpiresIn > 0) {
|
|
1432
|
-
expiresIn = responseExpiresIn;
|
|
1433
|
-
} else {
|
|
1434
|
-
const exp = jwtExpirySeconds(accessToken);
|
|
1435
|
-
const fromJwt = exp !== void 0 ? exp - Math.floor(Date.now() / 1e3) : 0;
|
|
1436
|
-
expiresIn = fromJwt > 0 ? fromJwt : DEFAULT_TOKEN_LIFETIME_SECONDS;
|
|
1437
|
-
}
|
|
1438
|
-
return {
|
|
1439
|
-
accessToken,
|
|
1440
|
-
refreshToken,
|
|
1441
|
-
expiresAt: Date.now() + expiresIn * 1e3,
|
|
1442
|
-
expiresIn,
|
|
1443
|
-
baseUrl: grokCliBaseUrl()
|
|
1444
|
-
};
|
|
1445
|
-
}
|
|
1446
|
-
async function requestDeviceAuthorization2() {
|
|
1447
|
-
const { status, data } = await postForm2("/oauth2/device/code", {
|
|
1448
|
-
client_id: clientId(),
|
|
1449
|
-
scope: OAUTH_SCOPE
|
|
1450
|
-
});
|
|
1451
|
-
if (status !== 200) {
|
|
1452
|
-
throw new Error(`Grok device authorization failed (${status}): ${errorDetail2(data)}`);
|
|
1453
|
-
}
|
|
1454
|
-
const userCode = data.user_code;
|
|
1455
|
-
const deviceCode = data.device_code;
|
|
1456
|
-
const verificationUriComplete = data.verification_uri_complete;
|
|
1457
|
-
if (typeof userCode !== "string" || typeof deviceCode !== "string") {
|
|
1458
|
-
throw new Error("Grok device authorization response missing user_code/device_code.");
|
|
1459
|
-
}
|
|
1460
|
-
return {
|
|
1461
|
-
userCode,
|
|
1462
|
-
deviceCode,
|
|
1463
|
-
verificationUri: typeof data.verification_uri === "string" ? data.verification_uri : "",
|
|
1464
|
-
verificationUriComplete: typeof verificationUriComplete === "string" ? verificationUriComplete : "",
|
|
1465
|
-
interval: Number(data.interval ?? 5) || 5,
|
|
1466
|
-
// RFC 8628 §3.2: the server states how long the device code lives. Honor it
|
|
1467
|
-
// rather than imposing our own budget — polling a code we know is dead only
|
|
1468
|
-
// burns requests, and a longer local window would keep a user waiting past
|
|
1469
|
-
// the point the code can ever succeed.
|
|
1470
|
-
expiresInMs: (Number(data.expires_in) || 0) > 0 ? Number(data.expires_in) * 1e3 : DEVICE_TIMEOUT_FALLBACK_MS
|
|
1471
|
-
};
|
|
1472
|
-
}
|
|
1473
|
-
async function pollDeviceToken2(deviceCode) {
|
|
1474
|
-
const { status, data } = await postForm2("/oauth2/token", {
|
|
1475
|
-
client_id: clientId(),
|
|
1476
|
-
device_code: deviceCode,
|
|
1477
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
1478
|
-
});
|
|
1479
|
-
if (status === 200 && typeof data.access_token === "string") {
|
|
1480
|
-
return { kind: "success", creds: credsFromTokenResponse2(data) };
|
|
1481
|
-
}
|
|
1482
|
-
if (status >= 500) {
|
|
1483
|
-
throw new Error(`Grok token polling server error (${status}): ${errorDetail2(data)}`);
|
|
1484
|
-
}
|
|
1485
|
-
const errorCode = typeof data.error === "string" ? data.error : "unknown_error";
|
|
1486
|
-
switch (errorCode) {
|
|
1487
|
-
case "authorization_pending":
|
|
1488
|
-
return { kind: "pending" };
|
|
1489
|
-
case "slow_down":
|
|
1490
|
-
return { kind: "slow_down" };
|
|
1491
|
-
case "expired_token":
|
|
1492
|
-
return { kind: "expired" };
|
|
1493
|
-
case "access_denied":
|
|
1494
|
-
return { kind: "denied" };
|
|
1495
|
-
default:
|
|
1496
|
-
throw new Error(`Grok token polling failed (${status}): ${errorDetail2(data)}`);
|
|
1497
917
|
}
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
918
|
+
return false;
|
|
919
|
+
}
|
|
920
|
+
function decodeSegment(segment) {
|
|
921
|
+
return JSON.parse(Buffer.from(segment, "base64url").toString("utf-8"));
|
|
922
|
+
}
|
|
923
|
+
async function verifyIdToken(idToken, expected) {
|
|
924
|
+
const [headerSeg, payloadSeg, signatureSeg] = idToken.split(".");
|
|
925
|
+
if (!headerSeg || !payloadSeg || !signatureSeg) throw new Error("Malformed ChatGPT ID token.");
|
|
926
|
+
const header = decodeSegment(headerSeg);
|
|
927
|
+
if (header.alg !== "RS256") throw new Error("Unexpected ChatGPT ID token algorithm.");
|
|
928
|
+
const response = await fetch(JWKS_URL, { signal: oauthRequestSignal() });
|
|
929
|
+
if (!response.ok) throw new Error(`Could not load OpenAI signing keys (${response.status}).`);
|
|
930
|
+
const { keys = [] } = await response.json();
|
|
931
|
+
const jwk = keys.find((k) => k.kid === header.kid);
|
|
932
|
+
if (!jwk) throw new Error("ChatGPT ID token is signed with an unknown key.");
|
|
933
|
+
const valid = crypto3.verify(
|
|
934
|
+
"RSA-SHA256",
|
|
935
|
+
Buffer.from(`${headerSeg}.${payloadSeg}`),
|
|
936
|
+
crypto3.createPublicKey({ key: jwk, format: "jwk" }),
|
|
937
|
+
Buffer.from(signatureSeg, "base64url")
|
|
1508
938
|
);
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
const
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
if (result.kind === "success") return result.creds;
|
|
1518
|
-
if (result.kind === "denied") {
|
|
1519
|
-
throw new Error("Grok authorization was denied.");
|
|
1520
|
-
}
|
|
1521
|
-
if (result.kind === "expired") {
|
|
1522
|
-
throw new Error("Grok device code expired. Please run login again.");
|
|
1523
|
-
}
|
|
1524
|
-
if (result.kind === "slow_down") {
|
|
1525
|
-
interval += 5;
|
|
1526
|
-
}
|
|
939
|
+
if (!valid) throw new Error("ChatGPT ID token signature is invalid.");
|
|
940
|
+
const claims = decodeSegment(payloadSeg);
|
|
941
|
+
const now = Math.floor((expected.now ?? Date.now()) / 1e3);
|
|
942
|
+
const aud = claims.aud;
|
|
943
|
+
const audiences = Array.isArray(aud) ? aud : [aud];
|
|
944
|
+
if (claims.iss !== ISSUER) throw new Error("ChatGPT ID token has the wrong issuer.");
|
|
945
|
+
if (!audiences.includes(expected.clientId)) {
|
|
946
|
+
throw new Error("ChatGPT ID token was issued to another app.");
|
|
1527
947
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
async function refreshXaiToken(refreshToken) {
|
|
1531
|
-
const { status, data } = await postForm2("/oauth2/token", {
|
|
1532
|
-
client_id: clientId(),
|
|
1533
|
-
grant_type: "refresh_token",
|
|
1534
|
-
refresh_token: refreshToken
|
|
1535
|
-
});
|
|
1536
|
-
if (status === 200 && typeof data.access_token === "string") {
|
|
1537
|
-
return credsFromTokenResponse2(data, { fallbackRefreshToken: refreshToken });
|
|
948
|
+
if (typeof claims.exp !== "number" || claims.exp + ID_TOKEN_CLOCK_SKEW_S < now) {
|
|
949
|
+
throw new Error("ChatGPT ID token has expired.");
|
|
1538
950
|
}
|
|
1539
|
-
|
|
1540
|
-
|
|
951
|
+
if (claims.nonce !== expected.nonce) throw new Error("ChatGPT ID token nonce mismatch.");
|
|
952
|
+
return claims;
|
|
1541
953
|
}
|
|
1542
954
|
|
|
1543
955
|
// src/file-lock.ts
|
|
1544
|
-
import
|
|
956
|
+
import fs4 from "fs/promises";
|
|
1545
957
|
import { setTimeout as setTimeout2 } from "timers/promises";
|
|
1546
958
|
var STALE_TIMEOUT_MS = 1e4;
|
|
1547
959
|
var RETRY_INTERVAL_MS = 50;
|
|
@@ -1562,7 +974,7 @@ async function acquireLock(lockPath) {
|
|
|
1562
974
|
while (true) {
|
|
1563
975
|
try {
|
|
1564
976
|
const info = { pid: process.pid, timestamp: Date.now() };
|
|
1565
|
-
await
|
|
977
|
+
await fs4.writeFile(lockPath, JSON.stringify(info), { flag: "wx" });
|
|
1566
978
|
return;
|
|
1567
979
|
} catch (err) {
|
|
1568
980
|
const code = err.code;
|
|
@@ -1575,26 +987,26 @@ async function acquireLock(lockPath) {
|
|
|
1575
987
|
continue;
|
|
1576
988
|
}
|
|
1577
989
|
try {
|
|
1578
|
-
const content = await
|
|
990
|
+
const content = await fs4.readFile(lockPath, "utf-8");
|
|
1579
991
|
const info = JSON.parse(content);
|
|
1580
992
|
const isProcessAlive = isAlive(info.pid);
|
|
1581
993
|
const isStale = Date.now() - info.timestamp > STALE_TIMEOUT_MS;
|
|
1582
994
|
if (!isProcessAlive || isStale) {
|
|
1583
|
-
await
|
|
995
|
+
await fs4.unlink(lockPath).catch(() => {
|
|
1584
996
|
});
|
|
1585
997
|
continue;
|
|
1586
998
|
}
|
|
1587
999
|
} catch {
|
|
1588
|
-
const stat = await
|
|
1000
|
+
const stat = await fs4.stat(lockPath).catch(() => null);
|
|
1589
1001
|
if (!stat) continue;
|
|
1590
1002
|
if (Date.now() - stat.mtimeMs > STALE_TIMEOUT_MS) {
|
|
1591
|
-
await
|
|
1003
|
+
await fs4.unlink(lockPath).catch(() => {
|
|
1592
1004
|
});
|
|
1593
1005
|
continue;
|
|
1594
1006
|
}
|
|
1595
1007
|
}
|
|
1596
1008
|
if (Date.now() - startTime > MAX_WAIT_MS) {
|
|
1597
|
-
await
|
|
1009
|
+
await fs4.unlink(lockPath).catch(() => {
|
|
1598
1010
|
});
|
|
1599
1011
|
continue;
|
|
1600
1012
|
}
|
|
@@ -1604,14 +1016,14 @@ async function acquireLock(lockPath) {
|
|
|
1604
1016
|
}
|
|
1605
1017
|
async function lockFileExists(lockPath) {
|
|
1606
1018
|
try {
|
|
1607
|
-
await
|
|
1019
|
+
await fs4.stat(lockPath);
|
|
1608
1020
|
return true;
|
|
1609
1021
|
} catch (err) {
|
|
1610
1022
|
return err.code !== "ENOENT";
|
|
1611
1023
|
}
|
|
1612
1024
|
}
|
|
1613
1025
|
async function releaseLock(lockPath) {
|
|
1614
|
-
await
|
|
1026
|
+
await fs4.unlink(lockPath).catch(() => {
|
|
1615
1027
|
});
|
|
1616
1028
|
}
|
|
1617
1029
|
function isAlive(pid) {
|
|
@@ -1625,7 +1037,7 @@ function isAlive(pid) {
|
|
|
1625
1037
|
}
|
|
1626
1038
|
|
|
1627
1039
|
// src/logger.ts
|
|
1628
|
-
import
|
|
1040
|
+
import fs5 from "fs";
|
|
1629
1041
|
import path3 from "path";
|
|
1630
1042
|
import { randomBytes } from "crypto";
|
|
1631
1043
|
import { environmentSecrets, redactText, redactValue } from "@kenkaiiii/gg-ai";
|
|
@@ -1643,14 +1055,14 @@ var cleanups = [];
|
|
|
1643
1055
|
var exactSecrets = [];
|
|
1644
1056
|
function rotateIfNeeded(filePath, force = false) {
|
|
1645
1057
|
try {
|
|
1646
|
-
const st =
|
|
1058
|
+
const st = fs5.statSync(filePath);
|
|
1647
1059
|
if (!force && st.size <= MAX_BYTES - MIN_HEADROOM_BYTES) return;
|
|
1648
1060
|
const rotated = `${filePath}.1`;
|
|
1649
1061
|
try {
|
|
1650
|
-
|
|
1062
|
+
fs5.unlinkSync(rotated);
|
|
1651
1063
|
} catch {
|
|
1652
1064
|
}
|
|
1653
|
-
|
|
1065
|
+
fs5.renameSync(filePath, rotated);
|
|
1654
1066
|
} catch {
|
|
1655
1067
|
}
|
|
1656
1068
|
}
|
|
@@ -1658,18 +1070,18 @@ function rotateOpenLog() {
|
|
|
1658
1070
|
if (fd === null || logPath === null) return false;
|
|
1659
1071
|
const sizeBefore = fileBytes;
|
|
1660
1072
|
try {
|
|
1661
|
-
|
|
1073
|
+
fs5.closeSync(fd);
|
|
1662
1074
|
} catch {
|
|
1663
1075
|
}
|
|
1664
1076
|
fd = null;
|
|
1665
1077
|
rotateIfNeeded(logPath, true);
|
|
1666
1078
|
try {
|
|
1667
|
-
fd =
|
|
1668
|
-
fileBytes =
|
|
1079
|
+
fd = fs5.openSync(logPath, "a");
|
|
1080
|
+
fileBytes = fs5.fstatSync(fd).size;
|
|
1669
1081
|
} catch {
|
|
1670
1082
|
if (fd !== null) {
|
|
1671
1083
|
try {
|
|
1672
|
-
|
|
1084
|
+
fs5.closeSync(fd);
|
|
1673
1085
|
} catch {
|
|
1674
1086
|
}
|
|
1675
1087
|
}
|
|
@@ -1685,22 +1097,22 @@ function openLog(filePath, name) {
|
|
|
1685
1097
|
appName = name;
|
|
1686
1098
|
exactSecrets = environmentSecrets(process.env);
|
|
1687
1099
|
try {
|
|
1688
|
-
|
|
1100
|
+
fs5.mkdirSync(path3.dirname(filePath), { recursive: true, mode: 448 });
|
|
1689
1101
|
} catch {
|
|
1690
1102
|
}
|
|
1691
1103
|
rotateIfNeeded(filePath);
|
|
1692
1104
|
try {
|
|
1693
|
-
fd =
|
|
1105
|
+
fd = fs5.openSync(filePath, "a");
|
|
1694
1106
|
} catch {
|
|
1695
1107
|
fd = null;
|
|
1696
1108
|
fileBytes = 0;
|
|
1697
1109
|
return false;
|
|
1698
1110
|
}
|
|
1699
1111
|
try {
|
|
1700
|
-
fileBytes =
|
|
1112
|
+
fileBytes = fs5.fstatSync(fd).size;
|
|
1701
1113
|
} catch {
|
|
1702
1114
|
try {
|
|
1703
|
-
|
|
1115
|
+
fs5.closeSync(fd);
|
|
1704
1116
|
} catch {
|
|
1705
1117
|
}
|
|
1706
1118
|
fd = null;
|
|
@@ -1713,7 +1125,7 @@ function openLog(filePath, name) {
|
|
|
1713
1125
|
sessionId = randomBytes(4).toString("hex");
|
|
1714
1126
|
try {
|
|
1715
1127
|
if (fileBytes < MAX_BYTES) {
|
|
1716
|
-
const written =
|
|
1128
|
+
const written = fs5.writeSync(fd, "\n");
|
|
1717
1129
|
fileBytes += written;
|
|
1718
1130
|
processBytes += written;
|
|
1719
1131
|
}
|
|
@@ -1748,9 +1160,9 @@ function log(level, category, message, data) {
|
|
|
1748
1160
|
`;
|
|
1749
1161
|
try {
|
|
1750
1162
|
if (fileBytes + Buffer.byteLength(capLine) <= MAX_BYTES) {
|
|
1751
|
-
fileBytes +=
|
|
1163
|
+
fileBytes += fs5.writeSync(fd, capLine);
|
|
1752
1164
|
}
|
|
1753
|
-
|
|
1165
|
+
fs5.closeSync(fd);
|
|
1754
1166
|
} catch {
|
|
1755
1167
|
}
|
|
1756
1168
|
fd = null;
|
|
@@ -1760,7 +1172,7 @@ function log(level, category, message, data) {
|
|
|
1760
1172
|
if (fileBytes + lineBytes > MAX_BYTES && !rotationFailed && !rotateOpenLog()) return;
|
|
1761
1173
|
if (fd === null) return;
|
|
1762
1174
|
try {
|
|
1763
|
-
const written =
|
|
1175
|
+
const written = fs5.writeSync(fd, line);
|
|
1764
1176
|
fileBytes += written;
|
|
1765
1177
|
processBytes += written;
|
|
1766
1178
|
} catch {
|
|
@@ -1773,7 +1185,7 @@ function closeLogger(opts) {
|
|
|
1773
1185
|
if (fd !== null) {
|
|
1774
1186
|
if (opts?.shutdownLine !== false) log("INFO", "shutdown", `${appName} shutting down`);
|
|
1775
1187
|
try {
|
|
1776
|
-
if (fd !== null)
|
|
1188
|
+
if (fd !== null) fs5.closeSync(fd);
|
|
1777
1189
|
} catch {
|
|
1778
1190
|
}
|
|
1779
1191
|
}
|
|
@@ -1789,7 +1201,7 @@ function closeLogger(opts) {
|
|
|
1789
1201
|
}
|
|
1790
1202
|
|
|
1791
1203
|
// src/hidden-providers.ts
|
|
1792
|
-
var HIDDEN_PROVIDERS = [
|
|
1204
|
+
var HIDDEN_PROVIDERS = [];
|
|
1793
1205
|
function isHiddenProvider(provider) {
|
|
1794
1206
|
return HIDDEN_PROVIDERS.includes(provider);
|
|
1795
1207
|
}
|
|
@@ -1880,22 +1292,76 @@ async function detectXiaomiTokenPlanBaseUrl(apiKey, options = {}) {
|
|
|
1880
1292
|
}
|
|
1881
1293
|
|
|
1882
1294
|
// src/auth-storage.ts
|
|
1883
|
-
var
|
|
1884
|
-
var
|
|
1885
|
-
var
|
|
1295
|
+
var OPENAI_CHATGPT_KEY = "openai-chatgpt";
|
|
1296
|
+
var KIMI_CODE_KEY = "kimi-code";
|
|
1297
|
+
var RETIRED_SIGN_INS = [
|
|
1298
|
+
{
|
|
1299
|
+
provider: "anthropic",
|
|
1300
|
+
storageKey: "anthropic",
|
|
1301
|
+
message: "Your old Claude sign-in no longer works. Sign in with Claude again to keep using your subscription."
|
|
1302
|
+
},
|
|
1303
|
+
{
|
|
1304
|
+
provider: "openai",
|
|
1305
|
+
storageKey: "openai",
|
|
1306
|
+
message: "Your old ChatGPT sign-in no longer works. Continue with ChatGPT again, or add an OpenAI API key."
|
|
1307
|
+
},
|
|
1886
1308
|
{
|
|
1887
1309
|
provider: "moonshot",
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
apiKeyLabel: "Moonshot API key",
|
|
1891
|
-
restoreHint: 'Run "ggcoder login" and choose Kimi OAuth to restore OAuth auth.'
|
|
1310
|
+
storageKey: "moonshot-oauth",
|
|
1311
|
+
message: "Kimi sign-in was removed. Paste a Kimi Code plan key from the Kimi Code Console, or a Moonshot API key."
|
|
1892
1312
|
},
|
|
1893
1313
|
{
|
|
1894
1314
|
provider: "xai",
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1315
|
+
storageKey: "xai-oauth",
|
|
1316
|
+
message: "Grok sign-in was removed: xAI offers SuperGrok and X Premium usage only in apps it partners with. Add an xAI API key instead."
|
|
1317
|
+
},
|
|
1318
|
+
{
|
|
1319
|
+
provider: "gemini",
|
|
1320
|
+
storageKey: "gemini",
|
|
1321
|
+
message: "Google sign-in was removed. Add a Gemini API key from Google AI Studio instead."
|
|
1322
|
+
}
|
|
1323
|
+
];
|
|
1324
|
+
function isRetiredEntry(retired, value) {
|
|
1325
|
+
if (!value) return false;
|
|
1326
|
+
switch (retired.storageKey) {
|
|
1327
|
+
case "anthropic":
|
|
1328
|
+
return isLegacyClaudeOAuth(value);
|
|
1329
|
+
// These slots now hold API keys; only a refreshable sign-in token is retired.
|
|
1330
|
+
case "openai":
|
|
1331
|
+
case "gemini":
|
|
1332
|
+
return Boolean(value.refreshToken) || Boolean(value.accountId) || Boolean(value.projectId);
|
|
1333
|
+
default:
|
|
1334
|
+
return true;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
function isRetiredStorageKey(data, storageKey) {
|
|
1338
|
+
return RETIRED_SIGN_INS.some(
|
|
1339
|
+
(r) => r.storageKey === storageKey && isRetiredEntry(r, data[storageKey])
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
function usableEntry(data, storageKey) {
|
|
1343
|
+
return isRetiredStorageKey(data, storageKey) ? void 0 : data[storageKey];
|
|
1344
|
+
}
|
|
1345
|
+
function storageKeyProvider(storageKey) {
|
|
1346
|
+
if (storageKey === OPENAI_CHATGPT_KEY) return "openai";
|
|
1347
|
+
if (storageKey === KIMI_CODE_KEY) return "moonshot";
|
|
1348
|
+
if (storageKey === XIAOMI_CREDITS_KEY) return "xiaomi";
|
|
1349
|
+
if (storageKey === GEMINI_API_KEY) return "gemini";
|
|
1350
|
+
return storageKey;
|
|
1351
|
+
}
|
|
1352
|
+
function apiKeyStorageKeys(provider) {
|
|
1353
|
+
if (provider === "moonshot") return [KIMI_CODE_KEY, "moonshot"];
|
|
1354
|
+
if (provider === "xiaomi") return ["xiaomi", XIAOMI_CREDITS_KEY];
|
|
1355
|
+
if (provider === "gemini") return [GEMINI_API_KEY];
|
|
1356
|
+
return [provider];
|
|
1357
|
+
}
|
|
1358
|
+
var DUAL_AUTH_PROVIDERS = [
|
|
1359
|
+
{
|
|
1360
|
+
provider: "openai",
|
|
1361
|
+
oauthKey: OPENAI_CHATGPT_KEY,
|
|
1362
|
+
oauthLabel: "ChatGPT plan",
|
|
1363
|
+
apiKeyLabel: "OpenAI API key",
|
|
1364
|
+
restoreHint: 'Run "ggcoder login" and choose Continue with ChatGPT to use your ChatGPT plan again.'
|
|
1899
1365
|
}
|
|
1900
1366
|
];
|
|
1901
1367
|
function dualAuthProvider(provider) {
|
|
@@ -1920,14 +1386,20 @@ function activeBaseUrlEntry(data, provider) {
|
|
|
1920
1386
|
const dual = dualAuthProvider(provider);
|
|
1921
1387
|
if (dual) {
|
|
1922
1388
|
const oauth = data[dual.oauthKey];
|
|
1389
|
+
const apiKey = usableEntry(data, dual.provider);
|
|
1923
1390
|
if (oauth) {
|
|
1924
1391
|
const exhaustedUntil = oauth.usageExhaustedUntil ?? 0;
|
|
1925
|
-
if (Date.now() < exhaustedUntil &&
|
|
1392
|
+
if (Date.now() < exhaustedUntil && apiKey) return apiKey;
|
|
1926
1393
|
return oauth;
|
|
1927
1394
|
}
|
|
1928
|
-
return
|
|
1395
|
+
return apiKey;
|
|
1929
1396
|
}
|
|
1930
|
-
|
|
1397
|
+
const key = apiKeyStorageKeys(provider).find((k) => usableEntry(data, k));
|
|
1398
|
+
const entry = key ? data[key] : void 0;
|
|
1399
|
+
if (key === KIMI_CODE_KEY && entry && !entry.baseUrl) {
|
|
1400
|
+
return { ...entry, baseUrl: KIMI_CODE_DEFAULT_BASE_URL };
|
|
1401
|
+
}
|
|
1402
|
+
return entry;
|
|
1931
1403
|
}
|
|
1932
1404
|
var MINIMAX_REGION_RETRY_MS = 5 * 60 * 1e3;
|
|
1933
1405
|
var miniMaxRegionProbes = /* @__PURE__ */ new Map();
|
|
@@ -1938,9 +1410,17 @@ function cachedMiniMaxRegion(apiKey) {
|
|
|
1938
1410
|
miniMaxRegionProbes.set(apiKey, { at: Date.now(), result });
|
|
1939
1411
|
return result;
|
|
1940
1412
|
}
|
|
1413
|
+
var kimiRegionProbes = /* @__PURE__ */ new Map();
|
|
1414
|
+
function cachedKimiRegion(apiKey) {
|
|
1415
|
+
const cached = kimiRegionProbes.get(apiKey);
|
|
1416
|
+
if (cached && Date.now() - cached.at < MINIMAX_REGION_RETRY_MS) return cached.result;
|
|
1417
|
+
const result = detectKimiCodeBaseUrl(apiKey).catch(() => void 0);
|
|
1418
|
+
kimiRegionProbes.set(apiKey, { at: Date.now(), result });
|
|
1419
|
+
return result;
|
|
1420
|
+
}
|
|
1941
1421
|
function readStoredBaseUrlSync(authFile, provider) {
|
|
1942
1422
|
try {
|
|
1943
|
-
const data = JSON.parse(
|
|
1423
|
+
const data = JSON.parse(readFileSync(authFile, "utf-8"));
|
|
1944
1424
|
return activeBaseUrlEntry(data, provider)?.baseUrl;
|
|
1945
1425
|
} catch {
|
|
1946
1426
|
return void 0;
|
|
@@ -1953,21 +1433,7 @@ function refreshThresholdMs(creds) {
|
|
|
1953
1433
|
return Math.max(MIN_REFRESH_THRESHOLD_MS, lifetimeMs * REFRESH_THRESHOLD_RATIO);
|
|
1954
1434
|
}
|
|
1955
1435
|
var USAGE_EXHAUSTED_DEFAULT_MS = 15 * 60 * 1e3;
|
|
1956
|
-
var
|
|
1957
|
-
// Anthropic is API-key only; Claude subscriptions run through Claude Code.
|
|
1958
|
-
"anthropic",
|
|
1959
|
-
"glm",
|
|
1960
|
-
"moonshot",
|
|
1961
|
-
"xiaomi",
|
|
1962
|
-
"minimax",
|
|
1963
|
-
"deepseek",
|
|
1964
|
-
"openrouter",
|
|
1965
|
-
"huggingface",
|
|
1966
|
-
"sakana",
|
|
1967
|
-
"xai",
|
|
1968
|
-
// Local endpoints: a fixed (usually placeholder) key, never refreshable.
|
|
1969
|
-
"local"
|
|
1970
|
-
]);
|
|
1436
|
+
var REFRESHABLE_STORAGE_KEYS = /* @__PURE__ */ new Set([OPENAI_CHATGPT_KEY]);
|
|
1971
1437
|
var AuthStorage = class {
|
|
1972
1438
|
data = {};
|
|
1973
1439
|
filePath;
|
|
@@ -1988,10 +1454,12 @@ var AuthStorage = class {
|
|
|
1988
1454
|
refreshLocks = /* @__PURE__ */ new Map();
|
|
1989
1455
|
detectMiniMaxRegion;
|
|
1990
1456
|
detectXiaomiCluster;
|
|
1457
|
+
detectKimiRegion;
|
|
1991
1458
|
constructor(filePath, options) {
|
|
1992
1459
|
this.filePath = filePath ?? getAppPaths().authFile;
|
|
1993
1460
|
this.detectMiniMaxRegion = options?.detectMiniMaxBaseUrl ?? cachedMiniMaxRegion;
|
|
1994
1461
|
this.detectXiaomiCluster = options?.detectXiaomiTokenPlanBaseUrl ?? detectXiaomiTokenPlanBaseUrl;
|
|
1462
|
+
this.detectKimiRegion = options?.detectKimiCodeBaseUrl ?? cachedKimiRegion;
|
|
1995
1463
|
}
|
|
1996
1464
|
/** Path to the on-disk auth file. Useful for status output. */
|
|
1997
1465
|
get path() {
|
|
@@ -2019,22 +1487,15 @@ var AuthStorage = class {
|
|
|
2019
1487
|
return keys.find((key) => Boolean(this.data[key]));
|
|
2020
1488
|
}
|
|
2021
1489
|
/**
|
|
2022
|
-
* True if the user has any usable auth for the logical provider
|
|
2023
|
-
*
|
|
2024
|
-
* satisfies it.
|
|
1490
|
+
* True if the user has any usable auth for the logical provider: the plan
|
|
1491
|
+
* sign-in or any of its API keys. A retired sign-in doesn't count.
|
|
2025
1492
|
*/
|
|
2026
1493
|
async hasProviderAuth(provider) {
|
|
2027
1494
|
if (isHiddenProvider(provider)) return false;
|
|
2028
1495
|
await this.ensureFresh();
|
|
2029
1496
|
const dual = dualAuthProvider(provider);
|
|
2030
1497
|
if (dual) {
|
|
2031
|
-
return Boolean(this.data[dual.oauthKey] || this.data
|
|
2032
|
-
}
|
|
2033
|
-
if (provider === "xiaomi") {
|
|
2034
|
-
return Boolean(this.data["xiaomi"] || this.data[XIAOMI_CREDITS_KEY]);
|
|
2035
|
-
}
|
|
2036
|
-
if (provider === "gemini") {
|
|
2037
|
-
return Boolean(this.data["gemini"] || this.data[GEMINI_API_KEY]);
|
|
1498
|
+
return Boolean(this.data[dual.oauthKey] || usableEntry(this.data, dual.provider));
|
|
2038
1499
|
}
|
|
2039
1500
|
if (provider === "anthropic") {
|
|
2040
1501
|
return Boolean(this.data.anthropic) && !isLegacyClaudeOAuth(this.data.anthropic) || isClaudeCodeAuthMarker(this.data[CLAUDE_CODE_AUTH_KEY]);
|
|
@@ -2042,7 +1503,25 @@ var AuthStorage = class {
|
|
|
2042
1503
|
if (provider === "local") {
|
|
2043
1504
|
return Object.keys(this.data).some((key) => key.startsWith(LOCAL_AUTH_KEY_PREFIX));
|
|
2044
1505
|
}
|
|
2045
|
-
return Boolean(this.data
|
|
1506
|
+
return apiKeyStorageKeys(provider).some((key) => Boolean(usableEntry(this.data, key)));
|
|
1507
|
+
}
|
|
1508
|
+
/** True if any of `provider`'s API keys is stored. A retired sign-in in a key slot doesn't count. */
|
|
1509
|
+
async hasApiKey(provider) {
|
|
1510
|
+
await this.ensureFresh();
|
|
1511
|
+
if (provider === "anthropic" && isLegacyClaudeOAuth(this.data.anthropic)) return false;
|
|
1512
|
+
return apiKeyStorageKeys(provider).some((key) => Boolean(usableEntry(this.data, key)));
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* `provider`'s first stored API key, never its plan sign-in. For calls that
|
|
1516
|
+
* only an API key may make (OpenAI's Images API isn't on the ChatGPT plan).
|
|
1517
|
+
*/
|
|
1518
|
+
async resolveApiKey(provider) {
|
|
1519
|
+
await this.ensureFresh();
|
|
1520
|
+
for (const key of apiKeyStorageKeys(provider)) {
|
|
1521
|
+
const creds = usableEntry(this.data, key);
|
|
1522
|
+
if (creds && !(provider === "anthropic" && isLegacyClaudeOAuth(creds))) return creds;
|
|
1523
|
+
}
|
|
1524
|
+
throw new NotLoggedInError(provider);
|
|
2046
1525
|
}
|
|
2047
1526
|
/** True when the user signed in with Claude through Claude Code (marker only, no token). */
|
|
2048
1527
|
async hasClaudeCodeLogin() {
|
|
@@ -2056,17 +1535,24 @@ var AuthStorage = class {
|
|
|
2056
1535
|
if (isLegacyClaudeOAuth(latest.anthropic)) delete latest.anthropic;
|
|
2057
1536
|
});
|
|
2058
1537
|
}
|
|
1538
|
+
/** Retired sign-ins still on disk, so the UI can ask the user to reconnect. */
|
|
1539
|
+
async legacySignIns() {
|
|
1540
|
+
await this.ensureFresh();
|
|
1541
|
+
return RETIRED_SIGN_INS.filter((r) => isRetiredEntry(r, this.data[r.storageKey]));
|
|
1542
|
+
}
|
|
2059
1543
|
/**
|
|
2060
|
-
* Remove
|
|
2061
|
-
*
|
|
1544
|
+
* Remove `provider`'s retired sign-in, if any. True when one was removed, so
|
|
1545
|
+
* the caller can tell the user once to reconnect.
|
|
2062
1546
|
*/
|
|
2063
|
-
async
|
|
1547
|
+
async takeLegacySignIn(provider) {
|
|
1548
|
+
const retired = RETIRED_SIGN_INS.find((r) => r.provider === provider);
|
|
1549
|
+
if (!retired) return false;
|
|
2064
1550
|
await this.ensureFresh();
|
|
2065
|
-
if (!
|
|
1551
|
+
if (!isRetiredEntry(retired, this.data[retired.storageKey])) return false;
|
|
2066
1552
|
let removed = false;
|
|
2067
1553
|
await this.mutateLatest((latest) => {
|
|
2068
|
-
if (!
|
|
2069
|
-
delete latest.
|
|
1554
|
+
if (!isRetiredEntry(retired, latest[retired.storageKey])) return;
|
|
1555
|
+
delete latest[retired.storageKey];
|
|
2070
1556
|
removed = true;
|
|
2071
1557
|
});
|
|
2072
1558
|
return removed;
|
|
@@ -2107,18 +1593,18 @@ var AuthStorage = class {
|
|
|
2107
1593
|
const oauthCreds = dual ? this.data[dual.oauthKey] : void 0;
|
|
2108
1594
|
if (dual && oauthCreds) {
|
|
2109
1595
|
const exhaustedUntil = oauthCreds.usageExhaustedUntil ?? 0;
|
|
2110
|
-
const apiKeyActive = Date.now() < exhaustedUntil && Boolean(this.data
|
|
1596
|
+
const apiKeyActive = Date.now() < exhaustedUntil && Boolean(usableEntry(this.data, dual.provider));
|
|
2111
1597
|
if (!apiKeyActive) return false;
|
|
2112
1598
|
}
|
|
2113
|
-
return
|
|
1599
|
+
return !REFRESHABLE_STORAGE_KEYS.has(provider);
|
|
2114
1600
|
}
|
|
2115
1601
|
/**
|
|
2116
1602
|
* The base URL on the credential that is active right now, if any.
|
|
2117
1603
|
* Synchronous — call only after load()/resolveCredentials() populated the
|
|
2118
|
-
* snapshot. For a dual-auth provider this is the
|
|
2119
|
-
*
|
|
2120
|
-
*
|
|
2121
|
-
*
|
|
1604
|
+
* snapshot. For a dual-auth provider this is the plan endpoint whenever the
|
|
1605
|
+
* sign-in is the one resolveCredentials would serve (i.e. not currently
|
|
1606
|
+
* usage-exhausted with an API key configured); for a plan/pay-per-token key
|
|
1607
|
+
* split it is the plan key's endpoint when one is stored.
|
|
2122
1608
|
*/
|
|
2123
1609
|
getStoredBaseUrl(provider) {
|
|
2124
1610
|
return activeBaseUrlEntry(this.data, provider)?.baseUrl;
|
|
@@ -2127,7 +1613,7 @@ var AuthStorage = class {
|
|
|
2127
1613
|
const first = !this.loaded;
|
|
2128
1614
|
await withFileLock(this.filePath, async () => {
|
|
2129
1615
|
try {
|
|
2130
|
-
const content = await
|
|
1616
|
+
const content = await fs6.readFile(this.filePath, "utf-8");
|
|
2131
1617
|
this.data = JSON.parse(content);
|
|
2132
1618
|
if (first) {
|
|
2133
1619
|
log("INFO", "auth", `Loaded credentials from ${this.filePath}`, {
|
|
@@ -2173,7 +1659,7 @@ var AuthStorage = class {
|
|
|
2173
1659
|
}
|
|
2174
1660
|
let changed;
|
|
2175
1661
|
try {
|
|
2176
|
-
const stat = await
|
|
1662
|
+
const stat = await fs6.stat(this.filePath);
|
|
2177
1663
|
changed = stat.ino !== this.snapshotIno || stat.mtimeMs !== this.snapshotMtimeMs || stat.size !== this.snapshotSize;
|
|
2178
1664
|
} catch {
|
|
2179
1665
|
changed = this.snapshotSize !== -1;
|
|
@@ -2186,7 +1672,7 @@ var AuthStorage = class {
|
|
|
2186
1672
|
*/
|
|
2187
1673
|
async rememberSnapshot() {
|
|
2188
1674
|
try {
|
|
2189
|
-
const stat = await
|
|
1675
|
+
const stat = await fs6.stat(this.filePath);
|
|
2190
1676
|
this.snapshotMtimeMs = stat.mtimeMs;
|
|
2191
1677
|
this.snapshotSize = stat.size;
|
|
2192
1678
|
this.snapshotIno = stat.ino;
|
|
@@ -2241,9 +1727,34 @@ var AuthStorage = class {
|
|
|
2241
1727
|
});
|
|
2242
1728
|
return { ...creds, baseUrl };
|
|
2243
1729
|
}
|
|
2244
|
-
|
|
1730
|
+
/**
|
|
1731
|
+
* A Kimi Code key saved without an endpoint gets the host that issued it
|
|
1732
|
+
* (`api.kimi.com` or `api.kimi.ai`), saved so the check runs once.
|
|
1733
|
+
*/
|
|
1734
|
+
async withKimiRegion(creds) {
|
|
1735
|
+
const baseUrl = await this.detectKimiRegion(creds.accessToken);
|
|
1736
|
+
if (!baseUrl) return { ...creds, baseUrl: KIMI_CODE_DEFAULT_BASE_URL };
|
|
1737
|
+
log("INFO", "auth", `Kimi Code key belongs to ${baseUrl} \u2014 saved for future requests`);
|
|
1738
|
+
await this.mutateLatest((latest) => {
|
|
1739
|
+
const current = latest[KIMI_CODE_KEY];
|
|
1740
|
+
if (current?.accessToken === creds.accessToken && !current.baseUrl) {
|
|
1741
|
+
current.baseUrl = baseUrl;
|
|
1742
|
+
}
|
|
1743
|
+
});
|
|
1744
|
+
return { ...creds, baseUrl };
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Store `creds` under `storageKey`. Connecting a provider again also removes
|
|
1748
|
+
* its retired sign-in, which is what the reconnect notice asked for.
|
|
1749
|
+
*/
|
|
1750
|
+
async setCredentials(storageKey, creds) {
|
|
1751
|
+
const provider = storageKeyProvider(storageKey);
|
|
2245
1752
|
await this.mutateLatest((latest) => {
|
|
2246
|
-
|
|
1753
|
+
for (const retired of RETIRED_SIGN_INS) {
|
|
1754
|
+
if (retired.provider !== provider || retired.storageKey === storageKey) continue;
|
|
1755
|
+
if (isRetiredEntry(retired, latest[retired.storageKey])) delete latest[retired.storageKey];
|
|
1756
|
+
}
|
|
1757
|
+
latest[storageKey] = creds;
|
|
2247
1758
|
});
|
|
2248
1759
|
}
|
|
2249
1760
|
/**
|
|
@@ -2280,9 +1791,9 @@ var AuthStorage = class {
|
|
|
2280
1791
|
* Mark the credential stored under `storageKey` as usage-exhausted until
|
|
2281
1792
|
* `resetsAt` (unix SECONDS, from the provider's rate-limit response) or a
|
|
2282
1793
|
* 15-minute default when no reset time is known. While the mark is in the
|
|
2283
|
-
* future, `resolveCredentials("
|
|
2284
|
-
*
|
|
2285
|
-
*
|
|
1794
|
+
* future, `resolveCredentials("openai")` serves the OpenAI API key instead of
|
|
1795
|
+
* the ChatGPT plan sign-in (when both are configured) — the plan stays the
|
|
1796
|
+
* preferred credential and is retried automatically once the mark
|
|
2286
1797
|
* lapses. Persisted to auth.json so a restart (or another gg-app window)
|
|
2287
1798
|
* doesn't burn a request rediscovering the same exhausted window. No-op if
|
|
2288
1799
|
* nothing is stored under `storageKey`.
|
|
@@ -2327,12 +1838,12 @@ var AuthStorage = class {
|
|
|
2327
1838
|
await this.ensureFresh();
|
|
2328
1839
|
}
|
|
2329
1840
|
const directStorageKeys = opts?.storageKeys && !(opts.storageKeys.length === 1 && opts.storageKeys[0] === provider) ? opts.storageKeys : providerStorageKeys(provider);
|
|
2330
|
-
if (!directStorageKeys.some((key) => Boolean(this.data
|
|
1841
|
+
if (!directStorageKeys.some((key) => Boolean(usableEntry(this.data, key))) && !(provider === "anthropic" && this.data[CLAUDE_CODE_AUTH_KEY])) {
|
|
2331
1842
|
await this.reloadLatest();
|
|
2332
1843
|
}
|
|
2333
1844
|
if (opts?.storageKeys && !(opts.storageKeys.length === 1 && opts.storageKeys[0] === provider)) {
|
|
2334
1845
|
for (const key of opts.storageKeys) {
|
|
2335
|
-
const creds2 = this.data
|
|
1846
|
+
const creds2 = usableEntry(this.data, key);
|
|
2336
1847
|
if (!creds2) continue;
|
|
2337
1848
|
if (key !== provider && dualAuthProviderByOAuthKey(key)) {
|
|
2338
1849
|
return await this.resolveCredentials(key, {
|
|
@@ -2346,6 +1857,7 @@ var AuthStorage = class {
|
|
|
2346
1857
|
...opts.rejectedToken !== void 0 ? { rejectedToken: opts.rejectedToken } : {}
|
|
2347
1858
|
});
|
|
2348
1859
|
}
|
|
1860
|
+
if (key === KIMI_CODE_KEY && !creds2.baseUrl) return await this.withKimiRegion(creds2);
|
|
2349
1861
|
return creds2;
|
|
2350
1862
|
}
|
|
2351
1863
|
throw new NotLoggedInError(provider);
|
|
@@ -2358,15 +1870,16 @@ var AuthStorage = class {
|
|
|
2358
1870
|
}
|
|
2359
1871
|
const dual = dualAuthProvider(provider);
|
|
2360
1872
|
const dualOAuthCreds = dual ? this.data[dual.oauthKey] : void 0;
|
|
1873
|
+
const dualApiKey = dual ? usableEntry(this.data, dual.provider) : void 0;
|
|
2361
1874
|
if (dual && dualOAuthCreds) {
|
|
2362
1875
|
const exhaustedUntil = dualOAuthCreds.usageExhaustedUntil ?? 0;
|
|
2363
|
-
if (Date.now() < exhaustedUntil &&
|
|
1876
|
+
if (Date.now() < exhaustedUntil && dualApiKey) {
|
|
2364
1877
|
log(
|
|
2365
1878
|
"WARN",
|
|
2366
1879
|
"auth",
|
|
2367
1880
|
`${dual.oauthLabel} usage window is exhausted \u2014 using the ${dual.apiKeyLabel} until ${new Date(exhaustedUntil).toISOString()} (OAuth resumes automatically).`
|
|
2368
1881
|
);
|
|
2369
|
-
return
|
|
1882
|
+
return dualApiKey;
|
|
2370
1883
|
}
|
|
2371
1884
|
try {
|
|
2372
1885
|
return await this.resolveCredentials(dual.oauthKey, {
|
|
@@ -2374,23 +1887,24 @@ var AuthStorage = class {
|
|
|
2374
1887
|
...opts?.rejectedToken !== void 0 ? { rejectedToken: opts.rejectedToken } : {}
|
|
2375
1888
|
});
|
|
2376
1889
|
} catch (err) {
|
|
2377
|
-
if (err instanceof NotLoggedInError &&
|
|
1890
|
+
if (err instanceof NotLoggedInError && dualApiKey) {
|
|
2378
1891
|
log(
|
|
2379
1892
|
"WARN",
|
|
2380
1893
|
"auth",
|
|
2381
1894
|
`${dual.oauthLabel} credential is no longer valid \u2014 falling back to the ${dual.apiKeyLabel}. ${dual.restoreHint}`
|
|
2382
1895
|
);
|
|
2383
|
-
return
|
|
1896
|
+
return dualApiKey;
|
|
2384
1897
|
}
|
|
2385
1898
|
throw err;
|
|
2386
1899
|
}
|
|
2387
1900
|
}
|
|
2388
|
-
const creds = this.data
|
|
1901
|
+
const creds = usableEntry(this.data, provider);
|
|
2389
1902
|
if (!creds) {
|
|
2390
1903
|
throw new NotLoggedInError(provider);
|
|
2391
1904
|
}
|
|
2392
|
-
if (
|
|
1905
|
+
if (!REFRESHABLE_STORAGE_KEYS.has(provider)) {
|
|
2393
1906
|
if (provider === "minimax" && !creds.baseUrl) return await this.withMiniMaxRegion(creds);
|
|
1907
|
+
if (provider === KIMI_CODE_KEY && !creds.baseUrl) return await this.withKimiRegion(creds);
|
|
2394
1908
|
return creds;
|
|
2395
1909
|
}
|
|
2396
1910
|
if (!opts?.forceRefresh && Date.now() < creds.expiresAt - refreshThresholdMs(creds)) {
|
|
@@ -2418,13 +1932,14 @@ var AuthStorage = class {
|
|
|
2418
1932
|
this.data = latest;
|
|
2419
1933
|
return latestCreds;
|
|
2420
1934
|
}
|
|
2421
|
-
const refreshFn = provider === "gemini" ? refreshGeminiToken : provider === MOONSHOT_OAUTH_KEY ? refreshKimiToken : provider === XAI_OAUTH_KEY ? refreshXaiToken : refreshOpenAIToken;
|
|
2422
1935
|
let refreshed;
|
|
2423
1936
|
try {
|
|
2424
|
-
refreshed = await
|
|
1937
|
+
refreshed = await refreshChatGPTToken(latestCreds);
|
|
2425
1938
|
} catch (err) {
|
|
2426
1939
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2427
|
-
const isAuthFailure = /\((401|400)\)/.test(msg) || /invalid_grant|invalid_token|invalid.*refresh
|
|
1940
|
+
const isAuthFailure = /\((401|400)\)/.test(msg) || /invalid_grant|invalid_token|invalid.*refresh|token_expired|refresh_token_(?:expired|invalidated|reused)/i.test(
|
|
1941
|
+
msg
|
|
1942
|
+
) || /unauthorized/i.test(msg);
|
|
2428
1943
|
if (isAuthFailure) {
|
|
2429
1944
|
const current = await readAuthData(this.filePath);
|
|
2430
1945
|
const currentCreds = current[provider];
|
|
@@ -2444,15 +1959,10 @@ var AuthStorage = class {
|
|
|
2444
1959
|
}
|
|
2445
1960
|
throw err;
|
|
2446
1961
|
}
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
refreshed.projectId = latestCreds.projectId;
|
|
2452
|
-
}
|
|
2453
|
-
if (!refreshed.baseUrl && latestCreds.baseUrl) {
|
|
2454
|
-
refreshed.baseUrl = latestCreds.baseUrl;
|
|
2455
|
-
}
|
|
1962
|
+
refreshed = {
|
|
1963
|
+
...pick(latestCreds, ["accountId", "baseUrl", "clientId", "idToken", "scopes"]),
|
|
1964
|
+
...refreshed
|
|
1965
|
+
};
|
|
2456
1966
|
latest[provider] = refreshed;
|
|
2457
1967
|
this.data = latest;
|
|
2458
1968
|
await atomicWriteFile(this.filePath, JSON.stringify(latest, null, 2));
|
|
@@ -2475,9 +1985,14 @@ var AuthStorage = class {
|
|
|
2475
1985
|
return creds.accessToken;
|
|
2476
1986
|
}
|
|
2477
1987
|
};
|
|
1988
|
+
function pick(value, keys) {
|
|
1989
|
+
const out = {};
|
|
1990
|
+
for (const key of keys) if (value[key] !== void 0) out[key] = value[key];
|
|
1991
|
+
return out;
|
|
1992
|
+
}
|
|
2478
1993
|
async function readAuthData(filePath) {
|
|
2479
1994
|
try {
|
|
2480
|
-
const content = await
|
|
1995
|
+
const content = await fs6.readFile(filePath, "utf-8");
|
|
2481
1996
|
return JSON.parse(content);
|
|
2482
1997
|
} catch (error) {
|
|
2483
1998
|
if (error.code === "ENOENT") return {};
|
|
@@ -2485,12 +2000,12 @@ async function readAuthData(filePath) {
|
|
|
2485
2000
|
}
|
|
2486
2001
|
}
|
|
2487
2002
|
async function atomicWriteFile(filePath, content) {
|
|
2488
|
-
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.${
|
|
2003
|
+
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.${crypto4.randomUUID().slice(0, 8)}.tmp`;
|
|
2489
2004
|
try {
|
|
2490
|
-
await
|
|
2491
|
-
await
|
|
2005
|
+
await fs6.writeFile(tmpPath, content, { encoding: "utf-8", mode: 384 });
|
|
2006
|
+
await fs6.rename(tmpPath, filePath);
|
|
2492
2007
|
} catch (err) {
|
|
2493
|
-
await
|
|
2008
|
+
await fs6.unlink(tmpPath).catch(() => {
|
|
2494
2009
|
});
|
|
2495
2010
|
throw err;
|
|
2496
2011
|
}
|
|
@@ -2522,7 +2037,8 @@ var CODEX_FAST_BILLING = {
|
|
|
2522
2037
|
detail: "Codex Fast mode uses your ChatGPT plan limits at 2.5x the standard rate (purchased credits at 2x).",
|
|
2523
2038
|
paid: false
|
|
2524
2039
|
};
|
|
2525
|
-
var GEMINI_AUTH_KEYS = [GEMINI_API_KEY
|
|
2040
|
+
var GEMINI_AUTH_KEYS = [GEMINI_API_KEY];
|
|
2041
|
+
var KIMI_AUTH_KEYS = [KIMI_CODE_KEY, "moonshot"];
|
|
2526
2042
|
var MODELS = [
|
|
2527
2043
|
// ── Anthropic ──────────────────────────────────────────
|
|
2528
2044
|
// NOTE: Claude Mythos 5.1 (`claude-mythos-5-1`, 2026-09-01) is kept commented
|
|
@@ -2652,14 +2168,13 @@ var MODELS = [
|
|
|
2652
2168
|
{
|
|
2653
2169
|
// GPT-6 Astra — "Our most capable model for complex, demanding work."
|
|
2654
2170
|
// (Codex catalog priority 2, listed for every ChatGPT plan, requires a
|
|
2655
|
-
// Codex client >= 0.153.0
|
|
2656
|
-
// 1.05M on the public Responses API, 272K on the ChatGPT
|
|
2171
|
+
// Codex client >= 0.153.0). Same split as 5.6:
|
|
2172
|
+
// 1.05M on the public Responses API, 272K on the ChatGPT plan route
|
|
2657
2173
|
// (openai/codex models.json, `gpt-6-astra`). Reasoning ladder low → medium
|
|
2658
2174
|
// → high → xhigh → max → ultra; `ultra` is the Codex orchestration preset
|
|
2659
2175
|
// (multi_agent v2) and is Codex-only — the public API tops out at `max`.
|
|
2660
|
-
//
|
|
2661
|
-
//
|
|
2662
|
-
// Codex route is the supported way to use it as an agent.
|
|
2176
|
+
// OpenAI requires the Responses API for tool calling on Astra, so both
|
|
2177
|
+
// the ChatGPT plan and API keys ride gg-ai's Responses transport.
|
|
2663
2178
|
id: "gpt-6-astra",
|
|
2664
2179
|
name: "GPT-6 Astra",
|
|
2665
2180
|
provider: "openai",
|
|
@@ -2702,9 +2217,7 @@ var MODELS = [
|
|
|
2702
2217
|
// next start.
|
|
2703
2218
|
{
|
|
2704
2219
|
// GPT-6.1 Sol — "Latest workhorse model for coding and everyday work."
|
|
2705
|
-
// (Codex priority 1, default low).
|
|
2706
|
-
// but the ChatGPT backend only serves it from 0.159.0 (see
|
|
2707
|
-
// CODEX_CLIENT_VERSION). $2/$10 MTok, cached input $0.10. Ladder low →
|
|
2220
|
+
// (Codex priority 1, default low). $2/$10 MTok, cached input $0.10. Ladder low →
|
|
2708
2221
|
// medium → high → xhigh → max → ultra; ultra is the Codex orchestration
|
|
2709
2222
|
// preset (xhigh effort on the wire + proactive local subagent delegation).
|
|
2710
2223
|
id: "gpt-6.1-sol",
|
|
@@ -2891,8 +2404,8 @@ var MODELS = [
|
|
|
2891
2404
|
maxThinkingLevel: "high",
|
|
2892
2405
|
authStorageKeys: GEMINI_AUTH_KEYS
|
|
2893
2406
|
},
|
|
2894
|
-
// Keep 3.1 Flash Lite first for the
|
|
2895
|
-
//
|
|
2407
|
+
// Keep 3.1 Flash Lite first for the default and fast-model routing.
|
|
2408
|
+
// Gemini 3.7
|
|
2896
2409
|
// Flash is retired: Google deprecated it on 2026-10-08 and routes every
|
|
2897
2410
|
// `gemini-3.7-flash` request to 3.8 Flash (ai.google.dev/gemini-api/docs/
|
|
2898
2411
|
// changelog), so a saved session on it falls back to the provider default.
|
|
@@ -2925,9 +2438,7 @@ var MODELS = [
|
|
|
2925
2438
|
authStorageKeys: GEMINI_AUTH_KEYS
|
|
2926
2439
|
},
|
|
2927
2440
|
{
|
|
2928
|
-
// Wire name `gemini-3-flash
|
|
2929
|
-
// display string `gemini-3.5-flash` with a 404, so gemini-cli keeps this
|
|
2930
|
-
// alternative name (SECONDARY_GEMINI_3_5_FLASH_MODEL) for that endpoint.
|
|
2441
|
+
// Wire name `gemini-3-flash`, the id Google serves 3.5 Flash under.
|
|
2931
2442
|
id: "gemini-3-flash",
|
|
2932
2443
|
name: "Gemini 3.5 Flash",
|
|
2933
2444
|
provider: "gemini",
|
|
@@ -2942,9 +2453,7 @@ var MODELS = [
|
|
|
2942
2453
|
authStorageKeys: GEMINI_AUTH_KEYS
|
|
2943
2454
|
},
|
|
2944
2455
|
{
|
|
2945
|
-
// Gemini 3.1 Pro is public preview
|
|
2946
|
-
// enablement, so free/personal OAuth accounts 404 on it (see
|
|
2947
|
-
// ACCOUNT_GATED_MODELS in gg-ai's gemini provider).
|
|
2456
|
+
// Gemini 3.1 Pro is public preview.
|
|
2948
2457
|
id: "gemini-3.1-pro-preview",
|
|
2949
2458
|
name: "Gemini 3.1 Pro (Preview)",
|
|
2950
2459
|
provider: "gemini",
|
|
@@ -2961,8 +2470,8 @@ var MODELS = [
|
|
|
2961
2470
|
// ── Moonshot (Kimi) ────────────────────────────────────
|
|
2962
2471
|
// K3 is Kimi's 2.8T-parameter flagship for long-horizon coding, knowledge
|
|
2963
2472
|
// work, and deep reasoning. Its effort ladder is server-declared as
|
|
2964
|
-
// low/high/max on both the public API (default max) and the Kimi
|
|
2965
|
-
//
|
|
2473
|
+
// low/high/max on both the public API (default max) and the Kimi Code plan
|
|
2474
|
+
// endpoint (default high); thinking can also be fully disabled.
|
|
2966
2475
|
{
|
|
2967
2476
|
id: "kimi-k3",
|
|
2968
2477
|
name: "Kimi K3",
|
|
@@ -2976,13 +2485,14 @@ var MODELS = [
|
|
|
2976
2485
|
supportsVideo: true,
|
|
2977
2486
|
maxVideoBytes: 100 * 1024 * 1024,
|
|
2978
2487
|
costTier: "high",
|
|
2979
|
-
maxThinkingLevel: "max"
|
|
2488
|
+
maxThinkingLevel: "max",
|
|
2489
|
+
authStorageKeys: KIMI_AUTH_KEYS
|
|
2980
2490
|
},
|
|
2981
|
-
// K2.8 Preview (2026-09-11) is served only on the Kimi
|
|
2982
|
-
//
|
|
2491
|
+
// K2.8 Preview (2026-09-11) is served only on the Kimi Code plan endpoint,
|
|
2492
|
+
// under its rolling `kimi-for-coding` id (live /models, 2026-09-28:
|
|
2983
2493
|
// display_name "K2.8 Preview", 1M context, image + video input, efforts
|
|
2984
|
-
// low/high/max default max). The public API
|
|
2985
|
-
//
|
|
2494
|
+
// low/high/max default max). The public Moonshot API does not serve it, so
|
|
2495
|
+
// it resolves from a Kimi Code plan key only.
|
|
2986
2496
|
{
|
|
2987
2497
|
id: "kimi-for-coding",
|
|
2988
2498
|
name: "Kimi K2.8 Preview",
|
|
@@ -2995,7 +2505,7 @@ var MODELS = [
|
|
|
2995
2505
|
maxVideoBytes: 100 * 1024 * 1024,
|
|
2996
2506
|
costTier: "medium",
|
|
2997
2507
|
maxThinkingLevel: "max",
|
|
2998
|
-
authStorageKeys: [
|
|
2508
|
+
authStorageKeys: [KIMI_CODE_KEY]
|
|
2999
2509
|
},
|
|
3000
2510
|
// K2.7 Code is requested by its pinned id (not the `kimi-for-coding` alias
|
|
3001
2511
|
// that moved to K2.8), so it stays the real K2.7 on both endpoints.
|
|
@@ -3010,7 +2520,8 @@ var MODELS = [
|
|
|
3010
2520
|
supportsVideo: true,
|
|
3011
2521
|
maxVideoBytes: 100 * 1024 * 1024,
|
|
3012
2522
|
costTier: "medium",
|
|
3013
|
-
maxThinkingLevel: "high"
|
|
2523
|
+
maxThinkingLevel: "high",
|
|
2524
|
+
authStorageKeys: KIMI_AUTH_KEYS
|
|
3014
2525
|
},
|
|
3015
2526
|
// K2.7 Code HighSpeed — the same model as K2.7 Code served at ~180 tok/s
|
|
3016
2527
|
// (up to ~260 on short contexts), 256K context (platform.kimi.ai model list,
|
|
@@ -3029,7 +2540,8 @@ var MODELS = [
|
|
|
3029
2540
|
supportsVideo: true,
|
|
3030
2541
|
maxVideoBytes: 100 * 1024 * 1024,
|
|
3031
2542
|
costTier: "medium",
|
|
3032
|
-
maxThinkingLevel: "high"
|
|
2543
|
+
maxThinkingLevel: "high",
|
|
2544
|
+
authStorageKeys: KIMI_AUTH_KEYS
|
|
3033
2545
|
},
|
|
3034
2546
|
// ── Z.AI (GLM) ─────────────────────────────────────────
|
|
3035
2547
|
// Two GLM entries, both live on the coding endpoint (verified against its
|
|
@@ -3442,11 +2954,10 @@ function getSummaryModel(provider, currentModelId) {
|
|
|
3442
2954
|
}
|
|
3443
2955
|
|
|
3444
2956
|
export {
|
|
3445
|
-
|
|
3446
|
-
|
|
2957
|
+
KIMI_CODE_DEFAULT_BASE_URL,
|
|
2958
|
+
KIMI_CODE_OVERSEAS_BASE_URL,
|
|
2959
|
+
detectKimiCodeBaseUrl,
|
|
3447
2960
|
isKimiCodingEndpoint,
|
|
3448
|
-
loginKimi,
|
|
3449
|
-
refreshKimiToken,
|
|
3450
2961
|
CLAUDE_CODE_MANIFEST,
|
|
3451
2962
|
CLAUDE_CODE_AUTH_KEY,
|
|
3452
2963
|
CLAUDE_CODE_AUTH_MARKER,
|
|
@@ -3465,15 +2976,18 @@ export {
|
|
|
3465
2976
|
claudeCodeCallbackUrl,
|
|
3466
2977
|
loginClaudeCode,
|
|
3467
2978
|
generatePKCE,
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
2979
|
+
CHATGPT_PLAN_SCOPE,
|
|
2980
|
+
CHATGPT_PLAN_BASE_URL,
|
|
2981
|
+
CHATGPT_USAGE_URL,
|
|
2982
|
+
CHATGPT_PLAN_NOT_ENABLED_MESSAGE,
|
|
2983
|
+
ChatGPTPlanNotEnabledError,
|
|
2984
|
+
getChatGPTHostId,
|
|
2985
|
+
loginChatGPT,
|
|
2986
|
+
fetchChatGPTPlanModels,
|
|
2987
|
+
parseCallbackInput,
|
|
2988
|
+
refreshChatGPTToken,
|
|
2989
|
+
revokeChatGPTSignIn,
|
|
2990
|
+
verifyIdToken,
|
|
3477
2991
|
withFileLock,
|
|
3478
2992
|
openLog,
|
|
3479
2993
|
getSessionId,
|
|
@@ -3488,8 +3002,11 @@ export {
|
|
|
3488
3002
|
detectMiniMaxBaseUrl,
|
|
3489
3003
|
XIAOMI_TOKEN_PLAN_BASE_URLS,
|
|
3490
3004
|
detectXiaomiTokenPlanBaseUrl,
|
|
3491
|
-
|
|
3492
|
-
|
|
3005
|
+
OPENAI_CHATGPT_KEY,
|
|
3006
|
+
KIMI_CODE_KEY,
|
|
3007
|
+
RETIRED_SIGN_INS,
|
|
3008
|
+
storageKeyProvider,
|
|
3009
|
+
apiKeyStorageKeys,
|
|
3493
3010
|
dualAuthProvider,
|
|
3494
3011
|
dualAuthProviderByOAuthKey,
|
|
3495
3012
|
oauthStorageKey,
|
|
@@ -3524,4 +3041,4 @@ export {
|
|
|
3524
3041
|
getDefaultThinkingLevel,
|
|
3525
3042
|
getSummaryModel
|
|
3526
3043
|
};
|
|
3527
|
-
//# sourceMappingURL=chunk-
|
|
3044
|
+
//# sourceMappingURL=chunk-J7W5HS6X.js.map
|