@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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getAppPaths
3
- } from "./chunk-WCIV25KV.js";
3
+ } from "./chunk-CRU3SSNX.js";
4
4
 
5
5
  // src/oauth/kimi.ts
6
6
  import { execFileSync } from "child_process";
@@ -256,6 +256,7 @@ import path2 from "path";
256
256
  import { randomBytes } from "crypto";
257
257
  import { environmentSecrets, redactText, redactValue } from "@prestyj/ai";
258
258
  var MAX_BYTES = 10 * 1024 * 1024;
259
+ var MIN_HEADROOM_BYTES = 64 * 1024;
259
260
  var fd = null;
260
261
  var bytesWritten = 0;
261
262
  var capped = false;
@@ -266,7 +267,7 @@ var exactSecrets = [];
266
267
  function rotateIfNeeded(filePath) {
267
268
  try {
268
269
  const st = fs.statSync(filePath);
269
- if (st.size < MAX_BYTES) return;
270
+ if (st.size <= MAX_BYTES - MIN_HEADROOM_BYTES) return;
270
271
  const rotated = `${filePath}.1`;
271
272
  try {
272
273
  fs.unlinkSync(rotated);
@@ -561,6 +562,9 @@ var TOKEN_URL = "https://auth.openai.com/oauth/token";
561
562
  var REDIRECT_URI2 = "http://localhost:1455/auth/callback";
562
563
  var SCOPE = "openid profile email offline_access api.connectors.read api.connectors.invoke";
563
564
  var JWT_CLAIM_PATH = "https://api.openai.com/auth";
565
+ var CALLBACK_TIMEOUT_MS = 12e4;
566
+ var MAX_PASTE_ATTEMPTS = 3;
567
+ var MIN_RAW_CODE_LENGTH = 10;
564
568
  async function loginOpenAI(callbacks) {
565
569
  const { verifier, challenge } = await generatePKCE();
566
570
  const state = crypto3.randomBytes(16).toString("hex");
@@ -576,20 +580,7 @@ async function loginOpenAI(callbacks) {
576
580
  url.searchParams.set("id_token_add_organizations", "true");
577
581
  url.searchParams.set("codex_cli_simplified_flow", "true");
578
582
  url.searchParams.set("originator", "ezcoder");
579
- let code;
580
- try {
581
- code = await loginWithServer(url.toString(), state, callbacks);
582
- } catch {
583
- callbacks.onOpenUrl(url.toString());
584
- const raw = await callbacks.onPromptCode(
585
- "Could not start local server. Paste the callback URL or code from the browser:"
586
- );
587
- const parsed = parseAuthorizationInput(raw);
588
- if (!parsed.code) {
589
- throw new Error("No authorization code found in input.");
590
- }
591
- code = parsed.code;
592
- }
583
+ const code = await acquireAuthorizationCode(url.toString(), state, callbacks);
593
584
  const creds = await exchangeOpenAICode(code, verifier);
594
585
  const accountId = getAccountId(creds.accessToken);
595
586
  if (!accountId) {
@@ -620,6 +611,7 @@ function parseAuthorizationInput(input) {
620
611
  state: params.get("state") ?? void 0
621
612
  };
622
613
  }
614
+ if (/\s/.test(value) || value.length < MIN_RAW_CODE_LENGTH) return {};
623
615
  return { code: value };
624
616
  }
625
617
  function decodeJwt(token) {
@@ -638,7 +630,55 @@ function getAccountId(accessToken) {
638
630
  const accountId = auth?.chatgpt_account_id;
639
631
  return typeof accountId === "string" && accountId.length > 0 ? accountId : null;
640
632
  }
641
- async function loginWithServer(authUrl, expectedState, callbacks) {
633
+ async function acquireAuthorizationCode(authUrl, expectedState, callbacks) {
634
+ callbacks.onOpenUrl(authUrl);
635
+ callbacks.onStatus("Waiting for browser callback...");
636
+ const done = new AbortController();
637
+ try {
638
+ return await new Promise((resolve, reject) => {
639
+ let serverFailed = false;
640
+ let pasteError;
641
+ const failIfExhausted = () => {
642
+ if (!serverFailed || pasteError === void 0) return;
643
+ reject(
644
+ pasteError instanceof Error ? pasteError : new Error("Could not obtain an authorization code.")
645
+ );
646
+ };
647
+ listenForCallback(expectedState, done.signal).then(resolve, () => {
648
+ serverFailed = true;
649
+ failIfExhausted();
650
+ });
651
+ promptForCode(expectedState, callbacks, done.signal).then(resolve, (err) => {
652
+ if (err instanceof FatalLoginError) {
653
+ reject(err);
654
+ return;
655
+ }
656
+ pasteError = err;
657
+ failIfExhausted();
658
+ });
659
+ });
660
+ } finally {
661
+ done.abort();
662
+ }
663
+ }
664
+ var FatalLoginError = class extends Error {
665
+ };
666
+ async function promptForCode(expectedState, callbacks, signal) {
667
+ let message = "If the browser is on another machine (SSH/headless), paste the callback URL or code here:";
668
+ for (let attempt = 0; attempt < MAX_PASTE_ATTEMPTS; attempt++) {
669
+ if (signal.aborted) throw new Error("Authorization code already received.");
670
+ const raw = await callbacks.onPromptCode(message, signal);
671
+ if (signal.aborted) throw new Error("Authorization code already received.");
672
+ const parsed = parseAuthorizationInput(raw);
673
+ if (parsed.code && parsed.state && parsed.state !== expectedState) {
674
+ throw new FatalLoginError("Authorization state mismatch \u2014 start the login again.");
675
+ }
676
+ if (parsed.code) return parsed.code;
677
+ message = "That didn't contain an authorization code. Paste the full callback URL:";
678
+ }
679
+ throw new Error("No authorization code found in input.");
680
+ }
681
+ async function listenForCallback(expectedState, signal) {
642
682
  return new Promise((resolve, reject) => {
643
683
  let receivedCode = null;
644
684
  const server = http.createServer((req, res) => {
@@ -661,18 +701,18 @@ async function loginWithServer(authUrl, expectedState, callbacks) {
661
701
  server.on("error", (err) => {
662
702
  reject(err);
663
703
  });
664
- server.listen(1455, "127.0.0.1", () => {
665
- callbacks.onOpenUrl(authUrl);
666
- callbacks.onStatus("Waiting for browser callback...");
667
- });
704
+ server.listen(1455, "127.0.0.1");
668
705
  const timeout = setTimeout(() => {
669
- if (!receivedCode) {
670
- server.close();
671
- }
672
- }, 12e4);
706
+ if (!receivedCode) server.close();
707
+ }, CALLBACK_TIMEOUT_MS);
673
708
  timeout.unref();
709
+ const onAbort = () => {
710
+ server.close();
711
+ };
712
+ signal.addEventListener("abort", onAbort, { once: true });
674
713
  server.on("close", () => {
675
714
  clearTimeout(timeout);
715
+ signal.removeEventListener("abort", onAbort);
676
716
  if (receivedCode) {
677
717
  resolve(receivedCode);
678
718
  } else {
@@ -734,8 +774,8 @@ async function refreshOpenAIToken(refreshToken) {
734
774
  // src/oauth/gemini.ts
735
775
  import http2 from "http";
736
776
  import crypto4 from "crypto";
737
- var CLIENT_ID_ENV = "GGCODER_GEMINI_OAUTH_CLIENT_ID";
738
- var CLIENT_SECRET_ENV = "GGCODER_GEMINI_OAUTH_CLIENT_SECRET";
777
+ var CLIENT_ID_ENV = "EZCODER_GEMINI_OAUTH_CLIENT_ID";
778
+ var CLIENT_SECRET_ENV = "EZCODER_GEMINI_OAUTH_CLIENT_SECRET";
739
779
  var DEFAULT_CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com";
740
780
  var DEFAULT_CLIENT_SECRET = "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";
741
781
  var AUTHORIZE_URL3 = "https://accounts.google.com/o/oauth2/v2/auth";
@@ -765,13 +805,13 @@ var CodeAssistHttpError = class extends Error {
765
805
  }
766
806
  };
767
807
  async function loginGemini(callbacks) {
768
- const { clientId, clientSecret } = getGeminiOAuthClientCredentials();
808
+ const { clientId: clientId2, clientSecret } = getGeminiOAuthClientCredentials();
769
809
  const { verifier, challenge } = await generatePKCE();
770
810
  const state = crypto4.randomBytes(32).toString("hex");
771
811
  const redirectUri = await getLoopbackRedirectUri();
772
812
  const url = new URL(AUTHORIZE_URL3);
773
813
  url.searchParams.set("response_type", "code");
774
- url.searchParams.set("client_id", clientId);
814
+ url.searchParams.set("client_id", clientId2);
775
815
  url.searchParams.set("redirect_uri", redirectUri);
776
816
  url.searchParams.set("scope", SCOPE2);
777
817
  url.searchParams.set("access_type", "offline");
@@ -781,7 +821,7 @@ async function loginGemini(callbacks) {
781
821
  url.searchParams.set("state", state);
782
822
  let code;
783
823
  try {
784
- code = await loginWithServer2(url.toString(), redirectUri, state, callbacks);
824
+ code = await loginWithServer(url.toString(), redirectUri, state, callbacks);
785
825
  } catch {
786
826
  callbacks.onOpenUrl(url.toString());
787
827
  const raw = await callbacks.onPromptCode(
@@ -796,7 +836,7 @@ async function loginGemini(callbacks) {
796
836
  }
797
837
  code = parsed.code;
798
838
  }
799
- const creds = await exchangeGeminiCode(code, verifier, redirectUri, clientId, clientSecret);
839
+ const creds = await exchangeGeminiCode(code, verifier, redirectUri, clientId2, clientSecret);
800
840
  callbacks.onStatus("Setting up Gemini Code Assist access...");
801
841
  const projectId = await setupCodeAssistProject(creds.accessToken, callbacks);
802
842
  return {
@@ -805,11 +845,11 @@ async function loginGemini(callbacks) {
805
845
  };
806
846
  }
807
847
  async function refreshGeminiToken(refreshToken) {
808
- const { clientId, clientSecret } = getGeminiOAuthClientCredentials();
848
+ const { clientId: clientId2, clientSecret } = getGeminiOAuthClientCredentials();
809
849
  const data = await postTokenRequest2({
810
850
  grant_type: "refresh_token",
811
851
  refresh_token: refreshToken,
812
- client_id: clientId,
852
+ client_id: clientId2,
813
853
  client_secret: clientSecret
814
854
  });
815
855
  return {
@@ -819,9 +859,9 @@ async function refreshGeminiToken(refreshToken) {
819
859
  };
820
860
  }
821
861
  function getGeminiOAuthClientCredentials() {
822
- const clientId = process.env[CLIENT_ID_ENV]?.trim() || DEFAULT_CLIENT_ID;
862
+ const clientId2 = process.env[CLIENT_ID_ENV]?.trim() || DEFAULT_CLIENT_ID;
823
863
  const clientSecret = process.env[CLIENT_SECRET_ENV]?.trim() || DEFAULT_CLIENT_SECRET;
824
- return { clientId, clientSecret };
864
+ return { clientId: clientId2, clientSecret };
825
865
  }
826
866
  async function getLoopbackRedirectUri() {
827
867
  return new Promise((resolve, reject) => {
@@ -859,7 +899,7 @@ function parseAuthorizationInput2(input) {
859
899
  }
860
900
  return { code: value };
861
901
  }
862
- async function loginWithServer2(authUrl, redirectUri, expectedState, callbacks) {
902
+ async function loginWithServer(authUrl, redirectUri, expectedState, callbacks) {
863
903
  const redirect = new URL(redirectUri);
864
904
  const port = Number(redirect.port);
865
905
  return new Promise((resolve, reject) => {
@@ -900,10 +940,10 @@ async function loginWithServer2(authUrl, redirectUri, expectedState, callbacks)
900
940
  });
901
941
  });
902
942
  }
903
- async function exchangeGeminiCode(code, verifier, redirectUri, clientId, clientSecret) {
943
+ async function exchangeGeminiCode(code, verifier, redirectUri, clientId2, clientSecret) {
904
944
  const data = await postTokenRequest2({
905
945
  grant_type: "authorization_code",
906
- client_id: clientId,
946
+ client_id: clientId2,
907
947
  client_secret: clientSecret,
908
948
  code,
909
949
  redirect_uri: redirectUri,
@@ -1094,6 +1134,204 @@ function codeAssistHeaders(accessToken) {
1094
1134
  };
1095
1135
  }
1096
1136
 
1137
+ // src/oauth/xai.ts
1138
+ var DEFAULT_CLIENT_ID2 = "b1a00492-073a-47ea-816f-4c329264a828";
1139
+ var DEFAULT_ISSUER = "https://auth.x.ai";
1140
+ var DEFAULT_CLI_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
1141
+ var OAUTH_SCOPE = "openid profile email offline_access grok-cli:access api:access";
1142
+ var DEFAULT_GROK_CLI_VERSION = "0.2.101";
1143
+ var DEVICE_TIMEOUT_FALLBACK_MS = 10 * 60 * 1e3;
1144
+ var DEFAULT_TOKEN_LIFETIME_SECONDS = 3600;
1145
+ function issuer() {
1146
+ return (process.env.XAI_OAUTH_ISSUER ?? DEFAULT_ISSUER).replace(/\/+$/, "");
1147
+ }
1148
+ function clientId() {
1149
+ return process.env.XAI_OAUTH_CLIENT_ID ?? DEFAULT_CLIENT_ID2;
1150
+ }
1151
+ function grokCliBaseUrl() {
1152
+ return (process.env.XAI_CLI_BASE_URL ?? DEFAULT_CLI_BASE_URL).replace(/\/+$/, "");
1153
+ }
1154
+ function grokCliVersion() {
1155
+ const raw = process.env.GROK_CLI_VERSION ?? DEFAULT_GROK_CLI_VERSION;
1156
+ const cleaned = raw.replace(/[^\u0020-\u007E]/g, "").trim();
1157
+ return cleaned.length > 0 ? cleaned : DEFAULT_GROK_CLI_VERSION;
1158
+ }
1159
+ function grokCliHeaders(modelId) {
1160
+ return {
1161
+ "X-XAI-Token-Auth": "xai-grok-cli",
1162
+ "x-grok-client-version": grokCliVersion(),
1163
+ "x-grok-client-identifier": "ezcoder",
1164
+ ...modelId ? { "x-grok-model-override": modelId } : {}
1165
+ };
1166
+ }
1167
+ function isGrokCliEndpoint(baseUrl) {
1168
+ if (typeof baseUrl !== "string" || baseUrl.length === 0) return false;
1169
+ const normalized = baseUrl.replace(/\/+$/, "");
1170
+ return normalized === grokCliBaseUrl() || /(^|\.)grok\.com/i.test(normalized);
1171
+ }
1172
+ async function postForm2(endpoint, params) {
1173
+ const response = await fetch(`${issuer()}${endpoint}`, {
1174
+ method: "POST",
1175
+ headers: {
1176
+ "Content-Type": "application/x-www-form-urlencoded",
1177
+ Accept: "application/json",
1178
+ "User-Agent": `grok-cli/${grokCliVersion()}`
1179
+ },
1180
+ body: new URLSearchParams(params).toString()
1181
+ });
1182
+ let data = {};
1183
+ try {
1184
+ const parsed = await response.json();
1185
+ if (parsed && typeof parsed === "object") data = parsed;
1186
+ } catch {
1187
+ }
1188
+ return { status: response.status, data };
1189
+ }
1190
+ function errorDetail2(data) {
1191
+ const desc = data.error_description ?? data.message ?? data.error;
1192
+ return typeof desc === "string" && desc.length > 0 ? desc : "unknown error";
1193
+ }
1194
+ function jwtExpirySeconds(token) {
1195
+ const payload = token.split(".")[1];
1196
+ if (!payload) return void 0;
1197
+ try {
1198
+ const json = Buffer.from(payload.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString(
1199
+ "utf-8"
1200
+ );
1201
+ const claims = JSON.parse(json);
1202
+ if (!claims || typeof claims !== "object") return void 0;
1203
+ const exp = claims.exp;
1204
+ return typeof exp === "number" && Number.isFinite(exp) && exp > 0 ? exp : void 0;
1205
+ } catch {
1206
+ return void 0;
1207
+ }
1208
+ }
1209
+ function credsFromTokenResponse2(data, opts) {
1210
+ const accessToken = data.access_token;
1211
+ const responseRefreshToken = data.refresh_token;
1212
+ if (typeof accessToken !== "string" || accessToken.length === 0) {
1213
+ throw new Error("Grok OAuth response missing access_token.");
1214
+ }
1215
+ const refreshToken = typeof responseRefreshToken === "string" && responseRefreshToken.length > 0 ? responseRefreshToken : opts?.fallbackRefreshToken ?? "";
1216
+ if (refreshToken.length === 0) {
1217
+ throw new Error(
1218
+ "Grok OAuth response missing refresh_token \u2014 the offline_access scope was not granted."
1219
+ );
1220
+ }
1221
+ const responseExpiresIn = Number(data.expires_in);
1222
+ let expiresIn;
1223
+ if (Number.isFinite(responseExpiresIn) && responseExpiresIn > 0) {
1224
+ expiresIn = responseExpiresIn;
1225
+ } else {
1226
+ const exp = jwtExpirySeconds(accessToken);
1227
+ const fromJwt = exp !== void 0 ? exp - Math.floor(Date.now() / 1e3) : 0;
1228
+ expiresIn = fromJwt > 0 ? fromJwt : DEFAULT_TOKEN_LIFETIME_SECONDS;
1229
+ }
1230
+ return {
1231
+ accessToken,
1232
+ refreshToken,
1233
+ expiresAt: Date.now() + expiresIn * 1e3,
1234
+ expiresIn,
1235
+ baseUrl: grokCliBaseUrl()
1236
+ };
1237
+ }
1238
+ async function requestDeviceAuthorization2() {
1239
+ const { status, data } = await postForm2("/oauth2/device/code", {
1240
+ client_id: clientId(),
1241
+ scope: OAUTH_SCOPE
1242
+ });
1243
+ if (status !== 200) {
1244
+ throw new Error(`Grok device authorization failed (${status}): ${errorDetail2(data)}`);
1245
+ }
1246
+ const userCode = data.user_code;
1247
+ const deviceCode = data.device_code;
1248
+ const verificationUriComplete = data.verification_uri_complete;
1249
+ if (typeof userCode !== "string" || typeof deviceCode !== "string") {
1250
+ throw new Error("Grok device authorization response missing user_code/device_code.");
1251
+ }
1252
+ return {
1253
+ userCode,
1254
+ deviceCode,
1255
+ verificationUri: typeof data.verification_uri === "string" ? data.verification_uri : "",
1256
+ verificationUriComplete: typeof verificationUriComplete === "string" ? verificationUriComplete : "",
1257
+ interval: Number(data.interval ?? 5) || 5,
1258
+ // RFC 8628 §3.2: the server states how long the device code lives. Honor it
1259
+ // rather than imposing our own budget — polling a code we know is dead only
1260
+ // burns requests, and a longer local window would keep a user waiting past
1261
+ // the point the code can ever succeed.
1262
+ expiresInMs: (Number(data.expires_in) || 0) > 0 ? Number(data.expires_in) * 1e3 : DEVICE_TIMEOUT_FALLBACK_MS
1263
+ };
1264
+ }
1265
+ async function pollDeviceToken2(deviceCode) {
1266
+ const { status, data } = await postForm2("/oauth2/token", {
1267
+ client_id: clientId(),
1268
+ device_code: deviceCode,
1269
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
1270
+ });
1271
+ if (status === 200 && typeof data.access_token === "string") {
1272
+ return { kind: "success", creds: credsFromTokenResponse2(data) };
1273
+ }
1274
+ if (status >= 500) {
1275
+ throw new Error(`Grok token polling server error (${status}): ${errorDetail2(data)}`);
1276
+ }
1277
+ const errorCode = typeof data.error === "string" ? data.error : "unknown_error";
1278
+ switch (errorCode) {
1279
+ case "authorization_pending":
1280
+ return { kind: "pending" };
1281
+ case "slow_down":
1282
+ return { kind: "slow_down" };
1283
+ case "expired_token":
1284
+ return { kind: "expired" };
1285
+ case "access_denied":
1286
+ return { kind: "denied" };
1287
+ default:
1288
+ throw new Error(`Grok token polling failed (${status}): ${errorDetail2(data)}`);
1289
+ }
1290
+ }
1291
+ function sleep2(ms) {
1292
+ return new Promise((resolve) => {
1293
+ setTimeout(resolve, ms);
1294
+ });
1295
+ }
1296
+ async function loginXai(callbacks) {
1297
+ const auth = await requestDeviceAuthorization2();
1298
+ callbacks.onStatus(
1299
+ `Visit ${auth.verificationUri || auth.verificationUriComplete} and enter code: ${auth.userCode}`
1300
+ );
1301
+ callbacks.onOpenUrl(auth.verificationUriComplete || auth.verificationUri);
1302
+ callbacks.onStatus("Waiting for you to authorize in the browser...");
1303
+ const deadline = Date.now() + auth.expiresInMs;
1304
+ let interval = Math.max(auth.interval, 1);
1305
+ while (Date.now() < deadline) {
1306
+ const remaining = deadline - Date.now();
1307
+ await sleep2(Math.min(interval * 1e3, remaining));
1308
+ const result = await pollDeviceToken2(auth.deviceCode);
1309
+ if (result.kind === "success") return result.creds;
1310
+ if (result.kind === "denied") {
1311
+ throw new Error("Grok authorization was denied.");
1312
+ }
1313
+ if (result.kind === "expired") {
1314
+ throw new Error("Grok device code expired. Please run login again.");
1315
+ }
1316
+ if (result.kind === "slow_down") {
1317
+ interval += 5;
1318
+ }
1319
+ }
1320
+ throw new Error("Grok login timed out. Please run login again.");
1321
+ }
1322
+ async function refreshXaiToken(refreshToken) {
1323
+ const { status, data } = await postForm2("/oauth2/token", {
1324
+ client_id: clientId(),
1325
+ grant_type: "refresh_token",
1326
+ refresh_token: refreshToken
1327
+ });
1328
+ if (status === 200 && typeof data.access_token === "string") {
1329
+ return credsFromTokenResponse2(data, { fallbackRefreshToken: refreshToken });
1330
+ }
1331
+ const errorCode = typeof data.error === "string" ? data.error : "";
1332
+ throw new Error(`Grok token refresh failed (${status}): ${errorCode || errorDetail2(data)}`);
1333
+ }
1334
+
1097
1335
  // src/file-lock.ts
1098
1336
  import fs3 from "fs/promises";
1099
1337
  import { setTimeout as setTimeout2 } from "timers/promises";
@@ -1158,18 +1396,49 @@ function isAlive(pid) {
1158
1396
 
1159
1397
  // src/auth-storage.ts
1160
1398
  var MOONSHOT_OAUTH_KEY = "moonshot-oauth";
1399
+ var XAI_OAUTH_KEY = "xai-oauth";
1400
+ var DUAL_AUTH_PROVIDERS = [
1401
+ {
1402
+ provider: "moonshot",
1403
+ oauthKey: MOONSHOT_OAUTH_KEY,
1404
+ oauthLabel: "Kimi OAuth",
1405
+ apiKeyLabel: "Moonshot API key",
1406
+ restoreHint: 'Run "ezcoder login" and choose Kimi OAuth to restore OAuth auth.'
1407
+ },
1408
+ {
1409
+ provider: "xai",
1410
+ oauthKey: XAI_OAUTH_KEY,
1411
+ oauthLabel: "Grok OAuth",
1412
+ apiKeyLabel: "xAI API key",
1413
+ restoreHint: 'Run "ezcoder login" and choose Grok OAuth to restore OAuth auth.'
1414
+ }
1415
+ ];
1416
+ function dualAuthProvider(provider) {
1417
+ return DUAL_AUTH_PROVIDERS.find((entry) => entry.provider === provider);
1418
+ }
1419
+ function dualAuthProviderByOAuthKey(storageKey) {
1420
+ return DUAL_AUTH_PROVIDERS.find((entry) => entry.oauthKey === storageKey);
1421
+ }
1422
+ function oauthStorageKey(provider) {
1423
+ return dualAuthProvider(provider)?.oauthKey;
1424
+ }
1425
+ function providerStorageKeys(provider) {
1426
+ const dual = dualAuthProvider(provider);
1427
+ return dual ? [dual.oauthKey, dual.provider] : [provider];
1428
+ }
1161
1429
  var XIAOMI_CREDITS_KEY = "xiaomi-credits";
1162
1430
  var LOCAL_AUTH_KEY_PREFIX = "local:";
1163
1431
  var LOCAL_CREDENTIAL_LIFETIME_MS = 100 * 365 * 24 * 60 * 60 * 1e3;
1164
1432
  function activeBaseUrlEntry(data, provider) {
1165
- if (provider === "moonshot") {
1166
- const oauth = data[MOONSHOT_OAUTH_KEY];
1433
+ const dual = dualAuthProvider(provider);
1434
+ if (dual) {
1435
+ const oauth = data[dual.oauthKey];
1167
1436
  if (oauth) {
1168
1437
  const exhaustedUntil = oauth.usageExhaustedUntil ?? 0;
1169
- if (Date.now() < exhaustedUntil && data["moonshot"]) return data["moonshot"];
1438
+ if (Date.now() < exhaustedUntil && data[dual.provider]) return data[dual.provider];
1170
1439
  return oauth;
1171
1440
  }
1172
- return data["moonshot"];
1441
+ return data[dual.provider];
1173
1442
  }
1174
1443
  return data[provider];
1175
1444
  }
@@ -1204,6 +1473,16 @@ var AuthStorage = class {
1204
1473
  data = {};
1205
1474
  filePath;
1206
1475
  loaded = false;
1476
+ /**
1477
+ * mtime+size of the file as of the cached snapshot (`size: -1` = no file).
1478
+ * auth.json is shared: the desktop app writes API keys and disconnects
1479
+ * NATIVELY (so they work with no daemon running), and every window/process has
1480
+ * its own AuthStorage. A load-once cache therefore goes stale — the sidecar
1481
+ * would keep listing models for a provider just disconnected, and hide the
1482
+ * ones just connected, until the daemon restarted.
1483
+ */
1484
+ snapshotMtimeMs = 0;
1485
+ snapshotSize = -1;
1207
1486
  /** Per-provider lock to serialize concurrent refresh calls. */
1208
1487
  refreshLocks = /* @__PURE__ */ new Map();
1209
1488
  constructor(filePath) {
@@ -1215,12 +1494,12 @@ var AuthStorage = class {
1215
1494
  }
1216
1495
  /** List provider keys with stored credentials. */
1217
1496
  async listProviders() {
1218
- await this.ensureLoaded();
1497
+ await this.ensureFresh();
1219
1498
  return Object.keys(this.data);
1220
1499
  }
1221
1500
  /** True if credentials exist for `provider`. */
1222
1501
  async hasCredentials(provider) {
1223
- await this.ensureLoaded();
1502
+ await this.ensureFresh();
1224
1503
  return Boolean(this.data[provider]);
1225
1504
  }
1226
1505
  /**
@@ -1231,18 +1510,19 @@ var AuthStorage = class {
1231
1510
  * instead of re-deriving the same order.
1232
1511
  */
1233
1512
  async pickStorageKey(keys) {
1234
- await this.ensureLoaded();
1513
+ await this.ensureFresh();
1235
1514
  return keys.find((key) => Boolean(this.data[key]));
1236
1515
  }
1237
1516
  /**
1238
- * True if the user has any usable auth for the logical provider. For
1239
- * `moonshot` this is satisfied by either the Kimi OAuth credential or the
1240
- * Moonshot API key.
1517
+ * True if the user has any usable auth for the logical provider. For a
1518
+ * dual-auth provider (Kimi/Grok) either the OAuth credential or the API key
1519
+ * satisfies it.
1241
1520
  */
1242
1521
  async hasProviderAuth(provider) {
1243
- await this.ensureLoaded();
1244
- if (provider === "moonshot") {
1245
- return Boolean(this.data[MOONSHOT_OAUTH_KEY] || this.data["moonshot"]);
1522
+ await this.ensureFresh();
1523
+ const dual = dualAuthProvider(provider);
1524
+ if (dual) {
1525
+ return Boolean(this.data[dual.oauthKey] || this.data[dual.provider]);
1246
1526
  }
1247
1527
  if (provider === "xiaomi") {
1248
1528
  return Boolean(this.data["xiaomi"] || this.data[XIAOMI_CREDITS_KEY]);
@@ -1276,14 +1556,16 @@ var AuthStorage = class {
1276
1556
  }
1277
1557
  /**
1278
1558
  * True if the active credential for `provider` is a static API key with no
1279
- * refresh mechanism. For `moonshot` this is only true when the Kimi OAuth
1280
- * credential is absent (a present OAuth credential is refreshable).
1559
+ * refresh mechanism. For a dual-auth provider this is only true when its OAuth
1560
+ * credential is absent or sidelined (a live OAuth credential is refreshable).
1281
1561
  */
1282
1562
  async isStaticApiKey(provider) {
1283
- await this.ensureLoaded();
1284
- if (provider === "moonshot" && this.data[MOONSHOT_OAUTH_KEY]) {
1285
- const exhaustedUntil = this.data[MOONSHOT_OAUTH_KEY].usageExhaustedUntil ?? 0;
1286
- const apiKeyActive = Date.now() < exhaustedUntil && Boolean(this.data["moonshot"]);
1563
+ await this.ensureFresh();
1564
+ const dual = dualAuthProvider(provider);
1565
+ const oauthCreds = dual ? this.data[dual.oauthKey] : void 0;
1566
+ if (dual && oauthCreds) {
1567
+ const exhaustedUntil = oauthCreds.usageExhaustedUntil ?? 0;
1568
+ const apiKeyActive = Date.now() < exhaustedUntil && Boolean(this.data[dual.provider]);
1287
1569
  if (!apiKeyActive) return false;
1288
1570
  }
1289
1571
  return STATIC_API_KEY_PROVIDERS.has(provider);
@@ -1291,26 +1573,30 @@ var AuthStorage = class {
1291
1573
  /**
1292
1574
  * The base URL on the credential that is active right now, if any.
1293
1575
  * Synchronous — call only after load()/resolveCredentials() populated the
1294
- * snapshot. For `moonshot` this is the Kimi For Coding URL whenever the
1295
- * OAuth entry is the one resolveCredentials would serve (i.e. not currently
1296
- * usage-exhausted with an API key configured).
1576
+ * snapshot. For a dual-auth provider this is the subscription endpoint (Kimi
1577
+ * For Coding, the Grok CLI proxy) whenever the OAuth entry is the one
1578
+ * resolveCredentials would serve (i.e. not currently usage-exhausted with an
1579
+ * API key configured).
1297
1580
  */
1298
1581
  getStoredBaseUrl(provider) {
1299
1582
  return activeBaseUrlEntry(this.data, provider)?.baseUrl;
1300
1583
  }
1301
1584
  async load() {
1585
+ const first = !this.loaded;
1302
1586
  await withFileLock(this.filePath, async () => {
1303
1587
  try {
1304
1588
  const content = await fs4.readFile(this.filePath, "utf-8");
1305
1589
  this.data = JSON.parse(content);
1306
- log("INFO", "auth", `Loaded credentials from ${this.filePath}`, {
1307
- providers: Object.keys(this.data).join(",") || "(none)"
1308
- });
1590
+ if (first) {
1591
+ log("INFO", "auth", `Loaded credentials from ${this.filePath}`, {
1592
+ providers: Object.keys(this.data).join(",") || "(none)"
1593
+ });
1594
+ }
1309
1595
  } catch (err) {
1310
1596
  this.data = {};
1311
1597
  const code = err.code;
1312
1598
  if (code === "ENOENT") {
1313
- log("INFO", "auth", `No auth file found at ${this.filePath} (first run)`);
1599
+ if (first) log("INFO", "auth", `No auth file found at ${this.filePath} (first run)`);
1314
1600
  } else {
1315
1601
  log(
1316
1602
  "ERROR",
@@ -1322,6 +1608,7 @@ var AuthStorage = class {
1322
1608
  }
1323
1609
  });
1324
1610
  this.loaded = true;
1611
+ await this.rememberSnapshot();
1325
1612
  }
1326
1613
  async ensureLoaded() {
1327
1614
  if (!this.loaded) await this.load();
@@ -1335,6 +1622,42 @@ var AuthStorage = class {
1335
1622
  this.loaded = false;
1336
1623
  await this.load();
1337
1624
  }
1625
+ /**
1626
+ * Like {@link ensureLoaded}, but re-reads when the file changed since this
1627
+ * snapshot — a cheap stat, not a re-parse. Used by the "what is connected?"
1628
+ * readers, which must reflect writes made by another window, the CLI, or the
1629
+ * desktop app's native (daemon-free) API-key and disconnect paths.
1630
+ *
1631
+ * Deliberately NOT used by {@link resolveCredentials}: that path compares the
1632
+ * caller's snapshot against the latest file to detect a concurrent re-login,
1633
+ * and silently refreshing this instance's view first would destroy the
1634
+ * evidence that the token it just had rejected has already been replaced.
1635
+ */
1636
+ async ensureFresh() {
1637
+ if (!this.loaded) {
1638
+ await this.load();
1639
+ return;
1640
+ }
1641
+ let changed;
1642
+ try {
1643
+ const stat = await fs4.stat(this.filePath);
1644
+ changed = stat.mtimeMs !== this.snapshotMtimeMs || stat.size !== this.snapshotSize;
1645
+ } catch {
1646
+ changed = this.snapshotSize !== -1;
1647
+ }
1648
+ if (changed) await this.load();
1649
+ }
1650
+ /** Record the file identity behind the current snapshot. */
1651
+ async rememberSnapshot() {
1652
+ try {
1653
+ const stat = await fs4.stat(this.filePath);
1654
+ this.snapshotMtimeMs = stat.mtimeMs;
1655
+ this.snapshotSize = stat.size;
1656
+ } catch {
1657
+ this.snapshotMtimeMs = 0;
1658
+ this.snapshotSize = -1;
1659
+ }
1660
+ }
1338
1661
  /**
1339
1662
  * Apply one provider-scoped mutation to the latest on-disk snapshot.
1340
1663
  * AuthStorage instances live in every app session/process, so writing this
@@ -1350,14 +1673,16 @@ var AuthStorage = class {
1350
1673
  await atomicWriteFile(this.filePath, JSON.stringify(latest, null, 2));
1351
1674
  this.data = latest;
1352
1675
  });
1676
+ await this.rememberSnapshot();
1353
1677
  }
1354
1678
  async reloadLatest() {
1355
1679
  await withFileLock(this.filePath, async () => {
1356
1680
  this.data = await readAuthData(this.filePath);
1357
1681
  });
1682
+ await this.rememberSnapshot();
1358
1683
  }
1359
1684
  async getCredentials(provider) {
1360
- await this.ensureLoaded();
1685
+ await this.ensureFresh();
1361
1686
  return this.data[provider];
1362
1687
  }
1363
1688
  async setCredentials(provider, creds) {
@@ -1375,7 +1700,7 @@ var AuthStorage = class {
1375
1700
  * `resetsAt` (unix SECONDS, from the provider's rate-limit response) or a
1376
1701
  * 15-minute default when no reset time is known. While the mark is in the
1377
1702
  * future, `resolveCredentials("moonshot")` serves the Moonshot API key
1378
- * instead of the Kimi OAuth credential (when both are configured) — OAuth
1703
+ * instead of the subscription OAuth credential (when both are configured) — OAuth
1379
1704
  * stays the preferred credential and is retried automatically once the mark
1380
1705
  * lapses. Persisted to auth.json so a restart (or another ezcoder-app window)
1381
1706
  * doesn't burn a request rediscovering the same exhausted window. No-op if
@@ -1406,13 +1731,21 @@ var AuthStorage = class {
1406
1731
  }
1407
1732
  /**
1408
1733
  * Returns valid credentials, auto-refreshing if expired.
1734
+ *
1409
1735
  * If `forceRefresh` is true, refreshes even if the token hasn't expired
1410
- * (useful when the provider rejects a token with 401 before its stored expiry).
1736
+ * (useful when the provider rejects a token with 401 before its stored
1737
+ * expiry). Callers recovering from a rejection should also pass
1738
+ * `rejectedToken` — see the stampede guard below.
1739
+ *
1411
1740
  * Throws if not logged in.
1412
1741
  */
1413
1742
  async resolveCredentials(provider, opts) {
1414
- await this.ensureLoaded();
1415
- const directStorageKeys = opts?.storageKeys && !(opts.storageKeys.length === 1 && opts.storageKeys[0] === provider) ? opts.storageKeys : provider === "moonshot" ? [MOONSHOT_OAUTH_KEY, "moonshot"] : [provider];
1743
+ if (opts?.forceRefresh) {
1744
+ await this.ensureLoaded();
1745
+ } else {
1746
+ await this.ensureFresh();
1747
+ }
1748
+ const directStorageKeys = opts?.storageKeys && !(opts.storageKeys.length === 1 && opts.storageKeys[0] === provider) ? opts.storageKeys : providerStorageKeys(provider);
1416
1749
  if (!directStorageKeys.some((key) => Boolean(this.data[key]))) {
1417
1750
  await this.reloadLatest();
1418
1751
  }
@@ -1423,28 +1756,31 @@ var AuthStorage = class {
1423
1756
  }
1424
1757
  throw new NotLoggedInError(provider);
1425
1758
  }
1426
- if (provider === "moonshot" && this.data[MOONSHOT_OAUTH_KEY]) {
1427
- const exhaustedUntil = this.data[MOONSHOT_OAUTH_KEY].usageExhaustedUntil ?? 0;
1428
- if (Date.now() < exhaustedUntil && this.data["moonshot"]) {
1759
+ const dual = dualAuthProvider(provider);
1760
+ const dualOAuthCreds = dual ? this.data[dual.oauthKey] : void 0;
1761
+ if (dual && dualOAuthCreds) {
1762
+ const exhaustedUntil = dualOAuthCreds.usageExhaustedUntil ?? 0;
1763
+ if (Date.now() < exhaustedUntil && this.data[dual.provider]) {
1429
1764
  log(
1430
1765
  "WARN",
1431
1766
  "auth",
1432
- `Kimi OAuth usage window is exhausted \u2014 using the Moonshot API key until ${new Date(exhaustedUntil).toISOString()} (OAuth resumes automatically).`
1767
+ `${dual.oauthLabel} usage window is exhausted \u2014 using the ${dual.apiKeyLabel} until ${new Date(exhaustedUntil).toISOString()} (OAuth resumes automatically).`
1433
1768
  );
1434
- return this.data["moonshot"];
1769
+ return this.data[dual.provider];
1435
1770
  }
1436
1771
  try {
1437
- return await this.resolveCredentials(MOONSHOT_OAUTH_KEY, {
1438
- ...opts?.forceRefresh ? { forceRefresh: true } : {}
1772
+ return await this.resolveCredentials(dual.oauthKey, {
1773
+ ...opts?.forceRefresh ? { forceRefresh: true } : {},
1774
+ ...opts?.rejectedToken !== void 0 ? { rejectedToken: opts.rejectedToken } : {}
1439
1775
  });
1440
1776
  } catch (err) {
1441
- if (err instanceof NotLoggedInError && this.data["moonshot"]) {
1777
+ if (err instanceof NotLoggedInError && this.data[dual.provider]) {
1442
1778
  log(
1443
1779
  "WARN",
1444
1780
  "auth",
1445
- 'Kimi OAuth credential is no longer valid \u2014 falling back to the Moonshot API key. Run "ezcoder login" and choose Kimi OAuth to restore OAuth auth.'
1781
+ `${dual.oauthLabel} credential is no longer valid \u2014 falling back to the ${dual.apiKeyLabel}. ${dual.restoreHint}`
1446
1782
  );
1447
- return this.data["moonshot"];
1783
+ return this.data[dual.provider];
1448
1784
  }
1449
1785
  throw err;
1450
1786
  }
@@ -1469,11 +1805,19 @@ var AuthStorage = class {
1469
1805
  throw new NotLoggedInError(provider);
1470
1806
  }
1471
1807
  const credentialWasReplaced = latestCreds.accessToken !== creds.accessToken || latestCreds.refreshToken !== creds.refreshToken || latestCreds.expiresAt !== creds.expiresAt;
1472
- if (credentialWasReplaced || !opts?.forceRefresh && Date.now() < latestCreds.expiresAt - refreshThresholdMs(latestCreds)) {
1808
+ const rotatedBySibling = opts?.rejectedToken !== void 0 && latestCreds.accessToken !== opts.rejectedToken;
1809
+ if (credentialWasReplaced || rotatedBySibling || !opts?.forceRefresh && Date.now() < latestCreds.expiresAt - refreshThresholdMs(latestCreds)) {
1810
+ if (rotatedBySibling && !credentialWasReplaced) {
1811
+ log(
1812
+ "INFO",
1813
+ "auth",
1814
+ `${provider} token was rotated by another EZ Coder process \u2014 adopting it instead of refreshing again`
1815
+ );
1816
+ }
1473
1817
  this.data = latest;
1474
1818
  return latestCreds;
1475
1819
  }
1476
- const refreshFn = provider === "anthropic" ? refreshAnthropicToken : provider === "gemini" ? refreshGeminiToken : provider === MOONSHOT_OAUTH_KEY ? refreshKimiToken : refreshOpenAIToken;
1820
+ const refreshFn = provider === "anthropic" ? refreshAnthropicToken : provider === "gemini" ? refreshGeminiToken : provider === MOONSHOT_OAUTH_KEY ? refreshKimiToken : provider === XAI_OAUTH_KEY ? refreshXaiToken : refreshOpenAIToken;
1477
1821
  let refreshed;
1478
1822
  try {
1479
1823
  refreshed = await refreshFn(latestCreds.refreshToken);
@@ -1507,6 +1851,7 @@ var AuthStorage = class {
1507
1851
  return await refreshPromise;
1508
1852
  } finally {
1509
1853
  this.refreshLocks.delete(provider);
1854
+ await this.rememberSnapshot();
1510
1855
  }
1511
1856
  }
1512
1857
  /**
@@ -1770,7 +2115,7 @@ var MODELS = [
1770
2115
  {
1771
2116
  // Gemini 3.1 Pro is public preview — gated behind Code Assist preview
1772
2117
  // enablement, so free/personal OAuth accounts 404 on it (see
1773
- // ACCOUNT_GATED_MODELS in gg-ai's gemini provider).
2118
+ // ACCOUNT_GATED_MODELS in @prestyj/ai's gemini provider).
1774
2119
  id: "gemini-3.1-pro-preview",
1775
2120
  name: "Gemini 3.1 Pro (Preview)",
1776
2121
  provider: "gemini",
@@ -2093,8 +2438,18 @@ export {
2093
2438
  refreshOpenAIToken,
2094
2439
  loginGemini,
2095
2440
  refreshGeminiToken,
2441
+ grokCliBaseUrl,
2442
+ grokCliHeaders,
2443
+ isGrokCliEndpoint,
2444
+ loginXai,
2445
+ refreshXaiToken,
2096
2446
  withFileLock,
2097
2447
  MOONSHOT_OAUTH_KEY,
2448
+ XAI_OAUTH_KEY,
2449
+ dualAuthProvider,
2450
+ dualAuthProviderByOAuthKey,
2451
+ oauthStorageKey,
2452
+ providerStorageKeys,
2098
2453
  XIAOMI_CREDITS_KEY,
2099
2454
  LOCAL_AUTH_KEY_PREFIX,
2100
2455
  readStoredBaseUrlSync,
@@ -2119,4 +2474,4 @@ export {
2119
2474
  getSummaryModel,
2120
2475
  getFastModel
2121
2476
  };
2122
- //# sourceMappingURL=chunk-TPGCBL7I.js.map
2477
+ //# sourceMappingURL=chunk-234BODO4.js.map