sidekick-shared 0.27.5 → 0.28.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/CHANGELOG.md +58 -0
- package/README.md +93 -52
- package/dist/accountHealth.d.ts +14 -0
- package/dist/accountHealth.js +61 -14
- package/dist/accountLaunch.d.ts +6 -0
- package/dist/accountLaunch.js +61 -4
- package/dist/accountLease.d.ts +5 -0
- package/dist/accountLease.js +98 -0
- package/dist/accountManager.d.ts +7 -0
- package/dist/accountManager.js +226 -89
- package/dist/accountSync.d.ts +1 -5
- package/dist/accountSync.js +22 -6
- package/dist/accountSyncTypes.d.ts +9 -0
- package/dist/accounts.d.ts +75 -18
- package/dist/accounts.js +712 -189
- package/dist/browser.d.ts +13 -2
- package/dist/browser.js +30 -2
- package/dist/claudeCredentialDoctor.d.ts +50 -0
- package/dist/claudeCredentialDoctor.js +277 -0
- package/dist/claudeCredentialLedger.d.ts +15 -0
- package/dist/claudeCredentialLedger.js +130 -0
- package/dist/claudeCredentials.d.ts +20 -3
- package/dist/claudeCredentials.js +50 -3
- package/dist/claudeLiveGuard.d.ts +33 -0
- package/dist/claudeLiveGuard.js +46 -0
- package/dist/claudePaths.d.ts +11 -0
- package/dist/claudePaths.js +61 -0
- package/dist/claudeProfiles.d.ts +15 -8
- package/dist/claudeProfiles.js +30 -23
- package/dist/claudeRefreshLock.d.ts +15 -0
- package/dist/claudeRefreshLock.js +145 -0
- package/dist/codexAuth.d.ts +4 -0
- package/dist/codexAuth.js +18 -0
- package/dist/codexCredentialDoctor.d.ts +33 -0
- package/dist/codexCredentialDoctor.js +302 -0
- package/dist/codexCredentialStore.d.ts +29 -0
- package/dist/codexCredentialStore.js +213 -0
- package/dist/codexKeepAlive.d.ts +2 -0
- package/dist/codexKeepAlive.js +116 -0
- package/dist/codexPaths.d.ts +30 -2
- package/dist/codexPaths.js +118 -7
- package/dist/codexProfiles.d.ts +38 -35
- package/dist/codexProfiles.js +848 -715
- package/dist/credentialIO.d.ts +67 -7
- package/dist/credentialIO.js +211 -47
- package/dist/doctor.js +2 -2
- package/dist/ensureDefaultAccounts.js +1 -8
- package/dist/esm/accountChangeSignal.js +15 -0
- package/dist/esm/accountChanges.js +152 -0
- package/dist/esm/accountHealth.js +288 -0
- package/dist/esm/accountLaunch.js +145 -0
- package/dist/esm/accountLease.js +60 -0
- package/dist/esm/accountManager.js +606 -0
- package/dist/esm/accountRegistry.js +214 -0
- package/dist/esm/accountStatus.js +51 -0
- package/dist/esm/accountSwitch.js +74 -0
- package/dist/esm/accountSync.js +156 -0
- package/dist/esm/accountSyncTypes.js +4 -0
- package/dist/esm/accounts.js +1291 -0
- package/dist/esm/aggregation/EventAggregator.js +2034 -0
- package/dist/esm/aggregation/FrequencyTracker.js +69 -0
- package/dist/esm/aggregation/HeatmapTracker.js +89 -0
- package/dist/esm/aggregation/PatternExtractor.js +168 -0
- package/dist/esm/aggregation/costProvenance.js +31 -0
- package/dist/esm/aggregation/snapshot.js +161 -0
- package/dist/esm/aggregation/types.js +5 -0
- package/dist/esm/analytics/codeImpact.js +18 -0
- package/dist/esm/analytics/compactionLedger.js +37 -0
- package/dist/esm/analytics/qualityScore.js +112 -0
- package/dist/esm/autoSwitch.js +185 -0
- package/dist/esm/browser.js +34 -0
- package/dist/esm/claudeCredentialDoctor.js +239 -0
- package/dist/esm/claudeCredentialLedger.js +91 -0
- package/dist/esm/claudeCredentials.js +111 -0
- package/dist/esm/claudeLiveGuard.js +43 -0
- package/dist/esm/claudePaths.js +22 -0
- package/dist/esm/claudeProfiles.js +142 -0
- package/dist/esm/claudeRefreshLock.js +106 -0
- package/dist/esm/codexAuth.js +101 -0
- package/dist/esm/codexCredentialDoctor.js +265 -0
- package/dist/esm/codexCredentialStore.js +164 -0
- package/dist/esm/codexKeepAlive.js +112 -0
- package/dist/esm/codexPaths.js +172 -0
- package/dist/esm/codexProfiles.js +1251 -0
- package/dist/esm/codexQuota.js +564 -0
- package/dist/esm/codexQuotaWatcher.js +363 -0
- package/dist/esm/context/composer.js +71 -0
- package/dist/esm/context/sessionContext.js +546 -0
- package/dist/esm/credentialIO.js +283 -0
- package/dist/esm/credentials.js +49 -0
- package/dist/esm/doctor.js +305 -0
- package/dist/esm/ensureDefaultAccounts.js +62 -0
- package/dist/esm/errorHistory.js +56 -0
- package/dist/esm/extractors/errorTaxonomy.js +43 -0
- package/dist/esm/extractors/gatherAssets.js +32 -0
- package/dist/esm/extractors/sessionAssets.js +176 -0
- package/dist/esm/extractors/sources/claudeAssets.js +158 -0
- package/dist/esm/extractors/sources/codexAssets.js +234 -0
- package/dist/esm/extractors/tokenUsage.js +41 -0
- package/dist/esm/extractors/toolCall.js +142 -0
- package/dist/esm/failingTools.js +65 -0
- package/dist/esm/fileHead.js +27 -0
- package/dist/esm/formatters/eventHighlighter.js +332 -0
- package/dist/esm/formatters/noiseClassifier.js +209 -0
- package/dist/esm/formatters/sessionDump.js +328 -0
- package/dist/esm/formatters/toolSummary.js +226 -0
- package/dist/esm/formatting.js +82 -0
- package/dist/esm/handoffUrl.js +23 -0
- package/dist/esm/historicalStore.js +305 -0
- package/dist/esm/humanPrompt.js +88 -0
- package/dist/esm/index.js +213 -0
- package/dist/esm/keepAlive.js +189 -0
- package/dist/esm/liveCredentialGuard.js +33 -0
- package/dist/esm/modelAliases.js +51 -0
- package/dist/esm/modelCatalog.js +139 -0
- package/dist/esm/modelContext.js +280 -0
- package/dist/esm/modelInfo.js +832 -0
- package/dist/esm/multiProviderQuotaService.js +450 -0
- package/dist/esm/node.js +46 -0
- package/dist/esm/observedContextWindows.js +165 -0
- package/dist/esm/observedSessionCollector.js +939 -0
- package/dist/esm/parsers/changelogParser.js +71 -0
- package/dist/esm/parsers/codexHistory.js +89 -0
- package/dist/esm/parsers/codexParser.js +906 -0
- package/dist/esm/parsers/debugLogParser.js +126 -0
- package/dist/esm/parsers/jsonl.js +66 -0
- package/dist/esm/parsers/mcpToolName.js +28 -0
- package/dist/esm/parsers/openCodeParser.js +625 -0
- package/dist/esm/parsers/planExtractor.js +335 -0
- package/dist/esm/parsers/sessionActivityDetector.js +246 -0
- package/dist/esm/parsers/sessionPathResolver.js +713 -0
- package/dist/esm/parsers/subagentScanner.js +360 -0
- package/dist/esm/parsers/subagentStatsCache.js +45 -0
- package/dist/esm/parsers/subagentTraceParser.js +325 -0
- package/dist/esm/paths.js +134 -0
- package/dist/esm/peakHours.js +194 -0
- package/dist/esm/phrases.js +1330 -0
- package/dist/esm/pricingCatalog.js +247 -0
- package/dist/esm/processDetection.js +200 -0
- package/dist/esm/projectMigration.js +59 -0
- package/dist/esm/promptHistory.js +1221 -0
- package/dist/esm/providerFailure.js +362 -0
- package/dist/esm/providerIds.js +6 -0
- package/dist/esm/providerQuota.js +1 -0
- package/dist/esm/providerServiceStatus.js +145 -0
- package/dist/esm/providerServiceStatusTypes.js +1 -0
- package/dist/esm/providerStatus.js +117 -0
- package/dist/esm/providers/claudeCode.js +575 -0
- package/dist/esm/providers/claudeListing.js +121 -0
- package/dist/esm/providers/codex.js +1034 -0
- package/dist/esm/providers/codexDatabase.js +297 -0
- package/dist/esm/providers/codexRollouts.js +215 -0
- package/dist/esm/providers/detect.js +185 -0
- package/dist/esm/providers/diagnostics.js +83 -0
- package/dist/esm/providers/directoryListingCache.js +206 -0
- package/dist/esm/providers/factory.js +54 -0
- package/dist/esm/providers/openCode.js +2008 -0
- package/dist/esm/providers/openCodeDatabase.js +419 -0
- package/dist/esm/providers/rolloutWalker.js +252 -0
- package/dist/esm/providers/types.js +12 -0
- package/dist/esm/quota.js +182 -0
- package/dist/esm/quotaHistory.js +364 -0
- package/dist/esm/quotaPoller.js +230 -0
- package/dist/esm/quotaPresentation.js +97 -0
- package/dist/esm/quotaResolver.js +231 -0
- package/dist/esm/quotaSnapshots.js +130 -0
- package/dist/esm/quotaThresholds.js +85 -0
- package/dist/esm/readers/decisions.js +24 -0
- package/dist/esm/readers/handoff.js +20 -0
- package/dist/esm/readers/helpers.js +27 -0
- package/dist/esm/readers/history.js +9 -0
- package/dist/esm/readers/notes.js +48 -0
- package/dist/esm/readers/plans.js +225 -0
- package/dist/esm/readers/tasks.js +19 -0
- package/dist/esm/report/htmlHelpers.js +280 -0
- package/dist/esm/report/htmlReportGenerator.js +704 -0
- package/dist/esm/report/index.js +7 -0
- package/dist/esm/report/logo.js +2 -0
- package/dist/esm/report/openBrowser.js +19 -0
- package/dist/esm/report/sessionReportInputs.js +33 -0
- package/dist/esm/report/transcriptParser.js +83 -0
- package/dist/esm/report/types.js +4 -0
- package/dist/esm/schemas/accountManager.js +204 -0
- package/dist/esm/schemas/accountStatus.js +25 -0
- package/dist/esm/schemas/assistantTurn.js +74 -0
- package/dist/esm/schemas/index.js +25 -0
- package/dist/esm/schemas/observedSessionV1.js +77 -0
- package/dist/esm/schemas/providerFailure.js +37 -0
- package/dist/esm/schemas/quota.js +132 -0
- package/dist/esm/schemas/quotaHistory.js +40 -0
- package/dist/esm/schemas/sessionEvent.js +216 -0
- package/dist/esm/schemas/stateFile.js +75 -0
- package/dist/esm/schemas/tokenEstimation.js +7 -0
- package/dist/esm/schemas/transcript.js +86 -0
- package/dist/esm/schemas/usageNormalization.js +38 -0
- package/dist/esm/search/advancedFilter.js +197 -0
- package/dist/esm/search/sessionSearch.js +66 -0
- package/dist/esm/sessionFingerprint.js +40 -0
- package/dist/esm/sessionMonitor.js +198 -0
- package/dist/esm/sessionPreviews.js +522 -0
- package/dist/esm/sessionPromptHistory.js +254 -0
- package/dist/esm/sessionPromptSignals.js +364 -0
- package/dist/esm/sessionStats.js +160 -0
- package/dist/esm/sessionTokenTotals.js +98 -0
- package/dist/esm/sessionTranscripts.js +49 -0
- package/dist/esm/stateFile.js +119 -0
- package/dist/esm/statusline/BurnRateCalculator.js +40 -0
- package/dist/esm/statusline/claudeStatuslinePayload.js +148 -0
- package/dist/esm/statusline/formatter.js +122 -0
- package/dist/esm/statusline/index.js +8 -0
- package/dist/esm/terminalSync.js +95 -0
- package/dist/esm/testing/credentialGuard.js +197 -0
- package/dist/esm/testing/index.js +12 -0
- package/dist/esm/testing/sessionLogFixtures.js +128 -0
- package/dist/esm/tokenEstimation.js +67 -0
- package/dist/esm/tokenSummary.js +113 -0
- package/dist/esm/toolCategory.js +82 -0
- package/dist/esm/transcript.js +257 -0
- package/dist/esm/turns/assistantTurn.js +373 -0
- package/dist/esm/types/codex.js +23 -0
- package/dist/esm/types/decisionLog.js +5 -0
- package/dist/esm/types/historicalData.js +47 -0
- package/dist/esm/types/knowledgeNote.js +15 -0
- package/dist/esm/types/observedSessionV1.js +257 -0
- package/dist/esm/types/opencode.js +12 -0
- package/dist/esm/types/plan.js +7 -0
- package/dist/esm/types/sessionEvent.js +10 -0
- package/dist/esm/types/taskPersistence.js +12 -0
- package/dist/esm/usage/billingBlocks.js +148 -0
- package/dist/esm/usage/claudeUsageDedupe.js +189 -0
- package/dist/esm/usage/importSessionHistory.js +84 -0
- package/dist/esm/usage/subagentUsage.js +18 -0
- package/dist/esm/usage/usageEvents.js +402 -0
- package/dist/esm/usage/usageReports.js +202 -0
- package/dist/esm/usageNormalization.js +95 -0
- package/dist/esm/watchers/eventBridge.js +219 -0
- package/dist/esm/watchers/factory.js +63 -0
- package/dist/esm/watchers/jsonlTail.js +289 -0
- package/dist/esm/watchers/jsonlWatcher.js +435 -0
- package/dist/esm/watchers/providerReaderWatcher.js +118 -0
- package/dist/esm/watchers/sqliteWatcher.js +285 -0
- package/dist/esm/watchers/types.js +4 -0
- package/dist/esm/workspaceRoots.js +56 -0
- package/dist/esm/writers/atomic.js +375 -0
- package/dist/esm/writers/decisions.js +51 -0
- package/dist/esm/writers/notes.js +46 -0
- package/dist/esm/writers/tasks.js +63 -0
- package/dist/esm/zaiQuota.js +323 -0
- package/dist/esm/zaiQuotaApi.js +271 -0
- package/dist/esm/zaiQuotaWatcher.js +265 -0
- package/dist/fileHead.d.ts +22 -0
- package/dist/fileHead.js +33 -0
- package/dist/index.d.ts +30 -11
- package/dist/index.js +72 -15
- package/dist/keepAlive.d.ts +2 -1
- package/dist/keepAlive.js +116 -29
- package/dist/liveCredentialGuard.d.ts +28 -0
- package/dist/liveCredentialGuard.js +36 -0
- package/dist/node.d.ts +13 -1
- package/dist/node.js +43 -2
- package/dist/parsers/codexHistory.js +2 -2
- package/dist/parsers/codexParser.d.ts +12 -0
- package/dist/parsers/codexParser.js +34 -12
- package/dist/parsers/debugLogParser.js +2 -2
- package/dist/parsers/sessionPathResolver.js +5 -4
- package/dist/processDetection.d.ts +10 -3
- package/dist/processDetection.js +44 -11
- package/dist/promptHistory.d.ts +32 -0
- package/dist/promptHistory.js +115 -59
- package/dist/providerFailure.d.ts +12 -6
- package/dist/providerFailure.js +45 -0
- package/dist/providers/claudeCode.js +7 -99
- package/dist/providers/claudeListing.d.ts +22 -0
- package/dist/providers/claudeListing.js +158 -0
- package/dist/providers/codex.d.ts +3 -1
- package/dist/providers/codex.js +26 -227
- package/dist/providers/codexRollouts.d.ts +44 -0
- package/dist/providers/codexRollouts.js +261 -0
- package/dist/providers/detect.js +4 -4
- package/dist/readers/plans.js +3 -3
- package/dist/schemas/accountManager.d.ts +22 -0
- package/dist/schemas/accountManager.js +4 -0
- package/dist/schemas/assistantTurn.d.ts +2 -2
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.js +7 -2
- package/dist/schemas/providerFailure.d.ts +154 -0
- package/dist/schemas/providerFailure.js +40 -0
- package/dist/sessionPromptHistory.d.ts +12 -7
- package/dist/sessionPromptHistory.js +15 -3
- package/dist/sessionPromptSignals.d.ts +9 -0
- package/dist/sessionPromptSignals.js +13 -0
- package/dist/terminalSync.d.ts +12 -2
- package/dist/terminalSync.js +27 -2
- package/dist/testing/credentialGuard.d.ts +23 -0
- package/dist/testing/credentialGuard.js +233 -0
- package/dist/testing/index.d.ts +14 -0
- package/dist/testing/index.js +22 -0
- package/dist/testing/sessionLogFixtures.d.ts +28 -0
- package/dist/testing/sessionLogFixtures.js +169 -0
- package/dist/tokenEstimation.d.ts +2 -0
- package/dist/tokenEstimation.js +21 -11
- package/dist/toolCategory.d.ts +18 -0
- package/dist/toolCategory.js +86 -0
- package/dist/types/codex.d.ts +4 -0
- package/dist/usage/usageEvents.d.ts +16 -0
- package/dist/usage/usageEvents.js +53 -9
- package/dist/watchers/jsonlTail.d.ts +26 -1
- package/dist/watchers/jsonlTail.js +136 -28
- package/dist/watchers/jsonlWatcher.js +7 -0
- package/dist/watchers/providerReaderWatcher.js +1 -0
- package/dist/watchers/types.d.ts +6 -0
- package/dist/workspaceRoots.d.ts +23 -0
- package/dist/workspaceRoots.js +93 -0
- package/package.json +22 -2
package/dist/codexProfiles.js
CHANGED
|
@@ -33,8 +33,10 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.CODEX_KEYRING_FIX_HINT = exports.getCodexCredentialStoreMode = exports.getSystemCodexHome = exports.getCodexProfileHome = exports.getCodexProfilesDir = void 0;
|
|
37
|
-
exports.
|
|
36
|
+
exports.CODEX_KEYRING_FIX_HINT = exports.getCodexExecutionEnv = exports.resolveSidekickCodexHome = exports.getCodexMonitoringHomes = exports.getCodexCredentialStoreMode = exports.getSystemCodexHome = exports.getCodexProfileHome = exports.getCodexProfilesDir = void 0;
|
|
37
|
+
exports.readStoredCodexAccount = readStoredCodexAccount;
|
|
38
|
+
exports.isLiveCodexProfile = isLiveCodexProfile;
|
|
39
|
+
exports.storeCodexProfileAuthUnlocked = storeCodexProfileAuthUnlocked;
|
|
38
40
|
exports.storeCodexProfileAuth = storeCodexProfileAuth;
|
|
39
41
|
exports.parseCodexLoginStatusOutput = parseCodexLoginStatusOutput;
|
|
40
42
|
exports.getCodexLoginStatus = getCodexLoginStatus;
|
|
@@ -45,23 +47,29 @@ exports.isCodexProfileAuthenticated = isCodexProfileAuthenticated;
|
|
|
45
47
|
exports.isCodexProfileAuthenticatedAsync = isCodexProfileAuthenticatedAsync;
|
|
46
48
|
exports.listCodexAccounts = listCodexAccounts;
|
|
47
49
|
exports.getActiveCodexAccount = getActiveCodexAccount;
|
|
48
|
-
exports.resolveSidekickCodexHome = resolveSidekickCodexHome;
|
|
49
50
|
exports.resolveActiveCodexAccount = resolveActiveCodexAccount;
|
|
50
|
-
exports.getCodexExecutionEnv = getCodexExecutionEnv;
|
|
51
51
|
exports.prepareCodexAccount = prepareCodexAccount;
|
|
52
52
|
exports.prepareCodexAccountAsync = prepareCodexAccountAsync;
|
|
53
|
+
exports.discardCodexLogin = discardCodexLogin;
|
|
53
54
|
exports.finalizeCodexAccount = finalizeCodexAccount;
|
|
54
55
|
exports.finalizeCodexAccountAsync = finalizeCodexAccountAsync;
|
|
55
56
|
exports.withCodexAuthSwapLock = withCodexAuthSwapLock;
|
|
56
57
|
exports.dedupeCodexProfiles = dedupeCodexProfiles;
|
|
58
|
+
exports.hasPendingCodexRepair = hasPendingCodexRepair;
|
|
57
59
|
exports.syncCodexLiveStateUnlocked = syncCodexLiveStateUnlocked;
|
|
58
60
|
exports.syncCodexLiveState = syncCodexLiveState;
|
|
61
|
+
exports.syncCodexLiveStateAsync = syncCodexLiveStateAsync;
|
|
59
62
|
exports.cleanupAbandonedCodexLogins = cleanupAbandonedCodexLogins;
|
|
60
63
|
exports.switchToCodexAccount = switchToCodexAccount;
|
|
61
64
|
exports.switchToCodexAccountAsync = switchToCodexAccountAsync;
|
|
62
65
|
exports.reconcileCodexAuthState = reconcileCodexAuthState;
|
|
63
66
|
exports.reconcileCodexAuthStateAsync = reconcileCodexAuthStateAsync;
|
|
67
|
+
exports.getCodexKeepAliveBlocker = getCodexKeepAliveBlocker;
|
|
68
|
+
exports.beginCodexKeepAlive = beginCodexKeepAlive;
|
|
69
|
+
exports.finishCodexKeepAlive = finishCodexKeepAlive;
|
|
70
|
+
exports.restoreCodexProfileFromBackup = restoreCodexProfileFromBackup;
|
|
64
71
|
exports.removeCodexAccount = removeCodexAccount;
|
|
72
|
+
exports.cleanupAbandonedCodexLoginsAsync = cleanupAbandonedCodexLoginsAsync;
|
|
65
73
|
const fs = __importStar(require("fs"));
|
|
66
74
|
const path = __importStar(require("path"));
|
|
67
75
|
const child_process_1 = require("child_process");
|
|
@@ -78,57 +86,30 @@ const accountSwitch_1 = require("./accountSwitch");
|
|
|
78
86
|
const accountSyncTypes_1 = require("./accountSyncTypes");
|
|
79
87
|
const processDetection_1 = require("./processDetection");
|
|
80
88
|
const quotaSnapshots_1 = require("./quotaSnapshots");
|
|
89
|
+
const accountLease_1 = require("./accountLease");
|
|
90
|
+
const liveCredentialGuard_1 = require("./liveCredentialGuard");
|
|
91
|
+
const codexCredentialStore_1 = require("./codexCredentialStore");
|
|
81
92
|
var codexPaths_2 = require("./codexPaths");
|
|
82
93
|
Object.defineProperty(exports, "getCodexProfilesDir", { enumerable: true, get: function () { return codexPaths_2.getCodexProfilesDir; } });
|
|
83
94
|
Object.defineProperty(exports, "getCodexProfileHome", { enumerable: true, get: function () { return codexPaths_2.getCodexProfileHome; } });
|
|
84
95
|
Object.defineProperty(exports, "getSystemCodexHome", { enumerable: true, get: function () { return codexPaths_2.getSystemCodexHome; } });
|
|
85
96
|
Object.defineProperty(exports, "getCodexCredentialStoreMode", { enumerable: true, get: function () { return codexPaths_2.getCodexCredentialStoreMode; } });
|
|
97
|
+
Object.defineProperty(exports, "getCodexMonitoringHomes", { enumerable: true, get: function () { return codexPaths_2.getCodexMonitoringHomes; } });
|
|
98
|
+
Object.defineProperty(exports, "resolveSidekickCodexHome", { enumerable: true, get: function () { return codexPaths_2.resolveSidekickCodexHome; } });
|
|
99
|
+
Object.defineProperty(exports, "getCodexExecutionEnv", { enumerable: true, get: function () { return codexPaths_2.getCodexExecutionEnv; } });
|
|
86
100
|
exports.CODEX_KEYRING_FIX_HINT = 'Set `cli_auth_credentials_store = "file"` in ~/.codex/config.toml and run `codex login` again.';
|
|
87
|
-
function dedupePaths(paths) {
|
|
88
|
-
const seen = new Set();
|
|
89
|
-
const unique = [];
|
|
90
|
-
for (const candidate of paths) {
|
|
91
|
-
const normalized = path.resolve(candidate);
|
|
92
|
-
if (seen.has(normalized))
|
|
93
|
-
continue;
|
|
94
|
-
seen.add(normalized);
|
|
95
|
-
unique.push(candidate);
|
|
96
|
-
}
|
|
97
|
-
return unique;
|
|
98
|
-
}
|
|
99
|
-
function getCodexMonitoringHomes() {
|
|
100
|
-
const explicitHome = (0, codexPaths_1.getExplicitCodexHome)();
|
|
101
|
-
if (explicitHome)
|
|
102
|
-
return [explicitHome];
|
|
103
|
-
// The system home is the single live home; profile homes only matter for
|
|
104
|
-
// sessions recorded back when they doubled as live CODEX_HOMEs.
|
|
105
|
-
const homes = [(0, codexPaths_1.getDefaultSystemCodexHome)()];
|
|
106
|
-
for (const profile of listCodexAccounts()) {
|
|
107
|
-
const profileHome = (0, codexPaths_1.getCodexProfileHome)(profile.id);
|
|
108
|
-
if (fs.existsSync(path.join(profileHome, 'sessions'))) {
|
|
109
|
-
homes.push(profileHome);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return dedupePaths(homes);
|
|
113
|
-
}
|
|
114
101
|
function readPendingProfile(profileId) {
|
|
115
102
|
try {
|
|
116
|
-
return JSON.parse(fs.readFileSync((0, codexPaths_1.
|
|
103
|
+
return JSON.parse(fs.readFileSync((0, codexPaths_1.getCodexLoginStatePath)(profileId), 'utf8'));
|
|
117
104
|
}
|
|
118
105
|
catch {
|
|
119
106
|
return null;
|
|
120
107
|
}
|
|
121
108
|
}
|
|
122
109
|
function writePendingProfile(profileId, pending) {
|
|
123
|
-
(0, codexPaths_1.
|
|
124
|
-
(
|
|
125
|
-
|
|
126
|
-
function copyIfExists(source, destination) {
|
|
127
|
-
if (!fs.existsSync(source))
|
|
128
|
-
return false;
|
|
129
|
-
fs.mkdirSync(path.dirname(destination), { recursive: true, mode: 0o700 });
|
|
130
|
-
fs.copyFileSync(source, destination);
|
|
131
|
-
return true;
|
|
110
|
+
const dir = (0, codexPaths_1.getCodexLoginDir)(profileId);
|
|
111
|
+
fs.mkdirSync(path.join(dir, 'codex-home'), { recursive: true, mode: 0o700 });
|
|
112
|
+
(0, atomic_1.atomicWriteJsonSync)(path.join(dir, 'profile.json'), pending);
|
|
132
113
|
}
|
|
133
114
|
/**
|
|
134
115
|
* Copy the live `config.toml` into a profile home, forcing file-based
|
|
@@ -139,24 +120,143 @@ function copySourceCodexConfig(sourceHome, targetHome) {
|
|
|
139
120
|
const source = (0, codexPaths_1.readFileOrNull)(path.join(sourceHome, 'config.toml')) ?? '';
|
|
140
121
|
(0, atomic_1.atomicWriteFileSync)(path.join(targetHome, 'config.toml'), (0, codexPaths_1.forceFileCredentialStore)(source));
|
|
141
122
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
123
|
+
/** Read the freshest saved login without creating a CLI-usable copy. */
|
|
124
|
+
function readStoredCodexAccount(profileId) {
|
|
125
|
+
const home = (0, codexCredentialStore_1.readCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(profileId));
|
|
126
|
+
const backup = (0, codexCredentialStore_1.readCodexBackup)(profileId);
|
|
127
|
+
if (home.status === 'ok' && backup.status === 'ok') {
|
|
128
|
+
return (0, codexCredentialStore_1.codexLoginOnly)((0, codexAuth_1.compareCodexAuthFreshness)(backup.snapshot.authRaw, home.snapshot.authRaw) > 0
|
|
129
|
+
? backup.snapshot
|
|
130
|
+
: home.snapshot);
|
|
131
|
+
}
|
|
132
|
+
return home.status === 'ok'
|
|
133
|
+
? (0, codexCredentialStore_1.codexLoginOnly)(home.snapshot)
|
|
134
|
+
: backup.status === 'ok'
|
|
135
|
+
? backup.snapshot
|
|
136
|
+
: null;
|
|
137
|
+
}
|
|
138
|
+
function profileMatchesSnapshot(profile, snapshot) {
|
|
139
|
+
const identity = (0, codexAuth_1.readAuthIdentityFromRaw)(snapshot.authRaw);
|
|
140
|
+
if ((0, codexAuth_1.identitiesMatch)(identity, profileIdentity(profile)))
|
|
141
|
+
return true;
|
|
142
|
+
// API keys have no account claims. Match their complete credential bytes, never the active pointer.
|
|
143
|
+
if (identity?.authMode === 'api-key' ||
|
|
144
|
+
(!snapshot.authRaw && (0, codexCredentialStore_1.isLegacyCodexLogin)(snapshot.legacyRaw))) {
|
|
145
|
+
const saved = readStoredCodexAccount(profile.id);
|
|
146
|
+
return (saved !== null &&
|
|
147
|
+
(0, codexCredentialStore_1.codexSnapshotFingerprint)((0, codexCredentialStore_1.codexLoginOnly)(snapshot)) === (0, codexCredentialStore_1.codexSnapshotFingerprint)(saved));
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
function isLiveCodexProfile(profile) {
|
|
152
|
+
const live = (0, codexCredentialStore_1.readCodexCredentialStore)();
|
|
153
|
+
return live.status === 'ok' && profileMatchesSnapshot(profile, live.snapshot);
|
|
154
|
+
}
|
|
155
|
+
/** Caller holds the swap lock. Live-account credentials only enter private storage. */
|
|
156
|
+
function storeCodexProfileAuthUnlocked(profileId, authRaw, legacyRaw, source) {
|
|
157
|
+
const candidate = (0, codexCredentialStore_1.codexLoginOnly)({ authRaw, legacyRaw });
|
|
158
|
+
const profile = listCodexAccounts().find((p) => p.id === profileId);
|
|
159
|
+
const identity = (0, codexAuth_1.readAuthIdentityFromRaw)(authRaw);
|
|
160
|
+
if (profile &&
|
|
161
|
+
identity?.authMode === 'chatgpt' &&
|
|
162
|
+
(identity.email || identity.workspaceId) &&
|
|
163
|
+
(profile.email || profile.metadata?.workspaceId) &&
|
|
164
|
+
!(0, codexAuth_1.identitiesMatch)(identity, profileIdentity(profile))) {
|
|
165
|
+
throw new Error('The Codex credential belongs to a different saved account.');
|
|
166
|
+
}
|
|
167
|
+
const old = (0, codexCredentialStore_1.readCodexBackup)(profileId);
|
|
168
|
+
const chosen = old.status === 'ok' && (0, codexAuth_1.compareCodexAuthFreshness)(old.snapshot.authRaw, authRaw) > 0
|
|
169
|
+
? old.snapshot
|
|
170
|
+
: candidate;
|
|
171
|
+
(0, codexCredentialStore_1.writeCodexBackup)(profileId, chosen);
|
|
172
|
+
const live = (0, codexCredentialStore_1.readCodexCredentialStore)();
|
|
173
|
+
const sameLink = live.status === 'ok' &&
|
|
174
|
+
(0, codexAuth_1.codexRefreshFingerprint)(chosen.authRaw) !== null &&
|
|
175
|
+
(0, codexAuth_1.codexRefreshFingerprint)(chosen.authRaw) === (0, codexAuth_1.codexRefreshFingerprint)(live.snapshot.authRaw);
|
|
176
|
+
const isLive = live.status === 'ok' &&
|
|
177
|
+
(profile
|
|
178
|
+
? profileMatchesSnapshot(profile, live.snapshot)
|
|
179
|
+
: (0, codexAuth_1.identitiesMatch)((0, codexAuth_1.readAuthIdentityFromRaw)(chosen.authRaw), (0, codexAuth_1.readAuthIdentityFromRaw)(live.snapshot.authRaw)));
|
|
180
|
+
if (!(0, codexCredentialStore_1.isSidekickCodexHome)((0, codexPaths_1.getSystemCodexHome)()) &&
|
|
181
|
+
live.status !== 'error' &&
|
|
182
|
+
live.status !== 'corrupt' &&
|
|
183
|
+
!isLive &&
|
|
184
|
+
!sameLink) {
|
|
185
|
+
(0, codexCredentialStore_1.writeInactiveCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(profileId), chosen);
|
|
186
|
+
}
|
|
187
|
+
if (chosen.authRaw)
|
|
188
|
+
(0, accountHealth_1.writeCodexHealthSidecar)(profileId, chosen.authRaw, source);
|
|
146
189
|
}
|
|
147
|
-
/**
|
|
148
|
-
* The single chokepoint for storing a Codex credential in a profile: the raw
|
|
149
|
-
* `auth.json` bytes (never re-serialized), the legacy `.credentials.json`
|
|
150
|
-
* when present, and the secret-free health sidecar.
|
|
151
|
-
*/
|
|
152
190
|
function storeCodexProfileAuth(profileId, authRaw, legacyRaw, source) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
if (
|
|
159
|
-
|
|
191
|
+
withCodexAuthSwapLock(() => storeCodexProfileAuthUnlocked(profileId, authRaw, legacyRaw, source));
|
|
192
|
+
}
|
|
193
|
+
function consumerBlocker(scan) {
|
|
194
|
+
if (!scan?.ok)
|
|
195
|
+
return 'Could not verify that Codex consumers are stopped; retry after closing Codex.';
|
|
196
|
+
if (scan.consumers.length)
|
|
197
|
+
return 'Close running Codex CLI, app, and IDE sessions before changing credentials. --force cannot bypass this check.';
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
/** The sole installer of live Codex credentials, including rollback and repair. */
|
|
201
|
+
function installIntoLiveCodexStore(candidate, accountId, intent, context) {
|
|
202
|
+
if ((0, accountLease_1.isAccountLeaseHeld)('codex', accountId))
|
|
203
|
+
return { ok: false, reason: 'Keep-alive holds this account; retry when it finishes.' };
|
|
204
|
+
const blocker = consumerBlocker(context.scan);
|
|
205
|
+
if (blocker)
|
|
206
|
+
return { ok: false, reason: blocker };
|
|
207
|
+
const mode = (0, codexPaths_1.getCodexCredentialStoreMode)();
|
|
208
|
+
if (mode !== 'file' && mode !== 'unknown')
|
|
209
|
+
return { ok: false, reason: exports.CODEX_KEYRING_FIX_HINT };
|
|
210
|
+
if ((0, codexCredentialStore_1.isSidekickCodexHome)((0, codexPaths_1.getSystemCodexHome)()))
|
|
211
|
+
return { ok: false, reason: 'Unset CODEX_HOME; it points at a Sidekick-owned home.' };
|
|
212
|
+
const current = (0, codexCredentialStore_1.readCodexCredentialStore)();
|
|
213
|
+
if (context.before.status === 'error' || !(0, codexCredentialStore_1.codexStoreMatches)(current, context.before.snapshot)) {
|
|
214
|
+
return { ok: false, reason: 'The live Codex credential changed; nothing was overwritten.' };
|
|
215
|
+
}
|
|
216
|
+
const raw = current.status === 'ok' ? current.snapshot.authRaw : null;
|
|
217
|
+
const fp = (0, codexAuth_1.codexRefreshFingerprint)(raw);
|
|
218
|
+
const candidateIdentity = (0, codexAuth_1.readAuthIdentityFromRaw)(candidate.authRaw);
|
|
219
|
+
const candidateProfile = listCodexAccounts().find((p) => p.id === accountId);
|
|
220
|
+
const validIdentity = candidateIdentity?.authMode !== 'chatgpt' ||
|
|
221
|
+
(candidateProfile !== undefined &&
|
|
222
|
+
(0, codexAuth_1.identitiesMatch)(candidateIdentity, profileIdentity(candidateProfile)));
|
|
223
|
+
const decision = (0, liveCredentialGuard_1.decideLiveCredentialWrite)({
|
|
224
|
+
provider: 'Codex',
|
|
225
|
+
intent,
|
|
226
|
+
liveStatus: current.status,
|
|
227
|
+
candidateValid: validIdentity && ((0, codexCredentialStore_1.isCodexLogin)(candidate.authRaw) || (0, codexCredentialStore_1.isLegacyCodexLogin)(candidate.legacyRaw)),
|
|
228
|
+
liveHasCredential: current.status === 'ok',
|
|
229
|
+
liveAccountId: context.liveAccountId,
|
|
230
|
+
candidateAccountId: accountId,
|
|
231
|
+
sameLink: fp !== null && fp === (0, codexAuth_1.codexRefreshFingerprint)(candidate.authRaw),
|
|
232
|
+
strictlyNewer: (0, codexAuth_1.compareCodexAuthFreshness)(candidate.authRaw, raw) > 0,
|
|
233
|
+
capturedOutgoing: context.capturedOutgoing,
|
|
234
|
+
matchesWritten: context.written !== undefined && (0, codexCredentialStore_1.codexStoreMatches)(current, context.written),
|
|
235
|
+
rejectCorrupt: true,
|
|
236
|
+
});
|
|
237
|
+
if (!decision.allow)
|
|
238
|
+
return { ok: false, reason: decision.reason };
|
|
239
|
+
if (current.status === 'error')
|
|
240
|
+
return { ok: false, reason: 'The live Codex login could not be read.' };
|
|
241
|
+
let expected;
|
|
242
|
+
try {
|
|
243
|
+
expected = (0, codexCredentialStore_1.codexInstalledSnapshot)(current.snapshot, candidate);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return {
|
|
247
|
+
ok: false,
|
|
248
|
+
reason: 'Legacy credentials would replace unrelated MCP data; sign in again.',
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
(0, codexCredentialStore_1.writeLiveCodexCredentialStoreUnguarded)(expected);
|
|
253
|
+
if (!(0, codexCredentialStore_1.codexStoreMatches)((0, codexCredentialStore_1.readCodexCredentialStore)(), expected))
|
|
254
|
+
throw new Error('verification failed');
|
|
255
|
+
return { ok: true, installed: expected };
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return { ok: false, reason: 'The live Codex write did not verify.', attempted: expected };
|
|
259
|
+
}
|
|
160
260
|
}
|
|
161
261
|
function readMetadataFromAuthJson(codexHome) {
|
|
162
262
|
const identity = (0, codexAuth_1.readAuthIdentityFromRaw)((0, codexPaths_1.readFileOrNull)(path.join(codexHome, 'auth.json')));
|
|
@@ -212,7 +312,7 @@ function getCodexLoginStatus(codexHome) {
|
|
|
212
312
|
timeout: PROBE_TIMEOUT_MS,
|
|
213
313
|
killSignal: 'SIGKILL',
|
|
214
314
|
});
|
|
215
|
-
return parseCodexLoginStatusOutput(result.status, String(result.stdout ?? ''));
|
|
315
|
+
return parseCodexLoginStatusOutput(result.status, `${String(result.stdout ?? '')}\n${String(result.stderr ?? '')}`);
|
|
216
316
|
}
|
|
217
317
|
catch {
|
|
218
318
|
// Ignore missing CLI or spawn errors.
|
|
@@ -223,9 +323,11 @@ function getCodexLoginStatusAsync(codexHome) {
|
|
|
223
323
|
return new Promise((resolve) => {
|
|
224
324
|
try {
|
|
225
325
|
const env = { ...process.env, CODEX_HOME: codexHome };
|
|
226
|
-
(0, child_process_1.execFile)('codex', ['login', 'status'], { encoding: 'utf8', env, timeout: PROBE_TIMEOUT_MS, killSignal: 'SIGKILL' }, (error, stdout) => {
|
|
326
|
+
(0, child_process_1.execFile)('codex', ['login', 'status'], { encoding: 'utf8', env, timeout: PROBE_TIMEOUT_MS, killSignal: 'SIGKILL' }, (error, stdout, stderr) => {
|
|
227
327
|
// A missing CLI, non-zero exit, or timeout all mean "not logged in".
|
|
228
|
-
resolve(error
|
|
328
|
+
resolve(error
|
|
329
|
+
? { loggedIn: false }
|
|
330
|
+
: parseCodexLoginStatusOutput(0, `${String(stdout ?? '')}\n${String(stderr ?? '')}`));
|
|
229
331
|
});
|
|
230
332
|
}
|
|
231
333
|
catch {
|
|
@@ -258,15 +360,13 @@ async function readCodexAccountMetadataAsync(codexHome) {
|
|
|
258
360
|
metadataFromLoginStatus(await getCodexLoginStatusAsync(codexHome)));
|
|
259
361
|
}
|
|
260
362
|
function hasCodexCredentialFiles(codexHome) {
|
|
261
|
-
return (
|
|
262
|
-
fs.existsSync(path.join(codexHome, '.credentials.json')));
|
|
363
|
+
return (0, codexCredentialStore_1.readCodexCredentialStore)(codexHome).status === 'ok';
|
|
263
364
|
}
|
|
264
365
|
function isCodexProfileAuthenticated(codexHome) {
|
|
265
|
-
return hasCodexCredentialFiles(codexHome)
|
|
366
|
+
return hasCodexCredentialFiles(codexHome);
|
|
266
367
|
}
|
|
267
|
-
/** {@link isCodexProfileAuthenticated} without blocking the event loop on the CLI probe. */
|
|
268
368
|
async function isCodexProfileAuthenticatedAsync(codexHome) {
|
|
269
|
-
return
|
|
369
|
+
return isCodexProfileAuthenticated(codexHome);
|
|
270
370
|
}
|
|
271
371
|
function ensureUniqueCodexLabel(label, excludeId) {
|
|
272
372
|
const normalized = label.trim().toLowerCase();
|
|
@@ -279,11 +379,6 @@ function listCodexAccounts() {
|
|
|
279
379
|
function getActiveCodexAccount() {
|
|
280
380
|
return (0, accountRegistry_1.getActiveSavedAccount)('codex');
|
|
281
381
|
}
|
|
282
|
-
function resolveSidekickCodexHome() {
|
|
283
|
-
// Account switching swaps auth.json inside the system home, so the system
|
|
284
|
-
// home (or an explicit CODEX_HOME) is always the single live home.
|
|
285
|
-
return (0, codexPaths_1.getSystemCodexHome)();
|
|
286
|
-
}
|
|
287
382
|
function profileIdentity(profile) {
|
|
288
383
|
return {
|
|
289
384
|
email: profile.email ?? profile.metadata?.email,
|
|
@@ -305,7 +400,7 @@ function profileIdentity(profile) {
|
|
|
305
400
|
*/
|
|
306
401
|
function resolveActiveCodexAccount(options = {}) {
|
|
307
402
|
const selfHeal = options.selfHeal ?? true;
|
|
308
|
-
const identity = (0, codexAuth_1.readAuthIdentityFromRaw)((0, codexPaths_1.readFileOrNull)(path.join(resolveSidekickCodexHome(), 'auth.json')));
|
|
403
|
+
const identity = (0, codexAuth_1.readAuthIdentityFromRaw)((0, codexPaths_1.readFileOrNull)(path.join((0, codexPaths_1.resolveSidekickCodexHome)(), 'auth.json')));
|
|
309
404
|
if (identity && (identity.email || identity.workspaceId)) {
|
|
310
405
|
const match = findProfileForIdentity(identity);
|
|
311
406
|
if (match) {
|
|
@@ -344,226 +439,190 @@ function resolveActiveCodexAccount(options = {}) {
|
|
|
344
439
|
}
|
|
345
440
|
return { source: 'none' };
|
|
346
441
|
}
|
|
347
|
-
|
|
348
|
-
return {
|
|
349
|
-
...baseEnv,
|
|
350
|
-
CODEX_HOME: resolveSidekickCodexHome(),
|
|
351
|
-
};
|
|
352
|
-
}
|
|
442
|
+
// ── Prepare / finalize (add account) ─────────────────────────────────────
|
|
353
443
|
function keyringRefusal(mode) {
|
|
354
444
|
return {
|
|
355
445
|
success: false,
|
|
356
446
|
needsLogin: false,
|
|
357
|
-
error: `Codex
|
|
447
|
+
error: `Codex credential storage (${mode}) is not file-based. ${exports.CODEX_KEYRING_FIX_HINT}`,
|
|
358
448
|
};
|
|
359
449
|
}
|
|
360
|
-
function prepareCodexAccountCore(label) {
|
|
450
|
+
function prepareCodexAccountCore(label, importCurrent, scan) {
|
|
361
451
|
const trimmedLabel = label.trim();
|
|
362
|
-
if (!trimmedLabel)
|
|
363
|
-
return {
|
|
364
|
-
}
|
|
452
|
+
if (!trimmedLabel)
|
|
453
|
+
return { success: false, error: 'Codex accounts require a non-empty label.' };
|
|
365
454
|
const labelError = ensureUniqueCodexLabel(trimmedLabel);
|
|
366
|
-
if (labelError)
|
|
367
|
-
return {
|
|
368
|
-
}
|
|
455
|
+
if (labelError)
|
|
456
|
+
return { success: false, error: labelError };
|
|
369
457
|
const sourceHome = (0, codexPaths_1.getSystemCodexHome)();
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const imported = importCurrentCodexAuth(sourceHome, codexHome);
|
|
384
|
-
if (imported) {
|
|
385
|
-
// The live login may already be saved under another profile: fold into it
|
|
386
|
-
// instead of creating a duplicate seat.
|
|
387
|
-
const identity = (0, codexAuth_1.readAuthIdentityFromRaw)((0, codexPaths_1.readFileOrNull)(path.join(codexHome, 'auth.json')));
|
|
388
|
-
const existing = identity
|
|
389
|
-
? findProfileForIdentity(identity, { fallbackToActive: false })
|
|
390
|
-
: null;
|
|
391
|
-
if (existing) {
|
|
392
|
-
try {
|
|
393
|
-
const liveRefresh = (0, codexAuth_1.readLastRefresh)((0, codexPaths_1.readFileOrNull)(path.join(codexHome, 'auth.json')));
|
|
394
|
-
const storedRefresh = (0, codexAuth_1.readLastRefresh)((0, codexPaths_1.readFileOrNull)(path.join((0, codexPaths_1.getCodexProfileHome)(existing.id), 'auth.json')));
|
|
395
|
-
const storedIsNewer = storedRefresh !== null && liveRefresh !== null && storedRefresh > liveRefresh;
|
|
396
|
-
if (!storedIsNewer) {
|
|
397
|
-
storeCodexProfileAuth(existing.id, (0, codexPaths_1.readFileOrNull)(path.join(codexHome, 'auth.json')), (0, codexPaths_1.readFileOrNull)(path.join(codexHome, '.credentials.json')), 'login');
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
catch {
|
|
401
|
-
/* the existing backup stays as it was */
|
|
402
|
-
}
|
|
403
|
-
fs.rmSync((0, codexPaths_1.getCodexProfileDir)(profileId), { recursive: true, force: true });
|
|
404
|
-
const relabel = existing.metadata?.origin === 'live-sync' && existing.label === existing.email
|
|
405
|
-
? {
|
|
406
|
-
...existing,
|
|
407
|
-
label: trimmedLabel,
|
|
408
|
-
metadata: { ...existing.metadata, origin: 'manual' },
|
|
409
|
-
}
|
|
410
|
-
: existing;
|
|
411
|
-
if (relabel !== existing)
|
|
412
|
-
(0, accountRegistry_1.upsertSavedAccountProfile)(relabel);
|
|
458
|
+
const mode = (0, codexPaths_1.getCodexCredentialStoreMode)(sourceHome);
|
|
459
|
+
if (mode !== 'file' && mode !== 'unknown')
|
|
460
|
+
return keyringRefusal(mode);
|
|
461
|
+
if ((0, codexCredentialStore_1.isSidekickCodexHome)(sourceHome))
|
|
462
|
+
return {
|
|
463
|
+
success: false,
|
|
464
|
+
error: 'Unset CODEX_HOME before adding an account; it names a Sidekick-owned home.',
|
|
465
|
+
};
|
|
466
|
+
const live = (0, codexCredentialStore_1.readCodexCredentialStore)();
|
|
467
|
+
if (importCurrent && live.status === 'ok') {
|
|
468
|
+
const sync = syncCodexLiveStateUnlocked('live-sync', { scan });
|
|
469
|
+
const profile = listCodexAccounts().find((p) => profileMatchesSnapshot(p, live.snapshot));
|
|
470
|
+
if (!profile)
|
|
413
471
|
return {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
profileId: existing.id,
|
|
417
|
-
codexHome: (0, codexPaths_1.getCodexProfileHome)(existing.id),
|
|
418
|
-
needsLogin: false,
|
|
419
|
-
warning: `This Codex login is already saved as "${relabel.label ?? existing.email ?? existing.id}".`,
|
|
420
|
-
},
|
|
472
|
+
success: false,
|
|
473
|
+
error: sync.warnings[0] ?? 'The live Codex login has no identifiable account.',
|
|
421
474
|
};
|
|
422
|
-
|
|
423
|
-
|
|
475
|
+
const manual = profile.metadata?.origin === 'live-sync' || profile.label === profile.email;
|
|
476
|
+
if (manual)
|
|
477
|
+
(0, accountRegistry_1.upsertSavedAccountProfile)({
|
|
478
|
+
...profile,
|
|
479
|
+
label: trimmedLabel,
|
|
480
|
+
metadata: { ...profile.metadata, origin: 'manual' },
|
|
481
|
+
});
|
|
482
|
+
return {
|
|
483
|
+
success: true,
|
|
484
|
+
profileId: profile.id,
|
|
485
|
+
codexHome: sourceHome,
|
|
486
|
+
needsLogin: false,
|
|
487
|
+
warning: sync.warnings.join(' ') ||
|
|
488
|
+
(!sync.registered
|
|
489
|
+
? `This Codex login is already saved as ${profile.label ?? profile.id}.`
|
|
490
|
+
: undefined),
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
if (importCurrent && (live.status === 'corrupt' || live.status === 'error')) {
|
|
494
|
+
return {
|
|
495
|
+
success: false,
|
|
496
|
+
error: 'The live Codex credential could not be read; retry after Codex finishes writing it.',
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
const profileId = (0, crypto_1.randomUUID)();
|
|
500
|
+
try {
|
|
501
|
+
writePendingProfile(profileId, { label: trimmedLabel, addedAt: new Date().toISOString() });
|
|
502
|
+
const codexHome = (0, codexPaths_1.getCodexLoginHome)(profileId);
|
|
503
|
+
copySourceCodexConfig(sourceHome, codexHome);
|
|
504
|
+
return { success: true, profileId, codexHome, needsLogin: true };
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
discardCodexLogin(profileId);
|
|
508
|
+
return { success: false, error: 'Could not prepare the isolated Codex login.' };
|
|
424
509
|
}
|
|
425
|
-
return { result: { success: true, profileId, codexHome, needsLogin: true } };
|
|
426
510
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
return
|
|
511
|
+
/** Legacy callers can import current; explicit sign-in passes importCurrent: false. */
|
|
512
|
+
function prepareCodexAccount(label, options = {}) {
|
|
513
|
+
const scan = options.importCurrent === false ? undefined : (0, processDetection_1.inspectRunningAccountConsumersSync)('codex');
|
|
514
|
+
try {
|
|
515
|
+
return withCodexAuthSwapLock(() => prepareCodexAccountCore(label, options.importCurrent ?? true, scan));
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
return { success: false, error: 'Could not acquire the Codex account-switch lock.' };
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
async function prepareCodexAccountAsync(label, options = {}) {
|
|
522
|
+
const scan = options.importCurrent === false ? undefined : await (0, processDetection_1.inspectRunningAccountConsumers)('codex');
|
|
523
|
+
try {
|
|
524
|
+
return withCodexAuthSwapLock(() => prepareCodexAccountCore(label, options.importCurrent ?? true, scan));
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
return { success: false, error: 'Could not acquire the Codex account-switch lock.' };
|
|
432
528
|
}
|
|
433
|
-
return step.result;
|
|
434
529
|
}
|
|
435
|
-
/**
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
if (
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
442
|
-
return step.result;
|
|
530
|
+
/** Removes only a pending login, never a registered canonical profile. */
|
|
531
|
+
function discardCodexLogin(id) {
|
|
532
|
+
fs.rmSync((0, codexPaths_1.getCodexLoginDir)(id), { recursive: true, force: true });
|
|
533
|
+
if (!listCodexAccounts().some((p) => p.id === id) &&
|
|
534
|
+
fs.existsSync((0, codexPaths_1.getCodexProfileStatePath)(id))) {
|
|
535
|
+
fs.rmSync((0, codexPaths_1.getCodexProfileDir)(id), { recursive: true, force: true });
|
|
536
|
+
}
|
|
443
537
|
}
|
|
444
|
-
function finalizeCodexAccountCore(profileId, opts,
|
|
538
|
+
function finalizeCodexAccountCore(profileId, opts, scan) {
|
|
445
539
|
const pending = readPendingProfile(profileId);
|
|
446
540
|
if (!pending) {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
return { result: { success: false, error: 'Codex profile is not authenticated yet.' } };
|
|
461
|
-
}
|
|
462
|
-
// A fresh isolated login for an account that is already saved: fold into the
|
|
463
|
-
// existing profile rather than registering a second seat.
|
|
464
|
-
const authRaw = (0, codexPaths_1.readFileOrNull)(path.join(codexHome, 'auth.json'));
|
|
465
|
-
const identity = (0, codexAuth_1.readAuthIdentityFromRaw)(authRaw);
|
|
466
|
-
const existing = identity ? findProfileForIdentity(identity, { fallbackToActive: false }) : null;
|
|
467
|
-
if (existing && existing.id !== profileId) {
|
|
468
|
-
storeCodexProfileAuth(existing.id, authRaw, (0, codexPaths_1.readFileOrNull)(path.join(codexHome, '.credentials.json')), 'login');
|
|
469
|
-
fs.rmSync((0, codexPaths_1.getCodexProfileDir)(profileId), { recursive: true, force: true });
|
|
470
|
-
const merged = {
|
|
471
|
-
...existing,
|
|
472
|
-
label: existing.metadata?.origin === 'live-sync' ? pending.label : existing.label,
|
|
473
|
-
email: probes.metadata.email ?? existing.email,
|
|
474
|
-
metadata: { ...existing.metadata, ...probes.metadata, origin: 'login' },
|
|
541
|
+
const saved = listCodexAccounts().find((p) => p.id === profileId);
|
|
542
|
+
if (!saved)
|
|
543
|
+
return { success: false, error: `Codex profile ${profileId} was not prepared.` };
|
|
544
|
+
return opts.activate === false
|
|
545
|
+
? { success: true, profileId }
|
|
546
|
+
: performCodexAuthSwapCore(saved, {}, { scan });
|
|
547
|
+
}
|
|
548
|
+
const home = (0, codexPaths_1.getCodexLoginHome)(profileId);
|
|
549
|
+
const read = (0, codexCredentialStore_1.readCodexCredentialStore)(home);
|
|
550
|
+
if (read.status !== 'ok')
|
|
551
|
+
return {
|
|
552
|
+
success: false,
|
|
553
|
+
error: 'Codex profile is not authenticated with readable file credentials yet.',
|
|
475
554
|
};
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
555
|
+
const identity = (0, codexAuth_1.readAuthIdentityFromRaw)(read.snapshot.authRaw);
|
|
556
|
+
const metadata = readCodexAccountMetadataFromFiles(home) ?? {};
|
|
557
|
+
const existing = identity ? findProfileForIdentity(identity, { fallbackToActive: false }) : null;
|
|
558
|
+
// Legacy pending homes must not become a saved profile before their credential is moved out.
|
|
559
|
+
const id = existing?.id ?? (home === (0, codexPaths_1.getCodexProfileHome)(profileId) ? (0, crypto_1.randomUUID)() : profileId);
|
|
481
560
|
const profile = {
|
|
482
|
-
|
|
561
|
+
...existing,
|
|
562
|
+
id,
|
|
483
563
|
providerId: 'codex',
|
|
484
|
-
label: pending.label,
|
|
485
|
-
email:
|
|
486
|
-
addedAt: pending.addedAt,
|
|
487
|
-
metadata: { ...
|
|
564
|
+
label: existing?.label ?? pending.label,
|
|
565
|
+
email: metadata.email ?? existing?.email,
|
|
566
|
+
addedAt: existing?.addedAt ?? pending.addedAt,
|
|
567
|
+
metadata: { ...existing?.metadata, ...metadata, origin: 'login' },
|
|
488
568
|
};
|
|
489
|
-
(0, accountRegistry_1.upsertSavedAccountProfile)(profile);
|
|
490
|
-
if (authRaw)
|
|
491
|
-
(0, accountHealth_1.writeCodexHealthSidecar)(profileId, authRaw, 'login');
|
|
492
569
|
try {
|
|
493
|
-
|
|
570
|
+
// Preserve first. No successful sign-in is lost if activation is blocked.
|
|
571
|
+
const previous = (0, codexCredentialStore_1.readCodexBackup)(id);
|
|
572
|
+
(0, codexCredentialStore_1.writeCodexBackup)(id, previous.status === 'ok' &&
|
|
573
|
+
(0, codexAuth_1.compareCodexAuthFreshness)(previous.snapshot.authRaw, read.snapshot.authRaw) > 0
|
|
574
|
+
? previous.snapshot
|
|
575
|
+
: read.snapshot);
|
|
576
|
+
(0, codexCredentialStore_1.removeCodexProfileCredentials)(home, read.snapshot);
|
|
577
|
+
(0, accountRegistry_1.upsertSavedAccountProfile)(profile);
|
|
578
|
+
(0, codexPaths_1.ensureCodexProfileDirs)(id);
|
|
579
|
+
copySourceCodexConfig(home, (0, codexPaths_1.getCodexProfileHome)(id));
|
|
580
|
+
storeCodexProfileAuthUnlocked(id, read.snapshot.authRaw, read.snapshot.legacyRaw, 'login');
|
|
494
581
|
}
|
|
495
582
|
catch {
|
|
496
|
-
/* the pending marker is advisory */
|
|
497
|
-
}
|
|
498
|
-
if (opts.activate === false) {
|
|
499
|
-
return { result: { success: true, profileId } };
|
|
500
|
-
}
|
|
501
|
-
if (!hasCodexCredentialFiles(codexHome)) {
|
|
502
|
-
// Authenticated via the OS keyring — there are no credential files to
|
|
503
|
-
// swap, so the registry pointer is all we can update.
|
|
504
|
-
(0, accountRegistry_1.setActiveSavedAccount)('codex', profileId);
|
|
505
583
|
return {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
warning: `Codex stores credentials in the OS keyring; sidekick cannot swap them per account, so \`codex\` keeps using the keyring credentials. ${exports.CODEX_KEYRING_FIX_HINT}`,
|
|
510
|
-
},
|
|
584
|
+
success: false,
|
|
585
|
+
profileId: id,
|
|
586
|
+
error: 'Could not finish saving the Codex login; any verified private backup was kept.',
|
|
511
587
|
};
|
|
512
588
|
}
|
|
513
|
-
|
|
589
|
+
finally {
|
|
590
|
+
discardCodexLogin(profileId);
|
|
591
|
+
}
|
|
592
|
+
if (opts.activate === false)
|
|
593
|
+
return { success: true, profileId: id };
|
|
594
|
+
const result = performCodexAuthSwapCore(profile, {}, { scan });
|
|
595
|
+
return result.success
|
|
596
|
+
? { ...result, profileId: id }
|
|
597
|
+
: {
|
|
598
|
+
...result,
|
|
599
|
+
profileId: id,
|
|
600
|
+
needsLogin: false,
|
|
601
|
+
error: `The login was saved, but could not be activated: ${result.error}`,
|
|
602
|
+
};
|
|
514
603
|
}
|
|
515
604
|
function finalizeCodexAccount(profileId, opts = {}) {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
const step = finalizeCodexAccountCore(profileId, opts, {
|
|
524
|
-
authenticated,
|
|
525
|
-
metadata: authenticated ? readCodexAccountMetadata(codexHome) : {},
|
|
526
|
-
});
|
|
527
|
-
return 'swap' in step ? performCodexAuthSwap(step.swap, {}) : step.result;
|
|
605
|
+
const scan = (0, processDetection_1.inspectRunningAccountConsumersSync)('codex');
|
|
606
|
+
try {
|
|
607
|
+
return withCodexAuthSwapLock(() => finalizeCodexAccountCore(profileId, opts, scan));
|
|
608
|
+
}
|
|
609
|
+
catch {
|
|
610
|
+
return { success: false, error: 'Could not finalize the Codex login.' };
|
|
611
|
+
}
|
|
528
612
|
}
|
|
529
|
-
/** {@link finalizeCodexAccount} without blocking the event loop on CLI probes. */
|
|
530
613
|
async function finalizeCodexAccountAsync(profileId, opts = {}) {
|
|
531
|
-
|
|
532
|
-
// Already finalized: the core activates a saved profile without probing.
|
|
533
|
-
const step = finalizeCodexAccountCore(profileId, opts, { authenticated: false, metadata: {} });
|
|
534
|
-
return 'swap' in step ? performCodexAuthSwapAsync(step.swap, {}) : step.result;
|
|
535
|
-
}
|
|
536
|
-
const codexHome = (0, codexPaths_1.getCodexProfileHome)(profileId);
|
|
537
|
-
const authenticated = await isCodexProfileAuthenticatedAsync(codexHome);
|
|
538
|
-
const step = finalizeCodexAccountCore(profileId, opts, {
|
|
539
|
-
authenticated,
|
|
540
|
-
metadata: authenticated ? await readCodexAccountMetadataAsync(codexHome) : {},
|
|
541
|
-
});
|
|
542
|
-
return 'swap' in step ? performCodexAuthSwapAsync(step.swap, {}) : step.result;
|
|
543
|
-
}
|
|
544
|
-
// ── Stash / identity matching ────────────────────────────────────────────
|
|
545
|
-
function getCodexStashDir() {
|
|
546
|
-
return path.join((0, accountRegistry_1.getAccountsDir)(), 'codex', 'stash');
|
|
547
|
-
}
|
|
548
|
-
function stashLiveCodexAuth(liveAuthRaw, liveLegacyRaw) {
|
|
614
|
+
const scan = await (0, processDetection_1.inspectRunningAccountConsumers)('codex');
|
|
549
615
|
try {
|
|
550
|
-
|
|
551
|
-
let stashPath = null;
|
|
552
|
-
if (liveAuthRaw) {
|
|
553
|
-
stashPath = path.join(getCodexStashDir(), `auth-${stamp}.json`);
|
|
554
|
-
(0, atomic_1.atomicWriteFileSync)(stashPath, liveAuthRaw);
|
|
555
|
-
}
|
|
556
|
-
if (liveLegacyRaw) {
|
|
557
|
-
const legacyStashPath = path.join(getCodexStashDir(), `credentials-${stamp}.json`);
|
|
558
|
-
(0, atomic_1.atomicWriteFileSync)(legacyStashPath, liveLegacyRaw);
|
|
559
|
-
stashPath = stashPath ?? legacyStashPath;
|
|
560
|
-
}
|
|
561
|
-
return stashPath;
|
|
616
|
+
return withCodexAuthSwapLock(() => finalizeCodexAccountCore(profileId, opts, scan));
|
|
562
617
|
}
|
|
563
618
|
catch {
|
|
564
|
-
return
|
|
619
|
+
return { success: false, error: 'Could not finalize the Codex login.' };
|
|
565
620
|
}
|
|
566
621
|
}
|
|
622
|
+
// ── Stash / identity matching ────────────────────────────────────────────
|
|
623
|
+
function getCodexStashDir() {
|
|
624
|
+
return path.join((0, accountRegistry_1.getAccountsDir)(), 'codex', 'stash');
|
|
625
|
+
}
|
|
567
626
|
function findProfileForIdentity(identity, options = {}) {
|
|
568
627
|
const profiles = listCodexAccounts();
|
|
569
628
|
if (identity?.workspaceId) {
|
|
@@ -572,7 +631,8 @@ function findProfileForIdentity(identity, options = {}) {
|
|
|
572
631
|
return byWorkspace;
|
|
573
632
|
}
|
|
574
633
|
if (identity?.email) {
|
|
575
|
-
const byEmail = profiles.find((profile) => (profile.email ?? profile.metadata?.email) === identity.email
|
|
634
|
+
const byEmail = profiles.find((profile) => (profile.email ?? profile.metadata?.email) === identity.email &&
|
|
635
|
+
(!identity.workspaceId || !profile.metadata?.workspaceId));
|
|
576
636
|
if (byEmail)
|
|
577
637
|
return byEmail;
|
|
578
638
|
}
|
|
@@ -597,468 +657,485 @@ function withCodexAuthSwapLock(operation) {
|
|
|
597
657
|
fs.mkdirSync(lockDir, { recursive: true, mode: 0o700 });
|
|
598
658
|
return (0, atomic_1.withFileLockSync)(path.join(lockDir, 'auth-swap.lock'), operation);
|
|
599
659
|
}
|
|
600
|
-
/**
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
* Returns the removed ids. Caller holds the swap lock.
|
|
605
|
-
*/
|
|
606
|
-
function dedupeCodexProfiles() {
|
|
660
|
+
/** Merge owned profiles only after preserving their newest login in a verified backup. */
|
|
661
|
+
function dedupeCodexProfiles(scan) {
|
|
662
|
+
if (consumerBlocker(scan))
|
|
663
|
+
return [];
|
|
607
664
|
const removed = [];
|
|
608
|
-
const profiles = listCodexAccounts();
|
|
609
665
|
const groups = new Map();
|
|
610
|
-
for (const profile of
|
|
666
|
+
for (const profile of listCodexAccounts()) {
|
|
611
667
|
const key = profile.metadata?.workspaceId
|
|
612
668
|
? `ws:${profile.metadata.workspaceId}`
|
|
613
|
-
:
|
|
614
|
-
? `email:${profile.email
|
|
669
|
+
: profile.email
|
|
670
|
+
? `email:${profile.email}`
|
|
615
671
|
: `id:${profile.id}`;
|
|
616
672
|
groups.set(key, [...(groups.get(key) ?? []), profile]);
|
|
617
673
|
}
|
|
618
674
|
for (const group of groups.values()) {
|
|
619
|
-
if (group.length < 2)
|
|
675
|
+
if (group.length < 2 || group.some((p) => (0, accountLease_1.isAccountLeaseHeld)('codex', p.id)))
|
|
620
676
|
continue;
|
|
621
677
|
const sorted = [...group].sort((a, b) => Date.parse(a.addedAt) - Date.parse(b.addedAt));
|
|
622
678
|
const keeper = sorted[0];
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
679
|
+
let freshest = readStoredCodexAccount(keeper.id);
|
|
680
|
+
for (const p of sorted.slice(1)) {
|
|
681
|
+
const candidate = readStoredCodexAccount(p.id);
|
|
682
|
+
if (candidate &&
|
|
683
|
+
(!freshest || (0, codexAuth_1.compareCodexAuthFreshness)(candidate.authRaw, freshest.authRaw) > 0))
|
|
684
|
+
freshest = candidate;
|
|
685
|
+
}
|
|
686
|
+
if (!freshest)
|
|
687
|
+
continue;
|
|
688
|
+
(0, codexCredentialStore_1.writeCodexBackup)(keeper.id, freshest);
|
|
627
689
|
for (const loser of sorted.slice(1)) {
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
if (
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
catch {
|
|
643
|
-
/* quota re-key is best-effort */
|
|
644
|
-
}
|
|
645
|
-
try {
|
|
646
|
-
const stashDir = path.join(getCodexStashDir(), `dup-${loser.id}-${new Date().toISOString().replace(/[:.]/g, '-')}`);
|
|
647
|
-
fs.mkdirSync(path.dirname(stashDir), { recursive: true, mode: 0o700 });
|
|
648
|
-
fs.renameSync((0, codexPaths_1.getCodexProfileDir)(loser.id), stashDir);
|
|
649
|
-
}
|
|
650
|
-
catch {
|
|
651
|
-
fs.rmSync((0, codexPaths_1.getCodexProfileDir)(loser.id), { recursive: true, force: true });
|
|
652
|
-
}
|
|
690
|
+
const store = (0, codexCredentialStore_1.readCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(loser.id));
|
|
691
|
+
if (store.status === 'error' || store.status === 'corrupt')
|
|
692
|
+
continue;
|
|
693
|
+
if (store.status === 'ok')
|
|
694
|
+
(0, codexCredentialStore_1.removeCodexProfileCredentials)((0, codexPaths_1.getCodexProfileHome)(loser.id), store.snapshot);
|
|
695
|
+
const snapshot = (0, quotaSnapshots_1.readQuotaSnapshot)('codex', loser.id);
|
|
696
|
+
if (snapshot && !(0, quotaSnapshots_1.readQuotaSnapshot)('codex', keeper.id))
|
|
697
|
+
(0, quotaSnapshots_1.writeQuotaSnapshot)('codex', keeper.id, snapshot);
|
|
698
|
+
const archive = path.join(getCodexStashDir(), `dup-${loser.id}`);
|
|
699
|
+
fs.mkdirSync(path.dirname(archive), { recursive: true, mode: 0o700 });
|
|
700
|
+
if (fs.existsSync((0, codexPaths_1.getCodexProfileDir)(loser.id)))
|
|
701
|
+
fs.renameSync((0, codexPaths_1.getCodexProfileDir)(loser.id), archive);
|
|
702
|
+
if ((0, accountRegistry_1.getActiveSavedAccount)('codex')?.id === loser.id)
|
|
703
|
+
(0, accountRegistry_1.setActiveSavedAccount)('codex', keeper.id, { silent: true });
|
|
653
704
|
(0, accountRegistry_1.removeSavedAccountProfile)('codex', loser.id);
|
|
705
|
+
fs.rmSync((0, codexPaths_1.getCodexBackupPath)(loser.id), { force: true });
|
|
706
|
+
fs.rmSync((0, codexPaths_1.getCodexBackupPath)(loser.id, true), { force: true });
|
|
654
707
|
removed.push(loser.id);
|
|
655
|
-
if (activeId === loser.id)
|
|
656
|
-
(0, accountRegistry_1.setActiveSavedAccount)('codex', keeper.id, { silent: true });
|
|
657
708
|
}
|
|
658
|
-
|
|
659
|
-
storeCodexProfileAuth(keeper.id, freshestRaw, null, 'migration');
|
|
660
|
-
}
|
|
661
|
-
const metadata = readCodexAccountMetadataFromFiles((0, codexPaths_1.getCodexProfileHome)(keeper.id)) ?? {};
|
|
662
|
-
(0, accountRegistry_1.upsertSavedAccountProfile)({
|
|
663
|
-
...keeper,
|
|
664
|
-
email: metadata.email ?? keeper.email,
|
|
665
|
-
metadata: { ...keeper.metadata, ...metadata },
|
|
666
|
-
});
|
|
709
|
+
storeCodexProfileAuthUnlocked(keeper.id, freshest.authRaw, freshest.legacyRaw, 'migration');
|
|
667
710
|
}
|
|
668
711
|
return removed;
|
|
669
712
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
713
|
+
function codexJournalPath() {
|
|
714
|
+
return path.join((0, accountRegistry_1.getAccountsDir)(), 'codex', 'switch-journal.json');
|
|
715
|
+
}
|
|
716
|
+
function loginHash(snapshot) {
|
|
717
|
+
return (0, codexCredentialStore_1.codexSnapshotFingerprint)((0, codexCredentialStore_1.codexLoginOnly)(snapshot));
|
|
718
|
+
}
|
|
719
|
+
function clearCodexJournal() {
|
|
720
|
+
fs.rmSync(codexJournalPath(), { force: true });
|
|
721
|
+
}
|
|
722
|
+
function hasPendingCodexRepair() {
|
|
723
|
+
if (fs.existsSync(codexJournalPath()))
|
|
724
|
+
return true;
|
|
725
|
+
const live = (0, codexCredentialStore_1.readCodexCredentialStore)();
|
|
726
|
+
if (live.status === 'error' || live.status === 'corrupt')
|
|
727
|
+
return true;
|
|
728
|
+
if (live.status !== 'ok')
|
|
729
|
+
return false;
|
|
730
|
+
return listCodexAccounts().some((p) => {
|
|
731
|
+
if (!profileMatchesSnapshot(p, live.snapshot))
|
|
732
|
+
return false;
|
|
733
|
+
const home = (0, codexCredentialStore_1.readCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(p.id));
|
|
734
|
+
const backup = (0, codexCredentialStore_1.readCodexBackup)(p.id);
|
|
735
|
+
return (home.status !== 'absent' ||
|
|
736
|
+
(backup.status === 'ok' &&
|
|
737
|
+
(0, codexAuth_1.compareCodexAuthFreshness)(backup.snapshot.authRaw, live.snapshot.authRaw) > 0));
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
function resolveCodexJournal(report, scan) {
|
|
741
|
+
if (!fs.existsSync(codexJournalPath()))
|
|
742
|
+
return;
|
|
743
|
+
let journal;
|
|
744
|
+
try {
|
|
745
|
+
journal = JSON.parse(fs.readFileSync(codexJournalPath(), 'utf8'));
|
|
746
|
+
if (!journal.to || !journal.fromHash || !journal.toHash)
|
|
747
|
+
throw new Error();
|
|
748
|
+
}
|
|
749
|
+
catch {
|
|
750
|
+
report.warnings.push('The Codex switch journal is unreadable; run accounts doctor.');
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
const live = (0, codexCredentialStore_1.readCodexCredentialStore)();
|
|
754
|
+
if (live.status === 'error' || live.status === 'corrupt') {
|
|
755
|
+
report.warnings.push('An interrupted Codex switch is pending; the live login could not be read.');
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
const blocker = consumerBlocker(scan);
|
|
759
|
+
if (blocker) {
|
|
760
|
+
report.warnings.push(blocker);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
const hash = loginHash(live.snapshot);
|
|
764
|
+
const to = listCodexAccounts().find((p) => p.id === journal.to);
|
|
765
|
+
if (hash === journal.toHash ||
|
|
766
|
+
(live.status === 'ok' && to && profileMatchesSnapshot(to, live.snapshot))) {
|
|
767
|
+
if (live.status !== 'ok')
|
|
768
|
+
return;
|
|
769
|
+
(0, codexCredentialStore_1.writeCodexBackup)(journal.to, live.snapshot);
|
|
770
|
+
const target = (0, codexCredentialStore_1.readCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(journal.to));
|
|
771
|
+
if (target.status === 'error' || target.status === 'corrupt')
|
|
772
|
+
throw new Error('The interrupted switch target is unreadable.');
|
|
773
|
+
if (target.status === 'ok')
|
|
774
|
+
(0, codexCredentialStore_1.removeCodexProfileCredentials)((0, codexPaths_1.getCodexProfileHome)(journal.to), target.snapshot);
|
|
775
|
+
if (journal.from) {
|
|
776
|
+
const backup = (0, codexCredentialStore_1.readCodexBackup)(journal.from);
|
|
777
|
+
if (backup.status === 'ok' && loginHash(backup.snapshot) === journal.fromHash) {
|
|
778
|
+
if ((0, codexCredentialStore_1.readCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(journal.from)).status === 'absent')
|
|
779
|
+
(0, codexCredentialStore_1.writeInactiveCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(journal.from), backup.snapshot);
|
|
780
|
+
(0, accountHealth_1.writeCodexHealthSidecar)(journal.from, backup.snapshot.authRaw, 'switch');
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
(0, accountRegistry_1.setActiveSavedAccount)('codex', journal.to, { silent: true });
|
|
784
|
+
report.journal = 'finished';
|
|
785
|
+
}
|
|
786
|
+
else if (hash === journal.fromHash) {
|
|
787
|
+
const target = (0, codexCredentialStore_1.readCodexBackup)(journal.to);
|
|
788
|
+
if (target.status === 'ok' && loginHash(target.snapshot) === journal.toHash) {
|
|
789
|
+
if ((0, codexCredentialStore_1.readCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(journal.to)).status === 'absent')
|
|
790
|
+
(0, codexCredentialStore_1.writeInactiveCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(journal.to), target.snapshot);
|
|
791
|
+
}
|
|
792
|
+
if (journal.from) {
|
|
793
|
+
const outgoing = (0, codexCredentialStore_1.readCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(journal.from));
|
|
794
|
+
if (outgoing.status === 'ok')
|
|
795
|
+
(0, codexCredentialStore_1.removeCodexProfileCredentials)((0, codexPaths_1.getCodexProfileHome)(journal.from), outgoing.snapshot);
|
|
796
|
+
}
|
|
797
|
+
(0, accountRegistry_1.setActiveSavedAccount)('codex', journal.from, { silent: true });
|
|
798
|
+
report.journal = 'rolled-back';
|
|
799
|
+
}
|
|
800
|
+
else {
|
|
801
|
+
report.journal = 'cleared';
|
|
802
|
+
report.warnings.push('The live Codex login matches neither side of the interrupted switch; it was kept. Private backups are available for inspection.');
|
|
803
|
+
}
|
|
804
|
+
clearCodexJournal();
|
|
805
|
+
}
|
|
806
|
+
/** No subprocesses inside this function: hosts resolve process scans before taking the lock. */
|
|
676
807
|
function syncCodexLiveStateUnlocked(source = 'live-sync', probes = {}) {
|
|
677
808
|
const report = (0, accountSyncTypes_1.emptyProviderSyncReport)();
|
|
678
809
|
try {
|
|
679
|
-
const
|
|
810
|
+
const mode = (0, codexPaths_1.getCodexCredentialStoreMode)();
|
|
811
|
+
if (mode !== 'file' && mode !== 'unknown') {
|
|
812
|
+
report.skipped = 'keyring';
|
|
813
|
+
report.warnings.push(exports.CODEX_KEYRING_FIX_HINT);
|
|
814
|
+
return report;
|
|
815
|
+
}
|
|
816
|
+
if ((0, codexCredentialStore_1.isSidekickCodexHome)((0, codexPaths_1.getSystemCodexHome)())) {
|
|
817
|
+
report.skipped = 'managed-home';
|
|
818
|
+
report.warnings.push('Unset CODEX_HOME before syncing; it points at a Sidekick-owned home.');
|
|
819
|
+
return report;
|
|
820
|
+
}
|
|
821
|
+
resolveCodexJournal(report, probes.scan);
|
|
822
|
+
let live = (0, codexCredentialStore_1.readCodexCredentialStore)();
|
|
823
|
+
if (live.status === 'error' || live.status === 'corrupt') {
|
|
824
|
+
report.skipped = 'unreadable';
|
|
825
|
+
report.warnings.push('The live Codex login could not be read; nothing was replaced.');
|
|
826
|
+
return report;
|
|
827
|
+
}
|
|
828
|
+
if (live.status === 'absent') {
|
|
829
|
+
report.skipped = 'logged-out';
|
|
830
|
+
return report;
|
|
831
|
+
}
|
|
832
|
+
const merged = dedupeCodexProfiles(probes.scan);
|
|
680
833
|
if (merged.length)
|
|
681
834
|
report.merged = merged;
|
|
682
|
-
const
|
|
683
|
-
const
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
report.
|
|
691
|
-
|
|
692
|
-
else {
|
|
693
|
-
report.skipped = 'logged-out';
|
|
835
|
+
const identity = (0, codexAuth_1.readAuthIdentityFromRaw)(live.snapshot.authRaw);
|
|
836
|
+
const initialSnapshot = live.snapshot;
|
|
837
|
+
let profile = listCodexAccounts().find((p) => profileMatchesSnapshot(p, initialSnapshot));
|
|
838
|
+
if (!profile) {
|
|
839
|
+
if (!identity?.email &&
|
|
840
|
+
!identity?.workspaceId &&
|
|
841
|
+
identity?.authMode !== 'api-key' &&
|
|
842
|
+
!(0, codexCredentialStore_1.isLegacyCodexLogin)(live.snapshot.legacyRaw)) {
|
|
843
|
+
report.skipped = 'no-identity';
|
|
844
|
+
return report;
|
|
694
845
|
}
|
|
695
|
-
return report;
|
|
696
|
-
}
|
|
697
|
-
const identity = (0, codexAuth_1.readAuthIdentityFromRaw)(liveAuthRaw);
|
|
698
|
-
let profile = findProfileForIdentity(identity, { fallbackToActive: false });
|
|
699
|
-
if (!profile && identity && (identity.email || identity.workspaceId)) {
|
|
700
|
-
const profileId = (0, crypto_1.randomUUID)();
|
|
701
846
|
profile = {
|
|
702
|
-
id:
|
|
847
|
+
id: (0, crypto_1.randomUUID)(),
|
|
703
848
|
providerId: 'codex',
|
|
704
|
-
label: identity
|
|
705
|
-
email: identity
|
|
849
|
+
label: identity?.email ?? identity?.workspaceId ?? 'Codex API key',
|
|
850
|
+
email: identity?.email,
|
|
706
851
|
addedAt: new Date().toISOString(),
|
|
707
|
-
metadata: {
|
|
708
|
-
email: identity.email,
|
|
709
|
-
workspaceId: identity.workspaceId,
|
|
710
|
-
planType: identity.planType,
|
|
711
|
-
authMode: identity.authMode,
|
|
712
|
-
origin: 'live-sync',
|
|
713
|
-
},
|
|
852
|
+
metadata: { ...identity, authMode: identity?.authMode ?? 'api-key', origin: 'live-sync' },
|
|
714
853
|
};
|
|
715
|
-
|
|
716
|
-
|
|
854
|
+
// API-key identities require their verified bytes before registry matching can work.
|
|
855
|
+
(0, codexCredentialStore_1.writeCodexBackup)(profile.id, live.snapshot);
|
|
717
856
|
(0, accountRegistry_1.upsertSavedAccountProfile)(profile);
|
|
718
|
-
report.registered = { id:
|
|
857
|
+
report.registered = { id: profile.id, email: profile.email };
|
|
719
858
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
859
|
+
const home = (0, codexPaths_1.getCodexProfileHome)(profile.id);
|
|
860
|
+
(0, codexPaths_1.ensureCodexProfileDirs)(profile.id);
|
|
861
|
+
if (!fs.existsSync(path.join(home, 'config.toml')))
|
|
862
|
+
copySourceCodexConfig((0, codexPaths_1.getSystemCodexHome)(), home);
|
|
863
|
+
const duplicate = (0, codexCredentialStore_1.readCodexCredentialStore)(home);
|
|
864
|
+
const stored = readStoredCodexAccount(profile.id);
|
|
865
|
+
if (stored && (0, codexAuth_1.compareCodexAuthFreshness)(stored.authRaw, live.snapshot.authRaw) > 0) {
|
|
866
|
+
const install = installIntoLiveCodexStore(stored, profile.id, 'repair', {
|
|
867
|
+
before: live,
|
|
868
|
+
liveAccountId: profile.id,
|
|
869
|
+
scan: probes.scan,
|
|
870
|
+
});
|
|
871
|
+
if (install.ok) {
|
|
872
|
+
live = { status: 'ok', snapshot: install.installed };
|
|
873
|
+
report.repaired = profile.id;
|
|
726
874
|
}
|
|
727
875
|
else {
|
|
728
|
-
report.
|
|
729
|
-
return report;
|
|
876
|
+
report.warnings.push(`A newer Codex login is saved privately; repair is pending. ${install.reason}`);
|
|
730
877
|
}
|
|
731
878
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
(
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
});
|
|
750
|
-
report.folded = profile.id;
|
|
751
|
-
}
|
|
752
|
-
catch (err) {
|
|
753
|
-
report.warnings.push(`Could not back up the live Codex credentials: ${err}`);
|
|
879
|
+
const backup = (0, codexCredentialStore_1.readCodexBackup)(profile.id);
|
|
880
|
+
if (backup.status !== 'ok' ||
|
|
881
|
+
(0, codexAuth_1.compareCodexAuthFreshness)(backup.snapshot.authRaw, live.snapshot.authRaw) <= 0) {
|
|
882
|
+
(0, codexCredentialStore_1.writeCodexBackup)(profile.id, live.snapshot);
|
|
883
|
+
report.folded = profile.id;
|
|
884
|
+
}
|
|
885
|
+
(0, accountHealth_1.writeCodexHealthSidecar)(profile.id, live.snapshot.authRaw, source);
|
|
886
|
+
if (duplicate.status === 'ok') {
|
|
887
|
+
const blocker = consumerBlocker(probes.scan);
|
|
888
|
+
// Preserve a newer copy even when its promotion must wait.
|
|
889
|
+
if ((0, codexAuth_1.compareCodexAuthFreshness)(duplicate.snapshot.authRaw, live.snapshot.authRaw) > 0)
|
|
890
|
+
(0, codexCredentialStore_1.writeCodexBackup)(profile.id, duplicate.snapshot);
|
|
891
|
+
if (blocker || (0, accountLease_1.isAccountLeaseHeld)('codex', profile.id))
|
|
892
|
+
report.warnings.push(blocker ?? 'The profile is leased; duplicate cleanup is pending.');
|
|
893
|
+
else {
|
|
894
|
+
(0, codexCredentialStore_1.removeCodexProfileCredentials)(home, duplicate.snapshot);
|
|
895
|
+
report.demoted = profile.id;
|
|
754
896
|
}
|
|
755
897
|
}
|
|
756
|
-
|
|
757
|
-
|
|
898
|
+
else if (duplicate.status === 'corrupt' || duplicate.status === 'error') {
|
|
899
|
+
report.warnings.push('The live account has an unreadable profile credential; run accounts doctor.');
|
|
900
|
+
}
|
|
901
|
+
if ((0, accountRegistry_1.getActiveSavedAccount)('codex')?.id !== profile.id) {
|
|
758
902
|
(0, accountRegistry_1.setActiveSavedAccount)('codex', profile.id, { silent: true });
|
|
759
903
|
report.repointed = profile.id;
|
|
760
904
|
}
|
|
761
905
|
}
|
|
762
|
-
catch
|
|
763
|
-
report.warnings.push(
|
|
906
|
+
catch {
|
|
907
|
+
report.warnings.push('Codex account sync could not finish; verified credentials were preserved for the next retry.');
|
|
764
908
|
}
|
|
765
909
|
return report;
|
|
766
910
|
}
|
|
767
|
-
/** {@link syncCodexLiveStateUnlocked} under the swap lock. */
|
|
768
911
|
function syncCodexLiveState(source = 'live-sync') {
|
|
912
|
+
const scan = (0, processDetection_1.inspectRunningAccountConsumersSync)('codex');
|
|
769
913
|
try {
|
|
770
|
-
return withCodexAuthSwapLock(() => syncCodexLiveStateUnlocked(source));
|
|
914
|
+
return withCodexAuthSwapLock(() => syncCodexLiveStateUnlocked(source, { scan }));
|
|
771
915
|
}
|
|
772
|
-
catch
|
|
773
|
-
return { warnings: [
|
|
916
|
+
catch {
|
|
917
|
+
return { warnings: ['Could not acquire the Codex account-switch lock.'] };
|
|
774
918
|
}
|
|
775
919
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
* than `olderThanMs`. Returns the removed profile ids.
|
|
779
|
-
*/
|
|
780
|
-
function cleanupAbandonedCodexLogins(olderThanMs = 6 * 60 * 60 * 1000, now = Date.now()) {
|
|
781
|
-
const removed = [];
|
|
782
|
-
let entries = [];
|
|
920
|
+
async function syncCodexLiveStateAsync(source = 'live-sync') {
|
|
921
|
+
const scan = await (0, processDetection_1.inspectRunningAccountConsumers)('codex');
|
|
783
922
|
try {
|
|
784
|
-
|
|
923
|
+
return withCodexAuthSwapLock(() => syncCodexLiveStateUnlocked(source, { scan }));
|
|
785
924
|
}
|
|
786
925
|
catch {
|
|
787
|
-
return
|
|
926
|
+
return { warnings: ['Could not acquire the Codex account-switch lock.'] };
|
|
788
927
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
928
|
+
}
|
|
929
|
+
/** Only abandoned Sidekick-owned login homes; authenticated copies are preserved privately first. */
|
|
930
|
+
function cleanupAbandonedCodexLogins(olderThanMs = 24 * 60 * 60 * 1000, now = Date.now(), scan) {
|
|
931
|
+
const removed = [];
|
|
932
|
+
if (consumerBlocker(scan ?? (0, processDetection_1.inspectRunningAccountConsumersSync)('codex')))
|
|
933
|
+
return removed;
|
|
934
|
+
withCodexAuthSwapLock(() => {
|
|
935
|
+
for (const root of [(0, codexPaths_1.getCodexLoginsDir)(), (0, codexPaths_1.getCodexProfilesDir)()]) {
|
|
936
|
+
if (!fs.existsSync(root))
|
|
937
|
+
continue;
|
|
938
|
+
for (const id of fs.readdirSync(root)) {
|
|
939
|
+
if (listCodexAccounts().some((p) => p.id === id))
|
|
940
|
+
continue;
|
|
941
|
+
const pending = readPendingProfile(id);
|
|
942
|
+
if (!pending || now - Date.parse(pending.addedAt) < olderThanMs)
|
|
943
|
+
continue;
|
|
944
|
+
const home = (0, codexPaths_1.getCodexLoginHome)(id);
|
|
945
|
+
const read = (0, codexCredentialStore_1.readCodexCredentialStore)(home);
|
|
946
|
+
if (read.status === 'error' || read.status === 'corrupt')
|
|
947
|
+
continue;
|
|
948
|
+
if (read.status === 'ok') {
|
|
949
|
+
const identity = (0, codexAuth_1.readAuthIdentityFromRaw)(read.snapshot.authRaw);
|
|
950
|
+
const owner = identity
|
|
951
|
+
? findProfileForIdentity(identity, { fallbackToActive: false })
|
|
952
|
+
: null;
|
|
953
|
+
if (!owner)
|
|
954
|
+
continue; // An unregistered authenticated login needs a human decision.
|
|
955
|
+
const saved = readStoredCodexAccount(owner.id);
|
|
956
|
+
(0, codexCredentialStore_1.writeCodexBackup)(owner.id, saved && (0, codexAuth_1.compareCodexAuthFreshness)(saved.authRaw, read.snapshot.authRaw) >= 0
|
|
957
|
+
? saved
|
|
958
|
+
: read.snapshot);
|
|
959
|
+
}
|
|
960
|
+
discardCodexLogin(id);
|
|
961
|
+
removed.push(id);
|
|
962
|
+
}
|
|
808
963
|
}
|
|
809
|
-
}
|
|
964
|
+
});
|
|
810
965
|
return removed;
|
|
811
966
|
}
|
|
812
|
-
/**
|
|
813
|
-
* CLI probes are resolved before the auth-swap lock is taken — never spawn a
|
|
814
|
-
* child process while holding it. The keyring probe is skipped (false) when
|
|
815
|
-
* live credential files exist, because the swap only consults it in their
|
|
816
|
-
* absence.
|
|
817
|
-
*/
|
|
818
|
-
function resolveCodexSwapProbesSync() {
|
|
819
|
-
const systemHome = (0, codexPaths_1.getSystemCodexHome)();
|
|
820
|
-
return {
|
|
821
|
-
consumers: (0, processDetection_1.detectRunningAccountConsumersSync)('codex'),
|
|
822
|
-
systemKeyringLoggedIn: hasCodexCredentialFiles(systemHome)
|
|
823
|
-
? false
|
|
824
|
-
: getCodexLoginStatus(systemHome).loggedIn,
|
|
825
|
-
};
|
|
826
|
-
}
|
|
827
|
-
async function resolveCodexSwapProbesAsync() {
|
|
828
|
-
const systemHome = (0, codexPaths_1.getSystemCodexHome)();
|
|
829
|
-
return {
|
|
830
|
-
consumers: await (0, processDetection_1.detectRunningAccountConsumers)('codex'),
|
|
831
|
-
systemKeyringLoggedIn: hasCodexCredentialFiles(systemHome)
|
|
832
|
-
? false
|
|
833
|
-
: (await getCodexLoginStatusAsync(systemHome)).loggedIn,
|
|
834
|
-
};
|
|
835
|
-
}
|
|
836
|
-
function performCodexAuthSwap(target, options) {
|
|
837
|
-
const probes = resolveCodexSwapProbesSync();
|
|
838
|
-
try {
|
|
839
|
-
return withCodexAuthSwapLock(() => performCodexAuthSwapCore(target, options, probes));
|
|
840
|
-
}
|
|
841
|
-
catch (err) {
|
|
842
|
-
// Keep the non-throwing result contract when the lock cannot be acquired.
|
|
843
|
-
return (0, accountSwitch_1.switchFailure)('codex', target.id, `Could not acquire the account-switch lock: ${err}`);
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
async function performCodexAuthSwapAsync(target, options) {
|
|
847
|
-
const probes = await resolveCodexSwapProbesAsync();
|
|
848
|
-
let result;
|
|
849
|
-
try {
|
|
850
|
-
// The sync lock is fine here: probes are pre-resolved, so the critical
|
|
851
|
-
// section is only fast local-filesystem work.
|
|
852
|
-
result = withCodexAuthSwapLock(() => performCodexAuthSwapCore(target, options, probes));
|
|
853
|
-
}
|
|
854
|
-
catch (err) {
|
|
855
|
-
return (0, accountSwitch_1.switchFailure)('codex', target.id, `Could not acquire the account-switch lock: ${err}`);
|
|
856
|
-
}
|
|
857
|
-
if (result.success && options.verifyWithCli && !result.alreadyActive) {
|
|
858
|
-
const status = await getCodexLoginStatusAsync((0, codexPaths_1.getSystemCodexHome)());
|
|
859
|
-
result = status.loggedIn
|
|
860
|
-
? { ...result, verification: 'cli' }
|
|
861
|
-
: (0, accountSwitch_1.finishSwitchResult)({
|
|
862
|
-
...result,
|
|
863
|
-
warnings: [
|
|
864
|
-
...result.warnings,
|
|
865
|
-
'`codex login status` reports "Not logged in" after the switch; the stored token may have been revoked. Sign in again if codex prompts you.',
|
|
866
|
-
],
|
|
867
|
-
});
|
|
868
|
-
}
|
|
869
|
-
return result;
|
|
870
|
-
}
|
|
871
967
|
function performCodexAuthSwapCore(target, options, probes) {
|
|
872
|
-
const systemHome = (0, codexPaths_1.getSystemCodexHome)();
|
|
873
|
-
const liveAuthPath = path.join(systemHome, 'auth.json');
|
|
874
|
-
const liveLegacyPath = path.join(systemHome, '.credentials.json');
|
|
875
|
-
const targetName = target.label ?? target.email ?? target.id;
|
|
876
|
-
let previousAccountId = (0, accountRegistry_1.getActiveSavedAccount)('codex')?.id ?? null;
|
|
877
968
|
const base = {
|
|
878
969
|
success: true,
|
|
879
970
|
provider: 'codex',
|
|
880
971
|
accountId: target.id,
|
|
881
|
-
previousAccountId,
|
|
972
|
+
previousAccountId: (0, accountRegistry_1.getActiveSavedAccount)('codex')?.id ?? null,
|
|
882
973
|
verified: false,
|
|
883
974
|
verification: 'none',
|
|
884
975
|
warnings: [],
|
|
885
976
|
hints: [],
|
|
886
|
-
runningConsumers: probes.consumers,
|
|
977
|
+
runningConsumers: probes.scan.consumers,
|
|
887
978
|
email: target.email ?? target.metadata?.email,
|
|
888
979
|
};
|
|
889
|
-
const
|
|
890
|
-
const
|
|
891
|
-
if (
|
|
892
|
-
return (
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
systemKeyringLoggedIn: probes.systemKeyringLoggedIn,
|
|
911
|
-
});
|
|
912
|
-
base.warnings.push(...sync.warnings);
|
|
913
|
-
// Only now is the "previous" account known: the sync may have re-pointed or
|
|
914
|
-
// registered the login that was really live, and undo must return to it.
|
|
915
|
-
previousAccountId = (0, accountRegistry_1.getActiveSavedAccount)('codex')?.id ?? null;
|
|
916
|
-
base.previousAccountId = previousAccountId;
|
|
917
|
-
if (sync.skipped === 'no-identity' && liveBefore) {
|
|
918
|
-
const stashPath = stashLiveCodexAuth(liveBefore, liveLegacyBefore);
|
|
919
|
-
base.warnings.push(stashPath
|
|
920
|
-
? `Live Codex credentials did not match any saved account; stashed at ${stashPath}.`
|
|
921
|
-
: 'Live Codex credentials did not match any saved account and could not be stashed.');
|
|
922
|
-
}
|
|
923
|
-
// Re-read after the sync: the target may have received the live file.
|
|
924
|
-
const liveAuthRaw = (0, codexPaths_1.readFileOrNull)(liveAuthPath);
|
|
925
|
-
const liveLegacyRaw = (0, codexPaths_1.readFileOrNull)(liveLegacyPath);
|
|
926
|
-
const profileHome = (0, codexPaths_1.getCodexProfileHome)(target.id);
|
|
927
|
-
const targetAuthPath = path.join(profileHome, 'auth.json');
|
|
928
|
-
const targetAuthRaw = (0, codexPaths_1.readFileOrNull)(targetAuthPath);
|
|
929
|
-
const targetLegacyRaw = (0, codexPaths_1.readFileOrNull)(path.join(profileHome, '.credentials.json'));
|
|
930
|
-
if (!targetAuthRaw && !targetLegacyRaw) {
|
|
931
|
-
return (0, accountSwitch_1.finishSwitchResult)({
|
|
932
|
-
...base,
|
|
933
|
-
success: false,
|
|
934
|
-
needsLogin: true,
|
|
935
|
-
error: `No stored credentials for "${targetName}". Sign in again to this account.`,
|
|
936
|
-
});
|
|
937
|
-
}
|
|
938
|
-
if (targetAuthRaw && !(0, codexAuth_1.parseAuthJson)(targetAuthRaw)) {
|
|
939
|
-
return (0, accountSwitch_1.finishSwitchResult)({
|
|
940
|
-
...base,
|
|
941
|
-
success: false,
|
|
942
|
-
needsLogin: true,
|
|
943
|
-
error: `Stored credentials for "${targetName}" are corrupted. Sign in again to this account.`,
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
// If the live file already belongs to the target account it is the freshest
|
|
947
|
-
// copy (rotated refresh token included) — never replace it with a staler
|
|
948
|
-
// backup, which would permanently invalidate the login.
|
|
949
|
-
const liveIdentity = (0, codexAuth_1.readAuthIdentityFromRaw)(liveAuthRaw);
|
|
950
|
-
const liveMatchesTarget = Boolean((0, codexAuth_1.identitiesMatch)(liveIdentity, {
|
|
951
|
-
...profileIdentity(target),
|
|
952
|
-
...((0, codexAuth_1.readAuthIdentityFromRaw)(targetAuthRaw) ?? {}),
|
|
953
|
-
}) ||
|
|
954
|
-
(liveAuthRaw !== null && liveAuthRaw === targetAuthRaw) ||
|
|
955
|
-
(!liveAuthRaw && !targetAuthRaw && liveLegacyRaw !== null && liveLegacyRaw === targetLegacyRaw));
|
|
956
|
-
if (liveMatchesTarget) {
|
|
957
|
-
if (previousAccountId !== target.id)
|
|
958
|
-
(0, accountRegistry_1.setActiveSavedAccount)('codex', target.id);
|
|
980
|
+
const fail = (error, extra = {}) => (0, accountSwitch_1.finishSwitchResult)({ ...base, success: false, error, ...extra });
|
|
981
|
+
const mode = (0, codexPaths_1.getCodexCredentialStoreMode)();
|
|
982
|
+
if (mode !== 'file' && mode !== 'unknown')
|
|
983
|
+
return fail(exports.CODEX_KEYRING_FIX_HINT);
|
|
984
|
+
if ((0, codexCredentialStore_1.isSidekickCodexHome)((0, codexPaths_1.getSystemCodexHome)()))
|
|
985
|
+
return fail('CODEX_HOME points at a Sidekick-owned home; unset it before switching.');
|
|
986
|
+
let before = (0, codexCredentialStore_1.readCodexCredentialStore)();
|
|
987
|
+
if (before.status === 'error' || before.status === 'corrupt')
|
|
988
|
+
return fail('The live Codex login could not be read; it was left untouched.');
|
|
989
|
+
const saved = readStoredCodexAccount(target.id);
|
|
990
|
+
if (before.status === 'ok' && profileMatchesSnapshot(target, before.snapshot)) {
|
|
991
|
+
if (saved && (0, codexAuth_1.compareCodexAuthFreshness)(saved.authRaw, before.snapshot.authRaw) > 0) {
|
|
992
|
+
const result = installIntoLiveCodexStore(saved, target.id, 'relogin', {
|
|
993
|
+
before,
|
|
994
|
+
liveAccountId: target.id,
|
|
995
|
+
scan: probes.scan,
|
|
996
|
+
});
|
|
997
|
+
if (!result.ok)
|
|
998
|
+
return fail(result.reason);
|
|
999
|
+
}
|
|
1000
|
+
const sync = syncCodexLiveStateUnlocked('switch', probes);
|
|
959
1001
|
return (0, accountSwitch_1.finishSwitchResult)({
|
|
960
1002
|
...base,
|
|
961
1003
|
alreadyActive: true,
|
|
962
1004
|
verified: true,
|
|
963
1005
|
verification: 'store',
|
|
1006
|
+
warnings: sync.warnings,
|
|
964
1007
|
health: (0, accountHealth_1.getAccountHealth)('codex', target.id),
|
|
965
|
-
hints: [
|
|
1008
|
+
hints: ['That account is already the live Codex login.'],
|
|
966
1009
|
});
|
|
967
1010
|
}
|
|
968
|
-
|
|
1011
|
+
const blocker = consumerBlocker(probes.scan);
|
|
1012
|
+
if (blocker)
|
|
1013
|
+
return fail(blocker);
|
|
1014
|
+
if ((0, accountLease_1.isAccountLeaseHeld)('codex', target.id))
|
|
1015
|
+
return fail('Keep-alive is refreshing this account; retry after it finishes.');
|
|
1016
|
+
const sync = syncCodexLiveStateUnlocked('switch', probes);
|
|
1017
|
+
base.warnings.push(...sync.warnings);
|
|
1018
|
+
if (fs.existsSync(codexJournalPath()))
|
|
1019
|
+
return fail('An interrupted Codex switch still needs recovery; run accounts doctor.');
|
|
1020
|
+
before = (0, codexCredentialStore_1.readCodexCredentialStore)();
|
|
1021
|
+
if (before.status === 'error' || before.status === 'corrupt')
|
|
1022
|
+
return fail('The live Codex credential could not be read.');
|
|
1023
|
+
const beforeSnapshot = before.snapshot;
|
|
1024
|
+
const outgoing = before.status === 'ok'
|
|
1025
|
+
? listCodexAccounts().find((p) => profileMatchesSnapshot(p, beforeSnapshot))
|
|
1026
|
+
: null;
|
|
1027
|
+
if (before.status === 'ok' && !outgoing)
|
|
1028
|
+
return fail('The live login has no unambiguous account identity to save it under; sign in with Codex and retry.');
|
|
1029
|
+
base.previousAccountId = outgoing?.id ?? null;
|
|
1030
|
+
if (outgoing?.id === target.id)
|
|
1031
|
+
return fail('The live account changed during the switch; retry.');
|
|
1032
|
+
const targetHome = (0, codexPaths_1.getCodexProfileHome)(target.id);
|
|
1033
|
+
const targetBefore = (0, codexCredentialStore_1.readCodexCredentialStore)(targetHome);
|
|
1034
|
+
if (targetBefore.status === 'error' || targetBefore.status === 'corrupt')
|
|
1035
|
+
return fail('The target profile is unreadable; the switch was deferred.');
|
|
1036
|
+
const candidate = readStoredCodexAccount(target.id);
|
|
1037
|
+
if (!candidate)
|
|
1038
|
+
return fail('No readable stored credentials for this account; sign in again.', {
|
|
1039
|
+
needsLogin: true,
|
|
1040
|
+
});
|
|
1041
|
+
const candidateIdentity = (0, codexAuth_1.readAuthIdentityFromRaw)(candidate.authRaw);
|
|
1042
|
+
if (candidateIdentity?.authMode === 'chatgpt' &&
|
|
1043
|
+
!(0, codexAuth_1.identitiesMatch)(candidateIdentity, profileIdentity(target))) {
|
|
1044
|
+
return fail('The saved Codex credential does not belong to the selected account.');
|
|
1045
|
+
}
|
|
1046
|
+
const liveFp = (0, codexAuth_1.codexRefreshFingerprint)(beforeSnapshot.authRaw);
|
|
1047
|
+
if (liveFp && liveFp === (0, codexAuth_1.codexRefreshFingerprint)(candidate.authRaw))
|
|
1048
|
+
return fail('The target is another copy of the live refresh token; sign in again.');
|
|
969
1049
|
const health = (0, accountHealth_1.getAccountHealth)('codex', target.id, { probe: 'store' });
|
|
970
1050
|
base.health = health;
|
|
971
|
-
if ((health.state === 'expired' || health.state === 'missing') && !options.force)
|
|
972
|
-
return (
|
|
973
|
-
...base,
|
|
974
|
-
success: false,
|
|
1051
|
+
if ((health.state === 'expired' || health.state === 'missing') && !options.force)
|
|
1052
|
+
return fail('Stored credentials have expired or are missing; sign in again.', {
|
|
975
1053
|
needsLogin: true,
|
|
976
|
-
error: `Stored credentials for "${targetName}" have expired; sign in again.`,
|
|
977
1054
|
});
|
|
1055
|
+
const refreshed = (0, codexAuth_1.readLastRefresh)(candidate.authRaw);
|
|
1056
|
+
if (refreshed !== null && Date.now() - refreshed > codexAuth_1.STALE_AUTH_THRESHOLD_MS)
|
|
1057
|
+
base.warnings.push('The stored Codex login has not been refreshed in over 8 days.');
|
|
1058
|
+
// Capture and verify both sides before removing either usable copy. Mandatory even with force.
|
|
1059
|
+
try {
|
|
1060
|
+
if (outgoing)
|
|
1061
|
+
(0, codexCredentialStore_1.writeCodexBackup)(outgoing.id, beforeSnapshot);
|
|
1062
|
+
(0, codexCredentialStore_1.writeCodexBackup)(target.id, candidate);
|
|
1063
|
+
(0, atomic_1.atomicWriteJsonSync)(codexJournalPath(), {
|
|
1064
|
+
from: outgoing?.id ?? null,
|
|
1065
|
+
to: target.id,
|
|
1066
|
+
fromHash: loginHash(beforeSnapshot),
|
|
1067
|
+
toHash: loginHash(candidate),
|
|
1068
|
+
at: new Date().toISOString(),
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
catch {
|
|
1072
|
+
return fail('The outgoing login or switch journal could not be saved and verified; live was left untouched.');
|
|
1073
|
+
}
|
|
1074
|
+
if (!(0, codexCredentialStore_1.codexStoreMatches)((0, codexCredentialStore_1.readCodexCredentialStore)(), beforeSnapshot))
|
|
1075
|
+
return fail('The live login changed after capture; the switch was deferred.');
|
|
1076
|
+
const profileStore = (0, codexCredentialStore_1.readCodexCredentialStore)(targetHome);
|
|
1077
|
+
if (!(0, codexCredentialStore_1.codexStoreMatches)(profileStore, targetBefore.snapshot))
|
|
1078
|
+
return fail('The target credential changed after selection; the switch was deferred.');
|
|
1079
|
+
if (profileStore.status === 'error' || profileStore.status === 'corrupt')
|
|
1080
|
+
return fail('The target profile is unreadable; the switch was deferred.');
|
|
1081
|
+
try {
|
|
1082
|
+
if (profileStore.status === 'ok')
|
|
1083
|
+
(0, codexCredentialStore_1.removeCodexProfileCredentials)(targetHome, profileStore.snapshot);
|
|
978
1084
|
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
base.warnings.push(`Stored credentials for "${targetName}" have not been refreshed in over 8 days; codex may ask you to log in again.`);
|
|
1085
|
+
catch {
|
|
1086
|
+
return fail('The target copy could not be removed safely; the switch was deferred.');
|
|
982
1087
|
}
|
|
983
|
-
const
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1088
|
+
const install = installIntoLiveCodexStore(candidate, target.id, 'switch', {
|
|
1089
|
+
before,
|
|
1090
|
+
liveAccountId: outgoing?.id ?? null,
|
|
1091
|
+
capturedOutgoing: Boolean(outgoing),
|
|
1092
|
+
scan: probes.scan,
|
|
1093
|
+
});
|
|
1094
|
+
const recover = (written) => {
|
|
1095
|
+
if (written) {
|
|
1096
|
+
installIntoLiveCodexStore(beforeSnapshot, outgoing?.id ?? target.id, 'rollback', {
|
|
1097
|
+
before: (0, codexCredentialStore_1.readCodexCredentialStore)(),
|
|
1098
|
+
liveAccountId: target.id,
|
|
1099
|
+
written,
|
|
1100
|
+
scan: probes.scan,
|
|
1101
|
+
});
|
|
996
1102
|
}
|
|
1103
|
+
// Journal reconciliation never overwrites live and never restores a copy of its identity.
|
|
1104
|
+
resolveCodexJournal((0, accountSyncTypes_1.emptyProviderSyncReport)(), probes.scan);
|
|
1105
|
+
};
|
|
1106
|
+
if (!install.ok) {
|
|
997
1107
|
try {
|
|
998
|
-
(
|
|
1108
|
+
recover(install.attempted);
|
|
999
1109
|
}
|
|
1000
1110
|
catch {
|
|
1001
|
-
/*
|
|
1111
|
+
/* journal and private backups survive */
|
|
1002
1112
|
}
|
|
1003
|
-
|
|
1004
|
-
|
|
1113
|
+
return fail(`${install.reason} Verified private backups were kept; run accounts doctor if recovery is pending.`, { verification: 'failed' });
|
|
1114
|
+
}
|
|
1005
1115
|
try {
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
fs.rmSync(liveLegacyPath, { force: true });
|
|
1116
|
+
(0, accountRegistry_1.setActiveSavedAccount)('codex', target.id);
|
|
1117
|
+
}
|
|
1118
|
+
catch {
|
|
1119
|
+
try {
|
|
1120
|
+
recover(install.installed);
|
|
1012
1121
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
if (liveAuthRaw)
|
|
1016
|
-
fs.rmSync(liveAuthPath, { force: true });
|
|
1122
|
+
catch {
|
|
1123
|
+
/* journal survives */
|
|
1017
1124
|
}
|
|
1125
|
+
return fail('The account registry could not be updated; recovery retained the verified backups.');
|
|
1018
1126
|
}
|
|
1019
|
-
catch (err) {
|
|
1020
|
-
restoreLiveFiles();
|
|
1021
|
-
return (0, accountSwitch_1.finishSwitchResult)({
|
|
1022
|
-
...base,
|
|
1023
|
-
success: false,
|
|
1024
|
-
error: `Failed to write Codex credentials: ${err}`,
|
|
1025
|
-
});
|
|
1026
|
-
}
|
|
1027
|
-
// Verify by re-reading the live file.
|
|
1028
|
-
const installed = (0, codexPaths_1.readFileOrNull)(targetAuthRaw ? liveAuthPath : liveLegacyPath);
|
|
1029
|
-
const expected = targetAuthRaw ?? targetLegacyRaw;
|
|
1030
|
-
const installedIdentity = targetAuthRaw ? (0, codexAuth_1.readAuthIdentityFromRaw)(installed) : null;
|
|
1031
|
-
const verified = installed === expected &&
|
|
1032
|
-
(!targetAuthRaw ||
|
|
1033
|
-
(0, codexAuth_1.identitiesMatch)(installedIdentity, {
|
|
1034
|
-
...profileIdentity(target),
|
|
1035
|
-
...((0, codexAuth_1.readAuthIdentityFromRaw)(targetAuthRaw) ?? {}),
|
|
1036
|
-
}) ||
|
|
1037
|
-
installedIdentity?.authMode === 'api-key');
|
|
1038
|
-
if (!verified) {
|
|
1039
|
-
restoreLiveFiles();
|
|
1040
|
-
return (0, accountSwitch_1.finishSwitchResult)({
|
|
1041
|
-
...base,
|
|
1042
|
-
success: false,
|
|
1043
|
-
verification: 'failed',
|
|
1044
|
-
error: `The live Codex credential file did not reflect the switch to "${targetName}"; the previous login was restored.`,
|
|
1045
|
-
});
|
|
1046
|
-
}
|
|
1047
|
-
// Pointer last.
|
|
1048
1127
|
try {
|
|
1049
|
-
|
|
1128
|
+
if (outgoing)
|
|
1129
|
+
storeCodexProfileAuthUnlocked(outgoing.id, beforeSnapshot.authRaw, beforeSnapshot.legacyRaw, 'switch');
|
|
1130
|
+
(0, accountHealth_1.writeCodexHealthSidecar)(target.id, candidate.authRaw, 'switch');
|
|
1131
|
+
clearCodexJournal();
|
|
1050
1132
|
}
|
|
1051
|
-
catch
|
|
1052
|
-
|
|
1053
|
-
return (0, accountSwitch_1.finishSwitchResult)({
|
|
1054
|
-
...base,
|
|
1055
|
-
success: false,
|
|
1056
|
-
error: `Failed to update account registry: ${err}`,
|
|
1057
|
-
});
|
|
1133
|
+
catch {
|
|
1134
|
+
base.warnings.push('The live switch succeeded; profile cleanup is pending and will retry on sync.');
|
|
1058
1135
|
}
|
|
1059
1136
|
let undoToken;
|
|
1060
1137
|
try {
|
|
1061
|
-
undoToken = (0, accountSwitch_1.writeLastSwitch)('codex',
|
|
1138
|
+
undoToken = (0, accountSwitch_1.writeLastSwitch)('codex', outgoing?.id ?? null, target.id).token;
|
|
1062
1139
|
}
|
|
1063
1140
|
catch {
|
|
1064
1141
|
base.warnings.push('The switch succeeded but could not be recorded for undo.');
|
|
@@ -1068,123 +1145,174 @@ function performCodexAuthSwapCore(target, options, probes) {
|
|
|
1068
1145
|
verified: true,
|
|
1069
1146
|
verification: 'store',
|
|
1070
1147
|
undoToken,
|
|
1071
|
-
hints: [`New
|
|
1148
|
+
hints: [`New Codex sessions use ${target.label ?? target.email ?? target.id}.`],
|
|
1072
1149
|
});
|
|
1073
1150
|
}
|
|
1074
1151
|
function switchToCodexAccount(profileId, options = {}) {
|
|
1075
|
-
const target = listCodexAccounts().find((
|
|
1076
|
-
if (!target)
|
|
1152
|
+
const target = listCodexAccounts().find((p) => p.id === profileId);
|
|
1153
|
+
if (!target)
|
|
1077
1154
|
return (0, accountSwitch_1.switchFailure)('codex', profileId, `Codex account ${profileId} not found.`);
|
|
1155
|
+
const scan = (0, processDetection_1.inspectRunningAccountConsumersSync)('codex');
|
|
1156
|
+
try {
|
|
1157
|
+
return withCodexAuthSwapLock(() => performCodexAuthSwapCore(target, options, { scan }));
|
|
1158
|
+
}
|
|
1159
|
+
catch {
|
|
1160
|
+
return (0, accountSwitch_1.switchFailure)('codex', profileId, 'The Codex switch could not finish; private backups and any pending journal were kept.');
|
|
1078
1161
|
}
|
|
1079
|
-
return performCodexAuthSwap(target, options);
|
|
1080
1162
|
}
|
|
1081
|
-
/** {@link switchToCodexAccount} without blocking the event loop on CLI probes. */
|
|
1082
1163
|
async function switchToCodexAccountAsync(profileId, options = {}) {
|
|
1083
|
-
const target = listCodexAccounts().find((
|
|
1084
|
-
if (!target)
|
|
1164
|
+
const target = listCodexAccounts().find((p) => p.id === profileId);
|
|
1165
|
+
if (!target)
|
|
1085
1166
|
return (0, accountSwitch_1.switchFailure)('codex', profileId, `Codex account ${profileId} not found.`);
|
|
1167
|
+
const scan = await (0, processDetection_1.inspectRunningAccountConsumers)('codex');
|
|
1168
|
+
let result;
|
|
1169
|
+
try {
|
|
1170
|
+
result = withCodexAuthSwapLock(() => performCodexAuthSwapCore(target, options, { scan }));
|
|
1086
1171
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1172
|
+
catch {
|
|
1173
|
+
return (0, accountSwitch_1.switchFailure)('codex', profileId, 'The Codex switch could not finish; private backups and any pending journal were kept.');
|
|
1174
|
+
}
|
|
1175
|
+
if (result.success && options.verifyWithCli && !result.alreadyActive) {
|
|
1176
|
+
const status = await getCodexLoginStatusAsync((0, codexPaths_1.getSystemCodexHome)());
|
|
1177
|
+
if (status.loggedIn)
|
|
1178
|
+
result.verification = 'cli';
|
|
1179
|
+
else
|
|
1180
|
+
result = (0, accountSwitch_1.finishSwitchResult)({
|
|
1181
|
+
...result,
|
|
1182
|
+
warnings: [
|
|
1183
|
+
...result.warnings,
|
|
1184
|
+
'Codex could not read the installed login; sign in again if prompted.',
|
|
1185
|
+
],
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
return result;
|
|
1099
1189
|
}
|
|
1100
|
-
|
|
1101
|
-
// CODEX_HOMEs: the active profile's auth.json may hold a fresher rotated
|
|
1102
|
-
// refresh token than the system home. Best-effort: never throws.
|
|
1190
|
+
/** Legacy migration entry points now use the same guarded reconciliation as ordinary sync. */
|
|
1103
1191
|
function reconcileCodexAuthState() {
|
|
1192
|
+
syncCodexLiveState('migration');
|
|
1193
|
+
}
|
|
1194
|
+
async function reconcileCodexAuthStateAsync() {
|
|
1195
|
+
await syncCodexLiveStateAsync('migration');
|
|
1196
|
+
}
|
|
1197
|
+
function codexKeepAliveBlockerUnlocked(id) {
|
|
1198
|
+
if ((0, codexCredentialStore_1.isSidekickCodexHome)((0, codexPaths_1.getSystemCodexHome)()))
|
|
1199
|
+
return 'unset CODEX_HOME before managing accounts; it names a Sidekick-owned home';
|
|
1200
|
+
const mode = (0, codexPaths_1.getCodexCredentialStoreMode)();
|
|
1201
|
+
if (mode !== 'file' && mode !== 'unknown')
|
|
1202
|
+
return 'live credential storage is not readable file mode';
|
|
1203
|
+
const profile = listCodexAccounts().find((p) => p.id === id);
|
|
1204
|
+
if (!profile)
|
|
1205
|
+
return 'account not found';
|
|
1206
|
+
const live = (0, codexCredentialStore_1.readCodexCredentialStore)();
|
|
1207
|
+
if (live.status === 'error' || live.status === 'corrupt')
|
|
1208
|
+
return 'the live login could not be read';
|
|
1209
|
+
if (live.status === 'ok' && profileMatchesSnapshot(profile, live.snapshot))
|
|
1210
|
+
return 'live account; the CLI refreshes it in normal use';
|
|
1211
|
+
const store = (0, codexCredentialStore_1.readCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(id));
|
|
1212
|
+
if (store.status !== 'ok')
|
|
1213
|
+
return 'no readable profile copy to refresh (keep-alive never restores backups)';
|
|
1214
|
+
const fp = (0, codexAuth_1.codexRefreshFingerprint)(store.snapshot.authRaw);
|
|
1215
|
+
if (!fp)
|
|
1216
|
+
return 'no refreshable ChatGPT login';
|
|
1217
|
+
if (live.status === 'ok' && fp === (0, codexAuth_1.codexRefreshFingerprint)(live.snapshot.authRaw))
|
|
1218
|
+
return 'same refresh token as the live login';
|
|
1219
|
+
if ((0, accountHealth_1.readHealthSidecar)('codex', id)?.source === 'live-sync')
|
|
1220
|
+
return 'copied from the live login; waits for a switch away from it';
|
|
1221
|
+
const backup = (0, codexCredentialStore_1.readCodexBackup)(id);
|
|
1222
|
+
if (backup.status === 'error' || backup.status === 'corrupt')
|
|
1223
|
+
return 'its private backup could not be read';
|
|
1224
|
+
if (backup.status === 'ok' &&
|
|
1225
|
+
(0, codexAuth_1.compareCodexAuthFreshness)(backup.snapshot.authRaw, store.snapshot.authRaw) > 0)
|
|
1226
|
+
return 'its saved backup is newer than the profile copy';
|
|
1227
|
+
return null;
|
|
1228
|
+
}
|
|
1229
|
+
function getCodexKeepAliveBlocker(id) {
|
|
1104
1230
|
try {
|
|
1105
|
-
|
|
1106
|
-
? getCodexLoginStatus((0, codexPaths_1.getSystemCodexHome)()).loggedIn
|
|
1107
|
-
: false;
|
|
1108
|
-
withCodexAuthSwapLock(() => reconcileCodexAuthStateCore({ systemKeyringLoggedIn }));
|
|
1231
|
+
return withCodexAuthSwapLock(() => codexKeepAliveBlockerUnlocked(id));
|
|
1109
1232
|
}
|
|
1110
1233
|
catch {
|
|
1111
|
-
|
|
1234
|
+
return 'could not acquire the account-switch lock';
|
|
1112
1235
|
}
|
|
1113
1236
|
}
|
|
1114
|
-
|
|
1115
|
-
async function reconcileCodexAuthStateAsync() {
|
|
1237
|
+
function beginCodexKeepAlive(id, leaseMs) {
|
|
1116
1238
|
try {
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1239
|
+
return withCodexAuthSwapLock(() => {
|
|
1240
|
+
const reason = codexKeepAliveBlockerUnlocked(id);
|
|
1241
|
+
if (reason)
|
|
1242
|
+
return { ok: false, reason };
|
|
1243
|
+
return (0, accountLease_1.acquireAccountLease)('codex', id, leaseMs)
|
|
1244
|
+
? { ok: true }
|
|
1245
|
+
: { ok: false, reason: 'another keep-alive run holds this account' };
|
|
1246
|
+
});
|
|
1121
1247
|
}
|
|
1122
1248
|
catch {
|
|
1123
|
-
|
|
1249
|
+
return { ok: false, reason: 'could not acquire the account-switch lock' };
|
|
1124
1250
|
}
|
|
1125
1251
|
}
|
|
1126
|
-
function
|
|
1252
|
+
function finishCodexKeepAlive(id) {
|
|
1127
1253
|
try {
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
// The live credentials belong to a different account; the live state
|
|
1176
|
-
// wins — point the registry at the matching saved profile if there is
|
|
1177
|
-
// one, and refresh its backup.
|
|
1178
|
-
const matching = findProfileForIdentity(liveIdentity);
|
|
1179
|
-
if (matching && matching.id !== active.id) {
|
|
1180
|
-
storeCodexProfileAuth(matching.id, liveAuthRaw, null, 'migration');
|
|
1181
|
-
(0, accountRegistry_1.setActiveSavedAccount)('codex', matching.id);
|
|
1254
|
+
withCodexAuthSwapLock(() => {
|
|
1255
|
+
const profile = listCodexAccounts().find((p) => p.id === id);
|
|
1256
|
+
const store = (0, codexCredentialStore_1.readCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(id));
|
|
1257
|
+
if (!profile || isLiveCodexProfile(profile) || store.status !== 'ok')
|
|
1258
|
+
return;
|
|
1259
|
+
if (!(0, codexAuth_1.identitiesMatch)((0, codexAuth_1.readAuthIdentityFromRaw)(store.snapshot.authRaw), profileIdentity(profile)))
|
|
1260
|
+
return;
|
|
1261
|
+
const backup = (0, codexCredentialStore_1.readCodexBackup)(id);
|
|
1262
|
+
if (backup.status === 'ok' &&
|
|
1263
|
+
(0, codexAuth_1.compareCodexAuthFreshness)(backup.snapshot.authRaw, store.snapshot.authRaw) > 0)
|
|
1264
|
+
return;
|
|
1265
|
+
(0, codexCredentialStore_1.writeCodexBackup)(id, store.snapshot);
|
|
1266
|
+
(0, accountHealth_1.writeCodexHealthSidecar)(id, store.snapshot.authRaw, 'keepalive');
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
finally {
|
|
1270
|
+
(0, accountLease_1.releaseAccountLease)('codex', id);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
function restoreCodexProfileFromBackup(id, options = {}) {
|
|
1274
|
+
try {
|
|
1275
|
+
return withCodexAuthSwapLock(() => {
|
|
1276
|
+
if ((0, codexCredentialStore_1.isSidekickCodexHome)((0, codexPaths_1.getSystemCodexHome)()))
|
|
1277
|
+
return 'none';
|
|
1278
|
+
const profile = listCodexAccounts().find((p) => p.id === id);
|
|
1279
|
+
if (!profile)
|
|
1280
|
+
return 'none';
|
|
1281
|
+
const live = (0, codexCredentialStore_1.readCodexCredentialStore)();
|
|
1282
|
+
if (live.status === 'error' || live.status === 'corrupt')
|
|
1283
|
+
return 'none';
|
|
1284
|
+
if (isLiveCodexProfile(profile))
|
|
1285
|
+
return 'live';
|
|
1286
|
+
const home = (0, codexPaths_1.getCodexProfileHome)(id);
|
|
1287
|
+
const store = (0, codexCredentialStore_1.readCodexCredentialStore)(home);
|
|
1288
|
+
if (store.status === 'ok') {
|
|
1289
|
+
const fp = (0, codexAuth_1.codexRefreshFingerprint)(store.snapshot.authRaw);
|
|
1290
|
+
if (fp && live.status === 'ok' && fp === (0, codexAuth_1.codexRefreshFingerprint)(live.snapshot.authRaw))
|
|
1291
|
+
return 'none';
|
|
1292
|
+
if (!(0, codexAuth_1.identitiesMatch)((0, codexAuth_1.readAuthIdentityFromRaw)(store.snapshot.authRaw), profileIdentity(profile)) &&
|
|
1293
|
+
(0, codexAuth_1.readAuthIdentityFromRaw)(store.snapshot.authRaw)?.authMode !== 'api-key' &&
|
|
1294
|
+
!(0, codexCredentialStore_1.isLegacyCodexLogin)(store.snapshot.legacyRaw))
|
|
1295
|
+
return 'none';
|
|
1296
|
+
const backup = (0, codexCredentialStore_1.readCodexBackup)(id);
|
|
1297
|
+
if (backup.status === 'ok' &&
|
|
1298
|
+
(0, codexAuth_1.compareCodexAuthFreshness)(backup.snapshot.authRaw, store.snapshot.authRaw) > 0)
|
|
1299
|
+
return 'none';
|
|
1300
|
+
return 'present';
|
|
1182
1301
|
}
|
|
1183
|
-
|
|
1184
|
-
|
|
1302
|
+
if (store.status !== 'absent' || options.restore === false)
|
|
1303
|
+
return 'none';
|
|
1304
|
+
const backup = (0, codexCredentialStore_1.readCodexBackup)(id);
|
|
1305
|
+
if (backup.status !== 'ok')
|
|
1306
|
+
return 'none';
|
|
1307
|
+
const fp = (0, codexAuth_1.codexRefreshFingerprint)(backup.snapshot.authRaw);
|
|
1308
|
+
if (fp && live.status === 'ok' && fp === (0, codexAuth_1.codexRefreshFingerprint)(live.snapshot.authRaw))
|
|
1309
|
+
return 'none';
|
|
1310
|
+
(0, codexCredentialStore_1.writeInactiveCodexCredentialStore)(home, backup.snapshot);
|
|
1311
|
+
return 'restored';
|
|
1312
|
+
});
|
|
1185
1313
|
}
|
|
1186
1314
|
catch {
|
|
1187
|
-
|
|
1315
|
+
return 'none';
|
|
1188
1316
|
}
|
|
1189
1317
|
}
|
|
1190
1318
|
function removeCodexAccount(profileId) {
|
|
@@ -1193,5 +1321,10 @@ function removeCodexAccount(profileId) {
|
|
|
1193
1321
|
return { success: false, error: `Codex account ${profileId} not found.` };
|
|
1194
1322
|
}
|
|
1195
1323
|
fs.rmSync((0, codexPaths_1.getCodexProfileDir)(profileId), { recursive: true, force: true });
|
|
1324
|
+
fs.rmSync((0, codexPaths_1.getCodexBackupPath)(profileId), { force: true });
|
|
1325
|
+
fs.rmSync((0, codexPaths_1.getCodexBackupPath)(profileId, true), { force: true });
|
|
1196
1326
|
return { success: true };
|
|
1197
1327
|
}
|
|
1328
|
+
async function cleanupAbandonedCodexLoginsAsync(olderThanMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
1329
|
+
return cleanupAbandonedCodexLogins(olderThanMs, now, await (0, processDetection_1.inspectRunningAccountConsumers)('codex'));
|
|
1330
|
+
}
|