codex-multi-auth 0.1.8 → 1.1.10
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/README.md +6 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +36 -63
- package/dist/index.js.map +1 -1
- package/dist/lib/accounts.d.ts +1 -1
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +29 -10
- package/dist/lib/accounts.js.map +1 -1
- package/dist/lib/codex-manager.d.ts.map +1 -1
- package/dist/lib/codex-manager.js +583 -235
- package/dist/lib/codex-manager.js.map +1 -1
- package/dist/lib/entitlement-cache.d.ts +8 -5
- package/dist/lib/entitlement-cache.d.ts.map +1 -1
- package/dist/lib/entitlement-cache.js +24 -9
- package/dist/lib/entitlement-cache.js.map +1 -1
- package/dist/lib/oc-chatgpt-import-adapter.d.ts.map +1 -1
- package/dist/lib/oc-chatgpt-import-adapter.js +49 -33
- package/dist/lib/oc-chatgpt-import-adapter.js.map +1 -1
- package/dist/lib/schemas.d.ts +7 -0
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/schemas.js +1 -1
- package/dist/lib/schemas.js.map +1 -1
- package/dist/lib/storage/migrations.d.ts +2 -2
- package/dist/lib/storage/migrations.d.ts.map +1 -1
- package/dist/lib/storage.d.ts +25 -13
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/storage.js +307 -144
- package/dist/lib/storage.js.map +1 -1
- package/package.json +132 -132
- package/scripts/codex-routing.js +1 -0
package/dist/lib/storage.js
CHANGED
|
@@ -713,107 +713,224 @@ function selectNewestAccount(current, candidate) {
|
|
|
713
713
|
const candidateAddedAt = candidate.addedAt || 0;
|
|
714
714
|
return candidateAddedAt >= currentAddedAt ? candidate : current;
|
|
715
715
|
}
|
|
716
|
-
function
|
|
717
|
-
|
|
718
|
-
|
|
716
|
+
function normalizeAccountIdKey(accountId) {
|
|
717
|
+
if (!accountId)
|
|
718
|
+
return undefined;
|
|
719
|
+
const trimmed = accountId.trim();
|
|
720
|
+
return trimmed || undefined;
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Normalize email keys for case-insensitive account identity matching.
|
|
724
|
+
*/
|
|
725
|
+
export function normalizeEmailKey(email) {
|
|
726
|
+
if (!email)
|
|
727
|
+
return undefined;
|
|
728
|
+
const trimmed = email.trim();
|
|
729
|
+
if (!trimmed)
|
|
730
|
+
return undefined;
|
|
731
|
+
return trimmed.toLowerCase();
|
|
732
|
+
}
|
|
733
|
+
function normalizeRefreshTokenKey(refreshToken) {
|
|
734
|
+
if (!refreshToken)
|
|
735
|
+
return undefined;
|
|
736
|
+
const trimmed = refreshToken.trim();
|
|
737
|
+
return trimmed || undefined;
|
|
738
|
+
}
|
|
739
|
+
function toAccountIdentityRef(account) {
|
|
740
|
+
return {
|
|
741
|
+
accountId: normalizeAccountIdKey(account?.accountId),
|
|
742
|
+
emailKey: normalizeEmailKey(account?.email),
|
|
743
|
+
refreshToken: normalizeRefreshTokenKey(account?.refreshToken),
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
function collectDistinctIdentityValues(values) {
|
|
747
|
+
const distinct = new Set();
|
|
748
|
+
for (const value of values) {
|
|
749
|
+
if (value)
|
|
750
|
+
distinct.add(value);
|
|
751
|
+
}
|
|
752
|
+
return distinct;
|
|
753
|
+
}
|
|
754
|
+
export function getAccountIdentityKey(account) {
|
|
755
|
+
const ref = toAccountIdentityRef(account);
|
|
756
|
+
if (ref.accountId && ref.emailKey) {
|
|
757
|
+
return `account:${ref.accountId}::email:${ref.emailKey}`;
|
|
758
|
+
}
|
|
759
|
+
if (ref.accountId)
|
|
760
|
+
return `account:${ref.accountId}`;
|
|
761
|
+
if (ref.emailKey)
|
|
762
|
+
return `email:${ref.emailKey}`;
|
|
763
|
+
if (ref.refreshToken)
|
|
764
|
+
return `refresh:${ref.refreshToken}`;
|
|
765
|
+
return undefined;
|
|
766
|
+
}
|
|
767
|
+
function findNewestMatchingIndex(accounts, predicate) {
|
|
768
|
+
let matchIndex;
|
|
769
|
+
let match;
|
|
719
770
|
for (let i = 0; i < accounts.length; i += 1) {
|
|
720
771
|
const account = accounts[i];
|
|
721
772
|
if (!account)
|
|
722
773
|
continue;
|
|
723
|
-
const
|
|
724
|
-
if (!
|
|
774
|
+
const ref = toAccountIdentityRef(account);
|
|
775
|
+
if (!predicate(ref))
|
|
725
776
|
continue;
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
777
|
+
if (matchIndex === undefined) {
|
|
778
|
+
matchIndex = i;
|
|
779
|
+
match = account;
|
|
729
780
|
continue;
|
|
730
781
|
}
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
for (const idx of keyToIndex.values()) {
|
|
736
|
-
indicesToKeep.add(idx);
|
|
737
|
-
}
|
|
738
|
-
const result = [];
|
|
739
|
-
for (let i = 0; i < accounts.length; i += 1) {
|
|
740
|
-
if (indicesToKeep.has(i)) {
|
|
741
|
-
const account = accounts[i];
|
|
742
|
-
if (account)
|
|
743
|
-
result.push(account);
|
|
782
|
+
const newest = selectNewestAccount(match, account);
|
|
783
|
+
if (newest === account) {
|
|
784
|
+
matchIndex = i;
|
|
785
|
+
match = account;
|
|
744
786
|
}
|
|
745
787
|
}
|
|
746
|
-
return
|
|
788
|
+
return matchIndex;
|
|
747
789
|
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
*/
|
|
754
|
-
export function deduplicateAccounts(accounts) {
|
|
755
|
-
return deduplicateAccountsByKey(accounts);
|
|
790
|
+
function findCompositeAccountMatchIndex(accounts, candidateRef) {
|
|
791
|
+
if (!candidateRef.accountId || !candidateRef.emailKey)
|
|
792
|
+
return undefined;
|
|
793
|
+
return findNewestMatchingIndex(accounts, (ref) => ref.accountId === candidateRef.accountId &&
|
|
794
|
+
ref.emailKey === candidateRef.emailKey);
|
|
756
795
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
* Accounts without email are always preserved.
|
|
760
|
-
* @param accounts - Array of accounts to deduplicate
|
|
761
|
-
* @returns New array with email duplicates removed
|
|
762
|
-
*/
|
|
763
|
-
export function normalizeEmailKey(email) {
|
|
764
|
-
if (!email)
|
|
796
|
+
function findSafeEmailMatchIndex(accounts, candidateRef) {
|
|
797
|
+
if (!candidateRef.emailKey)
|
|
765
798
|
return undefined;
|
|
766
|
-
const
|
|
767
|
-
|
|
799
|
+
const emailAccountIds = [candidateRef.accountId];
|
|
800
|
+
let foundAny = false;
|
|
801
|
+
for (let i = 0; i < accounts.length; i += 1) {
|
|
802
|
+
const account = accounts[i];
|
|
803
|
+
if (!account)
|
|
804
|
+
continue;
|
|
805
|
+
const ref = toAccountIdentityRef(account);
|
|
806
|
+
if (ref.emailKey !== candidateRef.emailKey)
|
|
807
|
+
continue;
|
|
808
|
+
foundAny = true;
|
|
809
|
+
emailAccountIds.push(ref.accountId);
|
|
810
|
+
}
|
|
811
|
+
if (!foundAny)
|
|
768
812
|
return undefined;
|
|
769
|
-
|
|
813
|
+
if (collectDistinctIdentityValues(emailAccountIds).size > 1) {
|
|
814
|
+
return undefined;
|
|
815
|
+
}
|
|
816
|
+
return findNewestMatchingIndex(accounts, (ref) => ref.emailKey === candidateRef.emailKey);
|
|
770
817
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
818
|
+
function findCompatibleRefreshTokenMatchIndex(accounts, candidateRef) {
|
|
819
|
+
if (!candidateRef.refreshToken)
|
|
820
|
+
return undefined;
|
|
821
|
+
let matchingIndex;
|
|
822
|
+
let matchingAccount = null;
|
|
774
823
|
for (let i = 0; i < accounts.length; i += 1) {
|
|
775
824
|
const account = accounts[i];
|
|
776
825
|
if (!account)
|
|
777
826
|
continue;
|
|
778
|
-
const
|
|
779
|
-
if (
|
|
780
|
-
indicesToKeep.add(i);
|
|
781
|
-
continue;
|
|
782
|
-
}
|
|
783
|
-
const existingIndex = emailToNewestIndex.get(email);
|
|
784
|
-
if (existingIndex === undefined) {
|
|
785
|
-
emailToNewestIndex.set(email, i);
|
|
827
|
+
const ref = toAccountIdentityRef(account);
|
|
828
|
+
if (ref.refreshToken !== candidateRef.refreshToken)
|
|
786
829
|
continue;
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
830
|
+
if ((candidateRef.accountId &&
|
|
831
|
+
ref.accountId &&
|
|
832
|
+
ref.accountId !== candidateRef.accountId) ||
|
|
833
|
+
(candidateRef.emailKey &&
|
|
834
|
+
ref.emailKey &&
|
|
835
|
+
ref.emailKey !== candidateRef.emailKey)) {
|
|
836
|
+
return undefined;
|
|
837
|
+
}
|
|
838
|
+
if (matchingIndex !== undefined &&
|
|
839
|
+
!candidateRef.accountId &&
|
|
840
|
+
!candidateRef.emailKey) {
|
|
841
|
+
return undefined;
|
|
842
|
+
}
|
|
843
|
+
if (matchingIndex === undefined || matchingAccount === null) {
|
|
844
|
+
matchingIndex = i;
|
|
845
|
+
matchingAccount = account;
|
|
792
846
|
continue;
|
|
793
847
|
}
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
const isNewer = candidateLastUsed > existingLastUsed ||
|
|
799
|
-
(candidateLastUsed === existingLastUsed &&
|
|
800
|
-
candidateAddedAt > existingAddedAt);
|
|
801
|
-
if (isNewer) {
|
|
802
|
-
emailToNewestIndex.set(email, i);
|
|
848
|
+
const newest = selectNewestAccount(matchingAccount ?? undefined, account);
|
|
849
|
+
if (newest === account) {
|
|
850
|
+
matchingIndex = i;
|
|
851
|
+
matchingAccount = account;
|
|
803
852
|
}
|
|
804
853
|
}
|
|
805
|
-
|
|
806
|
-
|
|
854
|
+
return matchingIndex;
|
|
855
|
+
}
|
|
856
|
+
function findUniqueAccountIdMatchIndex(accounts, candidateRef, options) {
|
|
857
|
+
if (!candidateRef.accountId)
|
|
858
|
+
return undefined;
|
|
859
|
+
if (!candidateRef.emailKey &&
|
|
860
|
+
!options.allowUniqueAccountIdFallbackWithoutEmail) {
|
|
861
|
+
return undefined;
|
|
807
862
|
}
|
|
808
|
-
|
|
863
|
+
let matchingIndex;
|
|
864
|
+
let matchingEmailKey;
|
|
809
865
|
for (let i = 0; i < accounts.length; i += 1) {
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
866
|
+
const account = accounts[i];
|
|
867
|
+
if (!account)
|
|
868
|
+
continue;
|
|
869
|
+
const ref = toAccountIdentityRef(account);
|
|
870
|
+
if (ref.accountId !== candidateRef.accountId)
|
|
871
|
+
continue;
|
|
872
|
+
if (matchingIndex !== undefined) {
|
|
873
|
+
return undefined;
|
|
874
|
+
}
|
|
875
|
+
matchingIndex = i;
|
|
876
|
+
matchingEmailKey = ref.emailKey;
|
|
877
|
+
}
|
|
878
|
+
if (matchingIndex !== undefined &&
|
|
879
|
+
matchingEmailKey &&
|
|
880
|
+
candidateRef.emailKey &&
|
|
881
|
+
matchingEmailKey !== candidateRef.emailKey) {
|
|
882
|
+
return undefined;
|
|
883
|
+
}
|
|
884
|
+
return matchingIndex;
|
|
885
|
+
}
|
|
886
|
+
export function findMatchingAccountIndex(accounts, candidate, options = {}) {
|
|
887
|
+
const candidateRef = toAccountIdentityRef(candidate);
|
|
888
|
+
const byComposite = findCompositeAccountMatchIndex(accounts, candidateRef);
|
|
889
|
+
if (byComposite !== undefined)
|
|
890
|
+
return byComposite;
|
|
891
|
+
const byEmail = findSafeEmailMatchIndex(accounts, candidateRef);
|
|
892
|
+
if (byEmail !== undefined)
|
|
893
|
+
return byEmail;
|
|
894
|
+
if (candidateRef.refreshToken) {
|
|
895
|
+
const byRefresh = findCompatibleRefreshTokenMatchIndex(accounts, candidateRef);
|
|
896
|
+
if (byRefresh !== undefined)
|
|
897
|
+
return byRefresh;
|
|
898
|
+
}
|
|
899
|
+
return findUniqueAccountIdMatchIndex(accounts, candidateRef, options);
|
|
900
|
+
}
|
|
901
|
+
export function resolveAccountSelectionIndex(accounts, candidate, fallbackIndex = 0) {
|
|
902
|
+
if (accounts.length === 0)
|
|
903
|
+
return 0;
|
|
904
|
+
const matchedIndex = findMatchingAccountIndex(accounts, candidate, {
|
|
905
|
+
allowUniqueAccountIdFallbackWithoutEmail: true,
|
|
906
|
+
});
|
|
907
|
+
if (matchedIndex !== undefined)
|
|
908
|
+
return matchedIndex;
|
|
909
|
+
return clampIndex(fallbackIndex, accounts.length);
|
|
910
|
+
}
|
|
911
|
+
function deduplicateAccountsByIdentity(accounts) {
|
|
912
|
+
const deduplicated = [];
|
|
913
|
+
for (const account of accounts) {
|
|
914
|
+
if (!account)
|
|
915
|
+
continue;
|
|
916
|
+
const existingIndex = findMatchingAccountIndex(deduplicated, account);
|
|
917
|
+
if (existingIndex === undefined) {
|
|
918
|
+
deduplicated.push(account);
|
|
919
|
+
continue;
|
|
814
920
|
}
|
|
921
|
+
deduplicated[existingIndex] = selectNewestAccount(deduplicated[existingIndex], account);
|
|
815
922
|
}
|
|
816
|
-
return
|
|
923
|
+
return deduplicated;
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Removes duplicate accounts, keeping the most recently used entry for each
|
|
927
|
+
* safely matched identity.
|
|
928
|
+
*/
|
|
929
|
+
export function deduplicateAccounts(accounts) {
|
|
930
|
+
return deduplicateAccountsByIdentity(accounts);
|
|
931
|
+
}
|
|
932
|
+
export function deduplicateAccountsByEmail(accounts) {
|
|
933
|
+
return deduplicateAccountsByIdentity(accounts);
|
|
817
934
|
}
|
|
818
935
|
function isRecord(value) {
|
|
819
936
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -823,20 +940,17 @@ function clampIndex(index, length) {
|
|
|
823
940
|
return 0;
|
|
824
941
|
return Math.max(0, Math.min(index, length - 1));
|
|
825
942
|
}
|
|
826
|
-
function
|
|
827
|
-
return account.accountId || account.refreshToken;
|
|
828
|
-
}
|
|
829
|
-
function extractActiveKey(accounts, activeIndex) {
|
|
943
|
+
function extractActiveAccountRef(accounts, activeIndex) {
|
|
830
944
|
const candidate = accounts[activeIndex];
|
|
831
945
|
if (!isRecord(candidate))
|
|
832
|
-
return
|
|
833
|
-
|
|
834
|
-
? candidate.accountId
|
|
835
|
-
: undefined
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
946
|
+
return {};
|
|
947
|
+
return toAccountIdentityRef({
|
|
948
|
+
accountId: typeof candidate.accountId === "string" ? candidate.accountId : undefined,
|
|
949
|
+
email: typeof candidate.email === "string" ? candidate.email : undefined,
|
|
950
|
+
refreshToken: typeof candidate.refreshToken === "string"
|
|
951
|
+
? candidate.refreshToken
|
|
952
|
+
: undefined,
|
|
953
|
+
});
|
|
840
954
|
}
|
|
841
955
|
/**
|
|
842
956
|
* Normalizes and validates account storage data, migrating from v1 to v3 if needed.
|
|
@@ -864,7 +978,7 @@ export function normalizeAccountStorage(data) {
|
|
|
864
978
|
? data.activeIndex
|
|
865
979
|
: 0;
|
|
866
980
|
const rawActiveIndex = clampIndex(activeIndexValue, rawAccounts.length);
|
|
867
|
-
const
|
|
981
|
+
const activeRef = extractActiveAccountRef(rawAccounts, rawActiveIndex);
|
|
868
982
|
const fromVersion = data.version;
|
|
869
983
|
const baseStorage = fromVersion === 1
|
|
870
984
|
? migrateV1ToV3(data)
|
|
@@ -872,16 +986,15 @@ export function normalizeAccountStorage(data) {
|
|
|
872
986
|
const validAccounts = rawAccounts.filter((account) => isRecord(account) &&
|
|
873
987
|
typeof account.refreshToken === "string" &&
|
|
874
988
|
!!account.refreshToken.trim());
|
|
875
|
-
const deduplicatedAccounts =
|
|
989
|
+
const deduplicatedAccounts = deduplicateAccounts(validAccounts);
|
|
876
990
|
const activeIndex = (() => {
|
|
877
991
|
if (deduplicatedAccounts.length === 0)
|
|
878
992
|
return 0;
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
}
|
|
884
|
-
return clampIndex(rawActiveIndex, deduplicatedAccounts.length);
|
|
993
|
+
return resolveAccountSelectionIndex(deduplicatedAccounts, {
|
|
994
|
+
accountId: activeRef.accountId,
|
|
995
|
+
email: activeRef.emailKey,
|
|
996
|
+
refreshToken: activeRef.refreshToken,
|
|
997
|
+
}, rawActiveIndex);
|
|
885
998
|
})();
|
|
886
999
|
const activeIndexByFamily = {};
|
|
887
1000
|
const rawFamilyIndices = isRecord(baseStorage.activeIndexByFamily)
|
|
@@ -893,15 +1006,12 @@ export function normalizeAccountStorage(data) {
|
|
|
893
1006
|
? rawIndexValue
|
|
894
1007
|
: rawActiveIndex;
|
|
895
1008
|
const clampedRawIndex = clampIndex(rawIndex, rawAccounts.length);
|
|
896
|
-
const
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
activeIndexByFamily[family] = mappedIndex;
|
|
1009
|
+
const familyRef = extractActiveAccountRef(rawAccounts, clampedRawIndex);
|
|
1010
|
+
activeIndexByFamily[family] = resolveAccountSelectionIndex(deduplicatedAccounts, {
|
|
1011
|
+
accountId: familyRef.accountId,
|
|
1012
|
+
email: familyRef.emailKey,
|
|
1013
|
+
refreshToken: familyRef.refreshToken,
|
|
1014
|
+
}, rawIndex);
|
|
905
1015
|
}
|
|
906
1016
|
return {
|
|
907
1017
|
version: 3,
|
|
@@ -1314,6 +1424,17 @@ async function saveAccountsUnlocked(storage) {
|
|
|
1314
1424
|
throw new StorageError(`Failed to save accounts: ${err?.message || "Unknown error"}`, code, path, hint, err instanceof Error ? err : undefined);
|
|
1315
1425
|
}
|
|
1316
1426
|
}
|
|
1427
|
+
function cloneAccountStorageForPersistence(storage) {
|
|
1428
|
+
return {
|
|
1429
|
+
version: 3,
|
|
1430
|
+
accounts: structuredClone(storage?.accounts ?? []),
|
|
1431
|
+
activeIndex: typeof storage?.activeIndex === "number" &&
|
|
1432
|
+
Number.isFinite(storage.activeIndex)
|
|
1433
|
+
? storage.activeIndex
|
|
1434
|
+
: 0,
|
|
1435
|
+
activeIndexByFamily: structuredClone(storage?.activeIndexByFamily ?? {}),
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1317
1438
|
export async function withAccountStorageTransaction(handler) {
|
|
1318
1439
|
return withStorageLock(async () => {
|
|
1319
1440
|
const state = {
|
|
@@ -1328,6 +1449,40 @@ export async function withAccountStorageTransaction(handler) {
|
|
|
1328
1449
|
return transactionSnapshotContext.run(state, () => handler(current, persist));
|
|
1329
1450
|
});
|
|
1330
1451
|
}
|
|
1452
|
+
export async function withAccountAndFlaggedStorageTransaction(handler) {
|
|
1453
|
+
return withStorageLock(async () => {
|
|
1454
|
+
const state = {
|
|
1455
|
+
snapshot: await loadAccountsInternal(saveAccountsUnlocked),
|
|
1456
|
+
active: true,
|
|
1457
|
+
};
|
|
1458
|
+
const current = state.snapshot;
|
|
1459
|
+
const persist = async (accountStorage, flaggedStorage) => {
|
|
1460
|
+
const previousAccounts = cloneAccountStorageForPersistence(state.snapshot);
|
|
1461
|
+
const nextAccounts = cloneAccountStorageForPersistence(accountStorage);
|
|
1462
|
+
await saveAccountsUnlocked(nextAccounts);
|
|
1463
|
+
try {
|
|
1464
|
+
await saveFlaggedAccountsUnlocked(flaggedStorage);
|
|
1465
|
+
state.snapshot = nextAccounts;
|
|
1466
|
+
}
|
|
1467
|
+
catch (error) {
|
|
1468
|
+
try {
|
|
1469
|
+
await saveAccountsUnlocked(previousAccounts);
|
|
1470
|
+
state.snapshot = previousAccounts;
|
|
1471
|
+
}
|
|
1472
|
+
catch (rollbackError) {
|
|
1473
|
+
const combinedError = new AggregateError([error, rollbackError], "Flagged save failed and account storage rollback also failed");
|
|
1474
|
+
log.error("Failed to rollback account storage after flagged save failure", {
|
|
1475
|
+
error: String(error),
|
|
1476
|
+
rollbackError: String(rollbackError),
|
|
1477
|
+
});
|
|
1478
|
+
throw combinedError;
|
|
1479
|
+
}
|
|
1480
|
+
throw error;
|
|
1481
|
+
}
|
|
1482
|
+
};
|
|
1483
|
+
return transactionSnapshotContext.run(state, () => handler(current, persist));
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1331
1486
|
/**
|
|
1332
1487
|
* Persists account storage to disk using atomic write (temp file + rename).
|
|
1333
1488
|
* Creates the Codex multi-auth storage directory if it doesn't exist.
|
|
@@ -1397,7 +1552,10 @@ function normalizeFlaggedStorage(data) {
|
|
|
1397
1552
|
value === "id_token" ||
|
|
1398
1553
|
value === "org" ||
|
|
1399
1554
|
value === "manual";
|
|
1400
|
-
const isSwitchReason = (value) => value === "rate-limit" ||
|
|
1555
|
+
const isSwitchReason = (value) => value === "rate-limit" ||
|
|
1556
|
+
value === "initial" ||
|
|
1557
|
+
value === "rotation" ||
|
|
1558
|
+
value === "best";
|
|
1401
1559
|
const isCooldownReason = (value) => value === "auth-failure" ||
|
|
1402
1560
|
value === "network-error" ||
|
|
1403
1561
|
value === "rate-limit";
|
|
@@ -1516,48 +1674,53 @@ export async function loadFlaggedAccounts() {
|
|
|
1516
1674
|
return empty;
|
|
1517
1675
|
}
|
|
1518
1676
|
}
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
if (existsSync(path)) {
|
|
1528
|
-
try {
|
|
1529
|
-
await fs.copyFile(path, `${path}.bak`);
|
|
1530
|
-
}
|
|
1531
|
-
catch (backupError) {
|
|
1532
|
-
log.warn("Failed to create flagged backup snapshot", {
|
|
1533
|
-
path,
|
|
1534
|
-
error: String(backupError),
|
|
1535
|
-
});
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
const content = JSON.stringify(normalizeFlaggedStorage(storage), null, 2);
|
|
1539
|
-
await fs.writeFile(tempPath, content, { encoding: "utf-8", mode: 0o600 });
|
|
1540
|
-
await fs.rename(tempPath, path);
|
|
1677
|
+
async function saveFlaggedAccountsUnlocked(storage) {
|
|
1678
|
+
const path = getFlaggedAccountsPath();
|
|
1679
|
+
const markerPath = getIntentionalResetMarkerPath(path);
|
|
1680
|
+
const uniqueSuffix = `${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
|
|
1681
|
+
const tempPath = `${path}.${uniqueSuffix}.tmp`;
|
|
1682
|
+
try {
|
|
1683
|
+
await fs.mkdir(dirname(path), { recursive: true });
|
|
1684
|
+
if (existsSync(path)) {
|
|
1541
1685
|
try {
|
|
1542
|
-
await
|
|
1686
|
+
await copyFileWithRetry(path, `${path}.bak`, {
|
|
1687
|
+
allowMissingSource: true,
|
|
1688
|
+
});
|
|
1543
1689
|
}
|
|
1544
|
-
catch {
|
|
1545
|
-
|
|
1690
|
+
catch (backupError) {
|
|
1691
|
+
log.warn("Failed to create flagged backup snapshot", {
|
|
1692
|
+
path,
|
|
1693
|
+
error: String(backupError),
|
|
1694
|
+
});
|
|
1546
1695
|
}
|
|
1547
1696
|
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1697
|
+
const content = JSON.stringify(normalizeFlaggedStorage(storage), null, 2);
|
|
1698
|
+
await fs.writeFile(tempPath, content, { encoding: "utf-8", mode: 0o600 });
|
|
1699
|
+
await renameFileWithRetry(tempPath, path);
|
|
1700
|
+
try {
|
|
1701
|
+
await fs.unlink(markerPath);
|
|
1702
|
+
}
|
|
1703
|
+
catch {
|
|
1704
|
+
// Best effort cleanup.
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
catch (error) {
|
|
1708
|
+
try {
|
|
1709
|
+
await fs.unlink(tempPath);
|
|
1560
1710
|
}
|
|
1711
|
+
catch {
|
|
1712
|
+
// Ignore cleanup failures.
|
|
1713
|
+
}
|
|
1714
|
+
log.error("Failed to save flagged account storage", {
|
|
1715
|
+
path,
|
|
1716
|
+
error: String(error),
|
|
1717
|
+
});
|
|
1718
|
+
throw error;
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
export async function saveFlaggedAccounts(storage) {
|
|
1722
|
+
return withStorageLock(async () => {
|
|
1723
|
+
await saveFlaggedAccountsUnlocked(storage);
|
|
1561
1724
|
});
|
|
1562
1725
|
}
|
|
1563
1726
|
export async function clearFlaggedAccounts() {
|
|
@@ -1635,7 +1798,7 @@ export async function exportAccounts(filePath, force = true, beforeCommit) {
|
|
|
1635
1798
|
}
|
|
1636
1799
|
/**
|
|
1637
1800
|
* Imports accounts from a JSON file, merging with existing accounts.
|
|
1638
|
-
* Deduplicates by
|
|
1801
|
+
* Deduplicates by safe account identity, preserving most recently used entries.
|
|
1639
1802
|
* @param filePath - Source file path
|
|
1640
1803
|
* @throws Error if file is invalid or would exceed MAX_ACCOUNTS
|
|
1641
1804
|
*/
|
|
@@ -1662,12 +1825,12 @@ export async function importAccounts(filePath) {
|
|
|
1662
1825
|
const existingActiveIndex = existing?.activeIndex ?? 0;
|
|
1663
1826
|
const merged = [...existingAccounts, ...normalized.accounts];
|
|
1664
1827
|
if (merged.length > ACCOUNT_LIMITS.MAX_ACCOUNTS) {
|
|
1665
|
-
const deduped =
|
|
1828
|
+
const deduped = deduplicateAccounts(merged);
|
|
1666
1829
|
if (deduped.length > ACCOUNT_LIMITS.MAX_ACCOUNTS) {
|
|
1667
1830
|
throw new Error(`Import would exceed maximum of ${ACCOUNT_LIMITS.MAX_ACCOUNTS} accounts (would have ${deduped.length})`);
|
|
1668
1831
|
}
|
|
1669
1832
|
}
|
|
1670
|
-
const deduplicatedAccounts =
|
|
1833
|
+
const deduplicatedAccounts = deduplicateAccounts(merged);
|
|
1671
1834
|
const newStorage = {
|
|
1672
1835
|
version: 3,
|
|
1673
1836
|
accounts: deduplicatedAccounts,
|