@prestyj/core 5.11.0 → 5.13.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/dist/{chunk-WAG5K2MH.js → chunk-S6QBRJ4D.js} +448 -124
- package/dist/chunk-S6QBRJ4D.js.map +1 -0
- package/dist/index.cjs +469 -126
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +161 -13
- package/dist/index.d.ts +161 -13
- package/dist/index.js +31 -2
- package/dist/index.js.map +1 -1
- package/dist/model-registry.cjs +16 -43
- package/dist/model-registry.cjs.map +1 -1
- package/dist/model-registry.d.cts +5 -6
- package/dist/model-registry.d.ts +5 -6
- package/dist/model-registry.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-WAG5K2MH.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -41,6 +41,7 @@ __export(index_exports, {
|
|
|
41
41
|
NotLoggedInError: () => NotLoggedInError,
|
|
42
42
|
SubscriptionUsageError: () => SubscriptionUsageError,
|
|
43
43
|
TelegramBot: () => TelegramBot,
|
|
44
|
+
XAI_OAUTH_KEY: () => XAI_OAUTH_KEY,
|
|
44
45
|
XIAOMI_CREDITS_KEY: () => XIAOMI_CREDITS_KEY,
|
|
45
46
|
clearLocalDiscoveryCache: () => clearLocalDiscoveryCache,
|
|
46
47
|
clearRuntimeModels: () => clearRuntimeModels,
|
|
@@ -49,6 +50,8 @@ __export(index_exports, {
|
|
|
49
50
|
decodeOggOpus: () => decodeOggOpus,
|
|
50
51
|
discoverLocalModels: () => discoverLocalModels,
|
|
51
52
|
downmixToMono: () => downmixToMono,
|
|
53
|
+
dualAuthProvider: () => dualAuthProvider,
|
|
54
|
+
dualAuthProviderByOAuthKey: () => dualAuthProviderByOAuthKey,
|
|
52
55
|
endpointRoot: () => endpointRoot,
|
|
53
56
|
fetchSubscriptionUsage: () => fetchSubscriptionUsage,
|
|
54
57
|
findProbedModel: () => findProbedModel,
|
|
@@ -73,6 +76,9 @@ __export(index_exports, {
|
|
|
73
76
|
getSupportedThinkingLevels: () => getSupportedThinkingLevels,
|
|
74
77
|
getToolResultCharLimit: () => getToolResultCharLimit,
|
|
75
78
|
getVideoByteLimit: () => getVideoByteLimit,
|
|
79
|
+
grokCliBaseUrl: () => grokCliBaseUrl,
|
|
80
|
+
grokCliHeaders: () => grokCliHeaders,
|
|
81
|
+
isGrokCliEndpoint: () => isGrokCliEndpoint,
|
|
76
82
|
isKimiCodingEndpoint: () => isKimiCodingEndpoint,
|
|
77
83
|
isLocalModelId: () => isLocalModelId,
|
|
78
84
|
isLoggerOpen: () => isLoggerOpen,
|
|
@@ -86,14 +92,18 @@ __export(index_exports, {
|
|
|
86
92
|
loginGemini: () => loginGemini,
|
|
87
93
|
loginKimi: () => loginKimi,
|
|
88
94
|
loginOpenAI: () => loginOpenAI,
|
|
95
|
+
loginXai: () => loginXai,
|
|
96
|
+
oauthStorageKey: () => oauthStorageKey,
|
|
89
97
|
openLog: () => openLog,
|
|
90
98
|
parseLocalModelId: () => parseLocalModelId,
|
|
91
99
|
probeEndpoint: () => probeEndpoint,
|
|
100
|
+
providerStorageKeys: () => providerStorageKeys,
|
|
92
101
|
readStoredBaseUrlSync: () => readStoredBaseUrlSync,
|
|
93
102
|
refreshAnthropicToken: () => refreshAnthropicToken,
|
|
94
103
|
refreshGeminiToken: () => refreshGeminiToken,
|
|
95
104
|
refreshKimiToken: () => refreshKimiToken,
|
|
96
105
|
refreshOpenAIToken: () => refreshOpenAIToken,
|
|
106
|
+
refreshXaiToken: () => refreshXaiToken,
|
|
97
107
|
registerLogCleanup: () => registerLogCleanup,
|
|
98
108
|
registerRuntimeModels: () => registerRuntimeModels,
|
|
99
109
|
resample: () => resample,
|
|
@@ -387,6 +397,7 @@ var import_node_path3 = __toESM(require("path"), 1);
|
|
|
387
397
|
var import_node_crypto2 = require("crypto");
|
|
388
398
|
var import_ai = require("@prestyj/ai");
|
|
389
399
|
var MAX_BYTES = 10 * 1024 * 1024;
|
|
400
|
+
var MIN_HEADROOM_BYTES = 64 * 1024;
|
|
390
401
|
var fd = null;
|
|
391
402
|
var bytesWritten = 0;
|
|
392
403
|
var capped = false;
|
|
@@ -397,7 +408,7 @@ var exactSecrets = [];
|
|
|
397
408
|
function rotateIfNeeded(filePath) {
|
|
398
409
|
try {
|
|
399
410
|
const st = import_node_fs2.default.statSync(filePath);
|
|
400
|
-
if (st.size
|
|
411
|
+
if (st.size <= MAX_BYTES - MIN_HEADROOM_BYTES) return;
|
|
401
412
|
const rotated = `${filePath}.1`;
|
|
402
413
|
try {
|
|
403
414
|
import_node_fs2.default.unlinkSync(rotated);
|
|
@@ -692,6 +703,9 @@ var TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
|
692
703
|
var REDIRECT_URI2 = "http://localhost:1455/auth/callback";
|
|
693
704
|
var SCOPE = "openid profile email offline_access api.connectors.read api.connectors.invoke";
|
|
694
705
|
var JWT_CLAIM_PATH = "https://api.openai.com/auth";
|
|
706
|
+
var CALLBACK_TIMEOUT_MS = 12e4;
|
|
707
|
+
var MAX_PASTE_ATTEMPTS = 3;
|
|
708
|
+
var MIN_RAW_CODE_LENGTH = 10;
|
|
695
709
|
async function loginOpenAI(callbacks) {
|
|
696
710
|
const { verifier, challenge } = await generatePKCE();
|
|
697
711
|
const state = import_node_crypto4.default.randomBytes(16).toString("hex");
|
|
@@ -707,20 +721,7 @@ async function loginOpenAI(callbacks) {
|
|
|
707
721
|
url.searchParams.set("id_token_add_organizations", "true");
|
|
708
722
|
url.searchParams.set("codex_cli_simplified_flow", "true");
|
|
709
723
|
url.searchParams.set("originator", "ezcoder");
|
|
710
|
-
|
|
711
|
-
try {
|
|
712
|
-
code = await loginWithServer(url.toString(), state, callbacks);
|
|
713
|
-
} catch {
|
|
714
|
-
callbacks.onOpenUrl(url.toString());
|
|
715
|
-
const raw = await callbacks.onPromptCode(
|
|
716
|
-
"Could not start local server. Paste the callback URL or code from the browser:"
|
|
717
|
-
);
|
|
718
|
-
const parsed = parseAuthorizationInput(raw);
|
|
719
|
-
if (!parsed.code) {
|
|
720
|
-
throw new Error("No authorization code found in input.");
|
|
721
|
-
}
|
|
722
|
-
code = parsed.code;
|
|
723
|
-
}
|
|
724
|
+
const code = await acquireAuthorizationCode(url.toString(), state, callbacks);
|
|
724
725
|
const creds = await exchangeOpenAICode(code, verifier);
|
|
725
726
|
const accountId = getAccountId(creds.accessToken);
|
|
726
727
|
if (!accountId) {
|
|
@@ -751,6 +752,7 @@ function parseAuthorizationInput(input) {
|
|
|
751
752
|
state: params.get("state") ?? void 0
|
|
752
753
|
};
|
|
753
754
|
}
|
|
755
|
+
if (/\s/.test(value) || value.length < MIN_RAW_CODE_LENGTH) return {};
|
|
754
756
|
return { code: value };
|
|
755
757
|
}
|
|
756
758
|
function decodeJwt(token) {
|
|
@@ -769,7 +771,55 @@ function getAccountId(accessToken) {
|
|
|
769
771
|
const accountId = auth?.chatgpt_account_id;
|
|
770
772
|
return typeof accountId === "string" && accountId.length > 0 ? accountId : null;
|
|
771
773
|
}
|
|
772
|
-
async function
|
|
774
|
+
async function acquireAuthorizationCode(authUrl, expectedState, callbacks) {
|
|
775
|
+
callbacks.onOpenUrl(authUrl);
|
|
776
|
+
callbacks.onStatus("Waiting for browser callback...");
|
|
777
|
+
const done = new AbortController();
|
|
778
|
+
try {
|
|
779
|
+
return await new Promise((resolve, reject) => {
|
|
780
|
+
let serverFailed = false;
|
|
781
|
+
let pasteError;
|
|
782
|
+
const failIfExhausted = () => {
|
|
783
|
+
if (!serverFailed || pasteError === void 0) return;
|
|
784
|
+
reject(
|
|
785
|
+
pasteError instanceof Error ? pasteError : new Error("Could not obtain an authorization code.")
|
|
786
|
+
);
|
|
787
|
+
};
|
|
788
|
+
listenForCallback(expectedState, done.signal).then(resolve, () => {
|
|
789
|
+
serverFailed = true;
|
|
790
|
+
failIfExhausted();
|
|
791
|
+
});
|
|
792
|
+
promptForCode(expectedState, callbacks, done.signal).then(resolve, (err) => {
|
|
793
|
+
if (err instanceof FatalLoginError) {
|
|
794
|
+
reject(err);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
pasteError = err;
|
|
798
|
+
failIfExhausted();
|
|
799
|
+
});
|
|
800
|
+
});
|
|
801
|
+
} finally {
|
|
802
|
+
done.abort();
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
var FatalLoginError = class extends Error {
|
|
806
|
+
};
|
|
807
|
+
async function promptForCode(expectedState, callbacks, signal) {
|
|
808
|
+
let message = "If the browser is on another machine (SSH/headless), paste the callback URL or code here:";
|
|
809
|
+
for (let attempt = 0; attempt < MAX_PASTE_ATTEMPTS; attempt++) {
|
|
810
|
+
if (signal.aborted) throw new Error("Authorization code already received.");
|
|
811
|
+
const raw = await callbacks.onPromptCode(message, signal);
|
|
812
|
+
if (signal.aborted) throw new Error("Authorization code already received.");
|
|
813
|
+
const parsed = parseAuthorizationInput(raw);
|
|
814
|
+
if (parsed.code && parsed.state && parsed.state !== expectedState) {
|
|
815
|
+
throw new FatalLoginError("Authorization state mismatch \u2014 start the login again.");
|
|
816
|
+
}
|
|
817
|
+
if (parsed.code) return parsed.code;
|
|
818
|
+
message = "That didn't contain an authorization code. Paste the full callback URL:";
|
|
819
|
+
}
|
|
820
|
+
throw new Error("No authorization code found in input.");
|
|
821
|
+
}
|
|
822
|
+
async function listenForCallback(expectedState, signal) {
|
|
773
823
|
return new Promise((resolve, reject) => {
|
|
774
824
|
let receivedCode = null;
|
|
775
825
|
const server = import_node_http.default.createServer((req, res) => {
|
|
@@ -792,18 +842,18 @@ async function loginWithServer(authUrl, expectedState, callbacks) {
|
|
|
792
842
|
server.on("error", (err) => {
|
|
793
843
|
reject(err);
|
|
794
844
|
});
|
|
795
|
-
server.listen(1455, "127.0.0.1"
|
|
796
|
-
callbacks.onOpenUrl(authUrl);
|
|
797
|
-
callbacks.onStatus("Waiting for browser callback...");
|
|
798
|
-
});
|
|
845
|
+
server.listen(1455, "127.0.0.1");
|
|
799
846
|
const timeout = setTimeout(() => {
|
|
800
|
-
if (!receivedCode)
|
|
801
|
-
|
|
802
|
-
}
|
|
803
|
-
}, 12e4);
|
|
847
|
+
if (!receivedCode) server.close();
|
|
848
|
+
}, CALLBACK_TIMEOUT_MS);
|
|
804
849
|
timeout.unref();
|
|
850
|
+
const onAbort = () => {
|
|
851
|
+
server.close();
|
|
852
|
+
};
|
|
853
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
805
854
|
server.on("close", () => {
|
|
806
855
|
clearTimeout(timeout);
|
|
856
|
+
signal.removeEventListener("abort", onAbort);
|
|
807
857
|
if (receivedCode) {
|
|
808
858
|
resolve(receivedCode);
|
|
809
859
|
} else {
|
|
@@ -865,8 +915,8 @@ async function refreshOpenAIToken(refreshToken) {
|
|
|
865
915
|
// src/oauth/gemini.ts
|
|
866
916
|
var import_node_http2 = __toESM(require("http"), 1);
|
|
867
917
|
var import_node_crypto5 = __toESM(require("crypto"), 1);
|
|
868
|
-
var CLIENT_ID_ENV = "
|
|
869
|
-
var CLIENT_SECRET_ENV = "
|
|
918
|
+
var CLIENT_ID_ENV = "EZCODER_GEMINI_OAUTH_CLIENT_ID";
|
|
919
|
+
var CLIENT_SECRET_ENV = "EZCODER_GEMINI_OAUTH_CLIENT_SECRET";
|
|
870
920
|
var DEFAULT_CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com";
|
|
871
921
|
var DEFAULT_CLIENT_SECRET = "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";
|
|
872
922
|
var AUTHORIZE_URL3 = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
@@ -896,13 +946,13 @@ var CodeAssistHttpError = class extends Error {
|
|
|
896
946
|
}
|
|
897
947
|
};
|
|
898
948
|
async function loginGemini(callbacks) {
|
|
899
|
-
const { clientId, clientSecret } = getGeminiOAuthClientCredentials();
|
|
949
|
+
const { clientId: clientId2, clientSecret } = getGeminiOAuthClientCredentials();
|
|
900
950
|
const { verifier, challenge } = await generatePKCE();
|
|
901
951
|
const state = import_node_crypto5.default.randomBytes(32).toString("hex");
|
|
902
952
|
const redirectUri = await getLoopbackRedirectUri();
|
|
903
953
|
const url = new URL(AUTHORIZE_URL3);
|
|
904
954
|
url.searchParams.set("response_type", "code");
|
|
905
|
-
url.searchParams.set("client_id",
|
|
955
|
+
url.searchParams.set("client_id", clientId2);
|
|
906
956
|
url.searchParams.set("redirect_uri", redirectUri);
|
|
907
957
|
url.searchParams.set("scope", SCOPE2);
|
|
908
958
|
url.searchParams.set("access_type", "offline");
|
|
@@ -912,7 +962,7 @@ async function loginGemini(callbacks) {
|
|
|
912
962
|
url.searchParams.set("state", state);
|
|
913
963
|
let code;
|
|
914
964
|
try {
|
|
915
|
-
code = await
|
|
965
|
+
code = await loginWithServer(url.toString(), redirectUri, state, callbacks);
|
|
916
966
|
} catch {
|
|
917
967
|
callbacks.onOpenUrl(url.toString());
|
|
918
968
|
const raw = await callbacks.onPromptCode(
|
|
@@ -927,7 +977,7 @@ async function loginGemini(callbacks) {
|
|
|
927
977
|
}
|
|
928
978
|
code = parsed.code;
|
|
929
979
|
}
|
|
930
|
-
const creds = await exchangeGeminiCode(code, verifier, redirectUri,
|
|
980
|
+
const creds = await exchangeGeminiCode(code, verifier, redirectUri, clientId2, clientSecret);
|
|
931
981
|
callbacks.onStatus("Setting up Gemini Code Assist access...");
|
|
932
982
|
const projectId = await setupCodeAssistProject(creds.accessToken, callbacks);
|
|
933
983
|
return {
|
|
@@ -936,11 +986,11 @@ async function loginGemini(callbacks) {
|
|
|
936
986
|
};
|
|
937
987
|
}
|
|
938
988
|
async function refreshGeminiToken(refreshToken) {
|
|
939
|
-
const { clientId, clientSecret } = getGeminiOAuthClientCredentials();
|
|
989
|
+
const { clientId: clientId2, clientSecret } = getGeminiOAuthClientCredentials();
|
|
940
990
|
const data = await postTokenRequest2({
|
|
941
991
|
grant_type: "refresh_token",
|
|
942
992
|
refresh_token: refreshToken,
|
|
943
|
-
client_id:
|
|
993
|
+
client_id: clientId2,
|
|
944
994
|
client_secret: clientSecret
|
|
945
995
|
});
|
|
946
996
|
return {
|
|
@@ -950,9 +1000,9 @@ async function refreshGeminiToken(refreshToken) {
|
|
|
950
1000
|
};
|
|
951
1001
|
}
|
|
952
1002
|
function getGeminiOAuthClientCredentials() {
|
|
953
|
-
const
|
|
1003
|
+
const clientId2 = process.env[CLIENT_ID_ENV]?.trim() || DEFAULT_CLIENT_ID;
|
|
954
1004
|
const clientSecret = process.env[CLIENT_SECRET_ENV]?.trim() || DEFAULT_CLIENT_SECRET;
|
|
955
|
-
return { clientId, clientSecret };
|
|
1005
|
+
return { clientId: clientId2, clientSecret };
|
|
956
1006
|
}
|
|
957
1007
|
async function getLoopbackRedirectUri() {
|
|
958
1008
|
return new Promise((resolve, reject) => {
|
|
@@ -990,7 +1040,7 @@ function parseAuthorizationInput2(input) {
|
|
|
990
1040
|
}
|
|
991
1041
|
return { code: value };
|
|
992
1042
|
}
|
|
993
|
-
async function
|
|
1043
|
+
async function loginWithServer(authUrl, redirectUri, expectedState, callbacks) {
|
|
994
1044
|
const redirect = new URL(redirectUri);
|
|
995
1045
|
const port = Number(redirect.port);
|
|
996
1046
|
return new Promise((resolve, reject) => {
|
|
@@ -1031,10 +1081,10 @@ async function loginWithServer2(authUrl, redirectUri, expectedState, callbacks)
|
|
|
1031
1081
|
});
|
|
1032
1082
|
});
|
|
1033
1083
|
}
|
|
1034
|
-
async function exchangeGeminiCode(code, verifier, redirectUri,
|
|
1084
|
+
async function exchangeGeminiCode(code, verifier, redirectUri, clientId2, clientSecret) {
|
|
1035
1085
|
const data = await postTokenRequest2({
|
|
1036
1086
|
grant_type: "authorization_code",
|
|
1037
|
-
client_id:
|
|
1087
|
+
client_id: clientId2,
|
|
1038
1088
|
client_secret: clientSecret,
|
|
1039
1089
|
code,
|
|
1040
1090
|
redirect_uri: redirectUri,
|
|
@@ -1225,6 +1275,204 @@ function codeAssistHeaders(accessToken) {
|
|
|
1225
1275
|
};
|
|
1226
1276
|
}
|
|
1227
1277
|
|
|
1278
|
+
// src/oauth/xai.ts
|
|
1279
|
+
var DEFAULT_CLIENT_ID2 = "b1a00492-073a-47ea-816f-4c329264a828";
|
|
1280
|
+
var DEFAULT_ISSUER = "https://auth.x.ai";
|
|
1281
|
+
var DEFAULT_CLI_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
|
|
1282
|
+
var OAUTH_SCOPE = "openid profile email offline_access grok-cli:access api:access";
|
|
1283
|
+
var DEFAULT_GROK_CLI_VERSION = "0.2.101";
|
|
1284
|
+
var DEVICE_TIMEOUT_FALLBACK_MS = 10 * 60 * 1e3;
|
|
1285
|
+
var DEFAULT_TOKEN_LIFETIME_SECONDS = 3600;
|
|
1286
|
+
function issuer() {
|
|
1287
|
+
return (process.env.XAI_OAUTH_ISSUER ?? DEFAULT_ISSUER).replace(/\/+$/, "");
|
|
1288
|
+
}
|
|
1289
|
+
function clientId() {
|
|
1290
|
+
return process.env.XAI_OAUTH_CLIENT_ID ?? DEFAULT_CLIENT_ID2;
|
|
1291
|
+
}
|
|
1292
|
+
function grokCliBaseUrl() {
|
|
1293
|
+
return (process.env.XAI_CLI_BASE_URL ?? DEFAULT_CLI_BASE_URL).replace(/\/+$/, "");
|
|
1294
|
+
}
|
|
1295
|
+
function grokCliVersion() {
|
|
1296
|
+
const raw = process.env.GROK_CLI_VERSION ?? DEFAULT_GROK_CLI_VERSION;
|
|
1297
|
+
const cleaned = raw.replace(/[^\u0020-\u007E]/g, "").trim();
|
|
1298
|
+
return cleaned.length > 0 ? cleaned : DEFAULT_GROK_CLI_VERSION;
|
|
1299
|
+
}
|
|
1300
|
+
function grokCliHeaders(modelId) {
|
|
1301
|
+
return {
|
|
1302
|
+
"X-XAI-Token-Auth": "xai-grok-cli",
|
|
1303
|
+
"x-grok-client-version": grokCliVersion(),
|
|
1304
|
+
"x-grok-client-identifier": "ezcoder",
|
|
1305
|
+
...modelId ? { "x-grok-model-override": modelId } : {}
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
function isGrokCliEndpoint(baseUrl) {
|
|
1309
|
+
if (typeof baseUrl !== "string" || baseUrl.length === 0) return false;
|
|
1310
|
+
const normalized = baseUrl.replace(/\/+$/, "");
|
|
1311
|
+
return normalized === grokCliBaseUrl() || /(^|\.)grok\.com/i.test(normalized);
|
|
1312
|
+
}
|
|
1313
|
+
async function postForm2(endpoint, params) {
|
|
1314
|
+
const response = await fetch(`${issuer()}${endpoint}`, {
|
|
1315
|
+
method: "POST",
|
|
1316
|
+
headers: {
|
|
1317
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1318
|
+
Accept: "application/json",
|
|
1319
|
+
"User-Agent": `grok-cli/${grokCliVersion()}`
|
|
1320
|
+
},
|
|
1321
|
+
body: new URLSearchParams(params).toString()
|
|
1322
|
+
});
|
|
1323
|
+
let data = {};
|
|
1324
|
+
try {
|
|
1325
|
+
const parsed = await response.json();
|
|
1326
|
+
if (parsed && typeof parsed === "object") data = parsed;
|
|
1327
|
+
} catch {
|
|
1328
|
+
}
|
|
1329
|
+
return { status: response.status, data };
|
|
1330
|
+
}
|
|
1331
|
+
function errorDetail2(data) {
|
|
1332
|
+
const desc = data.error_description ?? data.message ?? data.error;
|
|
1333
|
+
return typeof desc === "string" && desc.length > 0 ? desc : "unknown error";
|
|
1334
|
+
}
|
|
1335
|
+
function jwtExpirySeconds(token) {
|
|
1336
|
+
const payload = token.split(".")[1];
|
|
1337
|
+
if (!payload) return void 0;
|
|
1338
|
+
try {
|
|
1339
|
+
const json = Buffer.from(payload.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString(
|
|
1340
|
+
"utf-8"
|
|
1341
|
+
);
|
|
1342
|
+
const claims = JSON.parse(json);
|
|
1343
|
+
if (!claims || typeof claims !== "object") return void 0;
|
|
1344
|
+
const exp = claims.exp;
|
|
1345
|
+
return typeof exp === "number" && Number.isFinite(exp) && exp > 0 ? exp : void 0;
|
|
1346
|
+
} catch {
|
|
1347
|
+
return void 0;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
function credsFromTokenResponse2(data, opts) {
|
|
1351
|
+
const accessToken = data.access_token;
|
|
1352
|
+
const responseRefreshToken = data.refresh_token;
|
|
1353
|
+
if (typeof accessToken !== "string" || accessToken.length === 0) {
|
|
1354
|
+
throw new Error("Grok OAuth response missing access_token.");
|
|
1355
|
+
}
|
|
1356
|
+
const refreshToken = typeof responseRefreshToken === "string" && responseRefreshToken.length > 0 ? responseRefreshToken : opts?.fallbackRefreshToken ?? "";
|
|
1357
|
+
if (refreshToken.length === 0) {
|
|
1358
|
+
throw new Error(
|
|
1359
|
+
"Grok OAuth response missing refresh_token \u2014 the offline_access scope was not granted."
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
const responseExpiresIn = Number(data.expires_in);
|
|
1363
|
+
let expiresIn;
|
|
1364
|
+
if (Number.isFinite(responseExpiresIn) && responseExpiresIn > 0) {
|
|
1365
|
+
expiresIn = responseExpiresIn;
|
|
1366
|
+
} else {
|
|
1367
|
+
const exp = jwtExpirySeconds(accessToken);
|
|
1368
|
+
const fromJwt = exp !== void 0 ? exp - Math.floor(Date.now() / 1e3) : 0;
|
|
1369
|
+
expiresIn = fromJwt > 0 ? fromJwt : DEFAULT_TOKEN_LIFETIME_SECONDS;
|
|
1370
|
+
}
|
|
1371
|
+
return {
|
|
1372
|
+
accessToken,
|
|
1373
|
+
refreshToken,
|
|
1374
|
+
expiresAt: Date.now() + expiresIn * 1e3,
|
|
1375
|
+
expiresIn,
|
|
1376
|
+
baseUrl: grokCliBaseUrl()
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
async function requestDeviceAuthorization2() {
|
|
1380
|
+
const { status, data } = await postForm2("/oauth2/device/code", {
|
|
1381
|
+
client_id: clientId(),
|
|
1382
|
+
scope: OAUTH_SCOPE
|
|
1383
|
+
});
|
|
1384
|
+
if (status !== 200) {
|
|
1385
|
+
throw new Error(`Grok device authorization failed (${status}): ${errorDetail2(data)}`);
|
|
1386
|
+
}
|
|
1387
|
+
const userCode = data.user_code;
|
|
1388
|
+
const deviceCode = data.device_code;
|
|
1389
|
+
const verificationUriComplete = data.verification_uri_complete;
|
|
1390
|
+
if (typeof userCode !== "string" || typeof deviceCode !== "string") {
|
|
1391
|
+
throw new Error("Grok device authorization response missing user_code/device_code.");
|
|
1392
|
+
}
|
|
1393
|
+
return {
|
|
1394
|
+
userCode,
|
|
1395
|
+
deviceCode,
|
|
1396
|
+
verificationUri: typeof data.verification_uri === "string" ? data.verification_uri : "",
|
|
1397
|
+
verificationUriComplete: typeof verificationUriComplete === "string" ? verificationUriComplete : "",
|
|
1398
|
+
interval: Number(data.interval ?? 5) || 5,
|
|
1399
|
+
// RFC 8628 §3.2: the server states how long the device code lives. Honor it
|
|
1400
|
+
// rather than imposing our own budget — polling a code we know is dead only
|
|
1401
|
+
// burns requests, and a longer local window would keep a user waiting past
|
|
1402
|
+
// the point the code can ever succeed.
|
|
1403
|
+
expiresInMs: (Number(data.expires_in) || 0) > 0 ? Number(data.expires_in) * 1e3 : DEVICE_TIMEOUT_FALLBACK_MS
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
async function pollDeviceToken2(deviceCode) {
|
|
1407
|
+
const { status, data } = await postForm2("/oauth2/token", {
|
|
1408
|
+
client_id: clientId(),
|
|
1409
|
+
device_code: deviceCode,
|
|
1410
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
1411
|
+
});
|
|
1412
|
+
if (status === 200 && typeof data.access_token === "string") {
|
|
1413
|
+
return { kind: "success", creds: credsFromTokenResponse2(data) };
|
|
1414
|
+
}
|
|
1415
|
+
if (status >= 500) {
|
|
1416
|
+
throw new Error(`Grok token polling server error (${status}): ${errorDetail2(data)}`);
|
|
1417
|
+
}
|
|
1418
|
+
const errorCode = typeof data.error === "string" ? data.error : "unknown_error";
|
|
1419
|
+
switch (errorCode) {
|
|
1420
|
+
case "authorization_pending":
|
|
1421
|
+
return { kind: "pending" };
|
|
1422
|
+
case "slow_down":
|
|
1423
|
+
return { kind: "slow_down" };
|
|
1424
|
+
case "expired_token":
|
|
1425
|
+
return { kind: "expired" };
|
|
1426
|
+
case "access_denied":
|
|
1427
|
+
return { kind: "denied" };
|
|
1428
|
+
default:
|
|
1429
|
+
throw new Error(`Grok token polling failed (${status}): ${errorDetail2(data)}`);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
function sleep2(ms) {
|
|
1433
|
+
return new Promise((resolve) => {
|
|
1434
|
+
setTimeout(resolve, ms);
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
async function loginXai(callbacks) {
|
|
1438
|
+
const auth = await requestDeviceAuthorization2();
|
|
1439
|
+
callbacks.onStatus(
|
|
1440
|
+
`Visit ${auth.verificationUri || auth.verificationUriComplete} and enter code: ${auth.userCode}`
|
|
1441
|
+
);
|
|
1442
|
+
callbacks.onOpenUrl(auth.verificationUriComplete || auth.verificationUri);
|
|
1443
|
+
callbacks.onStatus("Waiting for you to authorize in the browser...");
|
|
1444
|
+
const deadline = Date.now() + auth.expiresInMs;
|
|
1445
|
+
let interval = Math.max(auth.interval, 1);
|
|
1446
|
+
while (Date.now() < deadline) {
|
|
1447
|
+
const remaining = deadline - Date.now();
|
|
1448
|
+
await sleep2(Math.min(interval * 1e3, remaining));
|
|
1449
|
+
const result = await pollDeviceToken2(auth.deviceCode);
|
|
1450
|
+
if (result.kind === "success") return result.creds;
|
|
1451
|
+
if (result.kind === "denied") {
|
|
1452
|
+
throw new Error("Grok authorization was denied.");
|
|
1453
|
+
}
|
|
1454
|
+
if (result.kind === "expired") {
|
|
1455
|
+
throw new Error("Grok device code expired. Please run login again.");
|
|
1456
|
+
}
|
|
1457
|
+
if (result.kind === "slow_down") {
|
|
1458
|
+
interval += 5;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
throw new Error("Grok login timed out. Please run login again.");
|
|
1462
|
+
}
|
|
1463
|
+
async function refreshXaiToken(refreshToken) {
|
|
1464
|
+
const { status, data } = await postForm2("/oauth2/token", {
|
|
1465
|
+
client_id: clientId(),
|
|
1466
|
+
grant_type: "refresh_token",
|
|
1467
|
+
refresh_token: refreshToken
|
|
1468
|
+
});
|
|
1469
|
+
if (status === 200 && typeof data.access_token === "string") {
|
|
1470
|
+
return credsFromTokenResponse2(data, { fallbackRefreshToken: refreshToken });
|
|
1471
|
+
}
|
|
1472
|
+
const errorCode = typeof data.error === "string" ? data.error : "";
|
|
1473
|
+
throw new Error(`Grok token refresh failed (${status}): ${errorCode || errorDetail2(data)}`);
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1228
1476
|
// src/file-lock.ts
|
|
1229
1477
|
var import_promises2 = __toESM(require("fs/promises"), 1);
|
|
1230
1478
|
var import_promises3 = require("timers/promises");
|
|
@@ -1289,18 +1537,49 @@ function isAlive(pid) {
|
|
|
1289
1537
|
|
|
1290
1538
|
// src/auth-storage.ts
|
|
1291
1539
|
var MOONSHOT_OAUTH_KEY = "moonshot-oauth";
|
|
1540
|
+
var XAI_OAUTH_KEY = "xai-oauth";
|
|
1541
|
+
var DUAL_AUTH_PROVIDERS = [
|
|
1542
|
+
{
|
|
1543
|
+
provider: "moonshot",
|
|
1544
|
+
oauthKey: MOONSHOT_OAUTH_KEY,
|
|
1545
|
+
oauthLabel: "Kimi OAuth",
|
|
1546
|
+
apiKeyLabel: "Moonshot API key",
|
|
1547
|
+
restoreHint: 'Run "ezcoder login" and choose Kimi OAuth to restore OAuth auth.'
|
|
1548
|
+
},
|
|
1549
|
+
{
|
|
1550
|
+
provider: "xai",
|
|
1551
|
+
oauthKey: XAI_OAUTH_KEY,
|
|
1552
|
+
oauthLabel: "Grok OAuth",
|
|
1553
|
+
apiKeyLabel: "xAI API key",
|
|
1554
|
+
restoreHint: 'Run "ezcoder login" and choose Grok OAuth to restore OAuth auth.'
|
|
1555
|
+
}
|
|
1556
|
+
];
|
|
1557
|
+
function dualAuthProvider(provider) {
|
|
1558
|
+
return DUAL_AUTH_PROVIDERS.find((entry) => entry.provider === provider);
|
|
1559
|
+
}
|
|
1560
|
+
function dualAuthProviderByOAuthKey(storageKey) {
|
|
1561
|
+
return DUAL_AUTH_PROVIDERS.find((entry) => entry.oauthKey === storageKey);
|
|
1562
|
+
}
|
|
1563
|
+
function oauthStorageKey(provider) {
|
|
1564
|
+
return dualAuthProvider(provider)?.oauthKey;
|
|
1565
|
+
}
|
|
1566
|
+
function providerStorageKeys(provider) {
|
|
1567
|
+
const dual = dualAuthProvider(provider);
|
|
1568
|
+
return dual ? [dual.oauthKey, dual.provider] : [provider];
|
|
1569
|
+
}
|
|
1292
1570
|
var XIAOMI_CREDITS_KEY = "xiaomi-credits";
|
|
1293
1571
|
var LOCAL_AUTH_KEY_PREFIX = "local:";
|
|
1294
1572
|
var LOCAL_CREDENTIAL_LIFETIME_MS = 100 * 365 * 24 * 60 * 60 * 1e3;
|
|
1295
1573
|
function activeBaseUrlEntry(data, provider) {
|
|
1296
|
-
|
|
1297
|
-
|
|
1574
|
+
const dual = dualAuthProvider(provider);
|
|
1575
|
+
if (dual) {
|
|
1576
|
+
const oauth = data[dual.oauthKey];
|
|
1298
1577
|
if (oauth) {
|
|
1299
1578
|
const exhaustedUntil = oauth.usageExhaustedUntil ?? 0;
|
|
1300
|
-
if (Date.now() < exhaustedUntil && data[
|
|
1579
|
+
if (Date.now() < exhaustedUntil && data[dual.provider]) return data[dual.provider];
|
|
1301
1580
|
return oauth;
|
|
1302
1581
|
}
|
|
1303
|
-
return data[
|
|
1582
|
+
return data[dual.provider];
|
|
1304
1583
|
}
|
|
1305
1584
|
return data[provider];
|
|
1306
1585
|
}
|
|
@@ -1335,6 +1614,16 @@ var AuthStorage = class {
|
|
|
1335
1614
|
data = {};
|
|
1336
1615
|
filePath;
|
|
1337
1616
|
loaded = false;
|
|
1617
|
+
/**
|
|
1618
|
+
* mtime+size of the file as of the cached snapshot (`size: -1` = no file).
|
|
1619
|
+
* auth.json is shared: the desktop app writes API keys and disconnects
|
|
1620
|
+
* NATIVELY (so they work with no daemon running), and every window/process has
|
|
1621
|
+
* its own AuthStorage. A load-once cache therefore goes stale — the sidecar
|
|
1622
|
+
* would keep listing models for a provider just disconnected, and hide the
|
|
1623
|
+
* ones just connected, until the daemon restarted.
|
|
1624
|
+
*/
|
|
1625
|
+
snapshotMtimeMs = 0;
|
|
1626
|
+
snapshotSize = -1;
|
|
1338
1627
|
/** Per-provider lock to serialize concurrent refresh calls. */
|
|
1339
1628
|
refreshLocks = /* @__PURE__ */ new Map();
|
|
1340
1629
|
constructor(filePath) {
|
|
@@ -1346,12 +1635,12 @@ var AuthStorage = class {
|
|
|
1346
1635
|
}
|
|
1347
1636
|
/** List provider keys with stored credentials. */
|
|
1348
1637
|
async listProviders() {
|
|
1349
|
-
await this.
|
|
1638
|
+
await this.ensureFresh();
|
|
1350
1639
|
return Object.keys(this.data);
|
|
1351
1640
|
}
|
|
1352
1641
|
/** True if credentials exist for `provider`. */
|
|
1353
1642
|
async hasCredentials(provider) {
|
|
1354
|
-
await this.
|
|
1643
|
+
await this.ensureFresh();
|
|
1355
1644
|
return Boolean(this.data[provider]);
|
|
1356
1645
|
}
|
|
1357
1646
|
/**
|
|
@@ -1362,18 +1651,19 @@ var AuthStorage = class {
|
|
|
1362
1651
|
* instead of re-deriving the same order.
|
|
1363
1652
|
*/
|
|
1364
1653
|
async pickStorageKey(keys) {
|
|
1365
|
-
await this.
|
|
1654
|
+
await this.ensureFresh();
|
|
1366
1655
|
return keys.find((key) => Boolean(this.data[key]));
|
|
1367
1656
|
}
|
|
1368
1657
|
/**
|
|
1369
|
-
* True if the user has any usable auth for the logical provider. For
|
|
1370
|
-
*
|
|
1371
|
-
*
|
|
1658
|
+
* True if the user has any usable auth for the logical provider. For a
|
|
1659
|
+
* dual-auth provider (Kimi/Grok) either the OAuth credential or the API key
|
|
1660
|
+
* satisfies it.
|
|
1372
1661
|
*/
|
|
1373
1662
|
async hasProviderAuth(provider) {
|
|
1374
|
-
await this.
|
|
1375
|
-
|
|
1376
|
-
|
|
1663
|
+
await this.ensureFresh();
|
|
1664
|
+
const dual = dualAuthProvider(provider);
|
|
1665
|
+
if (dual) {
|
|
1666
|
+
return Boolean(this.data[dual.oauthKey] || this.data[dual.provider]);
|
|
1377
1667
|
}
|
|
1378
1668
|
if (provider === "xiaomi") {
|
|
1379
1669
|
return Boolean(this.data["xiaomi"] || this.data[XIAOMI_CREDITS_KEY]);
|
|
@@ -1407,14 +1697,16 @@ var AuthStorage = class {
|
|
|
1407
1697
|
}
|
|
1408
1698
|
/**
|
|
1409
1699
|
* True if the active credential for `provider` is a static API key with no
|
|
1410
|
-
* refresh mechanism. For
|
|
1411
|
-
* credential is absent (a
|
|
1700
|
+
* refresh mechanism. For a dual-auth provider this is only true when its OAuth
|
|
1701
|
+
* credential is absent or sidelined (a live OAuth credential is refreshable).
|
|
1412
1702
|
*/
|
|
1413
1703
|
async isStaticApiKey(provider) {
|
|
1414
|
-
await this.
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1704
|
+
await this.ensureFresh();
|
|
1705
|
+
const dual = dualAuthProvider(provider);
|
|
1706
|
+
const oauthCreds = dual ? this.data[dual.oauthKey] : void 0;
|
|
1707
|
+
if (dual && oauthCreds) {
|
|
1708
|
+
const exhaustedUntil = oauthCreds.usageExhaustedUntil ?? 0;
|
|
1709
|
+
const apiKeyActive = Date.now() < exhaustedUntil && Boolean(this.data[dual.provider]);
|
|
1418
1710
|
if (!apiKeyActive) return false;
|
|
1419
1711
|
}
|
|
1420
1712
|
return STATIC_API_KEY_PROVIDERS.has(provider);
|
|
@@ -1422,26 +1714,30 @@ var AuthStorage = class {
|
|
|
1422
1714
|
/**
|
|
1423
1715
|
* The base URL on the credential that is active right now, if any.
|
|
1424
1716
|
* Synchronous — call only after load()/resolveCredentials() populated the
|
|
1425
|
-
* snapshot. For
|
|
1426
|
-
*
|
|
1427
|
-
* usage-exhausted with an
|
|
1717
|
+
* snapshot. For a dual-auth provider this is the subscription endpoint (Kimi
|
|
1718
|
+
* For Coding, the Grok CLI proxy) whenever the OAuth entry is the one
|
|
1719
|
+
* resolveCredentials would serve (i.e. not currently usage-exhausted with an
|
|
1720
|
+
* API key configured).
|
|
1428
1721
|
*/
|
|
1429
1722
|
getStoredBaseUrl(provider) {
|
|
1430
1723
|
return activeBaseUrlEntry(this.data, provider)?.baseUrl;
|
|
1431
1724
|
}
|
|
1432
1725
|
async load() {
|
|
1726
|
+
const first = !this.loaded;
|
|
1433
1727
|
await withFileLock(this.filePath, async () => {
|
|
1434
1728
|
try {
|
|
1435
1729
|
const content = await import_promises4.default.readFile(this.filePath, "utf-8");
|
|
1436
1730
|
this.data = JSON.parse(content);
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1731
|
+
if (first) {
|
|
1732
|
+
log("INFO", "auth", `Loaded credentials from ${this.filePath}`, {
|
|
1733
|
+
providers: Object.keys(this.data).join(",") || "(none)"
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1440
1736
|
} catch (err) {
|
|
1441
1737
|
this.data = {};
|
|
1442
1738
|
const code = err.code;
|
|
1443
1739
|
if (code === "ENOENT") {
|
|
1444
|
-
log("INFO", "auth", `No auth file found at ${this.filePath} (first run)`);
|
|
1740
|
+
if (first) log("INFO", "auth", `No auth file found at ${this.filePath} (first run)`);
|
|
1445
1741
|
} else {
|
|
1446
1742
|
log(
|
|
1447
1743
|
"ERROR",
|
|
@@ -1453,6 +1749,7 @@ var AuthStorage = class {
|
|
|
1453
1749
|
}
|
|
1454
1750
|
});
|
|
1455
1751
|
this.loaded = true;
|
|
1752
|
+
await this.rememberSnapshot();
|
|
1456
1753
|
}
|
|
1457
1754
|
async ensureLoaded() {
|
|
1458
1755
|
if (!this.loaded) await this.load();
|
|
@@ -1466,6 +1763,42 @@ var AuthStorage = class {
|
|
|
1466
1763
|
this.loaded = false;
|
|
1467
1764
|
await this.load();
|
|
1468
1765
|
}
|
|
1766
|
+
/**
|
|
1767
|
+
* Like {@link ensureLoaded}, but re-reads when the file changed since this
|
|
1768
|
+
* snapshot — a cheap stat, not a re-parse. Used by the "what is connected?"
|
|
1769
|
+
* readers, which must reflect writes made by another window, the CLI, or the
|
|
1770
|
+
* desktop app's native (daemon-free) API-key and disconnect paths.
|
|
1771
|
+
*
|
|
1772
|
+
* Deliberately NOT used by {@link resolveCredentials}: that path compares the
|
|
1773
|
+
* caller's snapshot against the latest file to detect a concurrent re-login,
|
|
1774
|
+
* and silently refreshing this instance's view first would destroy the
|
|
1775
|
+
* evidence that the token it just had rejected has already been replaced.
|
|
1776
|
+
*/
|
|
1777
|
+
async ensureFresh() {
|
|
1778
|
+
if (!this.loaded) {
|
|
1779
|
+
await this.load();
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
let changed;
|
|
1783
|
+
try {
|
|
1784
|
+
const stat = await import_promises4.default.stat(this.filePath);
|
|
1785
|
+
changed = stat.mtimeMs !== this.snapshotMtimeMs || stat.size !== this.snapshotSize;
|
|
1786
|
+
} catch {
|
|
1787
|
+
changed = this.snapshotSize !== -1;
|
|
1788
|
+
}
|
|
1789
|
+
if (changed) await this.load();
|
|
1790
|
+
}
|
|
1791
|
+
/** Record the file identity behind the current snapshot. */
|
|
1792
|
+
async rememberSnapshot() {
|
|
1793
|
+
try {
|
|
1794
|
+
const stat = await import_promises4.default.stat(this.filePath);
|
|
1795
|
+
this.snapshotMtimeMs = stat.mtimeMs;
|
|
1796
|
+
this.snapshotSize = stat.size;
|
|
1797
|
+
} catch {
|
|
1798
|
+
this.snapshotMtimeMs = 0;
|
|
1799
|
+
this.snapshotSize = -1;
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1469
1802
|
/**
|
|
1470
1803
|
* Apply one provider-scoped mutation to the latest on-disk snapshot.
|
|
1471
1804
|
* AuthStorage instances live in every app session/process, so writing this
|
|
@@ -1481,14 +1814,16 @@ var AuthStorage = class {
|
|
|
1481
1814
|
await atomicWriteFile(this.filePath, JSON.stringify(latest, null, 2));
|
|
1482
1815
|
this.data = latest;
|
|
1483
1816
|
});
|
|
1817
|
+
await this.rememberSnapshot();
|
|
1484
1818
|
}
|
|
1485
1819
|
async reloadLatest() {
|
|
1486
1820
|
await withFileLock(this.filePath, async () => {
|
|
1487
1821
|
this.data = await readAuthData(this.filePath);
|
|
1488
1822
|
});
|
|
1823
|
+
await this.rememberSnapshot();
|
|
1489
1824
|
}
|
|
1490
1825
|
async getCredentials(provider) {
|
|
1491
|
-
await this.
|
|
1826
|
+
await this.ensureFresh();
|
|
1492
1827
|
return this.data[provider];
|
|
1493
1828
|
}
|
|
1494
1829
|
async setCredentials(provider, creds) {
|
|
@@ -1506,7 +1841,7 @@ var AuthStorage = class {
|
|
|
1506
1841
|
* `resetsAt` (unix SECONDS, from the provider's rate-limit response) or a
|
|
1507
1842
|
* 15-minute default when no reset time is known. While the mark is in the
|
|
1508
1843
|
* future, `resolveCredentials("moonshot")` serves the Moonshot API key
|
|
1509
|
-
* instead of the
|
|
1844
|
+
* instead of the subscription OAuth credential (when both are configured) — OAuth
|
|
1510
1845
|
* stays the preferred credential and is retried automatically once the mark
|
|
1511
1846
|
* lapses. Persisted to auth.json so a restart (or another ezcoder-app window)
|
|
1512
1847
|
* doesn't burn a request rediscovering the same exhausted window. No-op if
|
|
@@ -1537,13 +1872,21 @@ var AuthStorage = class {
|
|
|
1537
1872
|
}
|
|
1538
1873
|
/**
|
|
1539
1874
|
* Returns valid credentials, auto-refreshing if expired.
|
|
1875
|
+
*
|
|
1540
1876
|
* If `forceRefresh` is true, refreshes even if the token hasn't expired
|
|
1541
|
-
* (useful when the provider rejects a token with 401 before its stored
|
|
1877
|
+
* (useful when the provider rejects a token with 401 before its stored
|
|
1878
|
+
* expiry). Callers recovering from a rejection should also pass
|
|
1879
|
+
* `rejectedToken` — see the stampede guard below.
|
|
1880
|
+
*
|
|
1542
1881
|
* Throws if not logged in.
|
|
1543
1882
|
*/
|
|
1544
1883
|
async resolveCredentials(provider, opts) {
|
|
1545
|
-
|
|
1546
|
-
|
|
1884
|
+
if (opts?.forceRefresh) {
|
|
1885
|
+
await this.ensureLoaded();
|
|
1886
|
+
} else {
|
|
1887
|
+
await this.ensureFresh();
|
|
1888
|
+
}
|
|
1889
|
+
const directStorageKeys = opts?.storageKeys && !(opts.storageKeys.length === 1 && opts.storageKeys[0] === provider) ? opts.storageKeys : providerStorageKeys(provider);
|
|
1547
1890
|
if (!directStorageKeys.some((key) => Boolean(this.data[key]))) {
|
|
1548
1891
|
await this.reloadLatest();
|
|
1549
1892
|
}
|
|
@@ -1554,28 +1897,31 @@ var AuthStorage = class {
|
|
|
1554
1897
|
}
|
|
1555
1898
|
throw new NotLoggedInError(provider);
|
|
1556
1899
|
}
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1900
|
+
const dual = dualAuthProvider(provider);
|
|
1901
|
+
const dualOAuthCreds = dual ? this.data[dual.oauthKey] : void 0;
|
|
1902
|
+
if (dual && dualOAuthCreds) {
|
|
1903
|
+
const exhaustedUntil = dualOAuthCreds.usageExhaustedUntil ?? 0;
|
|
1904
|
+
if (Date.now() < exhaustedUntil && this.data[dual.provider]) {
|
|
1560
1905
|
log(
|
|
1561
1906
|
"WARN",
|
|
1562
1907
|
"auth",
|
|
1563
|
-
|
|
1908
|
+
`${dual.oauthLabel} usage window is exhausted \u2014 using the ${dual.apiKeyLabel} until ${new Date(exhaustedUntil).toISOString()} (OAuth resumes automatically).`
|
|
1564
1909
|
);
|
|
1565
|
-
return this.data[
|
|
1910
|
+
return this.data[dual.provider];
|
|
1566
1911
|
}
|
|
1567
1912
|
try {
|
|
1568
|
-
return await this.resolveCredentials(
|
|
1569
|
-
...opts?.forceRefresh ? { forceRefresh: true } : {}
|
|
1913
|
+
return await this.resolveCredentials(dual.oauthKey, {
|
|
1914
|
+
...opts?.forceRefresh ? { forceRefresh: true } : {},
|
|
1915
|
+
...opts?.rejectedToken !== void 0 ? { rejectedToken: opts.rejectedToken } : {}
|
|
1570
1916
|
});
|
|
1571
1917
|
} catch (err) {
|
|
1572
|
-
if (err instanceof NotLoggedInError && this.data[
|
|
1918
|
+
if (err instanceof NotLoggedInError && this.data[dual.provider]) {
|
|
1573
1919
|
log(
|
|
1574
1920
|
"WARN",
|
|
1575
1921
|
"auth",
|
|
1576
|
-
|
|
1922
|
+
`${dual.oauthLabel} credential is no longer valid \u2014 falling back to the ${dual.apiKeyLabel}. ${dual.restoreHint}`
|
|
1577
1923
|
);
|
|
1578
|
-
return this.data[
|
|
1924
|
+
return this.data[dual.provider];
|
|
1579
1925
|
}
|
|
1580
1926
|
throw err;
|
|
1581
1927
|
}
|
|
@@ -1600,11 +1946,19 @@ var AuthStorage = class {
|
|
|
1600
1946
|
throw new NotLoggedInError(provider);
|
|
1601
1947
|
}
|
|
1602
1948
|
const credentialWasReplaced = latestCreds.accessToken !== creds.accessToken || latestCreds.refreshToken !== creds.refreshToken || latestCreds.expiresAt !== creds.expiresAt;
|
|
1603
|
-
|
|
1949
|
+
const rotatedBySibling = opts?.rejectedToken !== void 0 && latestCreds.accessToken !== opts.rejectedToken;
|
|
1950
|
+
if (credentialWasReplaced || rotatedBySibling || !opts?.forceRefresh && Date.now() < latestCreds.expiresAt - refreshThresholdMs(latestCreds)) {
|
|
1951
|
+
if (rotatedBySibling && !credentialWasReplaced) {
|
|
1952
|
+
log(
|
|
1953
|
+
"INFO",
|
|
1954
|
+
"auth",
|
|
1955
|
+
`${provider} token was rotated by another EZ Coder process \u2014 adopting it instead of refreshing again`
|
|
1956
|
+
);
|
|
1957
|
+
}
|
|
1604
1958
|
this.data = latest;
|
|
1605
1959
|
return latestCreds;
|
|
1606
1960
|
}
|
|
1607
|
-
const refreshFn = provider === "anthropic" ? refreshAnthropicToken : provider === "gemini" ? refreshGeminiToken : provider === MOONSHOT_OAUTH_KEY ? refreshKimiToken : refreshOpenAIToken;
|
|
1961
|
+
const refreshFn = provider === "anthropic" ? refreshAnthropicToken : provider === "gemini" ? refreshGeminiToken : provider === MOONSHOT_OAUTH_KEY ? refreshKimiToken : provider === XAI_OAUTH_KEY ? refreshXaiToken : refreshOpenAIToken;
|
|
1608
1962
|
let refreshed;
|
|
1609
1963
|
try {
|
|
1610
1964
|
refreshed = await refreshFn(latestCreds.refreshToken);
|
|
@@ -1638,6 +1992,7 @@ var AuthStorage = class {
|
|
|
1638
1992
|
return await refreshPromise;
|
|
1639
1993
|
} finally {
|
|
1640
1994
|
this.refreshLocks.delete(provider);
|
|
1995
|
+
await this.rememberSnapshot();
|
|
1641
1996
|
}
|
|
1642
1997
|
}
|
|
1643
1998
|
/**
|
|
@@ -1901,7 +2256,7 @@ var MODELS = [
|
|
|
1901
2256
|
{
|
|
1902
2257
|
// Gemini 3.1 Pro is public preview — gated behind Code Assist preview
|
|
1903
2258
|
// enablement, so free/personal OAuth accounts 404 on it (see
|
|
1904
|
-
// ACCOUNT_GATED_MODELS in
|
|
2259
|
+
// ACCOUNT_GATED_MODELS in @prestyj/ai's gemini provider).
|
|
1905
2260
|
id: "gemini-3.1-pro-preview",
|
|
1906
2261
|
name: "Gemini 3.1 Pro (Preview)",
|
|
1907
2262
|
provider: "gemini",
|
|
@@ -1949,11 +2304,16 @@ var MODELS = [
|
|
|
1949
2304
|
maxThinkingLevel: "high"
|
|
1950
2305
|
},
|
|
1951
2306
|
// ── Z.AI (GLM) ─────────────────────────────────────────
|
|
1952
|
-
// GLM-5.
|
|
1953
|
-
//
|
|
2307
|
+
// GLM-5.3 is the only GLM entry: it supersedes 5.2 (same GLM-5 base, all
|
|
2308
|
+
// gains from post-training) and the coding endpoint already answers
|
|
2309
|
+
// `glm-5.2` requests as glm-5.3, so the older ids were menu clutter that
|
|
2310
|
+
// routed to strictly worse coding for the same plan quota.
|
|
2311
|
+
// Released 2026-08-14; live on the coding endpoint (verified), while the
|
|
2312
|
+
// standard paas API is still "coming soon". `max` is both the ceiling and
|
|
2313
|
+
// Z.AI's own default — the rungs below it live in thinking-level.ts.
|
|
1954
2314
|
{
|
|
1955
|
-
id: "glm-5.
|
|
1956
|
-
name: "GLM-5.
|
|
2315
|
+
id: "glm-5.3",
|
|
2316
|
+
name: "GLM-5.3",
|
|
1957
2317
|
provider: "glm",
|
|
1958
2318
|
contextWindow: 1e6,
|
|
1959
2319
|
maxOutputTokens: 131072,
|
|
@@ -1961,43 +2321,7 @@ var MODELS = [
|
|
|
1961
2321
|
supportsImages: false,
|
|
1962
2322
|
supportsVideo: false,
|
|
1963
2323
|
costTier: "medium",
|
|
1964
|
-
maxThinkingLevel: "
|
|
1965
|
-
},
|
|
1966
|
-
{
|
|
1967
|
-
id: "glm-5.1",
|
|
1968
|
-
name: "GLM-5.1",
|
|
1969
|
-
provider: "glm",
|
|
1970
|
-
contextWindow: 204800,
|
|
1971
|
-
maxOutputTokens: 131072,
|
|
1972
|
-
supportsThinking: true,
|
|
1973
|
-
supportsImages: false,
|
|
1974
|
-
supportsVideo: false,
|
|
1975
|
-
costTier: "medium",
|
|
1976
|
-
maxThinkingLevel: "high"
|
|
1977
|
-
},
|
|
1978
|
-
{
|
|
1979
|
-
id: "glm-4.7",
|
|
1980
|
-
name: "GLM-4.7",
|
|
1981
|
-
provider: "glm",
|
|
1982
|
-
contextWindow: 2e5,
|
|
1983
|
-
maxOutputTokens: 131072,
|
|
1984
|
-
supportsThinking: true,
|
|
1985
|
-
supportsImages: false,
|
|
1986
|
-
supportsVideo: false,
|
|
1987
|
-
costTier: "low",
|
|
1988
|
-
maxThinkingLevel: "high"
|
|
1989
|
-
},
|
|
1990
|
-
{
|
|
1991
|
-
id: "glm-4.7-flash",
|
|
1992
|
-
name: "GLM-4.7 Flash",
|
|
1993
|
-
provider: "glm",
|
|
1994
|
-
contextWindow: 2e5,
|
|
1995
|
-
maxOutputTokens: 131072,
|
|
1996
|
-
supportsThinking: true,
|
|
1997
|
-
supportsImages: false,
|
|
1998
|
-
supportsVideo: false,
|
|
1999
|
-
costTier: "low",
|
|
2000
|
-
maxThinkingLevel: "high"
|
|
2324
|
+
maxThinkingLevel: "max"
|
|
2001
2325
|
},
|
|
2002
2326
|
// ── MiniMax ────────────────────────────────────────────
|
|
2003
2327
|
{
|
|
@@ -2142,7 +2466,7 @@ function getDefaultModel(provider) {
|
|
|
2142
2466
|
if (provider === "xiaomi") return MODELS.find((m) => m.id === "mimo-v2.5-pro");
|
|
2143
2467
|
if (provider === "openai") return MODELS.find((m) => m.id === "gpt-5.6-sol");
|
|
2144
2468
|
if (provider === "gemini") return MODELS.find((m) => m.id === "gemini-3.1-flash-lite");
|
|
2145
|
-
if (provider === "glm") return MODELS.find((m) => m.id === "glm-5.
|
|
2469
|
+
if (provider === "glm") return MODELS.find((m) => m.id === "glm-5.3");
|
|
2146
2470
|
if (provider === "moonshot") return MODELS.find((m) => m.id === "kimi-k3");
|
|
2147
2471
|
if (provider === "minimax") return MODELS.find((m) => m.id === "MiniMax-M3");
|
|
2148
2472
|
if (provider === "deepseek") return MODELS.find((m) => m.id === "deepseek-v4-pro");
|
|
@@ -2229,6 +2553,7 @@ var ANTHROPIC_ADAPTIVE_THINKING_LEVELS = [
|
|
|
2229
2553
|
"max"
|
|
2230
2554
|
];
|
|
2231
2555
|
var MOONSHOT_K3_THINKING_LEVELS = ["low", "high", "max"];
|
|
2556
|
+
var GLM_THINKING_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
2232
2557
|
var LOCAL_THINKING_LEVELS = ["low", "medium", "high", "max"];
|
|
2233
2558
|
function isOpenAIGptModel(provider, model) {
|
|
2234
2559
|
return provider === "openai" && model.startsWith("gpt-");
|
|
@@ -2242,6 +2567,9 @@ function isXaiModel(provider) {
|
|
|
2242
2567
|
function isMoonshotK3Model(provider, model) {
|
|
2243
2568
|
return provider === "moonshot" && model === "kimi-k3";
|
|
2244
2569
|
}
|
|
2570
|
+
function isGlmModel(provider) {
|
|
2571
|
+
return provider === "glm";
|
|
2572
|
+
}
|
|
2245
2573
|
function isAnthropicXhighModel(provider, model) {
|
|
2246
2574
|
return provider === "anthropic" && /opus-5|opus-4-8|opus-4-7/.test(model);
|
|
2247
2575
|
}
|
|
@@ -2273,6 +2601,11 @@ function getSupportedThinkingLevels(provider, model) {
|
|
|
2273
2601
|
return XAI_THINKING_LEVELS.slice(0, maxIndex2 + 1);
|
|
2274
2602
|
}
|
|
2275
2603
|
if (isMoonshotK3Model(provider, model)) return MOONSHOT_K3_THINKING_LEVELS;
|
|
2604
|
+
if (isGlmModel(provider)) {
|
|
2605
|
+
const maxIndex2 = GLM_THINKING_LEVELS.indexOf(maxLevel);
|
|
2606
|
+
if (maxIndex2 === -1) return GLM_THINKING_LEVELS;
|
|
2607
|
+
return GLM_THINKING_LEVELS.slice(0, maxIndex2 + 1);
|
|
2608
|
+
}
|
|
2276
2609
|
if (!isOpenAIGptModel(provider, model)) return [maxLevel];
|
|
2277
2610
|
const levels = model.startsWith("gpt-5.6-") ? OPENAI_GPT_56_THINKING_LEVELS : OPENAI_GPT_THINKING_LEVELS;
|
|
2278
2611
|
const maxIndex = levels.indexOf(maxLevel);
|
|
@@ -2284,7 +2617,7 @@ function isThinkingLevelSupported(provider, model, level) {
|
|
|
2284
2617
|
}
|
|
2285
2618
|
function getNextThinkingLevel(provider, model, current) {
|
|
2286
2619
|
const supportedLevels = getSupportedThinkingLevels(provider, model);
|
|
2287
|
-
const shouldCycleLevels = isOpenAIGptModel(provider, model) || isAnthropicAdaptiveModel(provider, model) || isSakanaModel(provider) || isXaiModel(provider) || isMoonshotK3Model(provider, model) || // Local servers take a real effort level, not just on/off: Ollama accepts
|
|
2620
|
+
const shouldCycleLevels = isOpenAIGptModel(provider, model) || isAnthropicAdaptiveModel(provider, model) || isSakanaModel(provider) || isXaiModel(provider) || isMoonshotK3Model(provider, model) || isGlmModel(provider) || // Local servers take a real effort level, not just on/off: Ollama accepts
|
|
2288
2621
|
// low/medium/high on `reasoning_effort` (verified against 0.32) and the
|
|
2289
2622
|
// other OpenAI-compatible servers use the same three. A model that can't
|
|
2290
2623
|
// reason at all already has no supported levels, so it never gets here.
|
|
@@ -2837,7 +3170,7 @@ var TelegramBot = class {
|
|
|
2837
3170
|
} catch (err) {
|
|
2838
3171
|
if (!this.running) break;
|
|
2839
3172
|
console.error(`[telegram] Poll error: ${err instanceof Error ? err.message : err}`);
|
|
2840
|
-
await
|
|
3173
|
+
await sleep3(3e3);
|
|
2841
3174
|
}
|
|
2842
3175
|
}
|
|
2843
3176
|
}
|
|
@@ -3007,7 +3340,7 @@ function splitMessage(text) {
|
|
|
3007
3340
|
}
|
|
3008
3341
|
return chunks;
|
|
3009
3342
|
}
|
|
3010
|
-
function
|
|
3343
|
+
function sleep3(ms) {
|
|
3011
3344
|
return new Promise((r) => setTimeout(r, ms));
|
|
3012
3345
|
}
|
|
3013
3346
|
|
|
@@ -3272,6 +3605,7 @@ function createAutoUpdater(config) {
|
|
|
3272
3605
|
NotLoggedInError,
|
|
3273
3606
|
SubscriptionUsageError,
|
|
3274
3607
|
TelegramBot,
|
|
3608
|
+
XAI_OAUTH_KEY,
|
|
3275
3609
|
XIAOMI_CREDITS_KEY,
|
|
3276
3610
|
clearLocalDiscoveryCache,
|
|
3277
3611
|
clearRuntimeModels,
|
|
@@ -3280,6 +3614,8 @@ function createAutoUpdater(config) {
|
|
|
3280
3614
|
decodeOggOpus,
|
|
3281
3615
|
discoverLocalModels,
|
|
3282
3616
|
downmixToMono,
|
|
3617
|
+
dualAuthProvider,
|
|
3618
|
+
dualAuthProviderByOAuthKey,
|
|
3283
3619
|
endpointRoot,
|
|
3284
3620
|
fetchSubscriptionUsage,
|
|
3285
3621
|
findProbedModel,
|
|
@@ -3304,6 +3640,9 @@ function createAutoUpdater(config) {
|
|
|
3304
3640
|
getSupportedThinkingLevels,
|
|
3305
3641
|
getToolResultCharLimit,
|
|
3306
3642
|
getVideoByteLimit,
|
|
3643
|
+
grokCliBaseUrl,
|
|
3644
|
+
grokCliHeaders,
|
|
3645
|
+
isGrokCliEndpoint,
|
|
3307
3646
|
isKimiCodingEndpoint,
|
|
3308
3647
|
isLocalModelId,
|
|
3309
3648
|
isLoggerOpen,
|
|
@@ -3317,14 +3656,18 @@ function createAutoUpdater(config) {
|
|
|
3317
3656
|
loginGemini,
|
|
3318
3657
|
loginKimi,
|
|
3319
3658
|
loginOpenAI,
|
|
3659
|
+
loginXai,
|
|
3660
|
+
oauthStorageKey,
|
|
3320
3661
|
openLog,
|
|
3321
3662
|
parseLocalModelId,
|
|
3322
3663
|
probeEndpoint,
|
|
3664
|
+
providerStorageKeys,
|
|
3323
3665
|
readStoredBaseUrlSync,
|
|
3324
3666
|
refreshAnthropicToken,
|
|
3325
3667
|
refreshGeminiToken,
|
|
3326
3668
|
refreshKimiToken,
|
|
3327
3669
|
refreshOpenAIToken,
|
|
3670
|
+
refreshXaiToken,
|
|
3328
3671
|
registerLogCleanup,
|
|
3329
3672
|
registerRuntimeModels,
|
|
3330
3673
|
resample,
|