opencode-multi-account-core 0.2.127 → 0.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/index.d.ts CHANGED
@@ -52,6 +52,7 @@ declare const CredentialRefreshPatchSchema: v.ObjectSchema<{
52
52
  }, undefined>;
53
53
  declare const StoredAccountSchema: v.ObjectSchema<{
54
54
  readonly uuid: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
55
+ readonly credentialOwnerId: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
55
56
  readonly accountId: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
56
57
  readonly accountUuid: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
57
58
  readonly deviceId: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
@@ -88,6 +89,7 @@ declare const AccountStorageSchema: v.ObjectSchema<{
88
89
  readonly version: v.LiteralSchema<1, undefined>;
89
90
  readonly accounts: v.OptionalSchema<v.ArraySchema<v.ObjectSchema<{
90
91
  readonly uuid: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
92
+ readonly credentialOwnerId: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
91
93
  readonly accountId: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
92
94
  readonly accountUuid: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
93
95
  readonly deviceId: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
@@ -222,18 +224,26 @@ interface DiskCredentials {
222
224
  refreshToken: string;
223
225
  accessToken?: string;
224
226
  expiresAt?: number;
227
+ credentialOwnerId?: string;
225
228
  accountId?: string;
226
229
  accountUuid?: string;
227
230
  deviceId?: string;
228
231
  }
232
+ interface AccountCredentialRefreshResult {
233
+ result: TokenRefreshResult;
234
+ account: StoredAccount | null;
235
+ }
229
236
  declare class AccountStore {
230
237
  private readonly storagePath;
231
238
  constructor(filename?: string);
232
239
  private withLock;
240
+ private refreshLockPath;
233
241
  load(): Promise<AccountStorage>;
234
242
  readCredentials(uuid: string): Promise<DiskCredentials | null>;
243
+ refreshAccountCredentials(uuid: string, expected: DiskCredentials, refresh: (refreshToken: string) => Promise<TokenRefreshResult>): Promise<AccountCredentialRefreshResult>;
235
244
  mutateAccount(uuid: string, fn: (account: StoredAccount) => void): Promise<StoredAccount | null>;
236
245
  mutateStorage(fn: (storage: AccountStorage) => void): Promise<void>;
246
+ mutateStorageIfCredentialsMatch(uuid: string, expected: DiskCredentials, fn: (account: StoredAccount, storage: AccountStorage) => void): Promise<boolean>;
237
247
  addAccount(account: StoredAccount): Promise<void>;
238
248
  removeAccount(uuid: string): Promise<boolean>;
239
249
  setActiveUuid(uuid: string | undefined): Promise<void>;
@@ -272,7 +282,7 @@ interface AccountManagerInstance {
272
282
  markRateLimited(uuid: string, backoffMs?: number): Promise<void>;
273
283
  markRevoked(uuid: string): Promise<void>;
274
284
  markSuccess(uuid: string): Promise<void>;
275
- markAuthFailure(uuid: string, result: TokenRefreshResult): Promise<void>;
285
+ markAuthFailure(uuid: string, result: TokenRefreshResult, expected?: DiskCredentials): Promise<void>;
276
286
  applyUsageCache(uuid: string, usage: UsageLimits): Promise<void>;
277
287
  applyProfileCache(uuid: string, profile: ProfileData): Promise<void>;
278
288
  ensureValidToken(uuid: string, client: PluginClient): Promise<TokenRefreshResult>;
@@ -355,7 +365,7 @@ interface ExecutorAccountManager {
355
365
  refresh(): Promise<void>;
356
366
  selectAccount(stickyKey?: string): Promise<ManagedAccount | null>;
357
367
  markSuccess(uuid: string): Promise<void>;
358
- markAuthFailure(uuid: string, result: TokenRefreshResult): Promise<void>;
368
+ markAuthFailure(uuid: string, result: TokenRefreshResult, expected?: DiskCredentials): Promise<void>;
359
369
  markRevoked(uuid: string): Promise<void>;
360
370
  hasAnyUsableAccount(): boolean;
361
371
  getMinWaitTime(): number;
@@ -778,4 +788,4 @@ declare const __openCodeNativeBootstrapAuthTestUtils: {
778
788
 
779
789
  declare function zeroCostProviderModels(provider: unknown): Promise<Record<string, any>>;
780
790
 
781
- export { ACCOUNTS_FILENAME, ANSI, type AccountManagerClass, type AccountManagerDependencies, type AccountManagerInstance, type AccountMetadataPatch, type AccountSelectionStrategy, AccountSelectionStrategySchema, type AccountStorage, AccountStorageSchema, AccountStore, type BuildFailoverPlanOptions, CODEX_STARTUP_PROBE_MAX_BYTES, CODEX_UNKNOWN_RATE_LIMIT_BACKOFF_MS, type CascadeState, CascadeStateManager, type ChainConfig, ChainConfigSchema, type ChainEntryConfig, ChainEntryConfigSchema, type ClaimsManager, type ClaimsMap, type ConfigLoader, type CoreConfig, type CredentialRefreshPatch, CredentialRefreshPatchSchema, type DiskCredentials, type ExecutorAccountManager, type ExecutorDependencies, type ExecutorRuntimeFactory, type FailoverCandidate, type FailoverPlan, type FailoverSkip, type KeyAction, type ManagedAccount, type MenuItem, type NativePluginLifecycle, type NativePluginLifecycleOptions, type NativePluginLoaderResult, type NativePluginManagedAccount, type NativePluginManagerClass, type NativePluginManagerLike, type NativePluginRefreshQueueLike, type NativePluginRuntimeFactoryLike, type NativePluginStoreLike, type OAuthAdapter, type OAuthAdapterPlanLabels, type OAuthAdapterTransformConfig, type OAuthCredentials, OAuthCredentialsSchema, type OpenCodeNativeAuthLoaderOptions, type OpenCodeNativeAuthMethod, type OpenCodeNativeAuthMethodsOptions, type OpenCodeNativeBootstrapAccount, type OpenCodeNativeBootstrapAuthOptions, type OpenCodeNativeBootstrapStore, type OpenCodeNativeLoaderContext, type PluginClient, type PluginConfig, PluginConfigSchema, type PoolChainConfig, PoolChainConfigSchema, type PoolConfig, PoolConfigSchema, PoolManager, type ProactiveRefreshDependencies, type ProactiveRefreshQueueClass, type ProactiveRefreshQueueInstance, type ProfileData, type QuotaResetPaceOptions, type QuotaRoutingWindow, type RateLimitAccountManager, type RateLimitDependencies, type RuntimeFactoryLike, type SelectOptions, type SseStartupSupervisorOptions, type StoredAccount, StoredAccountSchema, type SupervisedTurnResponse, TokenRefreshError, type TokenRefreshResult, type TurnFailureClass, type TurnFailurePhase, type TurnFailureSignal, type TurnResponseSupervisor, type UsageLimitEntry, UsageLimitEntrySchema, type UsageLimits, UsageLimitsSchema, __openCodeNativeBootstrapAuthTestUtils, anthropicOAuthAdapter, classifyCodexFailure, classifyCodexJsonEventFailure, classifyCodexSseStartupFailure, confirm, createAccountManagerForProvider, createClaimsManager, createConfigLoader, createExecutorForProvider, createMinimalClient, createOpenCodeNativeAuthLoader, createOpenCodeNativeAuthMethods, createOpenCodeNativePluginLifecycle, createProactiveRefreshQueueForProvider, createRateLimitHandlers, debugLog, deduplicateAccounts, drainSseFrames, formatWaitTime, getAccountLabel, getClearedOAuthBody, getConfig, getConfigDir, getErrorCode, initCoreConfig, isClaimedByOther, isCodexStartupOutputEvent, isCodexStartupOutputFrame, isTTY, isTokenRefreshError, loadAccounts, loadConfig, loadPoolChainConfig, migrateFromAuthJson, openAIOAuthAdapter, parseCodexRetryAfterSeconds, parseKey, readClaims, readStorageFromDisk, releaseClaim, resetConfigCache, savePoolChainConfig, scoreQuotaResetPace, select, setAccountsFilename, setConfigGetter, showToast, sleep, superviseCodexResponseStartup, superviseSseResponseStartup, syncOpenCodeNativeBootstrapAuth, updateConfigField, withDirectoryLock, writeClaim, zeroCostProviderModels };
791
+ export { ACCOUNTS_FILENAME, ANSI, type AccountCredentialRefreshResult, type AccountManagerClass, type AccountManagerDependencies, type AccountManagerInstance, type AccountMetadataPatch, type AccountSelectionStrategy, AccountSelectionStrategySchema, type AccountStorage, AccountStorageSchema, AccountStore, type BuildFailoverPlanOptions, CODEX_STARTUP_PROBE_MAX_BYTES, CODEX_UNKNOWN_RATE_LIMIT_BACKOFF_MS, type CascadeState, CascadeStateManager, type ChainConfig, ChainConfigSchema, type ChainEntryConfig, ChainEntryConfigSchema, type ClaimsManager, type ClaimsMap, type ConfigLoader, type CoreConfig, type CredentialRefreshPatch, CredentialRefreshPatchSchema, type DiskCredentials, type ExecutorAccountManager, type ExecutorDependencies, type ExecutorRuntimeFactory, type FailoverCandidate, type FailoverPlan, type FailoverSkip, type KeyAction, type ManagedAccount, type MenuItem, type NativePluginLifecycle, type NativePluginLifecycleOptions, type NativePluginLoaderResult, type NativePluginManagedAccount, type NativePluginManagerClass, type NativePluginManagerLike, type NativePluginRefreshQueueLike, type NativePluginRuntimeFactoryLike, type NativePluginStoreLike, type OAuthAdapter, type OAuthAdapterPlanLabels, type OAuthAdapterTransformConfig, type OAuthCredentials, OAuthCredentialsSchema, type OpenCodeNativeAuthLoaderOptions, type OpenCodeNativeAuthMethod, type OpenCodeNativeAuthMethodsOptions, type OpenCodeNativeBootstrapAccount, type OpenCodeNativeBootstrapAuthOptions, type OpenCodeNativeBootstrapStore, type OpenCodeNativeLoaderContext, type PluginClient, type PluginConfig, PluginConfigSchema, type PoolChainConfig, PoolChainConfigSchema, type PoolConfig, PoolConfigSchema, PoolManager, type ProactiveRefreshDependencies, type ProactiveRefreshQueueClass, type ProactiveRefreshQueueInstance, type ProfileData, type QuotaResetPaceOptions, type QuotaRoutingWindow, type RateLimitAccountManager, type RateLimitDependencies, type RuntimeFactoryLike, type SelectOptions, type SseStartupSupervisorOptions, type StoredAccount, StoredAccountSchema, type SupervisedTurnResponse, TokenRefreshError, type TokenRefreshResult, type TurnFailureClass, type TurnFailurePhase, type TurnFailureSignal, type TurnResponseSupervisor, type UsageLimitEntry, UsageLimitEntrySchema, type UsageLimits, UsageLimitsSchema, __openCodeNativeBootstrapAuthTestUtils, anthropicOAuthAdapter, classifyCodexFailure, classifyCodexJsonEventFailure, classifyCodexSseStartupFailure, confirm, createAccountManagerForProvider, createClaimsManager, createConfigLoader, createExecutorForProvider, createMinimalClient, createOpenCodeNativeAuthLoader, createOpenCodeNativeAuthMethods, createOpenCodeNativePluginLifecycle, createProactiveRefreshQueueForProvider, createRateLimitHandlers, debugLog, deduplicateAccounts, drainSseFrames, formatWaitTime, getAccountLabel, getClearedOAuthBody, getConfig, getConfigDir, getErrorCode, initCoreConfig, isClaimedByOther, isCodexStartupOutputEvent, isCodexStartupOutputFrame, isTTY, isTokenRefreshError, loadAccounts, loadConfig, loadPoolChainConfig, migrateFromAuthJson, openAIOAuthAdapter, parseCodexRetryAfterSeconds, parseKey, readClaims, readStorageFromDisk, releaseClaim, resetConfigCache, savePoolChainConfig, scoreQuotaResetPace, select, setAccountsFilename, setConfigGetter, showToast, sleep, superviseCodexResponseStartup, superviseSseResponseStartup, syncOpenCodeNativeBootstrapAuth, updateConfigField, withDirectoryLock, writeClaim, zeroCostProviderModels };
package/dist/index.js CHANGED
@@ -46,6 +46,7 @@ var CredentialRefreshPatchSchema = v.object({
46
46
  });
47
47
  var StoredAccountSchema = v.object({
48
48
  uuid: v.optional(v.string()),
49
+ credentialOwnerId: v.optional(v.string()),
49
50
  accountId: v.optional(v.string()),
50
51
  accountUuid: v.optional(v.string()),
51
52
  deviceId: v.optional(v.string()),
@@ -581,11 +582,6 @@ function createAccountManagerForProvider(dependencies) {
581
582
  if (metadata.label) account.label = metadata.label;
582
583
  if (metadata.planTier !== void 0) account.planTier = metadata.planTier;
583
584
  }
584
- applyRefreshIdentityPatch(account, result) {
585
- if (result.patch.accountId) account.accountId = result.patch.accountId;
586
- if (result.patch.accountUuid && !account.accountUuid) account.accountUuid = result.patch.accountUuid;
587
- if (result.patch.deviceId && !account.deviceId) account.deviceId = result.patch.deviceId;
588
- }
589
585
  createNewAccount(auth, now, metadata) {
590
586
  const account = {
591
587
  uuid: randomUUID(),
@@ -947,32 +943,35 @@ function createAccountManagerForProvider(dependencies) {
947
943
  await this.refresh();
948
944
  await this.clearOpenCodeAuthIfNoAccountsRemain();
949
945
  }
950
- async markAuthFailure(uuid, result) {
951
- if (!result.ok && result.permanent) {
952
- await this.store.mutateStorage((storage) => {
953
- const account = storage.accounts.find((entry) => entry.uuid === uuid);
954
- if (!account) return;
946
+ async markAuthFailure(uuid, result, expected) {
947
+ const applyFailure = (account, storage) => {
948
+ if (!result.ok && result.permanent) {
955
949
  account.consecutiveAuthFailures = Math.max(
956
950
  (account.consecutiveAuthFailures ?? 0) + 1,
957
951
  getProviderConfig().max_consecutive_auth_failures
958
952
  );
959
953
  account.isAuthDisabled = true;
960
954
  account.authDisabledReason = "refresh failed permanently";
961
- });
962
- return;
963
- }
964
- await this.store.mutateStorage((storage) => {
965
- const account = storage.accounts.find((entry) => entry.uuid === uuid);
966
- if (!account) return;
955
+ return;
956
+ }
967
957
  account.consecutiveAuthFailures = (account.consecutiveAuthFailures ?? 0) + 1;
968
958
  const maxFailures = getProviderConfig().max_consecutive_auth_failures;
969
959
  const usableCount = storage.accounts.filter(
970
- (entry) => entry.enabled && !entry.isAuthDisabled && entry.uuid !== uuid
960
+ (entry) => entry.enabled && !entry.isAuthDisabled && entry.uuid !== account.uuid
971
961
  ).length;
972
962
  if (account.consecutiveAuthFailures >= maxFailures && usableCount > 0) {
973
963
  account.isAuthDisabled = true;
974
964
  account.authDisabledReason = `${maxFailures} consecutive auth failures`;
975
965
  }
966
+ };
967
+ if (expected) {
968
+ await this.store.mutateStorageIfCredentialsMatch(uuid, expected, applyFailure);
969
+ return;
970
+ }
971
+ await this.store.mutateStorage((storage) => {
972
+ const account = storage.accounts.find((entry) => entry.uuid === uuid);
973
+ if (!account) return;
974
+ applyFailure(account, storage);
976
975
  });
977
976
  }
978
977
  async applyUsageCache(uuid, usage) {
@@ -1004,26 +1003,20 @@ function createAccountManagerForProvider(dependencies) {
1004
1003
  patch: { accessToken: credentials.accessToken, expiresAt: credentials.expiresAt }
1005
1004
  };
1006
1005
  }
1007
- const result = await refreshToken(credentials.refreshToken, uuid, client);
1006
+ const { result, account: updated } = await this.store.refreshAccountCredentials(
1007
+ uuid,
1008
+ credentials,
1009
+ (refreshTokenValue) => refreshToken(refreshTokenValue, uuid, client)
1010
+ );
1008
1011
  if (!result.ok) return result;
1009
- const updated = await this.store.mutateAccount(uuid, (account) => {
1010
- account.accessToken = result.patch.accessToken;
1011
- account.expiresAt = result.patch.expiresAt;
1012
- if (result.patch.refreshToken) account.refreshToken = result.patch.refreshToken;
1013
- if (result.patch.uuid && result.patch.uuid !== uuid) account.uuid = result.patch.uuid;
1014
- this.applyRefreshIdentityPatch(account, result);
1015
- if (result.patch.email) account.email = result.patch.email;
1016
- account.consecutiveAuthFailures = 0;
1017
- account.isAuthDisabled = false;
1018
- account.authDisabledReason = void 0;
1019
- });
1020
- if (result.patch.uuid && result.patch.uuid !== uuid && this.activeAccountUuid === uuid) {
1021
- this.activeAccountUuid = result.patch.uuid;
1022
- this.store.setActiveUuid(result.patch.uuid).catch(() => {
1012
+ const nextUuid = updated?.uuid ?? uuid;
1013
+ if (nextUuid !== uuid && this.activeAccountUuid === uuid) {
1014
+ this.activeAccountUuid = nextUuid;
1015
+ this.store.setActiveUuid(nextUuid).catch(() => {
1023
1016
  });
1024
1017
  }
1025
- if (result.patch.uuid && result.patch.uuid !== uuid) {
1026
- this.replaceStickyBindingAccountUuid(uuid, result.patch.uuid);
1018
+ if (nextUuid !== uuid) {
1019
+ this.replaceStickyBindingAccountUuid(uuid, nextUuid);
1027
1020
  }
1028
1021
  if (updated && (uuid === this.activeAccountUuid || updated.uuid === this.activeAccountUuid)) {
1029
1022
  this.syncToOpenCode(updated);
@@ -1043,7 +1036,7 @@ function createAccountManagerForProvider(dependencies) {
1043
1036
  if (!account.uuid || !isTokenExpired(account)) return;
1044
1037
  const result = await this.ensureValidToken(account.uuid, client);
1045
1038
  if (!result.ok) {
1046
- await this.markAuthFailure(account.uuid, result);
1039
+ await this.markAuthFailure(account.uuid, result, account);
1047
1040
  }
1048
1041
  })
1049
1042
  );
@@ -1122,29 +1115,27 @@ function createAccountManagerForProvider(dependencies) {
1122
1115
  this.runtimeFactory?.invalidate(uuid);
1123
1116
  const credentials = await this.store.readCredentials(uuid);
1124
1117
  if (!credentials) return { ok: false, permanent: true };
1125
- const result = await refreshToken(credentials.refreshToken, uuid, client);
1118
+ const { result, account: refreshedAccount } = await this.store.refreshAccountCredentials(
1119
+ uuid,
1120
+ credentials,
1121
+ (refreshTokenValue) => refreshToken(refreshTokenValue, uuid, client)
1122
+ );
1126
1123
  if (result.ok) {
1127
- const updated = await this.store.mutateAccount(uuid, (account) => {
1128
- account.accessToken = result.patch.accessToken;
1129
- account.expiresAt = result.patch.expiresAt;
1130
- if (result.patch.refreshToken) account.refreshToken = result.patch.refreshToken;
1131
- if (result.patch.uuid) account.uuid = result.patch.uuid;
1132
- this.applyRefreshIdentityPatch(account, result);
1133
- if (result.patch.email) account.email = result.patch.email;
1124
+ const nextUuid = refreshedAccount?.uuid ?? uuid;
1125
+ const updated = await this.store.mutateAccount(nextUuid, (account) => {
1134
1126
  account.enabled = true;
1135
1127
  account.consecutiveAuthFailures = 0;
1136
1128
  });
1137
1129
  this.runtimeFactory?.invalidate(uuid);
1138
- if (result.patch.uuid) {
1139
- this.runtimeFactory?.invalidate(result.patch.uuid);
1130
+ if (nextUuid !== uuid) {
1131
+ this.runtimeFactory?.invalidate(nextUuid);
1140
1132
  }
1141
- const nextUuid = result.patch.uuid ?? uuid;
1142
- if (this.activeAccountUuid === uuid && result.patch.uuid && result.patch.uuid !== uuid) {
1143
- this.activeAccountUuid = result.patch.uuid;
1144
- await this.store.setActiveUuid(result.patch.uuid);
1133
+ if (this.activeAccountUuid === uuid && nextUuid !== uuid) {
1134
+ this.activeAccountUuid = nextUuid;
1135
+ await this.store.setActiveUuid(nextUuid);
1145
1136
  }
1146
- if (result.patch.uuid && result.patch.uuid !== uuid) {
1147
- this.replaceStickyBindingAccountUuid(uuid, result.patch.uuid);
1137
+ if (nextUuid !== uuid) {
1138
+ this.replaceStickyBindingAccountUuid(uuid, nextUuid);
1148
1139
  }
1149
1140
  if (updated && (uuid === this.activeAccountUuid || nextUuid === this.activeAccountUuid)) {
1150
1141
  const freshCredentials = await this.store.readCredentials(nextUuid);
@@ -1157,7 +1148,7 @@ function createAccountManagerForProvider(dependencies) {
1157
1148
  }
1158
1149
  }
1159
1150
  } else {
1160
- await this.markAuthFailure(uuid, result);
1151
+ await this.markAuthFailure(uuid, result, credentials);
1161
1152
  this.runtimeFactory?.invalidate(uuid);
1162
1153
  }
1163
1154
  return result;
@@ -1185,7 +1176,7 @@ function readRoutingUsageTiers(usage) {
1185
1176
 
1186
1177
  // src/account-store.ts
1187
1178
  import { promises as fs5 } from "fs";
1188
- import { randomBytes as randomBytes3 } from "crypto";
1179
+ import { createHash, randomBytes as randomBytes3 } from "crypto";
1189
1180
  import { dirname as dirname5, join as join5 } from "path";
1190
1181
  import * as v4 from "valibot";
1191
1182
 
@@ -1331,6 +1322,9 @@ async function withDirectoryLock(targetPath, fn, options) {
1331
1322
 
1332
1323
  // src/account-store.ts
1333
1324
  var FILE_MODE = 384;
1325
+ var REFRESH_LOCK_STALE_MS = 45e3;
1326
+ var REFRESH_LOCK_RETRY_DELAY_MS = 100;
1327
+ var REFRESH_LOCK_RETRIES = 31;
1334
1328
  function resolveStoragePath(filename) {
1335
1329
  return join5(getConfigDir2(), filename);
1336
1330
  }
@@ -1374,6 +1368,49 @@ async function ensureStorageFileExists(targetPath) {
1374
1368
  if (getErrorCode(error) !== "EEXIST") throw error;
1375
1369
  }
1376
1370
  }
1371
+ function sameCredentialSnapshot(account, expected) {
1372
+ return account.refreshToken === expected.refreshToken && account.accessToken === expected.accessToken && account.expiresAt === expected.expiresAt;
1373
+ }
1374
+ function findCredentialAccount(accounts, uuid, expected) {
1375
+ const exact = accounts.find((account) => account.uuid === uuid);
1376
+ if (exact) return exact;
1377
+ const ownerId = expected.credentialOwnerId ?? uuid;
1378
+ const ownerMatches = accounts.filter((account) => account.credentialOwnerId === ownerId);
1379
+ return ownerMatches.length === 1 ? ownerMatches[0] : void 0;
1380
+ }
1381
+ function refreshedCredentialPatch(account, previous) {
1382
+ if (!account.accessToken || !account.expiresAt) return null;
1383
+ const rotatedGrant = account.refreshToken !== previous.refreshToken;
1384
+ const newerExpiry = account.expiresAt > (previous.expiresAt ?? 0);
1385
+ const changedAccess = account.accessToken !== previous.accessToken;
1386
+ if (account.expiresAt <= Date.now() || !rotatedGrant && !newerExpiry && !changedAccess) return null;
1387
+ return {
1388
+ accessToken: account.accessToken,
1389
+ expiresAt: account.expiresAt,
1390
+ refreshToken: account.refreshToken,
1391
+ uuid: account.uuid,
1392
+ accountId: account.accountId,
1393
+ accountUuid: account.accountUuid,
1394
+ deviceId: account.deviceId,
1395
+ email: account.email
1396
+ };
1397
+ }
1398
+ function applyCredentialPatch(account, patch, credentialOwnerId) {
1399
+ account.accessToken = patch.accessToken;
1400
+ account.expiresAt = patch.expiresAt;
1401
+ if (patch.refreshToken) account.refreshToken = patch.refreshToken;
1402
+ if (patch.uuid && patch.uuid !== account.uuid) {
1403
+ account.credentialOwnerId ??= credentialOwnerId;
1404
+ account.uuid = patch.uuid;
1405
+ }
1406
+ if (patch.accountId) account.accountId = patch.accountId;
1407
+ if (patch.accountUuid && !account.accountUuid) account.accountUuid = patch.accountUuid;
1408
+ if (patch.deviceId && !account.deviceId) account.deviceId = patch.deviceId;
1409
+ if (patch.email) account.email = patch.email;
1410
+ account.consecutiveAuthFailures = 0;
1411
+ account.isAuthDisabled = false;
1412
+ account.authDisabledReason = void 0;
1413
+ }
1377
1414
  var AccountStore = class {
1378
1415
  storagePath;
1379
1416
  constructor(filename) {
@@ -1383,6 +1420,10 @@ var AccountStore = class {
1383
1420
  await ensureStorageFileExists(this.storagePath);
1384
1421
  return await withDirectoryLock(this.storagePath, () => fn(this.storagePath));
1385
1422
  }
1423
+ refreshLockPath(uuid, credentials) {
1424
+ const accountKey = createHash("sha256").update(credentials.credentialOwnerId ?? uuid).digest("hex");
1425
+ return `${this.storagePath}.refresh-${accountKey}`;
1426
+ }
1386
1427
  async load() {
1387
1428
  const storage = await loadAccounts(this.storagePath);
1388
1429
  return storage ?? createEmptyStorage();
@@ -1396,11 +1437,45 @@ var AccountStore = class {
1396
1437
  refreshToken: account.refreshToken,
1397
1438
  accessToken: account.accessToken,
1398
1439
  expiresAt: account.expiresAt,
1440
+ credentialOwnerId: account.credentialOwnerId,
1399
1441
  accountId: account.accountId,
1400
1442
  accountUuid: account.accountUuid,
1401
1443
  deviceId: account.deviceId
1402
1444
  };
1403
1445
  }
1446
+ async refreshAccountCredentials(uuid, expected, refresh) {
1447
+ const credentialOwnerId = expected.credentialOwnerId ?? uuid;
1448
+ return await withDirectoryLock(this.refreshLockPath(uuid, expected), async () => {
1449
+ const currentStorage = await readStorageFromDisk(this.storagePath, false);
1450
+ const current = currentStorage ? findCredentialAccount(currentStorage.accounts, uuid, expected) : void 0;
1451
+ if (!current) {
1452
+ return { result: { ok: false, permanent: true }, account: null };
1453
+ }
1454
+ let refreshSnapshot = expected;
1455
+ if (!sameCredentialSnapshot(current, expected)) {
1456
+ const patch = refreshedCredentialPatch(current, expected);
1457
+ if (patch) return { result: { ok: true, patch }, account: { ...current } };
1458
+ refreshSnapshot = current;
1459
+ }
1460
+ const result = await refresh(current.refreshToken);
1461
+ if (!result.ok) return { result, account: { ...current } };
1462
+ let persistedResult = result;
1463
+ const updated = await this.mutateAccount(current.uuid ?? uuid, (account) => {
1464
+ if (!sameCredentialSnapshot(account, refreshSnapshot)) {
1465
+ const patch = refreshedCredentialPatch(account, refreshSnapshot);
1466
+ persistedResult = patch ? { ok: true, patch } : { ok: false, permanent: false };
1467
+ return;
1468
+ }
1469
+ applyCredentialPatch(account, result.patch, credentialOwnerId);
1470
+ });
1471
+ if (!updated) return { result: { ok: false, permanent: false }, account: null };
1472
+ return { result: persistedResult, account: updated };
1473
+ }, {
1474
+ staleMs: REFRESH_LOCK_STALE_MS,
1475
+ retryDelayMs: REFRESH_LOCK_RETRY_DELAY_MS,
1476
+ retries: REFRESH_LOCK_RETRIES
1477
+ });
1478
+ }
1404
1479
  async mutateAccount(uuid, fn) {
1405
1480
  return await this.withLock(async (storagePath) => {
1406
1481
  const current = await readStorageFromDisk(storagePath, false);
@@ -1419,6 +1494,17 @@ var AccountStore = class {
1419
1494
  await writeStorageAtomic(storagePath, current);
1420
1495
  });
1421
1496
  }
1497
+ async mutateStorageIfCredentialsMatch(uuid, expected, fn) {
1498
+ return await this.withLock(async (storagePath) => {
1499
+ const current = await readStorageFromDisk(storagePath, false);
1500
+ if (!current) return false;
1501
+ const account = findCredentialAccount(current.accounts, uuid, expected);
1502
+ if (!account || !sameCredentialSnapshot(account, expected)) return false;
1503
+ fn(account, current);
1504
+ await writeStorageAtomic(storagePath, current);
1505
+ return true;
1506
+ });
1507
+ }
1422
1508
  async addAccount(account) {
1423
1509
  await this.withLock(async (storagePath) => {
1424
1510
  const current = await readStorageFromDisk(storagePath, false) ?? createEmptyStorage();
@@ -1457,7 +1543,7 @@ var AccountStore = class {
1457
1543
  };
1458
1544
 
1459
1545
  // src/executor.ts
1460
- import { createHash } from "crypto";
1546
+ import { createHash as createHash2 } from "crypto";
1461
1547
  var MIN_MAX_RETRIES = 6;
1462
1548
  var RETRIES_PER_ACCOUNT = 3;
1463
1549
  var MAX_SERVER_RETRIES_PER_ATTEMPT = 2;
@@ -1519,7 +1605,7 @@ function buildStickyKeyFromFirstUserText(text) {
1519
1605
  if (!text) {
1520
1606
  return void 0;
1521
1607
  }
1522
- return createHash("sha256").update(text).digest("hex").slice(0, 16);
1608
+ return createHash2("sha256").update(text).digest("hex").slice(0, 16);
1523
1609
  }
1524
1610
  function extractStickyKey(input, init) {
1525
1611
  const bodyText = init?.body ?? (input instanceof Request ? input.body : void 0);
@@ -1614,7 +1700,7 @@ function createExecutorForProvider(providerName, dependencies) {
1614
1700
  return { type: "retryOuter" };
1615
1701
  }
1616
1702
  }
1617
- await manager.markAuthFailure(accountUuid, { ok: false, permanent: false });
1703
+ await manager.markAuthFailure(accountUuid, { ok: false, permanent: false }, account);
1618
1704
  await manager.refresh();
1619
1705
  if (!manager.hasAnyUsableAccount()) {
1620
1706
  void showToast2(client, "All accounts have auth failures.", "error");
@@ -1704,7 +1790,7 @@ function createExecutorForProvider(providerName, dependencies) {
1704
1790
  await manager.markAuthFailure(accountUuid, {
1705
1791
  ok: false,
1706
1792
  permanent: error.permanent
1707
- });
1793
+ }, account);
1708
1794
  await manager.refresh();
1709
1795
  if (!manager.hasAnyUsableAccount()) {
1710
1796
  void showToast2(client, "All accounts have auth failures.", "error");
@@ -1833,24 +1919,15 @@ function createProactiveRefreshQueueForProvider(dependencies) {
1833
1919
  if (token !== this.runToken) return;
1834
1920
  const credentials = await this.store.readCredentials(account.uuid);
1835
1921
  if (!credentials || !this.needsProactiveRefresh(credentials)) continue;
1836
- const result = await refreshToken(credentials.refreshToken, account.uuid, this.client);
1922
+ const { result } = await this.store.refreshAccountCredentials(
1923
+ account.uuid,
1924
+ credentials,
1925
+ (refreshTokenValue) => refreshToken(refreshTokenValue, account.uuid, this.client)
1926
+ );
1837
1927
  if (result.ok) {
1838
- await this.store.mutateAccount(account.uuid, (target) => {
1839
- target.accessToken = result.patch.accessToken;
1840
- target.expiresAt = result.patch.expiresAt;
1841
- if (result.patch.refreshToken) target.refreshToken = result.patch.refreshToken;
1842
- if (result.patch.uuid) target.uuid = result.patch.uuid;
1843
- if (result.patch.email) target.email = result.patch.email;
1844
- if (result.patch.accountId) target.accountId = result.patch.accountId;
1845
- if (result.patch.accountUuid && !target.accountUuid) target.accountUuid = result.patch.accountUuid;
1846
- if (result.patch.deviceId && !target.deviceId) target.deviceId = result.patch.deviceId;
1847
- target.consecutiveAuthFailures = 0;
1848
- target.isAuthDisabled = false;
1849
- target.authDisabledReason = void 0;
1850
- });
1851
1928
  this.onInvalidate?.(account.uuid);
1852
1929
  } else {
1853
- await this.persistFailure(account, result.permanent);
1930
+ await this.persistFailure(account, result.permanent, credentials);
1854
1931
  }
1855
1932
  }
1856
1933
  } catch (error) {
@@ -1862,36 +1939,34 @@ function createProactiveRefreshQueueForProvider(dependencies) {
1862
1939
  }
1863
1940
  }
1864
1941
  }
1865
- async persistFailure(account, permanent) {
1942
+ async persistFailure(account, permanent, expected) {
1866
1943
  try {
1867
1944
  const accountUuid = account.uuid;
1868
1945
  if (!accountUuid) return;
1869
- if (permanent) {
1870
- await this.store.mutateStorage((storage) => {
1871
- const target = storage.accounts.find((entry) => entry.uuid === accountUuid);
1872
- if (!target) return;
1873
- target.consecutiveAuthFailures = Math.max(
1874
- (target.consecutiveAuthFailures ?? 0) + 1,
1875
- getConfig2().max_consecutive_auth_failures
1876
- );
1877
- target.isAuthDisabled = true;
1878
- target.authDisabledReason = "refresh failed permanently (proactive refresh)";
1879
- });
1880
- return;
1881
- }
1882
- await this.store.mutateStorage((storage) => {
1883
- const target = storage.accounts.find((entry) => entry.uuid === accountUuid);
1884
- if (!target) return;
1885
- target.consecutiveAuthFailures = (target.consecutiveAuthFailures ?? 0) + 1;
1886
- const maxFailures = getConfig2().max_consecutive_auth_failures;
1887
- const usableCount = storage.accounts.filter(
1888
- (entry) => entry.enabled && !entry.isAuthDisabled && entry.uuid !== accountUuid
1889
- ).length;
1890
- if (target.consecutiveAuthFailures >= maxFailures && usableCount > 0) {
1891
- target.isAuthDisabled = true;
1892
- target.authDisabledReason = `${maxFailures} consecutive auth failures (proactive refresh)`;
1946
+ await this.store.mutateStorageIfCredentialsMatch(
1947
+ accountUuid,
1948
+ expected,
1949
+ (target, storage) => {
1950
+ if (permanent) {
1951
+ target.consecutiveAuthFailures = Math.max(
1952
+ (target.consecutiveAuthFailures ?? 0) + 1,
1953
+ getConfig2().max_consecutive_auth_failures
1954
+ );
1955
+ target.isAuthDisabled = true;
1956
+ target.authDisabledReason = "refresh failed permanently (proactive refresh)";
1957
+ return;
1958
+ }
1959
+ target.consecutiveAuthFailures = (target.consecutiveAuthFailures ?? 0) + 1;
1960
+ const maxFailures = getConfig2().max_consecutive_auth_failures;
1961
+ const usableCount = storage.accounts.filter(
1962
+ (entry) => entry.enabled && !entry.isAuthDisabled && entry.uuid !== target.uuid
1963
+ ).length;
1964
+ if (target.consecutiveAuthFailures >= maxFailures && usableCount > 0) {
1965
+ target.isAuthDisabled = true;
1966
+ target.authDisabledReason = `${maxFailures} consecutive auth failures (proactive refresh)`;
1967
+ }
1893
1968
  }
1894
- });
1969
+ );
1895
1970
  } catch {
1896
1971
  debugLog2(this.client, `Failed to persist auth failure for ${account.uuid}`);
1897
1972
  }