@prestyj/core 5.10.0 → 5.12.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-TPGCBL7I.js → chunk-234BODO4.js} +438 -83
- package/dist/chunk-234BODO4.js.map +1 -0
- package/dist/{chunk-WCIV25KV.js → chunk-CRU3SSNX.js} +2 -1
- package/dist/chunk-CRU3SSNX.js.map +1 -0
- package/dist/index.cjs +449 -83
- 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 +22 -2
- package/dist/index.js.map +1 -1
- package/dist/model-registry.cjs +5 -1
- package/dist/model-registry.cjs.map +1 -1
- package/dist/model-registry.d.cts +1 -1
- package/dist/model-registry.d.ts +1 -1
- package/dist/model-registry.js +2 -2
- package/dist/paths.cjs +1 -0
- package/dist/paths.cjs.map +1 -1
- package/dist/paths.d.cts +2 -0
- package/dist/paths.d.ts +2 -0
- package/dist/paths.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-TPGCBL7I.js.map +0 -1
- package/dist/chunk-WCIV25KV.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,
|
|
@@ -128,6 +138,7 @@ function getAppPaths() {
|
|
|
128
138
|
agentHomeFile: import_node_path.default.join(agentDir, "agent-home.json"),
|
|
129
139
|
mcpFile: import_node_path.default.join(agentDir, "mcp.json"),
|
|
130
140
|
mcpAuthFile: import_node_path.default.join(agentDir, "mcp-auth.json"),
|
|
141
|
+
mcpCatalogFile: import_node_path.default.join(agentDir, "mcp-catalog.json"),
|
|
131
142
|
logFile: import_node_path.default.join(agentDir, "debug.log"),
|
|
132
143
|
skillsDir: import_node_path.default.join(agentDir, "skills"),
|
|
133
144
|
extensionsDir: import_node_path.default.join(agentDir, "extensions"),
|
|
@@ -386,6 +397,7 @@ var import_node_path3 = __toESM(require("path"), 1);
|
|
|
386
397
|
var import_node_crypto2 = require("crypto");
|
|
387
398
|
var import_ai = require("@prestyj/ai");
|
|
388
399
|
var MAX_BYTES = 10 * 1024 * 1024;
|
|
400
|
+
var MIN_HEADROOM_BYTES = 64 * 1024;
|
|
389
401
|
var fd = null;
|
|
390
402
|
var bytesWritten = 0;
|
|
391
403
|
var capped = false;
|
|
@@ -396,7 +408,7 @@ var exactSecrets = [];
|
|
|
396
408
|
function rotateIfNeeded(filePath) {
|
|
397
409
|
try {
|
|
398
410
|
const st = import_node_fs2.default.statSync(filePath);
|
|
399
|
-
if (st.size
|
|
411
|
+
if (st.size <= MAX_BYTES - MIN_HEADROOM_BYTES) return;
|
|
400
412
|
const rotated = `${filePath}.1`;
|
|
401
413
|
try {
|
|
402
414
|
import_node_fs2.default.unlinkSync(rotated);
|
|
@@ -691,6 +703,9 @@ var TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
|
691
703
|
var REDIRECT_URI2 = "http://localhost:1455/auth/callback";
|
|
692
704
|
var SCOPE = "openid profile email offline_access api.connectors.read api.connectors.invoke";
|
|
693
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;
|
|
694
709
|
async function loginOpenAI(callbacks) {
|
|
695
710
|
const { verifier, challenge } = await generatePKCE();
|
|
696
711
|
const state = import_node_crypto4.default.randomBytes(16).toString("hex");
|
|
@@ -706,20 +721,7 @@ async function loginOpenAI(callbacks) {
|
|
|
706
721
|
url.searchParams.set("id_token_add_organizations", "true");
|
|
707
722
|
url.searchParams.set("codex_cli_simplified_flow", "true");
|
|
708
723
|
url.searchParams.set("originator", "ezcoder");
|
|
709
|
-
|
|
710
|
-
try {
|
|
711
|
-
code = await loginWithServer(url.toString(), state, callbacks);
|
|
712
|
-
} catch {
|
|
713
|
-
callbacks.onOpenUrl(url.toString());
|
|
714
|
-
const raw = await callbacks.onPromptCode(
|
|
715
|
-
"Could not start local server. Paste the callback URL or code from the browser:"
|
|
716
|
-
);
|
|
717
|
-
const parsed = parseAuthorizationInput(raw);
|
|
718
|
-
if (!parsed.code) {
|
|
719
|
-
throw new Error("No authorization code found in input.");
|
|
720
|
-
}
|
|
721
|
-
code = parsed.code;
|
|
722
|
-
}
|
|
724
|
+
const code = await acquireAuthorizationCode(url.toString(), state, callbacks);
|
|
723
725
|
const creds = await exchangeOpenAICode(code, verifier);
|
|
724
726
|
const accountId = getAccountId(creds.accessToken);
|
|
725
727
|
if (!accountId) {
|
|
@@ -750,6 +752,7 @@ function parseAuthorizationInput(input) {
|
|
|
750
752
|
state: params.get("state") ?? void 0
|
|
751
753
|
};
|
|
752
754
|
}
|
|
755
|
+
if (/\s/.test(value) || value.length < MIN_RAW_CODE_LENGTH) return {};
|
|
753
756
|
return { code: value };
|
|
754
757
|
}
|
|
755
758
|
function decodeJwt(token) {
|
|
@@ -768,7 +771,55 @@ function getAccountId(accessToken) {
|
|
|
768
771
|
const accountId = auth?.chatgpt_account_id;
|
|
769
772
|
return typeof accountId === "string" && accountId.length > 0 ? accountId : null;
|
|
770
773
|
}
|
|
771
|
-
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) {
|
|
772
823
|
return new Promise((resolve, reject) => {
|
|
773
824
|
let receivedCode = null;
|
|
774
825
|
const server = import_node_http.default.createServer((req, res) => {
|
|
@@ -791,18 +842,18 @@ async function loginWithServer(authUrl, expectedState, callbacks) {
|
|
|
791
842
|
server.on("error", (err) => {
|
|
792
843
|
reject(err);
|
|
793
844
|
});
|
|
794
|
-
server.listen(1455, "127.0.0.1"
|
|
795
|
-
callbacks.onOpenUrl(authUrl);
|
|
796
|
-
callbacks.onStatus("Waiting for browser callback...");
|
|
797
|
-
});
|
|
845
|
+
server.listen(1455, "127.0.0.1");
|
|
798
846
|
const timeout = setTimeout(() => {
|
|
799
|
-
if (!receivedCode)
|
|
800
|
-
|
|
801
|
-
}
|
|
802
|
-
}, 12e4);
|
|
847
|
+
if (!receivedCode) server.close();
|
|
848
|
+
}, CALLBACK_TIMEOUT_MS);
|
|
803
849
|
timeout.unref();
|
|
850
|
+
const onAbort = () => {
|
|
851
|
+
server.close();
|
|
852
|
+
};
|
|
853
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
804
854
|
server.on("close", () => {
|
|
805
855
|
clearTimeout(timeout);
|
|
856
|
+
signal.removeEventListener("abort", onAbort);
|
|
806
857
|
if (receivedCode) {
|
|
807
858
|
resolve(receivedCode);
|
|
808
859
|
} else {
|
|
@@ -864,8 +915,8 @@ async function refreshOpenAIToken(refreshToken) {
|
|
|
864
915
|
// src/oauth/gemini.ts
|
|
865
916
|
var import_node_http2 = __toESM(require("http"), 1);
|
|
866
917
|
var import_node_crypto5 = __toESM(require("crypto"), 1);
|
|
867
|
-
var CLIENT_ID_ENV = "
|
|
868
|
-
var CLIENT_SECRET_ENV = "
|
|
918
|
+
var CLIENT_ID_ENV = "EZCODER_GEMINI_OAUTH_CLIENT_ID";
|
|
919
|
+
var CLIENT_SECRET_ENV = "EZCODER_GEMINI_OAUTH_CLIENT_SECRET";
|
|
869
920
|
var DEFAULT_CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com";
|
|
870
921
|
var DEFAULT_CLIENT_SECRET = "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";
|
|
871
922
|
var AUTHORIZE_URL3 = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
@@ -895,13 +946,13 @@ var CodeAssistHttpError = class extends Error {
|
|
|
895
946
|
}
|
|
896
947
|
};
|
|
897
948
|
async function loginGemini(callbacks) {
|
|
898
|
-
const { clientId, clientSecret } = getGeminiOAuthClientCredentials();
|
|
949
|
+
const { clientId: clientId2, clientSecret } = getGeminiOAuthClientCredentials();
|
|
899
950
|
const { verifier, challenge } = await generatePKCE();
|
|
900
951
|
const state = import_node_crypto5.default.randomBytes(32).toString("hex");
|
|
901
952
|
const redirectUri = await getLoopbackRedirectUri();
|
|
902
953
|
const url = new URL(AUTHORIZE_URL3);
|
|
903
954
|
url.searchParams.set("response_type", "code");
|
|
904
|
-
url.searchParams.set("client_id",
|
|
955
|
+
url.searchParams.set("client_id", clientId2);
|
|
905
956
|
url.searchParams.set("redirect_uri", redirectUri);
|
|
906
957
|
url.searchParams.set("scope", SCOPE2);
|
|
907
958
|
url.searchParams.set("access_type", "offline");
|
|
@@ -911,7 +962,7 @@ async function loginGemini(callbacks) {
|
|
|
911
962
|
url.searchParams.set("state", state);
|
|
912
963
|
let code;
|
|
913
964
|
try {
|
|
914
|
-
code = await
|
|
965
|
+
code = await loginWithServer(url.toString(), redirectUri, state, callbacks);
|
|
915
966
|
} catch {
|
|
916
967
|
callbacks.onOpenUrl(url.toString());
|
|
917
968
|
const raw = await callbacks.onPromptCode(
|
|
@@ -926,7 +977,7 @@ async function loginGemini(callbacks) {
|
|
|
926
977
|
}
|
|
927
978
|
code = parsed.code;
|
|
928
979
|
}
|
|
929
|
-
const creds = await exchangeGeminiCode(code, verifier, redirectUri,
|
|
980
|
+
const creds = await exchangeGeminiCode(code, verifier, redirectUri, clientId2, clientSecret);
|
|
930
981
|
callbacks.onStatus("Setting up Gemini Code Assist access...");
|
|
931
982
|
const projectId = await setupCodeAssistProject(creds.accessToken, callbacks);
|
|
932
983
|
return {
|
|
@@ -935,11 +986,11 @@ async function loginGemini(callbacks) {
|
|
|
935
986
|
};
|
|
936
987
|
}
|
|
937
988
|
async function refreshGeminiToken(refreshToken) {
|
|
938
|
-
const { clientId, clientSecret } = getGeminiOAuthClientCredentials();
|
|
989
|
+
const { clientId: clientId2, clientSecret } = getGeminiOAuthClientCredentials();
|
|
939
990
|
const data = await postTokenRequest2({
|
|
940
991
|
grant_type: "refresh_token",
|
|
941
992
|
refresh_token: refreshToken,
|
|
942
|
-
client_id:
|
|
993
|
+
client_id: clientId2,
|
|
943
994
|
client_secret: clientSecret
|
|
944
995
|
});
|
|
945
996
|
return {
|
|
@@ -949,9 +1000,9 @@ async function refreshGeminiToken(refreshToken) {
|
|
|
949
1000
|
};
|
|
950
1001
|
}
|
|
951
1002
|
function getGeminiOAuthClientCredentials() {
|
|
952
|
-
const
|
|
1003
|
+
const clientId2 = process.env[CLIENT_ID_ENV]?.trim() || DEFAULT_CLIENT_ID;
|
|
953
1004
|
const clientSecret = process.env[CLIENT_SECRET_ENV]?.trim() || DEFAULT_CLIENT_SECRET;
|
|
954
|
-
return { clientId, clientSecret };
|
|
1005
|
+
return { clientId: clientId2, clientSecret };
|
|
955
1006
|
}
|
|
956
1007
|
async function getLoopbackRedirectUri() {
|
|
957
1008
|
return new Promise((resolve, reject) => {
|
|
@@ -989,7 +1040,7 @@ function parseAuthorizationInput2(input) {
|
|
|
989
1040
|
}
|
|
990
1041
|
return { code: value };
|
|
991
1042
|
}
|
|
992
|
-
async function
|
|
1043
|
+
async function loginWithServer(authUrl, redirectUri, expectedState, callbacks) {
|
|
993
1044
|
const redirect = new URL(redirectUri);
|
|
994
1045
|
const port = Number(redirect.port);
|
|
995
1046
|
return new Promise((resolve, reject) => {
|
|
@@ -1030,10 +1081,10 @@ async function loginWithServer2(authUrl, redirectUri, expectedState, callbacks)
|
|
|
1030
1081
|
});
|
|
1031
1082
|
});
|
|
1032
1083
|
}
|
|
1033
|
-
async function exchangeGeminiCode(code, verifier, redirectUri,
|
|
1084
|
+
async function exchangeGeminiCode(code, verifier, redirectUri, clientId2, clientSecret) {
|
|
1034
1085
|
const data = await postTokenRequest2({
|
|
1035
1086
|
grant_type: "authorization_code",
|
|
1036
|
-
client_id:
|
|
1087
|
+
client_id: clientId2,
|
|
1037
1088
|
client_secret: clientSecret,
|
|
1038
1089
|
code,
|
|
1039
1090
|
redirect_uri: redirectUri,
|
|
@@ -1224,6 +1275,204 @@ function codeAssistHeaders(accessToken) {
|
|
|
1224
1275
|
};
|
|
1225
1276
|
}
|
|
1226
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
|
+
|
|
1227
1476
|
// src/file-lock.ts
|
|
1228
1477
|
var import_promises2 = __toESM(require("fs/promises"), 1);
|
|
1229
1478
|
var import_promises3 = require("timers/promises");
|
|
@@ -1288,18 +1537,49 @@ function isAlive(pid) {
|
|
|
1288
1537
|
|
|
1289
1538
|
// src/auth-storage.ts
|
|
1290
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
|
+
}
|
|
1291
1570
|
var XIAOMI_CREDITS_KEY = "xiaomi-credits";
|
|
1292
1571
|
var LOCAL_AUTH_KEY_PREFIX = "local:";
|
|
1293
1572
|
var LOCAL_CREDENTIAL_LIFETIME_MS = 100 * 365 * 24 * 60 * 60 * 1e3;
|
|
1294
1573
|
function activeBaseUrlEntry(data, provider) {
|
|
1295
|
-
|
|
1296
|
-
|
|
1574
|
+
const dual = dualAuthProvider(provider);
|
|
1575
|
+
if (dual) {
|
|
1576
|
+
const oauth = data[dual.oauthKey];
|
|
1297
1577
|
if (oauth) {
|
|
1298
1578
|
const exhaustedUntil = oauth.usageExhaustedUntil ?? 0;
|
|
1299
|
-
if (Date.now() < exhaustedUntil && data[
|
|
1579
|
+
if (Date.now() < exhaustedUntil && data[dual.provider]) return data[dual.provider];
|
|
1300
1580
|
return oauth;
|
|
1301
1581
|
}
|
|
1302
|
-
return data[
|
|
1582
|
+
return data[dual.provider];
|
|
1303
1583
|
}
|
|
1304
1584
|
return data[provider];
|
|
1305
1585
|
}
|
|
@@ -1334,6 +1614,16 @@ var AuthStorage = class {
|
|
|
1334
1614
|
data = {};
|
|
1335
1615
|
filePath;
|
|
1336
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;
|
|
1337
1627
|
/** Per-provider lock to serialize concurrent refresh calls. */
|
|
1338
1628
|
refreshLocks = /* @__PURE__ */ new Map();
|
|
1339
1629
|
constructor(filePath) {
|
|
@@ -1345,12 +1635,12 @@ var AuthStorage = class {
|
|
|
1345
1635
|
}
|
|
1346
1636
|
/** List provider keys with stored credentials. */
|
|
1347
1637
|
async listProviders() {
|
|
1348
|
-
await this.
|
|
1638
|
+
await this.ensureFresh();
|
|
1349
1639
|
return Object.keys(this.data);
|
|
1350
1640
|
}
|
|
1351
1641
|
/** True if credentials exist for `provider`. */
|
|
1352
1642
|
async hasCredentials(provider) {
|
|
1353
|
-
await this.
|
|
1643
|
+
await this.ensureFresh();
|
|
1354
1644
|
return Boolean(this.data[provider]);
|
|
1355
1645
|
}
|
|
1356
1646
|
/**
|
|
@@ -1361,18 +1651,19 @@ var AuthStorage = class {
|
|
|
1361
1651
|
* instead of re-deriving the same order.
|
|
1362
1652
|
*/
|
|
1363
1653
|
async pickStorageKey(keys) {
|
|
1364
|
-
await this.
|
|
1654
|
+
await this.ensureFresh();
|
|
1365
1655
|
return keys.find((key) => Boolean(this.data[key]));
|
|
1366
1656
|
}
|
|
1367
1657
|
/**
|
|
1368
|
-
* True if the user has any usable auth for the logical provider. For
|
|
1369
|
-
*
|
|
1370
|
-
*
|
|
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.
|
|
1371
1661
|
*/
|
|
1372
1662
|
async hasProviderAuth(provider) {
|
|
1373
|
-
await this.
|
|
1374
|
-
|
|
1375
|
-
|
|
1663
|
+
await this.ensureFresh();
|
|
1664
|
+
const dual = dualAuthProvider(provider);
|
|
1665
|
+
if (dual) {
|
|
1666
|
+
return Boolean(this.data[dual.oauthKey] || this.data[dual.provider]);
|
|
1376
1667
|
}
|
|
1377
1668
|
if (provider === "xiaomi") {
|
|
1378
1669
|
return Boolean(this.data["xiaomi"] || this.data[XIAOMI_CREDITS_KEY]);
|
|
@@ -1406,14 +1697,16 @@ var AuthStorage = class {
|
|
|
1406
1697
|
}
|
|
1407
1698
|
/**
|
|
1408
1699
|
* True if the active credential for `provider` is a static API key with no
|
|
1409
|
-
* refresh mechanism. For
|
|
1410
|
-
* 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).
|
|
1411
1702
|
*/
|
|
1412
1703
|
async isStaticApiKey(provider) {
|
|
1413
|
-
await this.
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
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]);
|
|
1417
1710
|
if (!apiKeyActive) return false;
|
|
1418
1711
|
}
|
|
1419
1712
|
return STATIC_API_KEY_PROVIDERS.has(provider);
|
|
@@ -1421,26 +1714,30 @@ var AuthStorage = class {
|
|
|
1421
1714
|
/**
|
|
1422
1715
|
* The base URL on the credential that is active right now, if any.
|
|
1423
1716
|
* Synchronous — call only after load()/resolveCredentials() populated the
|
|
1424
|
-
* snapshot. For
|
|
1425
|
-
*
|
|
1426
|
-
* 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).
|
|
1427
1721
|
*/
|
|
1428
1722
|
getStoredBaseUrl(provider) {
|
|
1429
1723
|
return activeBaseUrlEntry(this.data, provider)?.baseUrl;
|
|
1430
1724
|
}
|
|
1431
1725
|
async load() {
|
|
1726
|
+
const first = !this.loaded;
|
|
1432
1727
|
await withFileLock(this.filePath, async () => {
|
|
1433
1728
|
try {
|
|
1434
1729
|
const content = await import_promises4.default.readFile(this.filePath, "utf-8");
|
|
1435
1730
|
this.data = JSON.parse(content);
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1731
|
+
if (first) {
|
|
1732
|
+
log("INFO", "auth", `Loaded credentials from ${this.filePath}`, {
|
|
1733
|
+
providers: Object.keys(this.data).join(",") || "(none)"
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1439
1736
|
} catch (err) {
|
|
1440
1737
|
this.data = {};
|
|
1441
1738
|
const code = err.code;
|
|
1442
1739
|
if (code === "ENOENT") {
|
|
1443
|
-
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)`);
|
|
1444
1741
|
} else {
|
|
1445
1742
|
log(
|
|
1446
1743
|
"ERROR",
|
|
@@ -1452,6 +1749,7 @@ var AuthStorage = class {
|
|
|
1452
1749
|
}
|
|
1453
1750
|
});
|
|
1454
1751
|
this.loaded = true;
|
|
1752
|
+
await this.rememberSnapshot();
|
|
1455
1753
|
}
|
|
1456
1754
|
async ensureLoaded() {
|
|
1457
1755
|
if (!this.loaded) await this.load();
|
|
@@ -1465,6 +1763,42 @@ var AuthStorage = class {
|
|
|
1465
1763
|
this.loaded = false;
|
|
1466
1764
|
await this.load();
|
|
1467
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
|
+
}
|
|
1468
1802
|
/**
|
|
1469
1803
|
* Apply one provider-scoped mutation to the latest on-disk snapshot.
|
|
1470
1804
|
* AuthStorage instances live in every app session/process, so writing this
|
|
@@ -1480,14 +1814,16 @@ var AuthStorage = class {
|
|
|
1480
1814
|
await atomicWriteFile(this.filePath, JSON.stringify(latest, null, 2));
|
|
1481
1815
|
this.data = latest;
|
|
1482
1816
|
});
|
|
1817
|
+
await this.rememberSnapshot();
|
|
1483
1818
|
}
|
|
1484
1819
|
async reloadLatest() {
|
|
1485
1820
|
await withFileLock(this.filePath, async () => {
|
|
1486
1821
|
this.data = await readAuthData(this.filePath);
|
|
1487
1822
|
});
|
|
1823
|
+
await this.rememberSnapshot();
|
|
1488
1824
|
}
|
|
1489
1825
|
async getCredentials(provider) {
|
|
1490
|
-
await this.
|
|
1826
|
+
await this.ensureFresh();
|
|
1491
1827
|
return this.data[provider];
|
|
1492
1828
|
}
|
|
1493
1829
|
async setCredentials(provider, creds) {
|
|
@@ -1505,7 +1841,7 @@ var AuthStorage = class {
|
|
|
1505
1841
|
* `resetsAt` (unix SECONDS, from the provider's rate-limit response) or a
|
|
1506
1842
|
* 15-minute default when no reset time is known. While the mark is in the
|
|
1507
1843
|
* future, `resolveCredentials("moonshot")` serves the Moonshot API key
|
|
1508
|
-
* instead of the
|
|
1844
|
+
* instead of the subscription OAuth credential (when both are configured) — OAuth
|
|
1509
1845
|
* stays the preferred credential and is retried automatically once the mark
|
|
1510
1846
|
* lapses. Persisted to auth.json so a restart (or another ezcoder-app window)
|
|
1511
1847
|
* doesn't burn a request rediscovering the same exhausted window. No-op if
|
|
@@ -1536,13 +1872,21 @@ var AuthStorage = class {
|
|
|
1536
1872
|
}
|
|
1537
1873
|
/**
|
|
1538
1874
|
* Returns valid credentials, auto-refreshing if expired.
|
|
1875
|
+
*
|
|
1539
1876
|
* If `forceRefresh` is true, refreshes even if the token hasn't expired
|
|
1540
|
-
* (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
|
+
*
|
|
1541
1881
|
* Throws if not logged in.
|
|
1542
1882
|
*/
|
|
1543
1883
|
async resolveCredentials(provider, opts) {
|
|
1544
|
-
|
|
1545
|
-
|
|
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);
|
|
1546
1890
|
if (!directStorageKeys.some((key) => Boolean(this.data[key]))) {
|
|
1547
1891
|
await this.reloadLatest();
|
|
1548
1892
|
}
|
|
@@ -1553,28 +1897,31 @@ var AuthStorage = class {
|
|
|
1553
1897
|
}
|
|
1554
1898
|
throw new NotLoggedInError(provider);
|
|
1555
1899
|
}
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
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]) {
|
|
1559
1905
|
log(
|
|
1560
1906
|
"WARN",
|
|
1561
1907
|
"auth",
|
|
1562
|
-
|
|
1908
|
+
`${dual.oauthLabel} usage window is exhausted \u2014 using the ${dual.apiKeyLabel} until ${new Date(exhaustedUntil).toISOString()} (OAuth resumes automatically).`
|
|
1563
1909
|
);
|
|
1564
|
-
return this.data[
|
|
1910
|
+
return this.data[dual.provider];
|
|
1565
1911
|
}
|
|
1566
1912
|
try {
|
|
1567
|
-
return await this.resolveCredentials(
|
|
1568
|
-
...opts?.forceRefresh ? { forceRefresh: true } : {}
|
|
1913
|
+
return await this.resolveCredentials(dual.oauthKey, {
|
|
1914
|
+
...opts?.forceRefresh ? { forceRefresh: true } : {},
|
|
1915
|
+
...opts?.rejectedToken !== void 0 ? { rejectedToken: opts.rejectedToken } : {}
|
|
1569
1916
|
});
|
|
1570
1917
|
} catch (err) {
|
|
1571
|
-
if (err instanceof NotLoggedInError && this.data[
|
|
1918
|
+
if (err instanceof NotLoggedInError && this.data[dual.provider]) {
|
|
1572
1919
|
log(
|
|
1573
1920
|
"WARN",
|
|
1574
1921
|
"auth",
|
|
1575
|
-
|
|
1922
|
+
`${dual.oauthLabel} credential is no longer valid \u2014 falling back to the ${dual.apiKeyLabel}. ${dual.restoreHint}`
|
|
1576
1923
|
);
|
|
1577
|
-
return this.data[
|
|
1924
|
+
return this.data[dual.provider];
|
|
1578
1925
|
}
|
|
1579
1926
|
throw err;
|
|
1580
1927
|
}
|
|
@@ -1599,11 +1946,19 @@ var AuthStorage = class {
|
|
|
1599
1946
|
throw new NotLoggedInError(provider);
|
|
1600
1947
|
}
|
|
1601
1948
|
const credentialWasReplaced = latestCreds.accessToken !== creds.accessToken || latestCreds.refreshToken !== creds.refreshToken || latestCreds.expiresAt !== creds.expiresAt;
|
|
1602
|
-
|
|
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
|
+
}
|
|
1603
1958
|
this.data = latest;
|
|
1604
1959
|
return latestCreds;
|
|
1605
1960
|
}
|
|
1606
|
-
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;
|
|
1607
1962
|
let refreshed;
|
|
1608
1963
|
try {
|
|
1609
1964
|
refreshed = await refreshFn(latestCreds.refreshToken);
|
|
@@ -1637,6 +1992,7 @@ var AuthStorage = class {
|
|
|
1637
1992
|
return await refreshPromise;
|
|
1638
1993
|
} finally {
|
|
1639
1994
|
this.refreshLocks.delete(provider);
|
|
1995
|
+
await this.rememberSnapshot();
|
|
1640
1996
|
}
|
|
1641
1997
|
}
|
|
1642
1998
|
/**
|
|
@@ -1900,7 +2256,7 @@ var MODELS = [
|
|
|
1900
2256
|
{
|
|
1901
2257
|
// Gemini 3.1 Pro is public preview — gated behind Code Assist preview
|
|
1902
2258
|
// enablement, so free/personal OAuth accounts 404 on it (see
|
|
1903
|
-
// ACCOUNT_GATED_MODELS in
|
|
2259
|
+
// ACCOUNT_GATED_MODELS in @prestyj/ai's gemini provider).
|
|
1904
2260
|
id: "gemini-3.1-pro-preview",
|
|
1905
2261
|
name: "Gemini 3.1 Pro (Preview)",
|
|
1906
2262
|
provider: "gemini",
|
|
@@ -2836,7 +3192,7 @@ var TelegramBot = class {
|
|
|
2836
3192
|
} catch (err) {
|
|
2837
3193
|
if (!this.running) break;
|
|
2838
3194
|
console.error(`[telegram] Poll error: ${err instanceof Error ? err.message : err}`);
|
|
2839
|
-
await
|
|
3195
|
+
await sleep3(3e3);
|
|
2840
3196
|
}
|
|
2841
3197
|
}
|
|
2842
3198
|
}
|
|
@@ -3006,7 +3362,7 @@ function splitMessage(text) {
|
|
|
3006
3362
|
}
|
|
3007
3363
|
return chunks;
|
|
3008
3364
|
}
|
|
3009
|
-
function
|
|
3365
|
+
function sleep3(ms) {
|
|
3010
3366
|
return new Promise((r) => setTimeout(r, ms));
|
|
3011
3367
|
}
|
|
3012
3368
|
|
|
@@ -3271,6 +3627,7 @@ function createAutoUpdater(config) {
|
|
|
3271
3627
|
NotLoggedInError,
|
|
3272
3628
|
SubscriptionUsageError,
|
|
3273
3629
|
TelegramBot,
|
|
3630
|
+
XAI_OAUTH_KEY,
|
|
3274
3631
|
XIAOMI_CREDITS_KEY,
|
|
3275
3632
|
clearLocalDiscoveryCache,
|
|
3276
3633
|
clearRuntimeModels,
|
|
@@ -3279,6 +3636,8 @@ function createAutoUpdater(config) {
|
|
|
3279
3636
|
decodeOggOpus,
|
|
3280
3637
|
discoverLocalModels,
|
|
3281
3638
|
downmixToMono,
|
|
3639
|
+
dualAuthProvider,
|
|
3640
|
+
dualAuthProviderByOAuthKey,
|
|
3282
3641
|
endpointRoot,
|
|
3283
3642
|
fetchSubscriptionUsage,
|
|
3284
3643
|
findProbedModel,
|
|
@@ -3303,6 +3662,9 @@ function createAutoUpdater(config) {
|
|
|
3303
3662
|
getSupportedThinkingLevels,
|
|
3304
3663
|
getToolResultCharLimit,
|
|
3305
3664
|
getVideoByteLimit,
|
|
3665
|
+
grokCliBaseUrl,
|
|
3666
|
+
grokCliHeaders,
|
|
3667
|
+
isGrokCliEndpoint,
|
|
3306
3668
|
isKimiCodingEndpoint,
|
|
3307
3669
|
isLocalModelId,
|
|
3308
3670
|
isLoggerOpen,
|
|
@@ -3316,14 +3678,18 @@ function createAutoUpdater(config) {
|
|
|
3316
3678
|
loginGemini,
|
|
3317
3679
|
loginKimi,
|
|
3318
3680
|
loginOpenAI,
|
|
3681
|
+
loginXai,
|
|
3682
|
+
oauthStorageKey,
|
|
3319
3683
|
openLog,
|
|
3320
3684
|
parseLocalModelId,
|
|
3321
3685
|
probeEndpoint,
|
|
3686
|
+
providerStorageKeys,
|
|
3322
3687
|
readStoredBaseUrlSync,
|
|
3323
3688
|
refreshAnthropicToken,
|
|
3324
3689
|
refreshGeminiToken,
|
|
3325
3690
|
refreshKimiToken,
|
|
3326
3691
|
refreshOpenAIToken,
|
|
3692
|
+
refreshXaiToken,
|
|
3327
3693
|
registerLogCleanup,
|
|
3328
3694
|
registerRuntimeModels,
|
|
3329
3695
|
resample,
|