@bman654/clodex 1.2.2 → 1.3.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/cli.js CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  ensureLegacyAppHomeMigrated,
4
4
  findClaudeBinary,
5
5
  getAppHome,
6
+ getCredentialCleanupPath,
6
7
  getInstalledClaudeVersion,
7
8
  getLogsPath,
8
9
  getProvidersPath,
@@ -24,7 +25,7 @@ import {
24
25
  setServerListenMode,
25
26
  setServerMaskGatewayIds,
26
27
  unregisterServerRuntimeState
27
- } from "./chunk-3XM6UZWP.js";
28
+ } from "./chunk-ZN5X7YFE.js";
28
29
 
29
30
  // src/cli.ts
30
31
  import pc13 from "picocolors";
@@ -202,7 +203,7 @@ import { join } from "path";
202
203
  // package.json
203
204
  var package_default = {
204
205
  name: "@bman654/clodex",
205
- version: "1.2.2",
206
+ version: "1.3.0",
206
207
  publishConfig: {
207
208
  access: "public"
208
209
  },
@@ -315,7 +316,7 @@ var VERTEX_ANTHROPIC_NPM = "@ai-sdk/google-vertex/anthropic";
315
316
  var VERSION = package_default.version;
316
317
 
317
318
  // src/env.ts
318
- import { randomUUID as randomUUID2 } from "crypto";
319
+ import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
319
320
 
320
321
  // src/credential-helper.ts
321
322
  import { spawn } from "child_process";
@@ -563,11 +564,23 @@ function oauthCredentialToKeychainJson(cred) {
563
564
  return JSON.stringify(cred);
564
565
  }
565
566
  function tokensToStoredCredential(tokens, existingRefresh, accountId, providerData) {
567
+ const access = typeof tokens.access_token === "string" ? tokens.access_token.trim() : "";
568
+ if (!access) {
569
+ throw new Error("OAuth token response is missing a valid access token");
570
+ }
571
+ if (tokens.expires_in !== void 0 && (typeof tokens.expires_in !== "number" || !Number.isFinite(tokens.expires_in) || tokens.expires_in < 0)) {
572
+ throw new Error("OAuth token response has an invalid expiration");
573
+ }
574
+ const returnedRefresh = typeof tokens.refresh_token === "string" ? tokens.refresh_token.trim() : "";
575
+ const expires = Date.now() + (tokens.expires_in ?? 3600) * 1e3;
576
+ if (!Number.isFinite(expires)) {
577
+ throw new Error("OAuth token response has an invalid expiration");
578
+ }
566
579
  return {
567
580
  type: "oauth",
568
- access: tokens.access_token,
569
- refresh: tokens.refresh_token ?? existingRefresh ?? "",
570
- expires: Date.now() + (tokens.expires_in ?? 3600) * 1e3,
581
+ access,
582
+ refresh: returnedRefresh || existingRefresh || "",
583
+ expires,
571
584
  ...accountId ? { accountId } : {},
572
585
  ...providerData ? { providerData } : {}
573
586
  };
@@ -576,7 +589,7 @@ function parseStoredOAuthCredential(raw) {
576
589
  if (!raw?.trim().startsWith("{")) return null;
577
590
  try {
578
591
  const parsed = JSON.parse(raw);
579
- if (parsed.type === "oauth" && typeof parsed.access === "string" && typeof parsed.refresh === "string" && typeof parsed.expires === "number") {
592
+ if (parsed.type === "oauth" && typeof parsed.access === "string" && parsed.access.trim().length > 0 && typeof parsed.refresh === "string" && typeof parsed.expires === "number" && Number.isFinite(parsed.expires) && (parsed.accessRejected === void 0 || parsed.accessRejected === true)) {
580
593
  return parsed;
581
594
  }
582
595
  } catch {
@@ -616,23 +629,45 @@ async function sleepMs(ms) {
616
629
  }
617
630
 
618
631
  // src/oauth/refresh-http.ts
632
+ var OAUTH_REFRESH_TIMEOUT_MS = 3e4;
619
633
  async function postOAuthRefresh(url, body, options) {
620
634
  const isJson = options.contentType === "json";
621
- const response = await fetch(url, {
622
- method: "POST",
623
- headers: {
624
- "Content-Type": isJson ? "application/json" : "application/x-www-form-urlencoded",
625
- Accept: "application/json",
626
- ...options.headers
627
- },
628
- body: isJson ? JSON.stringify(body) : body.toString()
629
- });
630
- if (!response.ok) {
631
- const detail = options.includeBody ? await response.text().catch(() => "") : "";
632
- const status = options.includeStatus ? ` (${response.status})` : "";
633
- throw new Error(`${options.errorPrefix}${status}${detail ? `: ${detail}` : ""}`);
635
+ const abortController = new AbortController();
636
+ const timeout = setTimeout(() => {
637
+ abortController.abort(new DOMException(
638
+ "The operation was aborted due to timeout",
639
+ "TimeoutError"
640
+ ));
641
+ }, OAUTH_REFRESH_TIMEOUT_MS);
642
+ timeout.unref();
643
+ try {
644
+ const response = await fetch(url, {
645
+ method: "POST",
646
+ signal: abortController.signal,
647
+ headers: {
648
+ "Content-Type": isJson ? "application/json" : "application/x-www-form-urlencoded",
649
+ Accept: "application/json",
650
+ ...options.headers
651
+ },
652
+ body: isJson ? JSON.stringify(body) : body.toString()
653
+ });
654
+ if (!response.ok) {
655
+ let detail = "";
656
+ if (options.includeBody) {
657
+ detail = await response.text().catch(() => "");
658
+ } else {
659
+ try {
660
+ await response.body?.cancel();
661
+ } catch {
662
+ }
663
+ }
664
+ const status = options.includeStatus ? ` (${response.status})` : "";
665
+ throw new Error(`${options.errorPrefix}${status}${detail ? `: ${detail}` : ""}`);
666
+ }
667
+ return await response.json();
668
+ } finally {
669
+ clearTimeout(timeout);
634
670
  }
635
- return response.json();
636
671
  }
637
672
 
638
673
  // src/oauth/openai.ts
@@ -769,6 +804,7 @@ import {
769
804
  } from "fs";
770
805
  import { dirname } from "path";
771
806
  var DEFAULT_WAIT_MS = 3e4;
807
+ var DEFAULT_CREDENTIAL_MUTATION_WAIT_MS = 15e4;
772
808
  var DEFAULT_RETRY_MS = 25;
773
809
  var registryLockContext = new AsyncLocalStorage();
774
810
  var RegistryLockLostError = class extends Error {
@@ -1057,9 +1093,11 @@ function getCredentialMutationLockPath(authRef) {
1057
1093
  const digest = createHash2("sha256").update("clodex-credential-mutation\0").update(authRef).digest("hex");
1058
1094
  return `${getProvidersPath()}.credential-${digest}.lock`;
1059
1095
  }
1060
- function withCredentialMutationLock(authRef, operation) {
1096
+ function withCredentialMutationLock(authRef, operation, options = {}) {
1061
1097
  return withRegistryWriteLock(operation, {
1062
- lockPath: getCredentialMutationLockPath(authRef)
1098
+ ...options,
1099
+ lockPath: getCredentialMutationLockPath(authRef),
1100
+ waitMs: options.waitMs ?? DEFAULT_CREDENTIAL_MUTATION_WAIT_MS
1063
1101
  });
1064
1102
  }
1065
1103
 
@@ -1142,6 +1180,11 @@ function oauthProviderIdFromAccount(account) {
1142
1180
  return account.startsWith(prefix) ? account.slice(prefix.length) : null;
1143
1181
  }
1144
1182
  var oauthRefreshInflight = /* @__PURE__ */ new Map();
1183
+ var OAUTH_CREDENTIAL_CACHE_MAX_AGE_MS = 3e4;
1184
+ var oauthCredentialCache = /* @__PURE__ */ new Map();
1185
+ var rejectedEnvCredentialFingerprints = /* @__PURE__ */ new Map();
1186
+ var OAUTH_REFRESH_LOCK_WAIT_MS = 15e4;
1187
+ var OAUTH_STATE_KEY_SEPARATOR = "\0";
1145
1188
  function parseAuthRef(authRef) {
1146
1189
  if (authRef === "none:anonymous") return { kind: "none" };
1147
1190
  if (authRef.startsWith("keyring:")) {
@@ -1164,6 +1207,26 @@ function readEnvCredential(varName) {
1164
1207
  if (!raw?.trim()) return null;
1165
1208
  return raw.trim().split(/\r?\n/)[0]?.trim() || null;
1166
1209
  }
1210
+ function credentialFingerprint(value) {
1211
+ return createHash3("sha256").update(value).digest("hex");
1212
+ }
1213
+ function usableEnvCredential(source, value, rejectedAccessToken) {
1214
+ if (!value) {
1215
+ rejectedEnvCredentialFingerprints.delete(source);
1216
+ return null;
1217
+ }
1218
+ const fingerprint = credentialFingerprint(value);
1219
+ if (rejectedAccessToken !== void 0 && fingerprint === credentialFingerprint(rejectedAccessToken)) {
1220
+ rejectedEnvCredentialFingerprints.set(source, fingerprint);
1221
+ return null;
1222
+ }
1223
+ const rejectedFingerprint = rejectedEnvCredentialFingerprints.get(source);
1224
+ if (rejectedFingerprint === fingerprint) return null;
1225
+ if (rejectedFingerprint !== void 0) {
1226
+ rejectedEnvCredentialFingerprints.delete(source);
1227
+ }
1228
+ return value;
1229
+ }
1167
1230
  function readKeyringAccountFromService(Entry, service, account) {
1168
1231
  const value = new Entry(service, account).getPassword() ?? null;
1169
1232
  if (!value?.startsWith(KEYRING_CHUNK_PREFIX)) return value;
@@ -1263,16 +1326,25 @@ async function deleteStoredCredential(ref, diag) {
1263
1326
  return false;
1264
1327
  }
1265
1328
  }
1266
- async function resolveProviderCredential(providerId, authRef, diag) {
1329
+ async function resolveProviderCredential(providerId, authRef, diag, options = {}) {
1267
1330
  const parsed = parseAuthRef(authRef);
1268
1331
  if (parsed?.kind === "none") return null;
1269
- const namespaced = readEnvCredential(clodexKeyEnvVar(providerId));
1332
+ const namespacedVar = clodexKeyEnvVar(providerId);
1333
+ const namespaced = usableEnvCredential(
1334
+ `provider:${providerId}`,
1335
+ readEnvCredential(namespacedVar),
1336
+ options.rejectedAccessToken
1337
+ );
1270
1338
  if (namespaced) return namespaced;
1271
1339
  if (!parsed) return null;
1272
1340
  if (parsed.kind === "env") {
1273
- return readEnvCredential(parsed.varName);
1341
+ return usableEnvCredential(
1342
+ `provider:${providerId}:env:${parsed.varName}`,
1343
+ readEnvCredential(parsed.varName),
1344
+ options.rejectedAccessToken
1345
+ );
1274
1346
  }
1275
- return readProviderSecret(parsed, diag);
1347
+ return readProviderSecret(parsed, diag, options.rejectedAccessToken);
1276
1348
  }
1277
1349
  async function resolveProviderOAuthAccountId(authRef, diag) {
1278
1350
  const parsed = parseAuthRef(authRef);
@@ -1286,7 +1358,7 @@ async function resolveProviderOAuthProviderData(authRef, diag) {
1286
1358
  const raw = await readStoredCredential(parsed, diag);
1287
1359
  return parseStoredOAuthCredential(raw)?.providerData;
1288
1360
  }
1289
- function decodeProviderSecret(raw) {
1361
+ function decodeProviderSecret(raw, allowOpaqueJson = false) {
1290
1362
  if (!raw) return null;
1291
1363
  const trimmed = raw.trim();
1292
1364
  if (!trimmed.startsWith("{")) return trimmed;
@@ -1294,59 +1366,138 @@ function decodeProviderSecret(raw) {
1294
1366
  if (oauth) return oauth.access;
1295
1367
  try {
1296
1368
  const parsed = JSON.parse(trimmed);
1297
- if (parsed.type === "oauth" && typeof parsed.access === "string") return parsed.access;
1298
- if (parsed.type === "wellknown" && typeof parsed.token === "string") return parsed.token;
1369
+ if (parsed.type === "wellknown") {
1370
+ return typeof parsed.token === "string" && parsed.token.trim() ? parsed.token.trim() : null;
1371
+ }
1372
+ if (allowOpaqueJson && parsed.type === "oauth") {
1373
+ return typeof parsed.access === "string" && parsed.access.trim() ? parsed.access.trim() : null;
1374
+ }
1375
+ return allowOpaqueJson ? raw : null;
1299
1376
  } catch {
1377
+ return null;
1378
+ }
1379
+ }
1380
+ function oauthCredentialStateKey(providerId, authRef) {
1381
+ return `${providerId}${OAUTH_STATE_KEY_SEPARATOR}${authRef}`;
1382
+ }
1383
+ function clearOAuthCredentialCache(authRef) {
1384
+ const suffix = `${OAUTH_STATE_KEY_SEPARATOR}${authRef}`;
1385
+ for (const key of oauthCredentialCache.keys()) {
1386
+ if (key.endsWith(suffix)) oauthCredentialCache.delete(key);
1300
1387
  }
1301
- return trimmed;
1302
1388
  }
1303
- async function refreshOAuthStoredCredential(ref, providerId, diag) {
1389
+ function cacheOAuthCredential(stateKey, credential) {
1390
+ oauthCredentialCache.set(stateKey, {
1391
+ access: credential.access,
1392
+ expires: credential.expires,
1393
+ ...credential.accessRejected === true ? { accessRejected: true } : {},
1394
+ checkedAt: Date.now()
1395
+ });
1396
+ }
1397
+ function cachedOAuthCredentialIsUsable(credential, providerId, rejectedAccessToken) {
1398
+ if (!credential) return false;
1399
+ const age = Date.now() - credential.checkedAt;
1400
+ return age >= 0 && age < OAUTH_CREDENTIAL_CACHE_MAX_AGE_MS && credential.access !== rejectedAccessToken && credential.accessRejected !== true && !oauthCredentialShouldRefresh(credential, providerId);
1401
+ }
1402
+ async function readOAuthProviderSecret(ref, providerId, diag, rejectedAccessToken) {
1304
1403
  const authRef = storedCredentialAuthRef(ref);
1305
- const existing = oauthRefreshInflight.get(authRef);
1306
- if (existing) return existing;
1404
+ const stateKey = oauthCredentialStateKey(providerId, authRef);
1405
+ const existing = oauthRefreshInflight.get(stateKey);
1406
+ if (existing) {
1407
+ const resolved = await existing;
1408
+ if (resolved !== rejectedAccessToken) return resolved;
1409
+ return readOAuthProviderSecret(ref, providerId, diag, rejectedAccessToken);
1410
+ }
1411
+ const cached = oauthCredentialCache.get(stateKey);
1412
+ if (cached && cachedOAuthCredentialIsUsable(cached, providerId, rejectedAccessToken)) {
1413
+ return cached.access;
1414
+ }
1415
+ if (cached?.access === rejectedAccessToken) oauthCredentialCache.delete(stateKey);
1307
1416
  const work = withCredentialMutationLock(authRef, async () => {
1308
- const currentRaw = await readStoredCredential(ref, diag);
1309
- if (!currentRaw) return null;
1310
- const cred = parseStoredOAuthCredential(currentRaw);
1311
- if (!cred || !oauthCredentialShouldRefresh(cred, providerId)) {
1312
- return decodeProviderSecret(currentRaw);
1313
- }
1314
- try {
1315
- const refreshed = await refreshStoredOAuthCredential(providerId, cred);
1316
- const json = oauthCredentialToKeychainJson(refreshed);
1417
+ const latestCached = oauthCredentialCache.get(stateKey);
1418
+ if (latestCached && cachedOAuthCredentialIsUsable(latestCached, providerId, rejectedAccessToken)) {
1419
+ return latestCached.access;
1420
+ }
1421
+ for (let generation = 0; generation < 3; generation += 1) {
1422
+ const raw = await readStoredCredential(ref, diag);
1423
+ if (!raw) return null;
1424
+ const cred = parseStoredOAuthCredential(raw);
1425
+ if (!cred) {
1426
+ const decoded = decodeProviderSecret(raw);
1427
+ return decoded === rejectedAccessToken ? null : decoded;
1428
+ }
1429
+ cacheOAuthCredential(stateKey, cred);
1430
+ const forceRefresh = cred.access === rejectedAccessToken || cred.accessRejected === true;
1431
+ if (!forceRefresh && !oauthCredentialShouldRefresh(cred, providerId)) {
1432
+ return cred.access;
1433
+ }
1434
+ let refreshed;
1435
+ try {
1436
+ refreshed = await refreshStoredOAuthCredential(providerId, cred);
1437
+ } catch (err) {
1438
+ diag?.(err instanceof Error ? err.message : String(err));
1439
+ if (!forceRefresh && cred.access && cred.expires > Date.now()) return cred.access;
1440
+ oauthCredentialCache.delete(stateKey);
1441
+ throw err;
1442
+ }
1443
+ const accessStillRejected = rejectedAccessToken !== void 0 && refreshed.access === rejectedAccessToken || cred.accessRejected === true && refreshed.access === cred.access;
1444
+ const currentRaw = await readStoredCredential(ref, diag);
1445
+ if (currentRaw !== raw) {
1446
+ oauthCredentialCache.delete(stateKey);
1447
+ continue;
1448
+ }
1449
+ const credentialToSave = accessStillRejected ? { ...refreshed, accessRejected: true } : refreshed;
1450
+ const json = oauthCredentialToKeychainJson(credentialToSave);
1317
1451
  const saved = await saveProviderCredential(authRef, json, diag);
1318
- if (!saved) throw new Error("Could not persist refreshed OAuth credential");
1452
+ if (!saved) {
1453
+ oauthCredentialCache.delete(stateKey);
1454
+ throw new Error("Could not persist refreshed OAuth credential");
1455
+ }
1456
+ if (accessStillRejected) {
1457
+ oauthCredentialCache.delete(stateKey);
1458
+ return null;
1459
+ }
1319
1460
  return refreshed.access;
1320
- } catch (err) {
1321
- diag?.(err instanceof Error ? err.message : String(err));
1322
- if (cred.access && cred.expires > Date.now()) return cred.access;
1323
- throw err;
1324
1461
  }
1462
+ throw new Error("OAuth credential changed repeatedly while refresh was in progress");
1463
+ }, {
1464
+ waitMs: OAUTH_REFRESH_LOCK_WAIT_MS
1325
1465
  });
1326
- oauthRefreshInflight.set(authRef, work);
1466
+ oauthRefreshInflight.set(stateKey, work);
1327
1467
  try {
1328
1468
  return await work;
1329
1469
  } finally {
1330
- oauthRefreshInflight.delete(authRef);
1470
+ if (oauthRefreshInflight.get(stateKey) === work) {
1471
+ oauthRefreshInflight.delete(stateKey);
1472
+ }
1331
1473
  }
1332
1474
  }
1333
- async function readProviderSecret(ref, diag) {
1334
- const raw = await readStoredCredential(ref, diag);
1335
- if (!raw) return null;
1475
+ async function readProviderSecret(ref, diag, rejectedAccessToken) {
1336
1476
  const oauthProviderId = oauthProviderIdFromAccount(ref.account);
1337
- if (oauthProviderId && raw.trim().startsWith("{")) {
1338
- return refreshOAuthStoredCredential(ref, oauthProviderId, diag);
1477
+ if (oauthProviderId) {
1478
+ return readOAuthProviderSecret(ref, oauthProviderId, diag, rejectedAccessToken);
1339
1479
  }
1340
- return decodeProviderSecret(raw);
1480
+ const raw = await readStoredCredential(ref, diag);
1481
+ const decoded = decodeProviderSecret(raw, true);
1482
+ return decoded === rejectedAccessToken ? null : decoded;
1341
1483
  }
1342
1484
  async function saveProviderCredential(authRef, key, diag) {
1343
1485
  const parsed = parseAuthRef(authRef);
1344
1486
  if (!parsed || parsed.kind === "env" || parsed.kind === "none") return false;
1345
1487
  return withCredentialMutationLock(authRef, async () => {
1488
+ const cacheKey = storedCredentialAuthRef(parsed);
1489
+ clearOAuthCredentialCache(cacheKey);
1346
1490
  const written = await writeStoredCredential(parsed, key, diag);
1347
1491
  if (!written) return false;
1348
1492
  const readBack = await readStoredCredential(parsed, diag);
1349
- if (readBack === key) return true;
1493
+ if (readBack === key) {
1494
+ const oauth = parseStoredOAuthCredential(key);
1495
+ const oauthProviderId = oauthProviderIdFromAccount(parsed.account);
1496
+ if (oauth && oauthProviderId) {
1497
+ cacheOAuthCredential(oauthCredentialStateKey(oauthProviderId, cacheKey), oauth);
1498
+ }
1499
+ return true;
1500
+ }
1350
1501
  diag?.("credential store read-back verification failed");
1351
1502
  return false;
1352
1503
  });
@@ -1379,7 +1530,10 @@ async function probeProviderCredentialStore(authRef, diag) {
1379
1530
  async function deleteProviderCredential(authRef, diag) {
1380
1531
  const parsed = parseAuthRef(authRef);
1381
1532
  if (!parsed || parsed.kind === "env" || parsed.kind === "none") return false;
1382
- return withCredentialMutationLock(authRef, () => deleteStoredCredential(parsed, diag));
1533
+ return withCredentialMutationLock(authRef, () => {
1534
+ clearOAuthCredentialCache(storedCredentialAuthRef(parsed));
1535
+ return deleteStoredCredential(parsed, diag);
1536
+ });
1383
1537
  }
1384
1538
 
1385
1539
  // src/first-run.ts
@@ -1393,6 +1547,7 @@ import {
1393
1547
  closeSync as closeSync2,
1394
1548
  copyFileSync,
1395
1549
  existsSync,
1550
+ fsyncSync as fsyncSync2,
1396
1551
  mkdirSync as mkdirSync2,
1397
1552
  openSync as openSync2,
1398
1553
  readFileSync as readFileSync3,
@@ -1442,9 +1597,18 @@ function ensureSecureAppHome() {
1442
1597
  function writeSecureFile(path, content) {
1443
1598
  ensureSecureAppHome();
1444
1599
  mkdirSync2(dirname2(path), { recursive: true, mode: DIR_MODE });
1445
- const fd = openSync2(path, "w", FILE_MODE);
1600
+ const fd = openSync2(path, "wx", FILE_MODE);
1446
1601
  try {
1447
- writeSync(fd, content);
1602
+ const payload = Buffer.from(content);
1603
+ let offset = 0;
1604
+ while (offset < payload.length) {
1605
+ const written = writeSync(fd, payload, offset, payload.length - offset);
1606
+ if (written <= 0) {
1607
+ throw new Error(`Could not complete secure file write: ${path}`);
1608
+ }
1609
+ offset += written;
1610
+ }
1611
+ fsyncSync2(fd);
1448
1612
  } finally {
1449
1613
  closeSync2(fd);
1450
1614
  }
@@ -1453,6 +1617,18 @@ function writeSecureFile(path, content) {
1453
1617
  } catch {
1454
1618
  }
1455
1619
  }
1620
+ function syncParentDirectory(path) {
1621
+ let fd;
1622
+ try {
1623
+ fd = openSync2(dirname2(path), "r");
1624
+ fsyncSync2(fd);
1625
+ } catch (error) {
1626
+ const code = error.code;
1627
+ if (code !== "EINVAL" && code !== "ENOTSUP" && code !== "EPERM") throw error;
1628
+ } finally {
1629
+ if (fd !== void 0) closeSync2(fd);
1630
+ }
1631
+ }
1456
1632
  function parseProvider(raw) {
1457
1633
  if (!raw || typeof raw !== "object") return null;
1458
1634
  const p13 = raw;
@@ -1491,6 +1667,34 @@ function parseProvider(raw) {
1491
1667
  }
1492
1668
  return provider;
1493
1669
  }
1670
+ function hasOwn(record, key) {
1671
+ return Object.prototype.hasOwnProperty.call(record, key);
1672
+ }
1673
+ function hasValidStrictProviderFields(raw) {
1674
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return false;
1675
+ const provider = raw;
1676
+ if (hasOwn(provider, "subscriptionFilter") && provider.subscriptionFilter !== "free") {
1677
+ return false;
1678
+ }
1679
+ if (hasOwn(provider, "authType") && provider.authType !== "api" && provider.authType !== "oauth" && provider.authType !== "none") {
1680
+ return false;
1681
+ }
1682
+ if (hasOwn(provider, "refreshedAt") && typeof provider.refreshedAt !== "string") {
1683
+ return false;
1684
+ }
1685
+ if (hasOwn(provider, "modelsCache")) {
1686
+ const cache = provider.modelsCache;
1687
+ if (!cache || typeof cache !== "object" || Array.isArray(cache)) return false;
1688
+ const fields = cache;
1689
+ if (typeof fields.fetchedAt !== "string" || !Array.isArray(fields.models)) {
1690
+ return false;
1691
+ }
1692
+ if (fields.models.some((model) => !model || typeof model !== "object" || Array.isArray(model))) {
1693
+ return false;
1694
+ }
1695
+ }
1696
+ return true;
1697
+ }
1494
1698
  function parseRegistry(raw) {
1495
1699
  const empty = { schemaVersion: REGISTRY_SCHEMA_VERSION, providers: [] };
1496
1700
  if (!raw || typeof raw !== "object") return empty;
@@ -1510,6 +1714,27 @@ function parseRegistry(raw) {
1510
1714
  if (typeof data.pricingCacheAt === "string") registry.pricingCacheAt = data.pricingCacheAt;
1511
1715
  return registry;
1512
1716
  }
1717
+ function parseRegistryStrict(raw) {
1718
+ if (!raw || typeof raw !== "object") {
1719
+ throw new Error("Provider registry must be a JSON object.");
1720
+ }
1721
+ const data = raw;
1722
+ if (data.schemaVersion !== REGISTRY_SCHEMA_VERSION) {
1723
+ throw new Error("Provider registry has an unsupported schema version.");
1724
+ }
1725
+ if (!Array.isArray(data.providers)) {
1726
+ throw new Error("Provider registry is missing its providers list.");
1727
+ }
1728
+ for (const entry of data.providers) {
1729
+ if (!parseProvider(entry) || !hasValidStrictProviderFields(entry)) {
1730
+ throw new Error("Provider registry contains an invalid provider entry.");
1731
+ }
1732
+ }
1733
+ return parseRegistry(raw);
1734
+ }
1735
+ function readRegistryStrict(path) {
1736
+ return parseRegistryStrict(JSON.parse(readFileSync3(path, "utf8")));
1737
+ }
1513
1738
  function loadRegistry(path = getProvidersPath()) {
1514
1739
  ensureLegacyAppHomeMigrated();
1515
1740
  if (!existsSync(path)) {
@@ -1523,7 +1748,7 @@ function loadRegistry(path = getProvidersPath()) {
1523
1748
  try {
1524
1749
  withRegistryWriteLockSync(() => {
1525
1750
  if (!existsSync(path)) return;
1526
- const current = parseRegistry(JSON.parse(readFileSync3(path, "utf8")));
1751
+ const current = readRegistryStrict(path);
1527
1752
  if (migrateOAuthOpenAiProvider(current)) saveRegistry(current, path);
1528
1753
  }, { lockPath: `${path}.lock` });
1529
1754
  } catch {
@@ -1534,6 +1759,15 @@ function loadRegistry(path = getProvidersPath()) {
1534
1759
  return { schemaVersion: REGISTRY_SCHEMA_VERSION, providers: [] };
1535
1760
  }
1536
1761
  }
1762
+ function loadRegistryStrict(path = getProvidersPath()) {
1763
+ ensureLegacyAppHomeMigrated();
1764
+ if (!existsSync(path)) {
1765
+ return { schemaVersion: REGISTRY_SCHEMA_VERSION, providers: [] };
1766
+ }
1767
+ const registry = readRegistryStrict(path);
1768
+ migrateOAuthOpenAiProvider(registry);
1769
+ return registry;
1770
+ }
1537
1771
  function saveRegistry(registry, path = getProvidersPath()) {
1538
1772
  assertRegistryWriteOwnership(path);
1539
1773
  const payload = `${JSON.stringify(registry, null, 2)}
@@ -1550,6 +1784,7 @@ function saveRegistry(registry, path = getProvidersPath()) {
1550
1784
  writeSecureFile(tmp, payload);
1551
1785
  assertRegistryWriteOwnership(path);
1552
1786
  renameSync(tmp, path);
1787
+ syncParentDirectory(path);
1553
1788
  } finally {
1554
1789
  try {
1555
1790
  unlinkSync2(tmp);
@@ -2308,7 +2543,7 @@ function enrichPricingAsync(onComplete) {
2308
2543
  const fetched = await fetchPricingCache();
2309
2544
  const cache = fetched ?? loadPricingCache();
2310
2545
  const changed = await withRegistryWriteLock(() => {
2311
- const registry = loadRegistry();
2546
+ const registry = loadRegistryStrict();
2312
2547
  const updated = applyPricingToRegistryProviders(registry, cache);
2313
2548
  if (updated) saveRegistry(registry);
2314
2549
  return updated;
@@ -2795,6 +3030,7 @@ function localProvidersToServerModels(localProviders) {
2795
3030
  npm: model.modelFormat === "openai" ? model.npm || "@ai-sdk/openai-compatible" : model.npm,
2796
3031
  apiBaseUrl: model.apiBaseUrl,
2797
3032
  apiKey: provider.apiKey,
3033
+ authRef: provider.authRef,
2798
3034
  authType: provider.authType,
2799
3035
  oauthAccountId: provider.oauthAccountId,
2800
3036
  contextWindow: model.contextWindow,
@@ -2809,11 +3045,14 @@ function localProvidersToServerModels(localProviders) {
2809
3045
  );
2810
3046
  }
2811
3047
 
3048
+ // src/registry/add-template.ts
3049
+ import { randomUUID as randomUUID6 } from "crypto";
3050
+
2812
3051
  // src/provider-factory.ts
2813
3052
  import { wrapLanguageModel, extractReasoningMiddleware } from "ai";
2814
3053
 
2815
3054
  // src/oauth/responses-websocket.ts
2816
- import { createHash as createHash3 } from "crypto";
3055
+ import { createHash as createHash4 } from "crypto";
2817
3056
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
2818
3057
 
2819
3058
  // src/outbound-proxy.ts
@@ -2877,6 +3116,27 @@ async function outboundWsProxyAgent(wsUrl) {
2877
3116
 
2878
3117
  // src/upstream-error.ts
2879
3118
  import { APICallError, RetryError } from "ai";
3119
+ var DEFAULT_RETRY_AFTER_SECONDS = 5;
3120
+ var MAX_RETRY_AFTER_SECONDS = 60;
3121
+ function clampRetryAfterSeconds(value) {
3122
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
3123
+ return DEFAULT_RETRY_AFTER_SECONDS;
3124
+ }
3125
+ return Math.min(Math.round(value), MAX_RETRY_AFTER_SECONDS);
3126
+ }
3127
+ function numericRetryAfterSeconds(inner) {
3128
+ const data = inner.data;
3129
+ const fromBody = data?.error?.retry_after_seconds;
3130
+ if (typeof fromBody === "number" && Number.isFinite(fromBody) && fromBody >= 0) return fromBody;
3131
+ const fromHeader = inner.responseHeaders?.["retry-after"];
3132
+ if (typeof fromHeader === "string" && /^\d+$/.test(fromHeader.trim())) return Number(fromHeader.trim());
3133
+ for (const message of [data?.error?.message, inner.message]) {
3134
+ if (typeof message !== "string") continue;
3135
+ const match = /retry after (\d+)s\b/i.exec(message);
3136
+ if (match) return Number(match[1]);
3137
+ }
3138
+ return void 0;
3139
+ }
2880
3140
  function sdkUpstreamErrorDetails(err) {
2881
3141
  const retry = RetryError.isInstance(err) ? err : void 0;
2882
3142
  const inner = retry?.lastError ?? err;
@@ -2888,11 +3148,14 @@ function sdkUpstreamErrorDetails(err) {
2888
3148
  } catch {
2889
3149
  }
2890
3150
  }
3151
+ const rawRetryAfter = inner.statusCode === 429 ? numericRetryAfterSeconds(inner) : void 0;
3152
+ const retryAfterSeconds = rawRetryAfter === void 0 ? void 0 : clampRetryAfterSeconds(rawRetryAfter);
2891
3153
  return {
2892
3154
  statusCode: inner.statusCode,
2893
3155
  errorContent: errorContent || inner.message,
2894
3156
  isRetryable: inner.isRetryable,
2895
- attemptCount: retry?.errors.length ?? 1
3157
+ attemptCount: retry?.errors.length ?? 1,
3158
+ ...retryAfterSeconds !== void 0 ? { retryAfterSeconds } : {}
2896
3159
  };
2897
3160
  }
2898
3161
  function isContextLengthExceededError(err, formattedMessage = "") {
@@ -3057,7 +3320,7 @@ function hasResponsesLiteHeader(headers) {
3057
3320
  }
3058
3321
  function authorizationHeaderFingerprint(headers) {
3059
3322
  const authorization = Object.entries(headers).find(([key]) => key.toLowerCase() === "authorization")?.[1];
3060
- return authorization ? createHash3("sha256").update(authorization).digest("hex") : "";
3323
+ return authorization ? createHash4("sha256").update(authorization).digest("hex") : "";
3061
3324
  }
3062
3325
  function bodyToString(body) {
3063
3326
  if (body == null) return "";
@@ -3090,13 +3353,13 @@ function responsesWebSocketPromptFingerprint(payload) {
3090
3353
  delete stable.previous_response_id;
3091
3354
  delete stable.stream;
3092
3355
  delete stable.background;
3093
- return createHash3("sha256").update(canonicalJson(stable)).digest("hex");
3356
+ return createHash4("sha256").update(canonicalJson(stable)).digest("hex");
3094
3357
  }
3095
3358
  function responsesWebSocketPromptFieldHashes(payload) {
3096
3359
  const hashes = {};
3097
3360
  for (const key of Object.keys(payload).sort()) {
3098
3361
  if (key === "input" || key === "previous_response_id" || key === "stream" || key === "background") continue;
3099
- hashes[key] = createHash3("sha256").update(canonicalJson(payload[key])).digest("hex").slice(0, 12);
3362
+ hashes[key] = createHash4("sha256").update(canonicalJson(payload[key])).digest("hex").slice(0, 12);
3100
3363
  }
3101
3364
  return hashes;
3102
3365
  }
@@ -3132,7 +3395,7 @@ function responsesWebSocketPartitionKey(wsUrl, payload, options = {}, authorizat
3132
3395
  promptCacheKey,
3133
3396
  authorizationFingerprint
3134
3397
  ].join("");
3135
- return createHash3("sha256").update(material).digest("hex");
3398
+ return createHash4("sha256").update(material).digest("hex");
3136
3399
  }
3137
3400
  function inputArray(payload) {
3138
3401
  return Array.isArray(payload.input) ? payload.input : [];
@@ -3163,7 +3426,7 @@ function conversationItemKind(value) {
3163
3426
  return "object";
3164
3427
  }
3165
3428
  function conversationItemHash(value) {
3166
- return createHash3("sha256").update(canonicalJson(normalizeToolCallJson(value))).digest("hex").slice(0, 16);
3429
+ return createHash4("sha256").update(canonicalJson(normalizeToolCallJson(value))).digest("hex").slice(0, 16);
3167
3430
  }
3168
3431
  function continuationMismatchDetails(entry, payload) {
3169
3432
  const full = inputArray(payload);
@@ -3229,7 +3492,7 @@ function diagnosticTextFingerprint(field, value) {
3229
3492
  if (typeof value !== "string" || value.length === 0) return {};
3230
3493
  return {
3231
3494
  [`${field}Bytes`]: Buffer.byteLength(value),
3232
- [`${field}Hash`]: createHash3("sha256").update(value).digest("hex").slice(0, 16)
3495
+ [`${field}Hash`]: createHash4("sha256").update(value).digest("hex").slice(0, 16)
3233
3496
  };
3234
3497
  }
3235
3498
  function responseFailureDetails(event) {
@@ -3264,7 +3527,7 @@ function emitResponseErrorDiagnostic(entry, ctx, details) {
3264
3527
  emitContextDiagnostic(entry, ctx, { event: "ws_response_error", ...details });
3265
3528
  }
3266
3529
  function diagnosticItemIdHash(value) {
3267
- return typeof value === "string" && value.length > 0 ? createHash3("sha256").update(value).digest("hex").slice(0, 16) : void 0;
3530
+ return typeof value === "string" && value.length > 0 ? createHash4("sha256").update(value).digest("hex").slice(0, 16) : void 0;
3268
3531
  }
3269
3532
  function reasoningPartIndex(value) {
3270
3533
  return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
@@ -3499,7 +3762,7 @@ function deleteEntry(entry, closeSocket = true) {
3499
3762
  }
3500
3763
  }
3501
3764
  }
3502
- function failContext(entry, ctx, message, diagnosticDetails, statusCode) {
3765
+ function failContext(entry, ctx, message, diagnosticDetails, statusCode, retryAfterSeconds) {
3503
3766
  if (ctx.closed || entry.current !== ctx) return;
3504
3767
  entry.debug(`fail: ${message}`);
3505
3768
  emitResponseErrorDiagnostic(entry, ctx, {
@@ -3514,12 +3777,64 @@ function failContext(entry, ctx, message, diagnosticDetails, statusCode) {
3514
3777
  type: statusCode === void 0 ? "transport_error" : anthropicErrorType(statusCode),
3515
3778
  code: statusCode === void 0 ? "websocket_transport_error" : String(statusCode),
3516
3779
  message,
3517
- param: null
3780
+ param: null,
3781
+ ...retryAfterSeconds !== void 0 ? { retry_after_seconds: retryAfterSeconds } : {}
3518
3782
  }
3519
3783
  });
3520
3784
  deleteEntry(entry);
3521
3785
  closeContext(ctx);
3522
3786
  }
3787
+ function retryTransportFailure(entry, ctx, diagnosticDetails) {
3788
+ if (ctx.closed || entry.current !== ctx || ctx.retried || ctx.frameCount !== 0 || ctx.emittedModelData) {
3789
+ return false;
3790
+ }
3791
+ ctx.retried = true;
3792
+ ctx.transportRetryPending = true;
3793
+ entry.debug("transport failed before any response frame; retrying once with full context");
3794
+ emitContextDiagnostic(entry, ctx, {
3795
+ event: "ws_transport_retry",
3796
+ outcome: "started",
3797
+ ...diagnosticDetails
3798
+ });
3799
+ deleteEntry(entry);
3800
+ if (ctx.closed) {
3801
+ ctx.transportRetryPending = false;
3802
+ entry.debug("transport retry cancelled before replacement");
3803
+ emitContextDiagnostic(entry, ctx, {
3804
+ event: "ws_transport_retry",
3805
+ outcome: "cancelled"
3806
+ });
3807
+ return true;
3808
+ }
3809
+ resetContextForRetry(ctx);
3810
+ const replacement = ctx.createReplacement();
3811
+ if (ctx.closed) {
3812
+ ctx.transportRetryPending = false;
3813
+ deleteEntry(replacement);
3814
+ replacement.debug("transport retry cancelled while creating replacement");
3815
+ emitContextDiagnostic(replacement, ctx, {
3816
+ event: "ws_transport_retry",
3817
+ outcome: "cancelled"
3818
+ });
3819
+ return true;
3820
+ }
3821
+ dispatchContext(replacement, ctx);
3822
+ return true;
3823
+ }
3824
+ function handleTransportFailure(entry, ctx, message, diagnosticDetails) {
3825
+ if (retryTransportFailure(entry, ctx, diagnosticDetails)) return;
3826
+ if (ctx.closed || entry.current !== ctx) return;
3827
+ if (ctx.retried && ctx.frameCount === 0 && !ctx.emittedModelData) {
3828
+ ctx.transportRetryPending = false;
3829
+ entry.debug("transport retry exhausted before any response frame");
3830
+ emitContextDiagnostic(entry, ctx, {
3831
+ event: "ws_transport_retry",
3832
+ outcome: "exhausted",
3833
+ ...diagnosticDetails
3834
+ });
3835
+ }
3836
+ failContext(entry, ctx, message, diagnosticDetails);
3837
+ }
3523
3838
  function cleanupExpiredConnections(now) {
3524
3839
  const evictions = [];
3525
3840
  for (const entry of connectionEntries()) {
@@ -3567,7 +3882,27 @@ function sendContext(entry, ctx) {
3567
3882
  entry.debug(
3568
3883
  `connection=${entry.debugId} key=${debugKey(entry.key)} sending ${outgoing.length}B payload` + (ctx.continued ? " (continuation)" : "")
3569
3884
  );
3570
- entry.socket.send(outgoing);
3885
+ try {
3886
+ entry.socket.send(outgoing, (error) => {
3887
+ if (!error) return;
3888
+ handleTransportFailure(entry, ctx, error.message, {
3889
+ source: "socket_send",
3890
+ failureMode: "callback",
3891
+ socketErrorName: boundedDiagnosticIdentifier(error.name),
3892
+ socketErrorCode: boundedDiagnosticIdentifier(error.code),
3893
+ ...diagnosticTextFingerprint("errorMessage", error.message)
3894
+ });
3895
+ });
3896
+ } catch (error) {
3897
+ const failure = error instanceof Error ? error : new Error("WebSocket send failed");
3898
+ handleTransportFailure(entry, ctx, failure.message, {
3899
+ source: "socket_send",
3900
+ failureMode: "synchronous",
3901
+ socketErrorName: boundedDiagnosticIdentifier(failure.name),
3902
+ socketErrorCode: boundedDiagnosticIdentifier(failure.code),
3903
+ ...diagnosticTextFingerprint("errorMessage", failure.message)
3904
+ });
3905
+ }
3571
3906
  }
3572
3907
  function dispatchContext(entry, ctx) {
3573
3908
  const now = entry.options.now();
@@ -3600,6 +3935,14 @@ function handleSocketMessage(entry, data) {
3600
3935
  if (!ctx || ctx.closed) return;
3601
3936
  const text4 = Array.isArray(data) ? Buffer.concat(data).toString("utf8") : data.toString("utf8");
3602
3937
  ctx.frameCount += 1;
3938
+ if (ctx.transportRetryPending) {
3939
+ ctx.transportRetryPending = false;
3940
+ entry.debug("transport retry received its first response frame");
3941
+ emitContextDiagnostic(entry, ctx, {
3942
+ event: "ws_transport_retry",
3943
+ outcome: "recovered"
3944
+ });
3945
+ }
3603
3946
  let event;
3604
3947
  try {
3605
3948
  event = JSON.parse(text4);
@@ -3674,6 +4017,10 @@ function handleSocketMessage(entry, data) {
3674
4017
  closeContext(ctx);
3675
4018
  }
3676
4019
  }
4020
+ function numericRetryAfterHeader(value) {
4021
+ const single = Array.isArray(value) ? value[0] : value;
4022
+ return typeof single === "string" && /^\d+$/.test(single.trim()) ? Number(single.trim()) : void 0;
4023
+ }
3677
4024
  function createConnection(WebSocket, wsUrl, headers, persistent, key, options, debug, agent) {
3678
4025
  const now = options.now();
3679
4026
  const socket = new WebSocket(wsUrl, agent ? { headers, agent } : { headers });
@@ -3707,24 +4054,39 @@ function createConnection(WebSocket, wsUrl, headers, persistent, key, options, d
3707
4054
  debug(`unexpected-response status=${statusCode}`);
3708
4055
  response.resume();
3709
4056
  const ctx = entry.current;
3710
- if (ctx && !ctx.closed) {
3711
- failContext(entry, ctx, `WebSocket upgrade failed (HTTP ${statusCode})`, {
3712
- source: "unexpected_response",
3713
- httpStatusCode: statusCode
3714
- }, statusCode);
3715
- } else {
4057
+ if (!ctx || ctx.closed) {
3716
4058
  deleteEntry(entry);
4059
+ return;
3717
4060
  }
4061
+ if (statusCode === 403) {
4062
+ const retryAfterSeconds = clampRetryAfterSeconds(
4063
+ numericRetryAfterHeader(response.headers["retry-after"])
4064
+ );
4065
+ failContext(entry, ctx, `OpenAI edge throttled the Responses WebSocket upgrade (HTTP 403); retry after ${retryAfterSeconds}s`, {
4066
+ source: "unexpected_response",
4067
+ httpStatusCode: statusCode,
4068
+ mappedStatusCode: 429,
4069
+ retryAfterSeconds
4070
+ }, 429, retryAfterSeconds);
4071
+ return;
4072
+ }
4073
+ failContext(entry, ctx, `WebSocket upgrade failed (HTTP ${statusCode})`, {
4074
+ source: "unexpected_response",
4075
+ httpStatusCode: statusCode
4076
+ }, statusCode);
3718
4077
  });
3719
4078
  socket.on("message", (data) => handleSocketMessage(entry, data));
3720
4079
  socket.on("error", (error) => {
3721
4080
  const ctx = entry.current;
3722
- if (ctx) failContext(entry, ctx, error.message, {
3723
- source: "socket_error",
3724
- socketErrorName: boundedDiagnosticIdentifier(error.name),
3725
- socketErrorCode: boundedDiagnosticIdentifier(error.code)
3726
- });
3727
- else deleteEntry(entry);
4081
+ if (ctx) {
4082
+ const details = {
4083
+ source: "socket_error",
4084
+ socketErrorName: boundedDiagnosticIdentifier(error.name),
4085
+ socketErrorCode: boundedDiagnosticIdentifier(error.code),
4086
+ ...diagnosticTextFingerprint("errorMessage", error.message)
4087
+ };
4088
+ handleTransportFailure(entry, ctx, error.message, details);
4089
+ } else deleteEntry(entry);
3728
4090
  });
3729
4091
  socket.on("close", (code, reason) => {
3730
4092
  entry.open = false;
@@ -3733,7 +4095,7 @@ function createConnection(WebSocket, wsUrl, headers, persistent, key, options, d
3733
4095
  if (ctx && !ctx.closed) {
3734
4096
  const reasonText = reason?.length ? reason.toString("utf8") : "";
3735
4097
  const suffix = reasonText ? `: ${reasonText}` : "";
3736
- failContext(entry, ctx, `WebSocket closed (${code})${suffix}`, {
4098
+ handleTransportFailure(entry, ctx, `WebSocket closed (${code})${suffix}`, {
3737
4099
  source: "socket_close",
3738
4100
  closeCode: code,
3739
4101
  ...diagnosticTextFingerprint("closeReason", reasonText)
@@ -3851,7 +4213,7 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
3851
4213
  keyTuple: {
3852
4214
  wsUrl,
3853
4215
  providerId: options.providerId ?? "openai",
3854
- accountIdHash: options.accountId ? createHash3("sha256").update(options.accountId).digest("hex").slice(0, 16) : "",
4216
+ accountIdHash: options.accountId ? createHash4("sha256").update(options.accountId).digest("hex").slice(0, 16) : "",
3855
4217
  model: typeof payload.model === "string" ? payload.model : void 0,
3856
4218
  effort: typeof payload.reasoning?.effort === "string" ? String(payload.reasoning.effort).trim().toLowerCase() : "",
3857
4219
  promptCacheKey: typeof payload.prompt_cache_key === "string" ? payload.prompt_cache_key : void 0
@@ -3908,6 +4270,7 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
3908
4270
  frameCount: 0,
3909
4271
  pendingEvents: [],
3910
4272
  emittedModelData: false,
4273
+ transportRetryPending: false,
3911
4274
  outputByIndex: /* @__PURE__ */ new Map(),
3912
4275
  outputIndexByItemId: /* @__PURE__ */ new Map(),
3913
4276
  reasoningPartsByItemId: /* @__PURE__ */ new Map(),
@@ -3918,7 +4281,7 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
3918
4281
  WebSocket,
3919
4282
  wsUrl,
3920
4283
  headers,
3921
- Boolean(partitionKey),
4284
+ persistent,
3922
4285
  partitionKey,
3923
4286
  resolvedOptions,
3924
4287
  debug,
@@ -3966,7 +4329,7 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
3966
4329
  }
3967
4330
 
3968
4331
  // src/oauth/claude-identity.ts
3969
- import { createHash as createHash4, randomUUID as randomUUID4 } from "crypto";
4332
+ import { createHash as createHash5, randomUUID as randomUUID4 } from "crypto";
3970
4333
  var CLAUDE_CODE_CLI_VERSION = "2.1.195";
3971
4334
  var CLAUDE_CODE_USER_AGENT = `claude-cli/${CLAUDE_CODE_CLI_VERSION} (external, cli)`;
3972
4335
  var CLAUDE_CODE_ENTRYPOINT = process.env.CLAUDE_CODE_ENTRYPOINT ?? "cli";
@@ -3981,7 +4344,7 @@ function getOrCreateSessionId(seed) {
3981
4344
  return id;
3982
4345
  }
3983
4346
  function uuidFromHash(input) {
3984
- const h = createHash4("sha256").update(input).digest("hex");
4347
+ const h = createHash5("sha256").update(input).digest("hex");
3985
4348
  return [
3986
4349
  h.slice(0, 8),
3987
4350
  h.slice(8, 12),
@@ -3995,7 +4358,7 @@ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3995
4358
  function resolveCliUserID(providerData, seed) {
3996
4359
  const v = providerData?.cliUserID;
3997
4360
  if (typeof v === "string" && HEX64_RE.test(v)) return v;
3998
- return createHash4("sha256").update(`cliUserID:${seed}`).digest("hex");
4361
+ return createHash5("sha256").update(`cliUserID:${seed}`).digest("hex");
3999
4362
  }
4000
4363
  function resolveAccountUUID(providerData, seed) {
4001
4364
  const v = providerData?.accountUUID;
@@ -4717,20 +5080,331 @@ function thinkingProviderOptions(npm) {
4717
5080
  return void 0;
4718
5081
  }
4719
5082
 
4720
- // src/trace-log.ts
5083
+ // src/registry/credential-cleanup-journal.ts
5084
+ import { randomUUID as randomUUID5 } from "crypto";
4721
5085
  import {
4722
- chmodSync as chmodSync4,
5086
+ closeSync as closeSync3,
4723
5087
  existsSync as existsSync4,
5088
+ fstatSync as fstatSync2,
5089
+ fsyncSync as fsyncSync3,
5090
+ lstatSync,
4724
5091
  mkdirSync as mkdirSync5,
5092
+ openSync as openSync3,
4725
5093
  readFileSync as readFileSync6,
5094
+ renameSync as renameSync2,
4726
5095
  unlinkSync as unlinkSync3,
4727
5096
  writeFileSync as writeFileSync4
4728
5097
  } from "fs";
4729
- import { createHash as createHash5 } from "crypto";
4730
- import { join as join4 } from "path";
4731
- import pc2 from "picocolors";
5098
+ import { dirname as dirname5 } from "path";
5099
+ var JOURNAL_SCHEMA_VERSION = 1;
4732
5100
  var DIR_MODE2 = 448;
4733
5101
  var FILE_MODE4 = 384;
5102
+ var MAX_JOURNAL_BYTES = 1024 * 1024;
5103
+ var MAX_PENDING_CREDENTIAL_DELETES = 1024;
5104
+ var MAX_CREDENTIAL_REF_BYTES = 4096;
5105
+ var CREDENTIAL_INSTANCE_SEPARATOR = "::credential::";
5106
+ var CREDENTIAL_INSTANCE_PATTERN = /^v1:[0-9a-f]{32}$/;
5107
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
5108
+ function emptyJournal() {
5109
+ return {
5110
+ schemaVersion: JOURNAL_SCHEMA_VERSION,
5111
+ pendingCredentialDeletes: []
5112
+ };
5113
+ }
5114
+ function isStoredCredentialRef(value) {
5115
+ const parsed = parseAuthRef(value);
5116
+ return (parsed?.kind === "keyring" || parsed?.kind === "helper") && isManagedCredentialAccount(parsed.account);
5117
+ }
5118
+ function credentialAccountBase(account) {
5119
+ const separatorIndex = account.lastIndexOf(CREDENTIAL_INSTANCE_SEPARATOR);
5120
+ if (separatorIndex === -1) return account;
5121
+ if (separatorIndex === 0 || account.indexOf(CREDENTIAL_INSTANCE_SEPARATOR) !== separatorIndex || !CREDENTIAL_INSTANCE_PATTERN.test(
5122
+ account.slice(separatorIndex + CREDENTIAL_INSTANCE_SEPARATOR.length)
5123
+ )) {
5124
+ return null;
5125
+ }
5126
+ return account.slice(0, separatorIndex);
5127
+ }
5128
+ function isManagedCredentialAccount(account) {
5129
+ const base = credentialAccountBase(account);
5130
+ if (!base) return false;
5131
+ const oauth = /^oauth:provider:(.+)$/.exec(base);
5132
+ if (oauth) return isValidProviderId(oauth[1]);
5133
+ const provider = /^provider:([^:]+)(?::(.+))?$/.exec(base);
5134
+ if (!provider || !isValidProviderId(provider[1])) return false;
5135
+ const suffix = provider[2];
5136
+ if (!suffix) return true;
5137
+ if (UUID_PATTERN.test(suffix)) return true;
5138
+ return suffix.startsWith("replacement:") && UUID_PATTERN.test(suffix.slice("replacement:".length));
5139
+ }
5140
+ function normalizePendingCredentialDeletes(raw) {
5141
+ if (raw.length > MAX_PENDING_CREDENTIAL_DELETES) {
5142
+ throw new Error("Credential cleanup journal contains too many pending entries.");
5143
+ }
5144
+ const pending = [];
5145
+ for (const [index, value] of raw.entries()) {
5146
+ if (typeof value !== "string" || Buffer.byteLength(value) > MAX_CREDENTIAL_REF_BYTES || !isStoredCredentialRef(value)) {
5147
+ throw new Error(`Credential cleanup journal has an invalid entry at index ${index}.`);
5148
+ }
5149
+ if (!pending.includes(value)) pending.push(value);
5150
+ }
5151
+ return pending;
5152
+ }
5153
+ function parseJournal(raw) {
5154
+ if (!raw || typeof raw !== "object") {
5155
+ throw new Error("Credential cleanup journal must be a JSON object.");
5156
+ }
5157
+ const data = raw;
5158
+ if (data.schemaVersion !== JOURNAL_SCHEMA_VERSION) {
5159
+ throw new Error("Unsupported credential cleanup journal schema.");
5160
+ }
5161
+ if (!Array.isArray(data.pendingCredentialDeletes)) {
5162
+ throw new Error("Credential cleanup journal is missing its pending list.");
5163
+ }
5164
+ return {
5165
+ schemaVersion: JOURNAL_SCHEMA_VERSION,
5166
+ pendingCredentialDeletes: normalizePendingCredentialDeletes(
5167
+ data.pendingCredentialDeletes
5168
+ )
5169
+ };
5170
+ }
5171
+ function readJournalUnlocked(path) {
5172
+ if (!existsSync4(path)) return emptyJournal();
5173
+ let fd;
5174
+ try {
5175
+ const before = lstatSync(path);
5176
+ if (before.isSymbolicLink() || !before.isFile()) {
5177
+ throw new Error("Credential cleanup journal must be a regular file.");
5178
+ }
5179
+ fd = openSync3(path, "r");
5180
+ const opened = fstatSync2(fd);
5181
+ if (before.dev !== opened.dev || before.ino !== opened.ino) {
5182
+ throw new Error("Credential cleanup journal changed while opening.");
5183
+ }
5184
+ if (typeof process.getuid === "function") {
5185
+ if (opened.uid !== process.getuid()) {
5186
+ throw new Error("Credential cleanup journal is owned by another user.");
5187
+ }
5188
+ if ((opened.mode & 63) !== 0) {
5189
+ throw new Error("Credential cleanup journal permissions are too broad.");
5190
+ }
5191
+ }
5192
+ if (opened.size > MAX_JOURNAL_BYTES) {
5193
+ throw new Error("Credential cleanup journal is too large.");
5194
+ }
5195
+ return parseJournal(JSON.parse(readFileSync6(fd, "utf8")));
5196
+ } catch (error) {
5197
+ const message = error instanceof Error ? error.message : String(error);
5198
+ throw new Error(`Could not read credential cleanup journal: ${message}`);
5199
+ } finally {
5200
+ if (fd !== void 0) closeSync3(fd);
5201
+ }
5202
+ }
5203
+ function syncParentDirectory2(path) {
5204
+ let fd;
5205
+ try {
5206
+ fd = openSync3(dirname5(path), "r");
5207
+ fsyncSync3(fd);
5208
+ } catch (error) {
5209
+ const code = error.code;
5210
+ if (code !== "EINVAL" && code !== "ENOTSUP" && code !== "EPERM") throw error;
5211
+ } finally {
5212
+ if (fd !== void 0) closeSync3(fd);
5213
+ }
5214
+ }
5215
+ function writeJournalUnlocked(journal, path) {
5216
+ assertRegistryWriteOwnership(path);
5217
+ ensureSecureAppHome();
5218
+ mkdirSync5(dirname5(path), { recursive: true, mode: DIR_MODE2 });
5219
+ const tmp = `${path}.${process.pid}.${randomUUID5()}.tmp`;
5220
+ let fd;
5221
+ try {
5222
+ fd = openSync3(tmp, "wx", FILE_MODE4);
5223
+ writeFileSync4(fd, `${JSON.stringify(journal, null, 2)}
5224
+ `);
5225
+ fsyncSync3(fd);
5226
+ closeSync3(fd);
5227
+ fd = void 0;
5228
+ assertRegistryWriteOwnership(path);
5229
+ renameSync2(tmp, path);
5230
+ syncParentDirectory2(path);
5231
+ } finally {
5232
+ if (fd !== void 0) closeSync3(fd);
5233
+ try {
5234
+ unlinkSync3(tmp);
5235
+ } catch (error) {
5236
+ if (error.code !== "ENOENT") throw error;
5237
+ }
5238
+ }
5239
+ }
5240
+ function journalLockPath(path) {
5241
+ return `${path}.lock`;
5242
+ }
5243
+ async function loadPendingCredentialDeletes(path = getCredentialCleanupPath()) {
5244
+ ensureLegacyAppHomeMigrated();
5245
+ return withRegistryWriteLock(
5246
+ () => [...readJournalUnlocked(path).pendingCredentialDeletes],
5247
+ { lockPath: journalLockPath(path) }
5248
+ );
5249
+ }
5250
+ async function updatePendingCredentialDeletes(update, path = getCredentialCleanupPath()) {
5251
+ ensureLegacyAppHomeMigrated();
5252
+ return withRegistryWriteLock(() => {
5253
+ const journal = readJournalUnlocked(path);
5254
+ const before = [...journal.pendingCredentialDeletes];
5255
+ const after = normalizePendingCredentialDeletes(update(before));
5256
+ if (after.length !== before.length || after.some((value, index) => value !== before[index])) {
5257
+ writeJournalUnlocked(
5258
+ {
5259
+ schemaVersion: JOURNAL_SCHEMA_VERSION,
5260
+ pendingCredentialDeletes: after
5261
+ },
5262
+ path
5263
+ );
5264
+ }
5265
+ return { before, after };
5266
+ }, { lockPath: journalLockPath(path) });
5267
+ }
5268
+ async function queueCredentialDelete(authRef) {
5269
+ if (!isStoredCredentialRef(authRef)) return false;
5270
+ const result = await updatePendingCredentialDeletes((pending) => pending.includes(authRef) ? pending : [...pending, authRef]);
5271
+ return result.after.includes(authRef);
5272
+ }
5273
+ async function cancelCredentialDelete(authRef) {
5274
+ const result = await updatePendingCredentialDeletes((pending) => pending.filter((candidate) => candidate !== authRef));
5275
+ return result.before.includes(authRef) && !result.after.includes(authRef);
5276
+ }
5277
+
5278
+ // src/registry/credential-lifecycle.ts
5279
+ function errorMessage(error) {
5280
+ return error instanceof Error ? error.message : String(error);
5281
+ }
5282
+ function appendError(errors, context, error) {
5283
+ errors.push(`${context}: ${errorMessage(error)}`);
5284
+ }
5285
+ function credentialIsReferenced(registry, authRef) {
5286
+ return registry.providers.some((provider) => provider.authRef === authRef);
5287
+ }
5288
+ async function journalCredentialWrite(authRef) {
5289
+ if (!await queueCredentialDelete(authRef)) {
5290
+ throw new Error("Credential reference is not managed by Clodex.");
5291
+ }
5292
+ }
5293
+ async function reconcilePendingCredentialDelete(authRef) {
5294
+ if (!isStoredCredentialRef(authRef)) {
5295
+ try {
5296
+ await cancelCredentialDelete(authRef);
5297
+ return { deleted: false, cleared: true };
5298
+ } catch (error) {
5299
+ return {
5300
+ deleted: false,
5301
+ cleared: false,
5302
+ persistenceError: errorMessage(error)
5303
+ };
5304
+ }
5305
+ }
5306
+ try {
5307
+ return await withCredentialMutationLock(authRef, async () => {
5308
+ try {
5309
+ const clearedReferencedMarker = await withRegistryWriteLock(async () => {
5310
+ if (!credentialIsReferenced(loadRegistryStrict(), authRef)) return false;
5311
+ await cancelCredentialDelete(authRef);
5312
+ return true;
5313
+ });
5314
+ if (clearedReferencedMarker) {
5315
+ return { deleted: false, cleared: true };
5316
+ }
5317
+ } catch (error) {
5318
+ return {
5319
+ deleted: false,
5320
+ cleared: false,
5321
+ persistenceError: errorMessage(error)
5322
+ };
5323
+ }
5324
+ let deleted = false;
5325
+ try {
5326
+ deleted = await deleteProviderCredential(authRef);
5327
+ } catch {
5328
+ deleted = false;
5329
+ }
5330
+ if (!deleted) return { deleted: false, cleared: false };
5331
+ try {
5332
+ await cancelCredentialDelete(authRef);
5333
+ return { deleted: true, cleared: true };
5334
+ } catch (error) {
5335
+ return {
5336
+ deleted: true,
5337
+ cleared: false,
5338
+ persistenceError: errorMessage(error)
5339
+ };
5340
+ }
5341
+ });
5342
+ } catch (error) {
5343
+ return {
5344
+ deleted: false,
5345
+ cleared: false,
5346
+ persistenceError: errorMessage(error)
5347
+ };
5348
+ }
5349
+ }
5350
+ async function reconcilePendingCredentialDeletes() {
5351
+ let queued;
5352
+ try {
5353
+ queued = await loadPendingCredentialDeletes();
5354
+ } catch (error) {
5355
+ return {
5356
+ deleted: [],
5357
+ pending: [],
5358
+ persistenceError: `Could not read pending credential cleanup: ${errorMessage(error)}`
5359
+ };
5360
+ }
5361
+ const knownPending = new Set(queued);
5362
+ const deleted = [];
5363
+ const errors = [];
5364
+ for (const authRef of queued) {
5365
+ let result;
5366
+ try {
5367
+ result = await reconcilePendingCredentialDelete(authRef);
5368
+ } catch (error) {
5369
+ result = {
5370
+ deleted: false,
5371
+ cleared: false,
5372
+ persistenceError: errorMessage(error)
5373
+ };
5374
+ }
5375
+ if (result.deleted) deleted.push(authRef);
5376
+ if (result.cleared) knownPending.delete(authRef);
5377
+ if (result.persistenceError) {
5378
+ appendError(errors, `Cleanup for ${authRef}`, result.persistenceError);
5379
+ }
5380
+ }
5381
+ let pending = [...knownPending];
5382
+ try {
5383
+ pending = await loadPendingCredentialDeletes();
5384
+ } catch (error) {
5385
+ appendError(errors, "Could not confirm pending credential cleanup", error);
5386
+ }
5387
+ return {
5388
+ deleted,
5389
+ pending,
5390
+ ...errors.length > 0 ? { persistenceError: errors.join("; ") } : {}
5391
+ };
5392
+ }
5393
+
5394
+ // src/trace-log.ts
5395
+ import {
5396
+ chmodSync as chmodSync4,
5397
+ existsSync as existsSync5,
5398
+ mkdirSync as mkdirSync6,
5399
+ readFileSync as readFileSync7,
5400
+ unlinkSync as unlinkSync4,
5401
+ writeFileSync as writeFileSync5
5402
+ } from "fs";
5403
+ import { createHash as createHash6 } from "crypto";
5404
+ import { join as join4 } from "path";
5405
+ import pc2 from "picocolors";
5406
+ var DIR_MODE3 = 448;
5407
+ var FILE_MODE5 = 384;
4734
5408
  var CLAUDE_DEBUG_LOG = "claude-debug.log";
4735
5409
  var PROXY_DEBUG_LOG = "proxy-debug.log";
4736
5410
  var PROVIDER_DEBUG_LOG = "provider-debug.log";
@@ -4744,9 +5418,9 @@ function safeClaudeSessionId(value) {
4744
5418
  }
4745
5419
  function ensureLogsDir() {
4746
5420
  const dir = getLogsPath();
4747
- mkdirSync5(dir, { recursive: true, mode: DIR_MODE2 });
5421
+ mkdirSync6(dir, { recursive: true, mode: DIR_MODE3 });
4748
5422
  try {
4749
- chmodSync4(dir, DIR_MODE2);
5423
+ chmodSync4(dir, DIR_MODE3);
4750
5424
  } catch {
4751
5425
  }
4752
5426
  return dir;
@@ -4769,9 +5443,9 @@ function getInferenceRequestLogPath() {
4769
5443
  }
4770
5444
  function getSessionLogPath(label = "session", extension = "log") {
4771
5445
  const dir = join4(ensureLogsDir(), INFERENCE_SESSION_DIR);
4772
- mkdirSync5(dir, { recursive: true, mode: DIR_MODE2 });
5446
+ mkdirSync6(dir, { recursive: true, mode: DIR_MODE3 });
4773
5447
  try {
4774
- chmodSync4(dir, DIR_MODE2);
5448
+ chmodSync4(dir, DIR_MODE3);
4775
5449
  } catch {
4776
5450
  }
4777
5451
  const safeLabel = label.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "proxy";
@@ -4852,7 +5526,7 @@ function canonicalDiagnosticValue(value) {
4852
5526
  );
4853
5527
  }
4854
5528
  function diagnosticHash(value) {
4855
- return createHash5("sha256").update(JSON.stringify(canonicalDiagnosticValue(value)) ?? "undefined").digest("hex").slice(0, 16);
5529
+ return createHash6("sha256").update(JSON.stringify(canonicalDiagnosticValue(value)) ?? "undefined").digest("hex").slice(0, 16);
4856
5530
  }
4857
5531
  function diagnosticBytes(value) {
4858
5532
  return Buffer.byteLength(JSON.stringify(value) ?? "");
@@ -5031,9 +5705,9 @@ function makeTraceLogger(logPath) {
5031
5705
  }
5032
5706
  function resetTraceLog(path) {
5033
5707
  ensureLogsDir();
5034
- if (existsSync4(path)) {
5708
+ if (existsSync5(path)) {
5035
5709
  try {
5036
- unlinkSync3(path);
5710
+ unlinkSync4(path);
5037
5711
  } catch {
5038
5712
  }
5039
5713
  }
@@ -5063,15 +5737,15 @@ function writeSecureLogLine(path, line) {
5063
5737
  ensureLogsDir();
5064
5738
  const redacted = redactTraceLine(line);
5065
5739
  try {
5066
- writeFileSync4(path, `${redacted}
5067
- `, { flag: "a", mode: FILE_MODE4 });
5068
- chmodSync4(path, FILE_MODE4);
5740
+ writeFileSync5(path, `${redacted}
5741
+ `, { flag: "a", mode: FILE_MODE5 });
5742
+ chmodSync4(path, FILE_MODE5);
5069
5743
  } catch {
5070
5744
  }
5071
5745
  }
5072
5746
  function printTraceLog(debugLogPath) {
5073
- if (!existsSync4(debugLogPath)) return;
5074
- const raw = readFileSync6(debugLogPath, "utf8");
5747
+ if (!existsSync5(debugLogPath)) return;
5748
+ const raw = readFileSync7(debugLogPath, "utf8");
5075
5749
  const log12 = redactTraceLog(raw);
5076
5750
  const errorLines = log12.split("\n").filter(
5077
5751
  (l) => l.includes("error") || l.includes("Error") || l.includes('"type":"error"') || l.includes("status") || l.includes("resolveModel failed") || l.includes("resolveModel fallback")
@@ -5309,19 +5983,22 @@ async function addProviderFromTemplate(template, apiKey, opts) {
5309
5983
  if (!trimmedKey && !template.apiKeyOptional) {
5310
5984
  return { added: false, error: "API key cannot be empty." };
5311
5985
  }
5312
- const existingError = await withRegistryWriteLock(() => {
5313
- const registry = loadRegistry();
5986
+ const existingState = await withRegistryWriteLock(() => {
5987
+ const registry = loadRegistryStrict();
5314
5988
  const existing = registry.providers.find((p13) => p13.id === template.id);
5315
5989
  if (existing && !opts?.replaceExisting) {
5316
5990
  return {
5317
- added: false,
5318
- error: `${template.name} is already configured.`,
5319
- hint: `Remove it first with: clodex providers remove ${template.id}`
5991
+ existing: false,
5992
+ error: {
5993
+ added: false,
5994
+ error: `${template.name} is already configured.`,
5995
+ hint: `Remove it first with: clodex providers remove ${template.id}`
5996
+ }
5320
5997
  };
5321
5998
  }
5322
- return null;
5999
+ return { existing: existing !== void 0, error: null };
5323
6000
  });
5324
- if (existingError) return existingError;
6001
+ if (existingState.error) return existingState.error;
5325
6002
  const fetched = await fetchTemplateModels(template, trimmedKey, opts?.baseUrl);
5326
6003
  if (fetched.error || fetched.models.length === 0) {
5327
6004
  return {
@@ -5345,9 +6022,11 @@ async function addProviderFromTemplate(template, apiKey, opts) {
5345
6022
  buildPricingIndex(pricingCache),
5346
6023
  platform
5347
6024
  );
5348
- const authRef = trimmedKey ? credentialAuthRef(`provider:${template.id}`) : "none:anonymous";
5349
- const commitProvider = () => withRegistryWriteLock(() => {
5350
- const registry = loadRegistry();
6025
+ const authRef = trimmedKey ? credentialAuthRef(
6026
+ existingState.existing ? `provider:${template.id}:replacement:${randomUUID6()}` : `provider:${template.id}`
6027
+ ) : "none:anonymous";
6028
+ const commitProvider = () => withRegistryWriteLock(async () => {
6029
+ const registry = loadRegistryStrict();
5351
6030
  const existing = registry.providers.find((p13) => p13.id === template.id);
5352
6031
  if (existing && !opts?.replaceExisting) {
5353
6032
  return {
@@ -5382,12 +6061,28 @@ async function addProviderFromTemplate(template, apiKey, opts) {
5382
6061
  } else {
5383
6062
  registry.providers.push(entry);
5384
6063
  }
6064
+ if (existing?.authRef && existing.authRef !== authRef) {
6065
+ await queueCredentialDelete(existing.authRef);
6066
+ }
5385
6067
  saveRegistry(registry);
5386
- return { added: true, provider: entry, modelCount: pricedModels.length };
6068
+ let credentialCleanupPending = false;
6069
+ if (trimmedKey) {
6070
+ try {
6071
+ await cancelCredentialDelete(authRef);
6072
+ } catch {
6073
+ credentialCleanupPending = true;
6074
+ }
6075
+ }
6076
+ return {
6077
+ added: true,
6078
+ provider: entry,
6079
+ modelCount: pricedModels.length,
6080
+ ...credentialCleanupPending ? { credentialCleanupPending: true } : {}
6081
+ };
5387
6082
  });
5388
6083
  const result = trimmedKey ? await withCredentialMutationLock(authRef, async () => {
5389
- const commitError = await withRegistryWriteLock(() => {
5390
- const registry = loadRegistry();
6084
+ const prepareError = await withRegistryWriteLock(() => {
6085
+ const registry = loadRegistryStrict();
5391
6086
  const existing = registry.providers.find((p13) => p13.id === template.id);
5392
6087
  if (existing && !opts?.replaceExisting) {
5393
6088
  return {
@@ -5398,7 +6093,8 @@ async function addProviderFromTemplate(template, apiKey, opts) {
5398
6093
  }
5399
6094
  return null;
5400
6095
  });
5401
- if (commitError) return commitError;
6096
+ if (prepareError) return prepareError;
6097
+ await journalCredentialWrite(authRef);
5402
6098
  const saved = await saveProviderCredential(authRef, trimmedKey);
5403
6099
  if (!saved) {
5404
6100
  return {
@@ -5409,20 +6105,30 @@ async function addProviderFromTemplate(template, apiKey, opts) {
5409
6105
  }
5410
6106
  return commitProvider();
5411
6107
  }) : await commitProvider();
6108
+ if (result.added) {
6109
+ try {
6110
+ const cleanup = await reconcilePendingCredentialDeletes();
6111
+ result.credentialCleanupPending = cleanup.pending.length > 0 || cleanup.persistenceError !== void 0;
6112
+ } catch {
6113
+ result.credentialCleanupPending = true;
6114
+ }
6115
+ } else {
6116
+ try {
6117
+ const cleanup = await reconcilePendingCredentialDeletes();
6118
+ result.credentialCleanupPending = cleanup.pending.length > 0 || cleanup.persistenceError !== void 0;
6119
+ } catch {
6120
+ result.credentialCleanupPending = true;
6121
+ }
6122
+ }
6123
+ result.credentialCleanupReconciled = true;
5412
6124
  if (result.added) enrichPricingAsync();
5413
6125
  return result;
5414
6126
  }
5415
6127
 
5416
6128
  // src/registry/crud.ts
5417
- function credentialStillReferenced(authRef, remaining) {
5418
- return remaining.some((p13) => p13.authRef === authRef);
5419
- }
5420
- function isStoredCredentialRef(authRef) {
5421
- return authRef.startsWith("keyring:") || authRef.startsWith("helper:");
5422
- }
5423
6129
  async function removeProviderFromRegistry(id, opts) {
5424
- const removal = await withRegistryWriteLock(() => {
5425
- const registry = loadRegistry();
6130
+ const removal = await withRegistryWriteLock(async () => {
6131
+ const registry = loadRegistryStrict();
5426
6132
  const index = registry.providers.findIndex((p13) => p13.id === id);
5427
6133
  if (index < 0) {
5428
6134
  return {
@@ -5432,10 +6138,11 @@ async function removeProviderFromRegistry(id, opts) {
5432
6138
  credentialDeleted: false,
5433
6139
  error: `Provider not found: ${id}`
5434
6140
  },
5435
- authRefToDelete: null
6141
+ authRef: null
5436
6142
  };
5437
6143
  }
5438
6144
  const [removedProvider] = registry.providers.splice(index, 1);
6145
+ const cleanupQueued = opts?.deleteCredential !== false ? await queueCredentialDelete(removedProvider.authRef) : false;
5439
6146
  saveRegistry(registry);
5440
6147
  return {
5441
6148
  result: {
@@ -5444,30 +6151,24 @@ async function removeProviderFromRegistry(id, opts) {
5444
6151
  name: removedProvider.name,
5445
6152
  credentialDeleted: false
5446
6153
  },
5447
- authRefToDelete: opts?.deleteCredential !== false && isStoredCredentialRef(removedProvider.authRef) && !credentialStillReferenced(removedProvider.authRef, registry.providers) ? removedProvider.authRef : null
6154
+ authRef: cleanupQueued ? removedProvider.authRef : null
5448
6155
  };
5449
6156
  });
5450
- const authRefToDelete = removal.authRefToDelete;
5451
- if (authRefToDelete) {
5452
- await withCredentialMutationLock(authRefToDelete, async () => {
5453
- const referencedAgain = await withRegistryWriteLock(() => credentialStillReferenced(
5454
- authRefToDelete,
5455
- loadRegistry().providers
5456
- ));
5457
- if (referencedAgain) return;
5458
- removal.result.credentialDeleted = await deleteProviderCredential(
5459
- authRefToDelete
5460
- );
5461
- if (!removal.result.credentialDeleted) {
5462
- removal.result.error = `Provider ${removal.result.name ?? id} was removed, but credential cleanup failed for ${authRefToDelete}. The credential remains in the configured store and must be removed manually.`;
5463
- }
5464
- });
6157
+ if (removal.authRef) {
6158
+ try {
6159
+ const cleanup = await reconcilePendingCredentialDeletes();
6160
+ removal.result.credentialDeleted = cleanup.deleted.includes(removal.authRef);
6161
+ removal.result.credentialCleanupPending = cleanup.pending.includes(removal.authRef) || cleanup.persistenceError !== void 0;
6162
+ } catch {
6163
+ removal.result.credentialCleanupPending = true;
6164
+ }
6165
+ removal.result.credentialCleanupReconciled = true;
5465
6166
  }
5466
6167
  return removal.result;
5467
6168
  }
5468
6169
  function toggleProviderEnabled(id) {
5469
6170
  return withRegistryWriteLockSync(() => {
5470
- const registry = loadRegistry();
6171
+ const registry = loadRegistryStrict();
5471
6172
  const provider = registry.providers.find((p13) => p13.id === id);
5472
6173
  if (!provider) return { toggled: false, error: `Provider not found: ${id}` };
5473
6174
  provider.enabled = !provider.enabled;
@@ -5480,7 +6181,7 @@ function toggleProviderEnabled(id) {
5480
6181
  import { isDeepStrictEqual } from "util";
5481
6182
 
5482
6183
  // src/registry/custom-endpoint.ts
5483
- import { randomUUID as randomUUID5 } from "crypto";
6184
+ import { randomUUID as randomUUID7 } from "crypto";
5484
6185
 
5485
6186
  // src/registry/url-security.ts
5486
6187
  import { lookup } from "dns/promises";
@@ -6007,7 +6708,7 @@ function providerDiscoveryInputsMatch(current, started) {
6007
6708
  return current.authRef === started.authRef && current.authType === started.authType && current.templateId === started.templateId && isDeepStrictEqual(current.api, started.api);
6008
6709
  }
6009
6710
  async function refreshProviderModels(providerId, apiKey, registry) {
6010
- const workingRegistry = registry ?? loadRegistry();
6711
+ const workingRegistry = registry ?? loadRegistryStrict();
6011
6712
  const provider = workingRegistry.providers.find((p13) => p13.id === providerId);
6012
6713
  if (!provider) {
6013
6714
  return { id: providerId, name: providerId, ok: false, reason: "Provider not found." };
@@ -6099,7 +6800,7 @@ async function refreshProviderModels(providerId, apiKey, registry) {
6099
6800
  const platform = pricingPlatformForProvider(provider.templateId, provider.id);
6100
6801
  const enriched = enrichModelsWithPricing(models, buildPricingIndex(pricingCache), platform);
6101
6802
  await withRegistryWriteLock(() => {
6102
- const currentRegistry = loadRegistry();
6803
+ const currentRegistry = loadRegistryStrict();
6103
6804
  const currentProvider = currentRegistry.providers.find((candidate) => candidate.id === providerId);
6104
6805
  if (!currentProvider) throw new Error("Provider was removed while models were refreshing.");
6105
6806
  if (currentProvider.authRef !== provider.authRef) {
@@ -6131,7 +6832,7 @@ async function refreshProviderModels(providerId, apiKey, registry) {
6131
6832
  }
6132
6833
  async function refreshAllProviderModels(resolveKey) {
6133
6834
  const refreshed = [];
6134
- const registry = loadRegistry();
6835
+ const registry = loadRegistryStrict();
6135
6836
  const enabledProviders = registry.providers.filter((p13) => p13.enabled);
6136
6837
  for (const provider of enabledProviders) {
6137
6838
  const key = await resolveRefreshCredential(provider, resolveKey);
@@ -6177,41 +6878,83 @@ function oauthDisplayName(registryId, fallbackName) {
6177
6878
  if (registryId === "openai-oauth") return "OpenAI (ChatGPT)";
6178
6879
  return fallbackName;
6179
6880
  }
6180
- async function upsertOAuthProvider(providerId, cred, authRef) {
6181
- return withRegistryWriteLock(() => {
6881
+ async function persistOAuthProvider(providerId, cred, authRef) {
6882
+ const registryProvider = await withCredentialMutationLock(authRef, async () => {
6182
6883
  const registryId = toOAuthRegistryId(providerId);
6183
6884
  const templateId = providerId.replace(/-oauth$/, "") || providerId;
6184
- const registry = loadRegistry();
6185
- const template = getTemplateById(templateId);
6186
- let entry = registry.providers.find((pr) => pr.id === registryId);
6187
- if (!entry) {
6188
- if (!template) {
6885
+ await withRegistryWriteLock(() => {
6886
+ const registry = loadRegistryStrict();
6887
+ const previousEntry = registry.providers.find((provider) => provider.id === registryId);
6888
+ if (!previousEntry && !getTemplateById(templateId)) {
6889
+ throw new Error(`Provider "${providerId}" is not in your registry and has no template`);
6890
+ }
6891
+ });
6892
+ await journalCredentialWrite(authRef);
6893
+ let diagMsg = "";
6894
+ const saved = await saveProviderCredential(
6895
+ authRef,
6896
+ oauthCredentialToKeychainJson(cred),
6897
+ (msg) => {
6898
+ diagMsg = msg;
6899
+ }
6900
+ );
6901
+ if (!saved) {
6902
+ throw new Error(`Could not save OAuth tokens to the credential store${diagMsg ? ` \u2014 ${diagMsg}` : " \u2014 check access and try again"}`);
6903
+ }
6904
+ const committed = await withRegistryWriteLock(async () => {
6905
+ const registry = loadRegistryStrict();
6906
+ const template = getTemplateById(templateId);
6907
+ const previousEntry = registry.providers.find((provider) => provider.id === registryId);
6908
+ if (!previousEntry && !template) {
6189
6909
  throw new Error(`Provider "${providerId}" is not in your registry and has no template`);
6190
6910
  }
6191
- const displayName = oauthDisplayName(registryId, template.name);
6192
- entry = {
6193
- id: registryId,
6194
- templateId,
6195
- name: displayName,
6196
- enabled: true,
6197
- authRef,
6198
- authType: "oauth",
6199
- api: {
6200
- npm: template.npm,
6201
- url: template.defaultBaseUrl ?? "",
6202
- ...template.headers ? { headers: template.headers } : {}
6203
- },
6204
- addedAt: (/* @__PURE__ */ new Date()).toISOString()
6205
- };
6206
- } else {
6207
- entry = { ...entry, authType: "oauth", authRef, templateId };
6208
- }
6209
- const idx = registry.providers.findIndex((pr) => pr.id === registryId);
6210
- if (idx >= 0) registry.providers[idx] = entry;
6211
- else registry.providers.push(entry);
6212
- saveRegistry(registry);
6213
- return entry;
6911
+ let entry;
6912
+ if (!previousEntry) {
6913
+ if (!template) throw new Error(`Provider "${providerId}" has no template`);
6914
+ const displayName = oauthDisplayName(registryId, template.name);
6915
+ entry = {
6916
+ id: registryId,
6917
+ templateId,
6918
+ name: displayName,
6919
+ enabled: true,
6920
+ authRef,
6921
+ authType: "oauth",
6922
+ api: {
6923
+ npm: template.npm,
6924
+ url: template.defaultBaseUrl ?? "",
6925
+ ...template.headers ? { headers: template.headers } : {}
6926
+ },
6927
+ addedAt: (/* @__PURE__ */ new Date()).toISOString()
6928
+ };
6929
+ } else {
6930
+ entry = { ...previousEntry, authType: "oauth", authRef, templateId };
6931
+ }
6932
+ const idx = registry.providers.findIndex((provider) => provider.id === registryId);
6933
+ if (idx >= 0) registry.providers[idx] = entry;
6934
+ else registry.providers.push(entry);
6935
+ if (previousEntry?.authRef && previousEntry.authRef !== authRef) {
6936
+ await queueCredentialDelete(previousEntry.authRef);
6937
+ }
6938
+ saveRegistry(registry);
6939
+ try {
6940
+ await cancelCredentialDelete(authRef);
6941
+ } catch {
6942
+ }
6943
+ return entry;
6944
+ });
6945
+ return committed;
6214
6946
  });
6947
+ let credentialCleanupPending = true;
6948
+ try {
6949
+ const cleanup = await reconcilePendingCredentialDeletes();
6950
+ credentialCleanupPending = cleanup.pending.length > 0 || cleanup.persistenceError !== void 0;
6951
+ } catch {
6952
+ credentialCleanupPending = true;
6953
+ }
6954
+ return {
6955
+ registryProvider,
6956
+ credentialCleanupPending
6957
+ };
6215
6958
  }
6216
6959
  async function authenticateProvider(providerId, _options = {}) {
6217
6960
  const registryId = toOAuthRegistryId(providerId);
@@ -6229,28 +6972,7 @@ async function authenticateProvider(providerId, _options = {}) {
6229
6972
  );
6230
6973
  }
6231
6974
  const cred = await runNativeDeviceCode(providerId);
6232
- const persisted = await withCredentialMutationLock(authRef, async () => {
6233
- let nativeDiagMsg = "";
6234
- const saved = await saveProviderCredential(
6235
- authRef,
6236
- oauthCredentialToKeychainJson(cred),
6237
- (msg) => {
6238
- nativeDiagMsg = msg;
6239
- }
6240
- );
6241
- if (!saved) {
6242
- throw new Error(
6243
- `Could not save OAuth tokens to the credential store${nativeDiagMsg ? ` \u2014 ${nativeDiagMsg}` : " \u2014 check access and try again"}`
6244
- );
6245
- }
6246
- const registryProvider2 = await upsertOAuthProvider(
6247
- providerId,
6248
- cred,
6249
- authRef
6250
- );
6251
- return { registryProvider: registryProvider2 };
6252
- });
6253
- const { registryProvider } = persisted;
6975
+ const persisted = await persistOAuthProvider(providerId, cred, authRef);
6254
6976
  const refreshSpinner = p2.spinner();
6255
6977
  refreshSpinner.start("Refreshing model list...");
6256
6978
  try {
@@ -6259,7 +6981,12 @@ async function authenticateProvider(providerId, _options = {}) {
6259
6981
  } catch {
6260
6982
  refreshSpinner.stop("Could not refresh models \u2014 run clodex providers refresh-models later");
6261
6983
  }
6262
- return { providerId: registryId, credential: cred, registryProvider };
6984
+ return {
6985
+ providerId: registryId,
6986
+ credential: cred,
6987
+ registryProvider: persisted.registryProvider,
6988
+ credentialCleanupPending: persisted.credentialCleanupPending
6989
+ };
6263
6990
  }
6264
6991
  function providerAuthHelpText() {
6265
6992
  return `${pc3.bold("clodex providers auth")} \u2014 sign in with OAuth
@@ -6569,6 +7296,39 @@ async function pickLocalModel(provider, conflicts, prefs) {
6569
7296
  }
6570
7297
 
6571
7298
  // src/providers-command.ts
7299
+ var CREDENTIAL_CLEANUP_PENDING_MESSAGE = "Credential cleanup is pending and will be retried by the next provider command.";
7300
+ function reportCredentialCleanup(pending, state, reconciled = false) {
7301
+ if (state) {
7302
+ state.reconciled ||= reconciled;
7303
+ state.pending ||= pending;
7304
+ return;
7305
+ }
7306
+ if (pending) {
7307
+ p4.log.warn(CREDENTIAL_CLEANUP_PENDING_MESSAGE);
7308
+ }
7309
+ }
7310
+ async function reconcileCredentialCleanup() {
7311
+ try {
7312
+ const cleanup = await reconcilePendingCredentialDeletes();
7313
+ return cleanup.pending.length > 0 || cleanup.persistenceError !== void 0;
7314
+ } catch {
7315
+ return true;
7316
+ }
7317
+ }
7318
+ async function runWithCredentialCleanup(run) {
7319
+ const state = {
7320
+ reconciled: false,
7321
+ pending: false
7322
+ };
7323
+ try {
7324
+ return await run(state);
7325
+ } finally {
7326
+ if (!state.reconciled) {
7327
+ state.pending = await reconcileCredentialCleanup();
7328
+ }
7329
+ reportCredentialCleanup(state.pending);
7330
+ }
7331
+ }
6572
7332
  function parseProvidersArgs(args) {
6573
7333
  if (args.length === 0) return { subcommand: "hub", showHelp: false };
6574
7334
  const [first, ...rest] = args;
@@ -6632,10 +7392,11 @@ ${pc5.bold("Subcommands:")}
6632
7392
  function providerLabel(name, modelCount, enabled) {
6633
7393
  return `${fmtEnabledStar(enabled)} ${fmtProvider(name)} ${pc5.dim(`(${modelCount} model${modelCount === 1 ? "" : "s"})`)}`;
6634
7394
  }
6635
- async function runProvidersAuth(providerId, method) {
7395
+ async function runProvidersAuthWithCleanupState(providerId, method, cleanupState) {
6636
7396
  try {
6637
7397
  const result = await authenticateProvider(providerId, { method });
6638
7398
  p4.log.success(`Signed in to ${result.registryProvider.name} \u2014 credential saved to the credential store.`);
7399
+ reportCredentialCleanup(result.credentialCleanupPending, cleanupState, true);
6639
7400
  return 0;
6640
7401
  } catch (err) {
6641
7402
  if (err instanceof Error && err.message === "Cancelled") {
@@ -6646,6 +7407,9 @@ async function runProvidersAuth(providerId, method) {
6646
7407
  return 1;
6647
7408
  }
6648
7409
  }
7410
+ async function runProvidersAuth(providerId, method) {
7411
+ return runProvidersAuthWithCleanupState(providerId, method);
7412
+ }
6649
7413
  async function runProvidersRefreshModels(providerId) {
6650
7414
  const resolveKey = async (provider) => resolveProviderCredential(provider.id, provider.authRef);
6651
7415
  if (providerId) {
@@ -6723,7 +7487,7 @@ async function runProvidersList() {
6723
7487
  console.log("");
6724
7488
  return 0;
6725
7489
  }
6726
- async function runTemplateAddFlow() {
7490
+ async function runTemplateAddFlow(cleanupState) {
6727
7491
  const registry = loadRegistry();
6728
7492
  const configuredIds = registry.providers.map((p13) => p13.id);
6729
7493
  const template = listAddableTemplates(configuredIds).find((t) => t.id === "openai") ?? getTemplateById("openai");
@@ -6749,6 +7513,11 @@ async function runTemplateAddFlow() {
6749
7513
  spinner5.start(`Testing connection to ${template.name}...`);
6750
7514
  const result = await addProviderFromTemplate(template, apiKey);
6751
7515
  spinner5.stop("");
7516
+ reportCredentialCleanup(
7517
+ result.credentialCleanupPending === true,
7518
+ cleanupState,
7519
+ result.credentialCleanupReconciled === true
7520
+ );
6752
7521
  if (!result.added) {
6753
7522
  p4.log.error(result.error ?? "Could not add provider.");
6754
7523
  if (result.hint) p4.log.info(result.hint);
@@ -6757,7 +7526,7 @@ async function runTemplateAddFlow() {
6757
7526
  logConnected(template.name, result.modelCount ?? 0);
6758
7527
  return 0;
6759
7528
  }
6760
- async function runProvidersAdd() {
7529
+ async function runProvidersAddWithCleanupState(cleanupState) {
6761
7530
  const choice = await p4.select({
6762
7531
  message: "Add a provider",
6763
7532
  options: [
@@ -6777,11 +7546,16 @@ async function runProvidersAdd() {
6777
7546
  p4.cancel("Cancelled.");
6778
7547
  return 0;
6779
7548
  }
6780
- if (choice === "oauth") return runProvidersAuth("openai");
6781
- if (choice === "apikey") return runTemplateAddFlow();
7549
+ if (choice === "oauth") {
7550
+ return runProvidersAuthWithCleanupState("openai", void 0, cleanupState);
7551
+ }
7552
+ if (choice === "apikey") return runTemplateAddFlow(cleanupState);
6782
7553
  return 0;
6783
7554
  }
6784
- async function runProvidersRemove(id, interactive = false) {
7555
+ async function runProvidersAdd() {
7556
+ return runProvidersAddWithCleanupState();
7557
+ }
7558
+ async function runProvidersRemoveWithCleanupState(id, interactive = false, cleanupState) {
6785
7559
  const registry = loadRegistry();
6786
7560
  const provider = registry.providers.find((pr) => pr.id === id);
6787
7561
  if (!provider) {
@@ -6799,6 +7573,11 @@ async function runProvidersRemove(id, interactive = false) {
6799
7573
  }
6800
7574
  }
6801
7575
  const result = await removeProviderFromRegistry(id);
7576
+ reportCredentialCleanup(
7577
+ result.credentialCleanupPending === true,
7578
+ cleanupState,
7579
+ result.credentialCleanupReconciled === true
7580
+ );
6802
7581
  if (!result.removed) {
6803
7582
  p4.log.error(result.error ?? `Could not remove ${id}`);
6804
7583
  return 1;
@@ -6813,6 +7592,9 @@ async function runProvidersRemove(id, interactive = false) {
6813
7592
  }
6814
7593
  return 0;
6815
7594
  }
7595
+ async function runProvidersRemove(id, interactive = false) {
7596
+ return runProvidersRemoveWithCleanupState(id, interactive);
7597
+ }
6816
7598
  function providerHubChoiceValue(entry) {
6817
7599
  return `provider:${entry.id}`;
6818
7600
  }
@@ -6946,16 +7728,24 @@ async function runProvidersCommand(args) {
6946
7728
  console.log(providersHelpText());
6947
7729
  return 0;
6948
7730
  }
7731
+ const reconcilesDuringMutation = parsed.subcommand === "add" || parsed.subcommand === "remove" || parsed.subcommand === "auth" && !parsed.showHelp && parsed.removeId !== void 0;
7732
+ if (!reconcilesDuringMutation) {
7733
+ reportCredentialCleanup(await reconcileCredentialCleanup());
7734
+ }
6949
7735
  if (parsed.subcommand === "list") return runProvidersList();
6950
- if (parsed.subcommand === "add") return runProvidersAdd();
6951
- if (parsed.subcommand === "remove" && parsed.removeId) return runProvidersRemove(parsed.removeId);
7736
+ if (parsed.subcommand === "add") {
7737
+ return runWithCredentialCleanup((state) => runProvidersAddWithCleanupState(state));
7738
+ }
7739
+ if (parsed.subcommand === "remove" && parsed.removeId) {
7740
+ return runWithCredentialCleanup((state) => runProvidersRemoveWithCleanupState(parsed.removeId, false, state));
7741
+ }
6952
7742
  if (parsed.subcommand === "refresh-models") return runProvidersRefreshModels(parsed.removeId);
6953
7743
  if (parsed.subcommand === "auth") {
6954
7744
  if (parsed.showHelp || !parsed.removeId) {
6955
7745
  console.log(providerAuthHelpText());
6956
7746
  return 0;
6957
7747
  }
6958
- return runProvidersAuth(parsed.removeId, parsed.authMethod);
7748
+ return runWithCredentialCleanup((state) => runProvidersAuthWithCleanupState(parsed.removeId, parsed.authMethod, state));
6959
7749
  }
6960
7750
  relayIntro("Your OpenAI providers");
6961
7751
  return runProvidersHub();
@@ -6996,7 +7786,7 @@ async function runFirstRunWizard(_trace = false) {
6996
7786
 
6997
7787
  // src/proxy.ts
6998
7788
  import { createServer } from "http";
6999
- import { appendFileSync, openSync as openSync3, writeSync as writeSync2, closeSync as closeSync3 } from "fs";
7789
+ import { appendFileSync, openSync as openSync4, writeSync as writeSync2, closeSync as closeSync4 } from "fs";
7000
7790
 
7001
7791
  // src/http-utils.ts
7002
7792
  import * as zlib from "zlib";
@@ -7073,6 +7863,12 @@ function localModelToRoute(lp, model) {
7073
7863
  baseURL: model.apiBaseUrl,
7074
7864
  providerId: lp.id,
7075
7865
  authType: lp.authType,
7866
+ refreshToken: lp.authType === "oauth" && lp.authRef ? (rejectedAccessToken) => rejectedAccessToken === void 0 ? resolveProviderCredential(lp.id, lp.authRef) : resolveProviderCredential(
7867
+ lp.id,
7868
+ lp.authRef,
7869
+ void 0,
7870
+ { rejectedAccessToken }
7871
+ ) : void 0,
7076
7872
  oauthAccountId: lp.oauthAccountId,
7077
7873
  providerData: lp.providerData,
7078
7874
  headers: lp.headers,
@@ -7374,14 +8170,29 @@ var UpstreamUnreachableError = class extends Error {
7374
8170
  this.name = "UpstreamUnreachableError";
7375
8171
  }
7376
8172
  };
8173
+ async function resolveOAuthRetryReplacement(enabled, status, attempt, headersSent, apiKey, refreshToken) {
8174
+ if (!enabled || status !== 401 || attempt !== 0 || headersSent || !refreshToken) {
8175
+ return null;
8176
+ }
8177
+ const replacement = await refreshToken(apiKey).catch(() => null);
8178
+ return replacement && replacement !== apiKey ? replacement : null;
8179
+ }
7377
8180
  async function fetchWithOAuthRetry(apiKey, request3, refreshToken) {
7378
8181
  let response = await request3(apiKey);
7379
- if (response.status !== 401 || !refreshToken) {
8182
+ const refreshed = await resolveOAuthRetryReplacement(
8183
+ true,
8184
+ response.status,
8185
+ 0,
8186
+ false,
8187
+ apiKey,
8188
+ refreshToken
8189
+ );
8190
+ if (!refreshed) {
7380
8191
  return { response, apiKey, refreshed: false };
7381
8192
  }
7382
- const refreshed = await refreshToken().catch(() => null);
7383
- if (!refreshed || refreshed === apiKey) {
7384
- return { response, apiKey, refreshed: false };
8193
+ try {
8194
+ await response.body?.cancel?.();
8195
+ } catch {
7385
8196
  }
7386
8197
  response = await request3(refreshed);
7387
8198
  return { response, apiKey: refreshed, refreshed: true };
@@ -7446,10 +8257,10 @@ async function relayAnthropicMessages(res, messagesUrl, body, apiKey, clientWant
7446
8257
  }
7447
8258
 
7448
8259
  // src/proxy.ts
7449
- import { randomUUID as randomUUID6 } from "crypto";
8260
+ import { randomUUID as randomUUID8 } from "crypto";
7450
8261
 
7451
8262
  // src/sdk-adapter.ts
7452
- import { createHash as createHash6 } from "crypto";
8263
+ import { createHash as createHash7 } from "crypto";
7453
8264
  import { streamText, generateText, tool, jsonSchema } from "ai";
7454
8265
 
7455
8266
  // src/proxy-shared.ts
@@ -7613,7 +8424,7 @@ function extractClaudeSessionId(body, headerFallback) {
7613
8424
  return validClaudeSessionId(headerFallback);
7614
8425
  }
7615
8426
  function claudeSessionPromptCacheKey(sessionId) {
7616
- return "relay-session-" + createHash6("sha256").update(sessionId).digest("hex").slice(0, 32);
8427
+ return "relay-session-" + createHash7("sha256").update(sessionId).digest("hex").slice(0, 32);
7617
8428
  }
7618
8429
  function anthropicEffortFromRequest(body) {
7619
8430
  const effort = body.output_config?.effort;
@@ -7623,7 +8434,7 @@ function anthropicEffortFromRequest(body) {
7623
8434
  function openAiPromptCacheKey(system, tools) {
7624
8435
  const toolSig = (tools ?? []).map((t) => `${t.name}${t.description ?? ""}${JSON.stringify(t.input_schema ?? {})}`).join("");
7625
8436
  const material = `${system ?? ""}\0${toolSig}`;
7626
- return "relay-" + createHash6("sha256").update(material).digest("hex").slice(0, 32);
8437
+ return "relay-" + createHash7("sha256").update(material).digest("hex").slice(0, 32);
7627
8438
  }
7628
8439
  function supportsOpenAiPromptCacheBreakpoints(modelId) {
7629
8440
  const match = modelId.toLowerCase().match(/^gpt-(\d+)(?:\.(\d+))?(?:-|$)/);
@@ -8486,12 +9297,12 @@ function createTranslationLifecycle(logPath, requestId, modelId, provider) {
8486
9297
  function appendSecureLog(logPath, line) {
8487
9298
  const redacted = redactTraceLine(line);
8488
9299
  try {
8489
- const fd = openSync3(logPath, "a", 384);
9300
+ const fd = openSync4(logPath, "a", 384);
8490
9301
  try {
8491
9302
  writeSync2(fd, `${(/* @__PURE__ */ new Date()).toISOString()} ${redacted}
8492
9303
  `);
8493
9304
  } finally {
8494
- closeSync3(fd);
9305
+ closeSync4(fd);
8495
9306
  }
8496
9307
  } catch {
8497
9308
  try {
@@ -8531,7 +9342,7 @@ function lookupRoute(byAlias, id) {
8531
9342
  return void 0;
8532
9343
  }
8533
9344
  async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPath, debugLogPath, webSocketDiagnosticsLogPath, modelAliases) {
8534
- const proxyToken = randomUUID6();
9345
+ const proxyToken = randomUUID8();
8535
9346
  silenceSdkWarnings();
8536
9347
  if (routes.length === 0) {
8537
9348
  throw new Error("Proxy catalog requires at least one route");
@@ -8609,7 +9420,28 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
8609
9420
  const relayRequestIdRaw = req.headers["x-relay-request-id"];
8610
9421
  const relayRequestId = Array.isArray(relayRequestIdRaw) ? relayRequestIdRaw[0] : relayRequestIdRaw;
8611
9422
  const route = lookupRoute(byAlias, originalModel) ?? defaultRoute;
8612
- const apiKey = route.apiKey;
9423
+ if (messagesEndpoint === "count_tokens" && route.modelFormat !== "anthropic") {
9424
+ const inputTokens = estimateAnthropicInputTokens(anthropicBody);
9425
+ plog(() => `token-count: local estimate model=${originalModel} input_tokens=${inputTokens}`);
9426
+ res.setHeader("x-relay-token-count-source", "local-estimate");
9427
+ sendJson(res, 200, { input_tokens: inputTokens });
9428
+ return;
9429
+ }
9430
+ let apiKey = route.apiKey;
9431
+ if (route.authType === "oauth" && route.refreshToken) {
9432
+ try {
9433
+ const current = await route.refreshToken();
9434
+ if (!current) throw new Error("credential is missing");
9435
+ apiKey = current;
9436
+ route.apiKey = current;
9437
+ } catch (err) {
9438
+ plog(
9439
+ () => `oauth credential unavailable: ${err instanceof Error ? err.message : String(err)}`
9440
+ );
9441
+ anthropicError(res, 401, "OAuth credential is unavailable");
9442
+ return;
9443
+ }
9444
+ }
8613
9445
  const upstreamUrl = route.upstreamUrl;
8614
9446
  const routeAuthType = route.authType ?? "api";
8615
9447
  plog(
@@ -8617,13 +9449,6 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
8617
9449
  );
8618
9450
  const usesSdkAdapter = isSdkMigratedNpm(route.npm);
8619
9451
  if (messagesEndpoint === "count_tokens") {
8620
- if (route.modelFormat !== "anthropic") {
8621
- const inputTokens = estimateAnthropicInputTokens(anthropicBody);
8622
- plog(() => `token-count: local estimate model=${originalModel} input_tokens=${inputTokens}`);
8623
- res.setHeader("x-relay-token-count-source", "local-estimate");
8624
- sendJson(res, 200, { input_tokens: inputTokens });
8625
- return;
8626
- }
8627
9452
  if (!apiKey && routeAuthType !== "none") {
8628
9453
  anthropicError(res, 401, "Missing API key");
8629
9454
  return;
@@ -8712,7 +9537,7 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
8712
9537
  originalModel,
8713
9538
  route.providerId ?? route.aliasId.split(":")[1] ?? "unknown"
8714
9539
  );
8715
- try {
9540
+ const runSdkRequest = async () => {
8716
9541
  const claudeSessionIdHeader = Array.isArray(req.headers["x-claude-code-session-id"]) ? req.headers["x-claude-code-session-id"][0] : req.headers["x-claude-code-session-id"];
8717
9542
  const claudeSessionId = extractClaudeSessionId(anthropicBody, claudeSessionIdHeader);
8718
9543
  const params = translateRequest(anthropicBody, route.npm, {
@@ -8818,18 +9643,35 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
8818
9643
  translationLifecycle?.complete();
8819
9644
  sendJson(res, 200, anthropicResponse);
8820
9645
  }
8821
- } catch (err) {
9646
+ };
9647
+ let sdkAttempt = 0;
9648
+ const handleSdkError = async (err) => {
8822
9649
  if (clientAbort.signal.aborted) {
8823
9650
  translationLifecycle?.cancel();
8824
- return;
9651
+ return "cancelled";
9652
+ }
9653
+ const message = formatUpstreamError(err);
9654
+ const details = sdkUpstreamErrorDetails(err);
9655
+ const upstreamStatus = details?.statusCode ?? upstreamHttpStatus(err, message);
9656
+ const replacement = await resolveOAuthRetryReplacement(
9657
+ openAiOAuth,
9658
+ upstreamStatus,
9659
+ sdkAttempt,
9660
+ res.headersSent,
9661
+ apiKey,
9662
+ route.refreshToken
9663
+ );
9664
+ if (replacement) {
9665
+ apiKey = replacement;
9666
+ route.apiKey = replacement;
9667
+ sdkAttempt += 1;
9668
+ plog(() => "sdk oauth credential replaced after 401; retrying once");
9669
+ return "retry";
8825
9670
  }
8826
9671
  translationLifecycle?.fail(
8827
9672
  err instanceof Error ? err.name : "UpstreamError",
8828
9673
  sdkTranslationErrorSignature(err)
8829
9674
  );
8830
- const message = formatUpstreamError(err);
8831
- const details = sdkUpstreamErrorDetails(err);
8832
- const upstreamStatus = details?.statusCode ?? upstreamHttpStatus(err, message);
8833
9675
  const contextLengthExceeded = upstreamStatus === 400 && isContextLengthExceededError(err, message);
8834
9676
  const clientMessage = contextLengthExceeded ? anthropicPromptTooLongMessage(
8835
9677
  anthropicBody,
@@ -8849,11 +9691,14 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
8849
9691
  });
8850
9692
  }
8851
9693
  if (!res.headersSent) {
9694
+ if (details?.retryAfterSeconds !== void 0) {
9695
+ res.setHeader("retry-after", String(details.retryAfterSeconds));
9696
+ }
8852
9697
  anthropicError(
8853
9698
  res,
8854
9699
  upstreamStatus === 500 ? 502 : upstreamStatus,
8855
9700
  clientMessage,
8856
- contextLengthExceeded ? relayRequestId ?? randomUUID6() : void 0
9701
+ contextLengthExceeded ? relayRequestId ?? randomUUID8() : void 0
8857
9702
  );
8858
9703
  } else {
8859
9704
  const errorType = anthropicErrorType(upstreamStatus);
@@ -8861,12 +9706,24 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
8861
9706
  data: ${JSON.stringify({
8862
9707
  type: "error",
8863
9708
  error: { type: errorType, message: clientMessage },
8864
- ...contextLengthExceeded ? { request_id: relayRequestId ?? randomUUID6() } : {}
9709
+ ...contextLengthExceeded ? { request_id: relayRequestId ?? randomUUID8() } : {}
8865
9710
  })}
8866
9711
 
8867
9712
  `);
8868
9713
  res.end();
8869
9714
  }
9715
+ return "done";
9716
+ };
9717
+ for (; ; ) {
9718
+ try {
9719
+ await runSdkRequest();
9720
+ break;
9721
+ } catch (err) {
9722
+ const outcome = await handleSdkError(err);
9723
+ if (outcome === "retry") continue;
9724
+ if (outcome === "cancelled") return;
9725
+ break;
9726
+ }
8870
9727
  }
8871
9728
  return;
8872
9729
  }
@@ -9059,7 +9916,7 @@ async function askSaveServerPassword() {
9059
9916
 
9060
9917
  // src/server/router.ts
9061
9918
  import { createServer as createServer2 } from "http";
9062
- import { randomUUID as randomUUID7 } from "crypto";
9919
+ import { randomUUID as randomUUID9 } from "crypto";
9063
9920
 
9064
9921
  // src/openai-adapter.ts
9065
9922
  import { tool as tool2, jsonSchema as jsonSchema2, streamText as streamText2, generateText as generateText2 } from "ai";
@@ -9261,6 +10118,30 @@ function auditInference(options, entry) {
9261
10118
  function inferenceProvider(model) {
9262
10119
  return model.providerId ?? String(model.sourceBackend);
9263
10120
  }
10121
+ async function resolveModelApiKey(model, fallback, rejectedAccessToken) {
10122
+ if (model.authType === "oauth" && model.providerId && model.authRef) {
10123
+ let current;
10124
+ try {
10125
+ current = rejectedAccessToken === void 0 ? await resolveProviderCredential(model.providerId, model.authRef) : await resolveProviderCredential(
10126
+ model.providerId,
10127
+ model.authRef,
10128
+ void 0,
10129
+ { rejectedAccessToken }
10130
+ );
10131
+ } catch (cause) {
10132
+ throw new Error(
10133
+ `OAuth credential is unavailable for ${model.providerId}`,
10134
+ { cause }
10135
+ );
10136
+ }
10137
+ if (!current) {
10138
+ throw new Error(`OAuth credential is unavailable for ${model.providerId}`);
10139
+ }
10140
+ model.apiKey = current;
10141
+ return current;
10142
+ }
10143
+ return model.apiKey ?? fallback;
10144
+ }
9264
10145
  function auditSdkError(options, requestedModelId, model, err, message) {
9265
10146
  const details = sdkUpstreamErrorDetails(err);
9266
10147
  const statusCode = details?.statusCode ?? upstreamHttpStatus(err, message);
@@ -9275,7 +10156,7 @@ function auditSdkError(options, requestedModelId, model, err, message) {
9275
10156
  attemptCount: details?.attemptCount
9276
10157
  });
9277
10158
  }
9278
- return statusCode;
10159
+ return { statusCode, retryAfterSeconds: details?.retryAfterSeconds };
9279
10160
  }
9280
10161
  function openAiEffort(body) {
9281
10162
  if (typeof body.reasoning_effort === "string" && body.reasoning_effort.trim()) {
@@ -9319,7 +10200,16 @@ async function routeRequest(req, res, options, modelCache, plog) {
9319
10200
  return;
9320
10201
  }
9321
10202
  if (req.method === "GET" && pathname === "/models") {
9322
- sendJson(res, 200, { models: options.catalog.list().map(({ apiKey: _apiKey, headers: _headers, ...rest }) => rest) });
10203
+ sendJson(res, 200, {
10204
+ models: options.catalog.list().map(({
10205
+ apiKey: _apiKey,
10206
+ authRef: _authRef,
10207
+ headers: _headers,
10208
+ oauthAccountId: _oauthAccountId,
10209
+ providerData: _providerData,
10210
+ ...rest
10211
+ }) => rest)
10212
+ });
9323
10213
  return;
9324
10214
  }
9325
10215
  if (req.method === "GET" && pathname === "/anthropic/v1/models") {
@@ -9354,7 +10244,7 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
9354
10244
  plog(`model not found: ${body.model}`);
9355
10245
  return;
9356
10246
  }
9357
- const requestId = randomUUID7();
10247
+ const requestId = randomUUID9();
9358
10248
  const claudeSessionIdHeader = Array.isArray(req.headers["x-claude-code-session-id"]) ? req.headers["x-claude-code-session-id"][0] : req.headers["x-claude-code-session-id"];
9359
10249
  const claudeSessionId = extractClaudeSessionId(body, claudeSessionIdHeader);
9360
10250
  if (options.webSocketDiagnosticsLogPath) {
@@ -9378,7 +10268,15 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
9378
10268
  return;
9379
10269
  }
9380
10270
  const messagesUrl = `${model.baseUrl}/v1/messages`;
9381
- const apiKey = model.apiKey ?? options.apiKey;
10271
+ let apiKey;
10272
+ try {
10273
+ apiKey = await resolveModelApiKey(model, options.apiKey);
10274
+ } catch (err) {
10275
+ sendJson(res, 401, {
10276
+ error: { message: err instanceof Error ? err.message : String(err) }
10277
+ });
10278
+ return;
10279
+ }
9382
10280
  const betaHeaderRaw = req.headers["anthropic-beta"];
9383
10281
  const inboundBeta = Array.isArray(betaHeaderRaw) ? betaHeaderRaw.join(",") : betaHeaderRaw;
9384
10282
  const clientWantsStream = Boolean(body.stream);
@@ -9403,7 +10301,11 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
9403
10301
  claudeCodeSessionId = identity.sessionId;
9404
10302
  effectiveBeta = selectBetaFlags(forwardBody, upstreamModelId(model), inboundBeta);
9405
10303
  }
9406
- const refreshToken = isOAuth && model.providerId ? () => resolveProviderCredential(model.providerId, oauthAuthRef(model.providerId)) : void 0;
10304
+ const refreshToken = isOAuth && model.providerId && model.authRef ? (rejectedAccessToken) => resolveModelApiKey(
10305
+ model,
10306
+ options.apiKey,
10307
+ rejectedAccessToken
10308
+ ) : void 0;
9407
10309
  plog(() => `anthropic-passthrough \u2192 ${messagesUrl} oauth=${isOAuth} stream=${clientWantsStream}`);
9408
10310
  await relayAnthropicMessages(res, messagesUrl, forwardBody, apiKey, clientWantsStream, {
9409
10311
  inboundBeta: effectiveBeta,
@@ -9431,7 +10333,15 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
9431
10333
  sendJson(res, 400, { error: { message: `No SDK provider for model: ${model.id}` } });
9432
10334
  return;
9433
10335
  }
9434
- const apiKey = model.apiKey ?? options.apiKey;
10336
+ let apiKey;
10337
+ try {
10338
+ apiKey = await resolveModelApiKey(model, options.apiKey);
10339
+ } catch (err) {
10340
+ sendJson(res, 401, {
10341
+ error: { message: err instanceof Error ? err.message : String(err) }
10342
+ });
10343
+ return;
10344
+ }
9435
10345
  auditInference(options, {
9436
10346
  requestId,
9437
10347
  modelId: body.model,
@@ -9441,14 +10351,6 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
9441
10351
  route: "translated",
9442
10352
  requestPreview: getLatestMessagePreview(body.messages, body.system)
9443
10353
  });
9444
- const languageModel = await getOrInitLanguageModel(
9445
- modelCache,
9446
- model,
9447
- model.npm,
9448
- model.apiBaseUrl,
9449
- apiKey,
9450
- options.webSocketDiagnosticsLogPath
9451
- );
9452
10354
  const npmMaxTools = maxToolsForNpm(model.npm);
9453
10355
  const toolCount = Array.isArray(body.tools) ? body.tools.length : 0;
9454
10356
  if (npmMaxTools !== void 0 && toolCount > npmMaxTools) {
@@ -9472,63 +10374,93 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
9472
10374
  const clientWantsStream = Boolean(body.stream);
9473
10375
  const responseModelId = getResponseModelId(body.model, model, options);
9474
10376
  plog(() => `sdk npm=${model.npm} upstream=${upstreamModelId(model)} responseModel=${responseModelId} stream=${clientWantsStream}`);
9475
- try {
9476
- if (clientWantsStream) {
9477
- const writeStreamChunk = (chunk) => {
9478
- if (!res.headersSent) {
9479
- res.writeHead(200, {
9480
- "Content-Type": "text/event-stream",
9481
- "Cache-Control": "no-cache",
9482
- "Connection": "keep-alive"
9483
- });
9484
- }
9485
- res.write(chunk);
9486
- };
9487
- await withResponsesWebSocketDiagnosticContext(
9488
- { requestId, claudeSessionId },
9489
- () => streamAnthropicResponse(languageModel, params, responseModelId, writeStreamChunk, void 0, {
9490
- initialInputTokens: estimateAnthropicInputTokens(body)
9491
- })
9492
- );
9493
- if (!res.headersSent) writeStreamChunk("");
9494
- res.end();
9495
- } else {
9496
- const anthropicResponse = await withResponsesWebSocketDiagnosticContext(
9497
- { requestId, claudeSessionId },
9498
- () => generateAnthropicResponse(languageModel, params, responseModelId, { forceStream: openAiOAuth })
10377
+ let sdkAttempt = 0;
10378
+ for (; ; ) {
10379
+ try {
10380
+ const languageModel = await getOrInitLanguageModel(
10381
+ modelCache,
10382
+ model,
10383
+ model.npm,
10384
+ model.apiBaseUrl,
10385
+ apiKey,
10386
+ options.webSocketDiagnosticsLogPath
9499
10387
  );
9500
- sendJson(res, 200, anthropicResponse);
9501
- }
9502
- } catch (err) {
9503
- const message = formatUpstreamError(err);
9504
- const status = auditSdkError(options, body.model, model, err, message);
9505
- const contextLengthExceeded = status === 400 && isContextLengthExceededError(err, message);
9506
- const clientMessage = contextLengthExceeded ? anthropicPromptTooLongMessage(
9507
- body,
9508
- resolveContextWindow(upstreamModelId(model), model.contextWindow)
9509
- ) : message;
9510
- plog(`sdk error npm=${model.npm} upstream=${upstreamModelId(model)}: ${message}`);
9511
- if (!res.headersSent) {
9512
- if (contextLengthExceeded) {
9513
- sendJson(res, 400, {
9514
- type: "error",
9515
- error: { type: "invalid_request_error", message: clientMessage },
9516
- request_id: requestId
9517
- });
10388
+ if (clientWantsStream) {
10389
+ const writeStreamChunk = (chunk) => {
10390
+ if (!res.headersSent) {
10391
+ res.writeHead(200, {
10392
+ "Content-Type": "text/event-stream",
10393
+ "Cache-Control": "no-cache",
10394
+ "Connection": "keep-alive"
10395
+ });
10396
+ }
10397
+ res.write(chunk);
10398
+ };
10399
+ await withResponsesWebSocketDiagnosticContext(
10400
+ { requestId, claudeSessionId },
10401
+ () => streamAnthropicResponse(languageModel, params, responseModelId, writeStreamChunk, void 0, {
10402
+ initialInputTokens: estimateAnthropicInputTokens(body)
10403
+ })
10404
+ );
10405
+ if (!res.headersSent) writeStreamChunk("");
10406
+ res.end();
9518
10407
  } else {
9519
- sendJson(res, status === 500 ? 502 : status, { error: { message: clientMessage } });
10408
+ const anthropicResponse = await withResponsesWebSocketDiagnosticContext(
10409
+ { requestId, claudeSessionId },
10410
+ () => generateAnthropicResponse(languageModel, params, responseModelId, { forceStream: openAiOAuth })
10411
+ );
10412
+ sendJson(res, 200, anthropicResponse);
9520
10413
  }
9521
- } else {
9522
- const errorType = anthropicErrorType(status);
9523
- res.write(`event: error
10414
+ break;
10415
+ } catch (err) {
10416
+ const message = formatUpstreamError(err);
10417
+ const details = sdkUpstreamErrorDetails(err);
10418
+ const candidateStatus = details?.statusCode ?? upstreamHttpStatus(err, message);
10419
+ const replacement = await resolveOAuthRetryReplacement(
10420
+ openAiOAuth,
10421
+ candidateStatus,
10422
+ sdkAttempt,
10423
+ res.headersSent,
10424
+ apiKey,
10425
+ (rejectedAccessToken) => resolveModelApiKey(model, options.apiKey, rejectedAccessToken)
10426
+ );
10427
+ if (replacement) {
10428
+ apiKey = replacement;
10429
+ sdkAttempt += 1;
10430
+ plog("sdk oauth credential replaced after 401; retrying once");
10431
+ continue;
10432
+ }
10433
+ const { statusCode: status, retryAfterSeconds } = auditSdkError(options, body.model, model, err, message);
10434
+ const contextLengthExceeded = status === 400 && isContextLengthExceededError(err, message);
10435
+ const clientMessage = contextLengthExceeded ? anthropicPromptTooLongMessage(
10436
+ body,
10437
+ resolveContextWindow(upstreamModelId(model), model.contextWindow)
10438
+ ) : message;
10439
+ plog(`sdk error npm=${model.npm} upstream=${upstreamModelId(model)}: ${message}`);
10440
+ if (!res.headersSent) {
10441
+ if (contextLengthExceeded) {
10442
+ sendJson(res, 400, {
10443
+ type: "error",
10444
+ error: { type: "invalid_request_error", message: clientMessage },
10445
+ request_id: requestId
10446
+ });
10447
+ } else {
10448
+ if (retryAfterSeconds !== void 0) res.setHeader("retry-after", String(retryAfterSeconds));
10449
+ sendJson(res, status === 500 ? 502 : status, { error: { message: clientMessage } });
10450
+ }
10451
+ } else {
10452
+ const errorType = anthropicErrorType(status);
10453
+ res.write(`event: error
9524
10454
  data: ${JSON.stringify({
9525
- type: "error",
9526
- error: { type: errorType, message: clientMessage },
9527
- ...contextLengthExceeded ? { request_id: requestId } : {}
9528
- })}
10455
+ type: "error",
10456
+ error: { type: errorType, message: clientMessage },
10457
+ ...contextLengthExceeded ? { request_id: requestId } : {}
10458
+ })}
9529
10459
 
9530
10460
  `);
9531
- res.end();
10461
+ res.end();
10462
+ }
10463
+ break;
9532
10464
  }
9533
10465
  }
9534
10466
  return;
@@ -9553,7 +10485,15 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
9553
10485
  return;
9554
10486
  }
9555
10487
  const completionsUrl = model.completionsUrl;
9556
- const apiKey2 = model.apiKey ?? options.apiKey;
10488
+ let apiKey2;
10489
+ try {
10490
+ apiKey2 = await resolveModelApiKey(model, options.apiKey);
10491
+ } catch (err) {
10492
+ sendJson(res, 401, {
10493
+ error: { message: err instanceof Error ? err.message : String(err) }
10494
+ });
10495
+ return;
10496
+ }
9557
10497
  const forwardBody = body.model === upstreamModelId(model) ? body : { ...body, model: upstreamModelId(model) };
9558
10498
  auditInference(options, {
9559
10499
  modelId: body.model,
@@ -9562,9 +10502,19 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
9562
10502
  route: "passthrough",
9563
10503
  requestPreview: getLatestMessagePreview(body.messages, body.system)
9564
10504
  });
10505
+ const isOAuth = model.authType === "oauth";
10506
+ const refreshToken = isOAuth && model.providerId && model.authRef ? (rejectedAccessToken) => resolveModelApiKey(
10507
+ model,
10508
+ options.apiKey,
10509
+ rejectedAccessToken
10510
+ ) : void 0;
9565
10511
  await relayAnthropicMessages(res, completionsUrl, forwardBody, apiKey2, Boolean(body.stream), {
9566
10512
  authType: model.authType ?? "api",
9567
10513
  extraHeaders: model.headers,
10514
+ refreshToken,
10515
+ onTokenRefreshed: (refreshed) => {
10516
+ model.apiKey = refreshed;
10517
+ },
9568
10518
  onUpstreamError: options.inferenceLogPath ? (statusCode, errorContent) => writeInferenceResponseErrorLog(options.inferenceLogPath, {
9569
10519
  modelId: body.model,
9570
10520
  provider: inferenceProvider(model),
@@ -9580,7 +10530,15 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
9580
10530
  sendJson(res, 400, { error: { message: `No SDK provider for model: ${model.id}` } });
9581
10531
  return;
9582
10532
  }
9583
- const apiKey = model.apiKey ?? options.apiKey;
10533
+ let apiKey;
10534
+ try {
10535
+ apiKey = await resolveModelApiKey(model, options.apiKey);
10536
+ } catch (err) {
10537
+ sendJson(res, 401, {
10538
+ error: { message: err instanceof Error ? err.message : String(err) }
10539
+ });
10540
+ return;
10541
+ }
9584
10542
  auditInference(options, {
9585
10543
  modelId: body.model,
9586
10544
  effort: openAiEffort(body),
@@ -9589,42 +10547,70 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
9589
10547
  requestPreview: getLatestMessagePreview(body.messages, body.system)
9590
10548
  });
9591
10549
  const baseURL = model.modelFormat === "anthropic" ? model.baseUrl : model.apiBaseUrl;
9592
- const languageModel = await getOrInitLanguageModel(modelCache, model, npm, baseURL, apiKey);
9593
10550
  const openAiOAuth = npm === "@ai-sdk/openai" && model.authType === "oauth";
9594
10551
  const params = translateOpenAiRequest(body, { openAiOAuth });
9595
10552
  const clientWantsStream = Boolean(body.stream);
9596
10553
  const responseModelId = getResponseModelId(body.model, model, options);
9597
10554
  plog(() => `sdk-openai npm=${npm} upstream=${upstreamModelId(model)} responseModel=${responseModelId} stream=${clientWantsStream}`);
9598
- try {
9599
- if (clientWantsStream) {
9600
- const writeStreamChunk = (chunk) => {
9601
- if (!res.headersSent) {
9602
- res.writeHead(200, {
9603
- "Content-Type": "text/event-stream",
9604
- "Cache-Control": "no-cache",
9605
- "Connection": "keep-alive"
9606
- });
9607
- }
9608
- res.write(chunk);
9609
- };
9610
- await streamOpenAiResponse(languageModel, params, responseModelId, writeStreamChunk);
9611
- if (!res.headersSent) writeStreamChunk("");
9612
- res.end();
9613
- } else {
9614
- const response = await generateOpenAiResponse(languageModel, params, responseModelId, { forceStream: openAiOAuth });
9615
- sendJson(res, 200, response);
9616
- }
9617
- } catch (err) {
9618
- const message = formatUpstreamError(err);
9619
- const status = auditSdkError(options, body.model, model, err, message);
9620
- plog(`sdk error npm=${model.npm} upstream=${upstreamModelId(model)}: ${message}`);
9621
- if (!res.headersSent) {
9622
- sendJson(res, status === 500 ? 502 : status, { error: { message } });
9623
- } else {
9624
- res.write(`data: ${JSON.stringify({ error: { message, type: "upstream_error", code: status } })}
10555
+ let sdkAttempt = 0;
10556
+ for (; ; ) {
10557
+ try {
10558
+ const languageModel = await getOrInitLanguageModel(
10559
+ modelCache,
10560
+ model,
10561
+ npm,
10562
+ baseURL,
10563
+ apiKey
10564
+ );
10565
+ if (clientWantsStream) {
10566
+ const writeStreamChunk = (chunk) => {
10567
+ if (!res.headersSent) {
10568
+ res.writeHead(200, {
10569
+ "Content-Type": "text/event-stream",
10570
+ "Cache-Control": "no-cache",
10571
+ "Connection": "keep-alive"
10572
+ });
10573
+ }
10574
+ res.write(chunk);
10575
+ };
10576
+ await streamOpenAiResponse(languageModel, params, responseModelId, writeStreamChunk);
10577
+ if (!res.headersSent) writeStreamChunk("");
10578
+ res.end();
10579
+ } else {
10580
+ const response = await generateOpenAiResponse(languageModel, params, responseModelId, { forceStream: openAiOAuth });
10581
+ sendJson(res, 200, response);
10582
+ }
10583
+ break;
10584
+ } catch (err) {
10585
+ const message = formatUpstreamError(err);
10586
+ const details = sdkUpstreamErrorDetails(err);
10587
+ const candidateStatus = details?.statusCode ?? upstreamHttpStatus(err, message);
10588
+ const replacement = await resolveOAuthRetryReplacement(
10589
+ openAiOAuth,
10590
+ candidateStatus,
10591
+ sdkAttempt,
10592
+ res.headersSent,
10593
+ apiKey,
10594
+ (rejectedAccessToken) => resolveModelApiKey(model, options.apiKey, rejectedAccessToken)
10595
+ );
10596
+ if (replacement) {
10597
+ apiKey = replacement;
10598
+ sdkAttempt += 1;
10599
+ plog("sdk oauth credential replaced after 401; retrying once");
10600
+ continue;
10601
+ }
10602
+ const { statusCode: status, retryAfterSeconds } = auditSdkError(options, body.model, model, err, message);
10603
+ plog(`sdk error npm=${model.npm} upstream=${upstreamModelId(model)}: ${message}`);
10604
+ if (!res.headersSent) {
10605
+ if (retryAfterSeconds !== void 0) res.setHeader("retry-after", String(retryAfterSeconds));
10606
+ sendJson(res, status === 500 ? 502 : status, { error: { message } });
10607
+ } else {
10608
+ res.write(`data: ${JSON.stringify({ error: { message, type: "upstream_error", code: status } })}
9625
10609
 
9626
10610
  `);
9627
- res.end();
10611
+ res.end();
10612
+ }
10613
+ break;
9628
10614
  }
9629
10615
  }
9630
10616
  }
@@ -9648,9 +10634,9 @@ async function getOrInitLanguageModel(modelCache, model, npm, baseURL, apiKey, w
9648
10634
  npm,
9649
10635
  baseURL ?? ""
9650
10636
  ].join("");
9651
- let languageModel = modelCache.get(cacheKey);
9652
- if (!languageModel) {
9653
- languageModel = await createLanguageModel({
10637
+ let cached = modelCache.get(cacheKey);
10638
+ if (!cached || cached.apiKey !== apiKey) {
10639
+ const languageModel = await createLanguageModel({
9654
10640
  npm,
9655
10641
  modelId: upstreamModelId(model),
9656
10642
  apiKey,
@@ -9663,9 +10649,10 @@ async function getOrInitLanguageModel(modelCache, model, npm, baseURL, apiKey, w
9663
10649
  preferWebSockets: model.preferWebSockets,
9664
10650
  onWebSocketDiagnostic: webSocketDiagnosticsLogPath ? (event) => writeWebSocketDiagnosticLog(webSocketDiagnosticsLogPath, event) : void 0
9665
10651
  });
9666
- modelCache.set(cacheKey, languageModel);
10652
+ cached = { apiKey, languageModel };
10653
+ modelCache.set(cacheKey, cached);
9667
10654
  }
9668
- return languageModel;
10655
+ return cached.languageModel;
9669
10656
  }
9670
10657
  function getResponseModelId(bodyModel, model, options) {
9671
10658
  if (typeof bodyModel === "string" && options.aliasNames?.has(bodyModel)) return bodyModel;
@@ -9808,14 +10795,14 @@ import * as p8 from "@clack/prompts";
9808
10795
  import * as http from "http";
9809
10796
  import * as https from "https";
9810
10797
  import * as net from "net";
9811
- import { randomUUID as randomUUID8 } from "crypto";
10798
+ import { randomUUID as randomUUID10 } from "crypto";
9812
10799
  import { URL as URL2 } from "url";
9813
10800
  import { createBrotliDecompress, createGunzip, createInflate } from "zlib";
9814
10801
 
9815
10802
  // src/http-proxy/ca.ts
9816
10803
  import { randomBytes } from "crypto";
9817
- import { chmodSync as chmodSync5, existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
9818
- import { dirname as dirname5, join as join5, resolve } from "path";
10804
+ import { chmodSync as chmodSync5, existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
10805
+ import { dirname as dirname6, join as join5, resolve } from "path";
9819
10806
  import forge from "node-forge";
9820
10807
  var CERT_DIR = "http-proxy";
9821
10808
  var CA_CERT_FILE = "clodex-ca.pem";
@@ -9841,15 +10828,15 @@ function certPaths() {
9841
10828
  };
9842
10829
  }
9843
10830
  function writePrivate(path, value) {
9844
- writeFileSync5(path, value, { encoding: "utf8", mode: 384 });
10831
+ writeFileSync6(path, value, { encoding: "utf8", mode: 384 });
9845
10832
  chmodSync5(path, 384);
9846
10833
  }
9847
10834
  function writePublic(path, value) {
9848
- writeFileSync5(path, value, { encoding: "utf8", mode: 420 });
10835
+ writeFileSync6(path, value, { encoding: "utf8", mode: 420 });
9849
10836
  chmodSync5(path, 420);
9850
10837
  }
9851
10838
  function generateCertificates(paths) {
9852
- mkdirSync6(paths.dir, { recursive: true, mode: 448 });
10839
+ mkdirSync7(paths.dir, { recursive: true, mode: 448 });
9853
10840
  chmodSync5(paths.dir, 448);
9854
10841
  const caKeys = forge.pki.rsa.generateKeyPair(2048);
9855
10842
  const caCert = forge.pki.createCertificate();
@@ -9890,8 +10877,8 @@ function generateCertificates(paths) {
9890
10877
  }
9891
10878
  function storedCertificatesAreCurrent(paths) {
9892
10879
  try {
9893
- const ca = forge.pki.certificateFromPem(readFileSync7(paths.caCert, "utf8"));
9894
- const server = forge.pki.certificateFromPem(readFileSync7(paths.serverCert, "utf8"));
10880
+ const ca = forge.pki.certificateFromPem(readFileSync8(paths.caCert, "utf8"));
10881
+ const server = forge.pki.certificateFromPem(readFileSync8(paths.serverCert, "utf8"));
9895
10882
  const now = Date.now();
9896
10883
  const renewalBuffer = 7 * 24 * 60 * 60 * 1e3;
9897
10884
  return ca.validity.notBefore.getTime() <= now && ca.validity.notAfter.getTime() > now + renewalBuffer && server.validity.notBefore.getTime() <= now && server.validity.notAfter.getTime() > now + renewalBuffer && ca.verify(ca) && ca.verify(server);
@@ -9902,23 +10889,23 @@ function storedCertificatesAreCurrent(paths) {
9902
10889
  function ensureHttpProxyCertificates() {
9903
10890
  const paths = certPaths();
9904
10891
  const required = [paths.caCert, paths.caKey, paths.serverCert, paths.serverKey, paths.version];
9905
- const current = required.every(existsSync5) && readFileSync7(paths.version, "utf8") === CERT_VERSION && storedCertificatesAreCurrent(paths);
10892
+ const current = required.every(existsSync6) && readFileSync8(paths.version, "utf8") === CERT_VERSION && storedCertificatesAreCurrent(paths);
9906
10893
  if (!current) generateCertificates(paths);
9907
10894
  return {
9908
10895
  caCertPath: paths.caCert,
9909
- caCert: readFileSync7(paths.caCert, "utf8"),
9910
- serverCert: readFileSync7(paths.serverCert, "utf8"),
9911
- serverKey: readFileSync7(paths.serverKey, "utf8")
10896
+ caCert: readFileSync8(paths.caCert, "utf8"),
10897
+ serverCert: readFileSync8(paths.serverCert, "utf8"),
10898
+ serverKey: readFileSync8(paths.serverKey, "utf8")
9912
10899
  };
9913
10900
  }
9914
10901
  function ensureHttpProxyCaBundle(relayCaCertPath, additionalCaCertPath) {
9915
10902
  if (!additionalCaCertPath?.trim()) return relayCaCertPath;
9916
10903
  try {
9917
10904
  if (resolve(additionalCaCertPath) === resolve(relayCaCertPath)) return relayCaCertPath;
9918
- const relayCa = readFileSync7(relayCaCertPath, "utf8").trimEnd();
9919
- const additionalCa = readFileSync7(additionalCaCertPath, "utf8").trim();
10905
+ const relayCa = readFileSync8(relayCaCertPath, "utf8").trimEnd();
10906
+ const additionalCa = readFileSync8(additionalCaCertPath, "utf8").trim();
9920
10907
  if (!additionalCa) return relayCaCertPath;
9921
- const combinedPath = join5(dirname5(relayCaCertPath), "combined-ca.pem");
10908
+ const combinedPath = join5(dirname6(relayCaCertPath), "combined-ca.pem");
9922
10909
  writePublic(combinedPath, `${relayCa}
9923
10910
  ${additionalCa}
9924
10911
  `);
@@ -10472,7 +11459,7 @@ async function startHttpProxy(options) {
10472
11459
  }
10473
11460
  const messagesEndpoint = anthropicMessagesEndpoint(req.url);
10474
11461
  if (req.method === "POST" && messagesEndpoint) {
10475
- const requestId = randomUUID8();
11462
+ const requestId = randomUUID10();
10476
11463
  let parsed = null;
10477
11464
  let route;
10478
11465
  try {
@@ -10720,12 +11707,13 @@ function waitForShutdown() {
10720
11707
  }
10721
11708
  async function runHttpProxyServerCommand(debug = false, webSocketDiagnostics = false, port, noDiscovery = false) {
10722
11709
  const webSocketDiagnosticsLogPath = webSocketDiagnostics ? getSessionLogPath("server-websocket-diagnostics", "jsonl") : void 0;
11710
+ const inferenceLogPath = getInferenceRequestLogPath();
10723
11711
  let started;
10724
11712
  try {
10725
11713
  started = await startConfiguredHttpProxy(
10726
11714
  port ?? DEFAULT_SERVER_PORT,
10727
11715
  debug,
10728
- getInferenceRequestLogPath(),
11716
+ inferenceLogPath,
10729
11717
  void 0,
10730
11718
  webSocketDiagnosticsLogPath
10731
11719
  );
@@ -10734,6 +11722,13 @@ async function runHttpProxyServerCommand(debug = false, webSocketDiagnostics = f
10734
11722
  return 1;
10735
11723
  }
10736
11724
  const { handle, loaded } = started;
11725
+ writeProxyLifecycleLog(inferenceLogPath, {
11726
+ event: "proxy_started",
11727
+ pid: process.pid,
11728
+ parentPid: process.ppid,
11729
+ host: handle.host,
11730
+ port: handle.port
11731
+ });
10737
11732
  console.log("");
10738
11733
  console.log(pc9.bold(pc9.green("clodex proxy-mode server running")));
10739
11734
  console.log(` HTTPS_PROXY=http://127.0.0.1:${handle.port}`);
@@ -10761,8 +11756,23 @@ async function runHttpProxyServerCommand(debug = false, webSocketDiagnostics = f
10761
11756
  });
10762
11757
  }
10763
11758
  await waitForShutdown();
11759
+ writeProxyLifecycleLog(inferenceLogPath, {
11760
+ event: "proxy_stopping",
11761
+ pid: process.pid,
11762
+ parentPid: process.ppid,
11763
+ host: handle.host,
11764
+ port: handle.port,
11765
+ reason: "shutdown signal received"
11766
+ });
10764
11767
  if (!noDiscovery) unregisterServerRuntimeState();
10765
11768
  await handle.close();
11769
+ writeProxyLifecycleLog(inferenceLogPath, {
11770
+ event: "proxy_stopped",
11771
+ pid: process.pid,
11772
+ parentPid: process.ppid,
11773
+ host: handle.host,
11774
+ port: handle.port
11775
+ });
10766
11776
  return 0;
10767
11777
  }
10768
11778
 
@@ -11375,17 +12385,17 @@ function planLaunchWizard(opts) {
11375
12385
  }
11376
12386
 
11377
12387
  // src/patcher.ts
11378
- import { createHash as createHash7 } from "crypto";
12388
+ import { createHash as createHash8 } from "crypto";
11379
12389
  import {
11380
12390
  copyFileSync as copyFileSync2,
11381
- existsSync as existsSync6,
11382
- mkdirSync as mkdirSync7,
11383
- readFileSync as readFileSync8,
12391
+ existsSync as existsSync7,
12392
+ mkdirSync as mkdirSync8,
12393
+ readFileSync as readFileSync9,
11384
12394
  statSync as statSync4,
11385
- unlinkSync as unlinkSync4,
11386
- writeFileSync as writeFileSync6,
11387
- openSync as openSync4,
11388
- closeSync as closeSync4,
12395
+ unlinkSync as unlinkSync5,
12396
+ writeFileSync as writeFileSync7,
12397
+ openSync as openSync5,
12398
+ closeSync as closeSync5,
11389
12399
  realpathSync
11390
12400
  } from "fs";
11391
12401
  import { homedir as homedir2 } from "os";
@@ -11582,7 +12592,7 @@ function getPatchLockPath() {
11582
12592
  }
11583
12593
  function readPatchManifest(path = getPatchManifestPath()) {
11584
12594
  try {
11585
- const parsed = JSON.parse(readFileSync8(path, "utf8"));
12595
+ const parsed = JSON.parse(readFileSync9(path, "utf8"));
11586
12596
  if (parsed && typeof parsed.binaryPath === "string" && typeof parsed.configHash === "string") {
11587
12597
  return parsed;
11588
12598
  }
@@ -11591,8 +12601,8 @@ function readPatchManifest(path = getPatchManifestPath()) {
11591
12601
  return null;
11592
12602
  }
11593
12603
  function writePatchManifest(manifest, path = getPatchManifestPath()) {
11594
- mkdirSync7(getAppHome(), { recursive: true, mode: 448 });
11595
- writeFileSync6(path, `${JSON.stringify(manifest, null, 2)}
12604
+ mkdirSync8(getAppHome(), { recursive: true, mode: 448 });
12605
+ writeFileSync7(path, `${JSON.stringify(manifest, null, 2)}
11596
12606
  `, { encoding: "utf8", mode: 384 });
11597
12607
  }
11598
12608
  function buildPatchModelConfig(favorites, aliases, modelMetaFor) {
@@ -11620,7 +12630,7 @@ function computePatchConfigHash(config) {
11620
12630
  const entry = config[key];
11621
12631
  return [key, entry.alias ?? null, entry.context ?? null, entry.display ?? null];
11622
12632
  });
11623
- return createHash7("sha256").update(JSON.stringify(canonical)).digest("hex");
12633
+ return createHash8("sha256").update(JSON.stringify(canonical)).digest("hex");
11624
12634
  }
11625
12635
  function buildDesiredPatchConfig() {
11626
12636
  const prefs = loadPreferences();
@@ -11663,30 +12673,30 @@ function pidIsAlive(pid) {
11663
12673
  function tryAcquirePatchLock(lockPath = getPatchLockPath(), opts = {}) {
11664
12674
  const now = opts.now ?? Date.now();
11665
12675
  const isAlive = opts.isAlive ?? pidIsAlive;
11666
- mkdirSync7(join6(lockPath, ".."), { recursive: true, mode: 448 });
12676
+ mkdirSync8(join6(lockPath, ".."), { recursive: true, mode: 448 });
11667
12677
  for (let attempt = 0; attempt < 2; attempt++) {
11668
12678
  try {
11669
- const fd = openSync4(lockPath, "wx");
12679
+ const fd = openSync5(lockPath, "wx");
11670
12680
  const content = { pid: process.pid, startedAt: now };
11671
- writeFileSync6(fd, JSON.stringify(content));
11672
- closeSync4(fd);
12681
+ writeFileSync7(fd, JSON.stringify(content));
12682
+ closeSync5(fd);
11673
12683
  return () => {
11674
12684
  try {
11675
- unlinkSync4(lockPath);
12685
+ unlinkSync5(lockPath);
11676
12686
  } catch {
11677
12687
  }
11678
12688
  };
11679
12689
  } catch {
11680
12690
  let stale = false;
11681
12691
  try {
11682
- const existing = JSON.parse(readFileSync8(lockPath, "utf8"));
12692
+ const existing = JSON.parse(readFileSync9(lockPath, "utf8"));
11683
12693
  stale = !existing.pid || !isAlive(existing.pid) || typeof existing.startedAt === "number" && now - existing.startedAt > PATCH_LOCK_STALE_MS;
11684
12694
  } catch {
11685
12695
  stale = true;
11686
12696
  }
11687
12697
  if (!stale) return null;
11688
12698
  try {
11689
- unlinkSync4(lockPath);
12699
+ unlinkSync5(lockPath);
11690
12700
  } catch {
11691
12701
  }
11692
12702
  }
@@ -11694,12 +12704,12 @@ function tryAcquirePatchLock(lockPath = getPatchLockPath(), opts = {}) {
11694
12704
  return null;
11695
12705
  }
11696
12706
  function sha256File(path) {
11697
- return createHash7("sha256").update(readFileSync8(path)).digest("hex");
12707
+ return createHash8("sha256").update(readFileSync9(path)).digest("hex");
11698
12708
  }
11699
12709
  function resolveClaudeBinaryForPatch() {
11700
12710
  const envOverride = process.env["TWEAKCC_CC_INSTALLATION_PATH"];
11701
12711
  const nativeSymlink = join6(homedir2(), ".local", "bin", "claude");
11702
- const source = envOverride?.trim() || (existsSync6(nativeSymlink) ? nativeSymlink : null) || findClaudeBinary();
12712
+ const source = envOverride?.trim() || (existsSync7(nativeSymlink) ? nativeSymlink : null) || findClaudeBinary();
11703
12713
  if (!source) return null;
11704
12714
  let resolved;
11705
12715
  try {
@@ -11734,13 +12744,13 @@ function summarizePatchResults(results) {
11734
12744
  }
11735
12745
  async function applyPatch(binaryPath, version, desired, configHash, opts) {
11736
12746
  const backup = pristineBackupPath(version, binaryPath);
11737
- mkdirSync7(backupDir(), { recursive: true });
12747
+ mkdirSync8(backupDir(), { recursive: true });
11738
12748
  if (opts.restoreFirst) {
11739
- if (!existsSync6(backup)) {
12749
+ if (!existsSync7(backup)) {
11740
12750
  return { ok: false, message: `Cannot re-patch: pristine backup missing at ${backup}. Reinstall claude, then run clodex patch.` };
11741
12751
  }
11742
12752
  copyFileSync2(backup, binaryPath);
11743
- } else if (!existsSync6(backup)) {
12753
+ } else if (!existsSync7(backup)) {
11744
12754
  copyFileSync2(binaryPath, backup);
11745
12755
  }
11746
12756
  copyFileSync2(backup, join6(backupDir(), "native-binary.backup"));
@@ -11796,14 +12806,14 @@ async function runPatchCommand(opts = {}) {
11796
12806
  const { binaryPath, version } = resolved;
11797
12807
  if (opts.restore) {
11798
12808
  const manifest2 = readPatchManifest();
11799
- const backup = manifest2?.backupPath && existsSync6(manifest2.backupPath) ? manifest2.backupPath : pristineBackupPath(version, binaryPath);
11800
- if (!existsSync6(backup)) {
12809
+ const backup = manifest2?.backupPath && existsSync7(manifest2.backupPath) ? manifest2.backupPath : pristineBackupPath(version, binaryPath);
12810
+ if (!existsSync7(backup)) {
11801
12811
  p11.log.error(`No pristine backup found for claude ${version} (${backup}).`);
11802
12812
  return 1;
11803
12813
  }
11804
12814
  copyFileSync2(backup, binaryPath);
11805
12815
  try {
11806
- unlinkSync4(getPatchManifestPath());
12816
+ unlinkSync5(getPatchManifestPath());
11807
12817
  } catch {
11808
12818
  }
11809
12819
  p11.log.success(`Restored pristine claude ${version} from ${backup}.`);
@@ -11836,7 +12846,7 @@ async function runPatchCommand(opts = {}) {
11836
12846
  }
11837
12847
  try {
11838
12848
  const backup = pristineBackupPath(version, binaryPath);
11839
- const restoreFirst = existsSync6(backup) && sha256File(backup) !== sha256File(binaryPath);
12849
+ const restoreFirst = existsSync7(backup) && sha256File(backup) !== sha256File(binaryPath);
11840
12850
  if (restoreFirst) {
11841
12851
  p11.log.info("Binary differs from its pristine backup \u2014 restoring it before patching fresh.");
11842
12852
  }