sidekick-shared 0.27.4 → 0.27.6
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 +43 -0
- package/README.md +92 -45
- 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/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/claudeProfiles.d.ts +14 -0
- package/dist/claudeProfiles.js +22 -0
- 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 +8 -2
- package/dist/codexPaths.js +51 -7
- package/dist/codexProfiles.d.ts +37 -31
- package/dist/codexProfiles.js +842 -671
- package/dist/credentialIO.d.ts +67 -7
- package/dist/credentialIO.js +211 -47
- package/dist/ensureDefaultAccounts.js +1 -8
- package/dist/index.d.ts +17 -4
- package/dist/index.js +36 -11
- 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 +3 -0
- package/dist/node.js +4 -2
- package/dist/processDetection.d.ts +10 -3
- package/dist/processDetection.js +44 -11
- package/dist/promptHistory.d.ts +66 -0
- package/dist/promptHistory.js +37 -18
- 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/sessionPromptHistory.d.ts +120 -0
- package/dist/sessionPromptHistory.js +278 -0
- package/dist/sessionPromptSignals.d.ts +56 -0
- package/dist/sessionPromptSignals.js +355 -0
- package/dist/terminalSync.d.ts +12 -2
- package/dist/terminalSync.js +27 -2
- package/package.json +1 -1
package/dist/codexProfiles.js
CHANGED
|
@@ -35,6 +35,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.CODEX_KEYRING_FIX_HINT = exports.getCodexCredentialStoreMode = exports.getSystemCodexHome = exports.getCodexProfileHome = exports.getCodexProfilesDir = void 0;
|
|
37
37
|
exports.getCodexMonitoringHomes = getCodexMonitoringHomes;
|
|
38
|
+
exports.readStoredCodexAccount = readStoredCodexAccount;
|
|
39
|
+
exports.isLiveCodexProfile = isLiveCodexProfile;
|
|
40
|
+
exports.storeCodexProfileAuthUnlocked = storeCodexProfileAuthUnlocked;
|
|
38
41
|
exports.storeCodexProfileAuth = storeCodexProfileAuth;
|
|
39
42
|
exports.parseCodexLoginStatusOutput = parseCodexLoginStatusOutput;
|
|
40
43
|
exports.getCodexLoginStatus = getCodexLoginStatus;
|
|
@@ -50,18 +53,26 @@ exports.resolveActiveCodexAccount = resolveActiveCodexAccount;
|
|
|
50
53
|
exports.getCodexExecutionEnv = getCodexExecutionEnv;
|
|
51
54
|
exports.prepareCodexAccount = prepareCodexAccount;
|
|
52
55
|
exports.prepareCodexAccountAsync = prepareCodexAccountAsync;
|
|
56
|
+
exports.discardCodexLogin = discardCodexLogin;
|
|
53
57
|
exports.finalizeCodexAccount = finalizeCodexAccount;
|
|
54
58
|
exports.finalizeCodexAccountAsync = finalizeCodexAccountAsync;
|
|
55
59
|
exports.withCodexAuthSwapLock = withCodexAuthSwapLock;
|
|
56
60
|
exports.dedupeCodexProfiles = dedupeCodexProfiles;
|
|
61
|
+
exports.hasPendingCodexRepair = hasPendingCodexRepair;
|
|
57
62
|
exports.syncCodexLiveStateUnlocked = syncCodexLiveStateUnlocked;
|
|
58
63
|
exports.syncCodexLiveState = syncCodexLiveState;
|
|
64
|
+
exports.syncCodexLiveStateAsync = syncCodexLiveStateAsync;
|
|
59
65
|
exports.cleanupAbandonedCodexLogins = cleanupAbandonedCodexLogins;
|
|
60
66
|
exports.switchToCodexAccount = switchToCodexAccount;
|
|
61
67
|
exports.switchToCodexAccountAsync = switchToCodexAccountAsync;
|
|
62
68
|
exports.reconcileCodexAuthState = reconcileCodexAuthState;
|
|
63
69
|
exports.reconcileCodexAuthStateAsync = reconcileCodexAuthStateAsync;
|
|
70
|
+
exports.getCodexKeepAliveBlocker = getCodexKeepAliveBlocker;
|
|
71
|
+
exports.beginCodexKeepAlive = beginCodexKeepAlive;
|
|
72
|
+
exports.finishCodexKeepAlive = finishCodexKeepAlive;
|
|
73
|
+
exports.restoreCodexProfileFromBackup = restoreCodexProfileFromBackup;
|
|
64
74
|
exports.removeCodexAccount = removeCodexAccount;
|
|
75
|
+
exports.cleanupAbandonedCodexLoginsAsync = cleanupAbandonedCodexLoginsAsync;
|
|
65
76
|
const fs = __importStar(require("fs"));
|
|
66
77
|
const path = __importStar(require("path"));
|
|
67
78
|
const child_process_1 = require("child_process");
|
|
@@ -78,6 +89,9 @@ const accountSwitch_1 = require("./accountSwitch");
|
|
|
78
89
|
const accountSyncTypes_1 = require("./accountSyncTypes");
|
|
79
90
|
const processDetection_1 = require("./processDetection");
|
|
80
91
|
const quotaSnapshots_1 = require("./quotaSnapshots");
|
|
92
|
+
const accountLease_1 = require("./accountLease");
|
|
93
|
+
const liveCredentialGuard_1 = require("./liveCredentialGuard");
|
|
94
|
+
const codexCredentialStore_1 = require("./codexCredentialStore");
|
|
81
95
|
var codexPaths_2 = require("./codexPaths");
|
|
82
96
|
Object.defineProperty(exports, "getCodexProfilesDir", { enumerable: true, get: function () { return codexPaths_2.getCodexProfilesDir; } });
|
|
83
97
|
Object.defineProperty(exports, "getCodexProfileHome", { enumerable: true, get: function () { return codexPaths_2.getCodexProfileHome; } });
|
|
@@ -113,22 +127,16 @@ function getCodexMonitoringHomes() {
|
|
|
113
127
|
}
|
|
114
128
|
function readPendingProfile(profileId) {
|
|
115
129
|
try {
|
|
116
|
-
return JSON.parse(fs.readFileSync((0, codexPaths_1.
|
|
130
|
+
return JSON.parse(fs.readFileSync((0, codexPaths_1.getCodexLoginStatePath)(profileId), 'utf8'));
|
|
117
131
|
}
|
|
118
132
|
catch {
|
|
119
133
|
return null;
|
|
120
134
|
}
|
|
121
135
|
}
|
|
122
136
|
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;
|
|
137
|
+
const dir = (0, codexPaths_1.getCodexLoginDir)(profileId);
|
|
138
|
+
fs.mkdirSync(path.join(dir, 'codex-home'), { recursive: true, mode: 0o700 });
|
|
139
|
+
(0, atomic_1.atomicWriteJsonSync)(path.join(dir, 'profile.json'), pending);
|
|
132
140
|
}
|
|
133
141
|
/**
|
|
134
142
|
* Copy the live `config.toml` into a profile home, forcing file-based
|
|
@@ -139,24 +147,143 @@ function copySourceCodexConfig(sourceHome, targetHome) {
|
|
|
139
147
|
const source = (0, codexPaths_1.readFileOrNull)(path.join(sourceHome, 'config.toml')) ?? '';
|
|
140
148
|
(0, atomic_1.atomicWriteFileSync)(path.join(targetHome, 'config.toml'), (0, codexPaths_1.forceFileCredentialStore)(source));
|
|
141
149
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
150
|
+
/** Read the freshest saved login without creating a CLI-usable copy. */
|
|
151
|
+
function readStoredCodexAccount(profileId) {
|
|
152
|
+
const home = (0, codexCredentialStore_1.readCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(profileId));
|
|
153
|
+
const backup = (0, codexCredentialStore_1.readCodexBackup)(profileId);
|
|
154
|
+
if (home.status === 'ok' && backup.status === 'ok') {
|
|
155
|
+
return (0, codexCredentialStore_1.codexLoginOnly)((0, codexAuth_1.compareCodexAuthFreshness)(backup.snapshot.authRaw, home.snapshot.authRaw) > 0
|
|
156
|
+
? backup.snapshot
|
|
157
|
+
: home.snapshot);
|
|
158
|
+
}
|
|
159
|
+
return home.status === 'ok'
|
|
160
|
+
? (0, codexCredentialStore_1.codexLoginOnly)(home.snapshot)
|
|
161
|
+
: backup.status === 'ok'
|
|
162
|
+
? backup.snapshot
|
|
163
|
+
: null;
|
|
164
|
+
}
|
|
165
|
+
function profileMatchesSnapshot(profile, snapshot) {
|
|
166
|
+
const identity = (0, codexAuth_1.readAuthIdentityFromRaw)(snapshot.authRaw);
|
|
167
|
+
if ((0, codexAuth_1.identitiesMatch)(identity, profileIdentity(profile)))
|
|
168
|
+
return true;
|
|
169
|
+
// API keys have no account claims. Match their complete credential bytes, never the active pointer.
|
|
170
|
+
if (identity?.authMode === 'api-key' ||
|
|
171
|
+
(!snapshot.authRaw && (0, codexCredentialStore_1.isLegacyCodexLogin)(snapshot.legacyRaw))) {
|
|
172
|
+
const saved = readStoredCodexAccount(profile.id);
|
|
173
|
+
return (saved !== null &&
|
|
174
|
+
(0, codexCredentialStore_1.codexSnapshotFingerprint)((0, codexCredentialStore_1.codexLoginOnly)(snapshot)) === (0, codexCredentialStore_1.codexSnapshotFingerprint)(saved));
|
|
175
|
+
}
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
function isLiveCodexProfile(profile) {
|
|
179
|
+
const live = (0, codexCredentialStore_1.readCodexCredentialStore)();
|
|
180
|
+
return live.status === 'ok' && profileMatchesSnapshot(profile, live.snapshot);
|
|
181
|
+
}
|
|
182
|
+
/** Caller holds the swap lock. Live-account credentials only enter private storage. */
|
|
183
|
+
function storeCodexProfileAuthUnlocked(profileId, authRaw, legacyRaw, source) {
|
|
184
|
+
const candidate = (0, codexCredentialStore_1.codexLoginOnly)({ authRaw, legacyRaw });
|
|
185
|
+
const profile = listCodexAccounts().find((p) => p.id === profileId);
|
|
186
|
+
const identity = (0, codexAuth_1.readAuthIdentityFromRaw)(authRaw);
|
|
187
|
+
if (profile &&
|
|
188
|
+
identity?.authMode === 'chatgpt' &&
|
|
189
|
+
(identity.email || identity.workspaceId) &&
|
|
190
|
+
(profile.email || profile.metadata?.workspaceId) &&
|
|
191
|
+
!(0, codexAuth_1.identitiesMatch)(identity, profileIdentity(profile))) {
|
|
192
|
+
throw new Error('The Codex credential belongs to a different saved account.');
|
|
193
|
+
}
|
|
194
|
+
const old = (0, codexCredentialStore_1.readCodexBackup)(profileId);
|
|
195
|
+
const chosen = old.status === 'ok' && (0, codexAuth_1.compareCodexAuthFreshness)(old.snapshot.authRaw, authRaw) > 0
|
|
196
|
+
? old.snapshot
|
|
197
|
+
: candidate;
|
|
198
|
+
(0, codexCredentialStore_1.writeCodexBackup)(profileId, chosen);
|
|
199
|
+
const live = (0, codexCredentialStore_1.readCodexCredentialStore)();
|
|
200
|
+
const sameLink = live.status === 'ok' &&
|
|
201
|
+
(0, codexAuth_1.codexRefreshFingerprint)(chosen.authRaw) !== null &&
|
|
202
|
+
(0, codexAuth_1.codexRefreshFingerprint)(chosen.authRaw) === (0, codexAuth_1.codexRefreshFingerprint)(live.snapshot.authRaw);
|
|
203
|
+
const isLive = live.status === 'ok' &&
|
|
204
|
+
(profile
|
|
205
|
+
? profileMatchesSnapshot(profile, live.snapshot)
|
|
206
|
+
: (0, codexAuth_1.identitiesMatch)((0, codexAuth_1.readAuthIdentityFromRaw)(chosen.authRaw), (0, codexAuth_1.readAuthIdentityFromRaw)(live.snapshot.authRaw)));
|
|
207
|
+
if (!(0, codexCredentialStore_1.isSidekickCodexHome)((0, codexPaths_1.getSystemCodexHome)()) &&
|
|
208
|
+
live.status !== 'error' &&
|
|
209
|
+
live.status !== 'corrupt' &&
|
|
210
|
+
!isLive &&
|
|
211
|
+
!sameLink) {
|
|
212
|
+
(0, codexCredentialStore_1.writeInactiveCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(profileId), chosen);
|
|
213
|
+
}
|
|
214
|
+
if (chosen.authRaw)
|
|
215
|
+
(0, accountHealth_1.writeCodexHealthSidecar)(profileId, chosen.authRaw, source);
|
|
146
216
|
}
|
|
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
217
|
function storeCodexProfileAuth(profileId, authRaw, legacyRaw, source) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
if (
|
|
159
|
-
|
|
218
|
+
withCodexAuthSwapLock(() => storeCodexProfileAuthUnlocked(profileId, authRaw, legacyRaw, source));
|
|
219
|
+
}
|
|
220
|
+
function consumerBlocker(scan) {
|
|
221
|
+
if (!scan?.ok)
|
|
222
|
+
return 'Could not verify that Codex consumers are stopped; retry after closing Codex.';
|
|
223
|
+
if (scan.consumers.length)
|
|
224
|
+
return 'Close running Codex CLI, app, and IDE sessions before changing credentials. --force cannot bypass this check.';
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
/** The sole installer of live Codex credentials, including rollback and repair. */
|
|
228
|
+
function installIntoLiveCodexStore(candidate, accountId, intent, context) {
|
|
229
|
+
if ((0, accountLease_1.isAccountLeaseHeld)('codex', accountId))
|
|
230
|
+
return { ok: false, reason: 'Keep-alive holds this account; retry when it finishes.' };
|
|
231
|
+
const blocker = consumerBlocker(context.scan);
|
|
232
|
+
if (blocker)
|
|
233
|
+
return { ok: false, reason: blocker };
|
|
234
|
+
const mode = (0, codexPaths_1.getCodexCredentialStoreMode)();
|
|
235
|
+
if (mode !== 'file' && mode !== 'unknown')
|
|
236
|
+
return { ok: false, reason: exports.CODEX_KEYRING_FIX_HINT };
|
|
237
|
+
if ((0, codexCredentialStore_1.isSidekickCodexHome)((0, codexPaths_1.getSystemCodexHome)()))
|
|
238
|
+
return { ok: false, reason: 'Unset CODEX_HOME; it points at a Sidekick-owned home.' };
|
|
239
|
+
const current = (0, codexCredentialStore_1.readCodexCredentialStore)();
|
|
240
|
+
if (context.before.status === 'error' || !(0, codexCredentialStore_1.codexStoreMatches)(current, context.before.snapshot)) {
|
|
241
|
+
return { ok: false, reason: 'The live Codex credential changed; nothing was overwritten.' };
|
|
242
|
+
}
|
|
243
|
+
const raw = current.status === 'ok' ? current.snapshot.authRaw : null;
|
|
244
|
+
const fp = (0, codexAuth_1.codexRefreshFingerprint)(raw);
|
|
245
|
+
const candidateIdentity = (0, codexAuth_1.readAuthIdentityFromRaw)(candidate.authRaw);
|
|
246
|
+
const candidateProfile = listCodexAccounts().find((p) => p.id === accountId);
|
|
247
|
+
const validIdentity = candidateIdentity?.authMode !== 'chatgpt' ||
|
|
248
|
+
(candidateProfile !== undefined &&
|
|
249
|
+
(0, codexAuth_1.identitiesMatch)(candidateIdentity, profileIdentity(candidateProfile)));
|
|
250
|
+
const decision = (0, liveCredentialGuard_1.decideLiveCredentialWrite)({
|
|
251
|
+
provider: 'Codex',
|
|
252
|
+
intent,
|
|
253
|
+
liveStatus: current.status,
|
|
254
|
+
candidateValid: validIdentity && ((0, codexCredentialStore_1.isCodexLogin)(candidate.authRaw) || (0, codexCredentialStore_1.isLegacyCodexLogin)(candidate.legacyRaw)),
|
|
255
|
+
liveHasCredential: current.status === 'ok',
|
|
256
|
+
liveAccountId: context.liveAccountId,
|
|
257
|
+
candidateAccountId: accountId,
|
|
258
|
+
sameLink: fp !== null && fp === (0, codexAuth_1.codexRefreshFingerprint)(candidate.authRaw),
|
|
259
|
+
strictlyNewer: (0, codexAuth_1.compareCodexAuthFreshness)(candidate.authRaw, raw) > 0,
|
|
260
|
+
capturedOutgoing: context.capturedOutgoing,
|
|
261
|
+
matchesWritten: context.written !== undefined && (0, codexCredentialStore_1.codexStoreMatches)(current, context.written),
|
|
262
|
+
rejectCorrupt: true,
|
|
263
|
+
});
|
|
264
|
+
if (!decision.allow)
|
|
265
|
+
return { ok: false, reason: decision.reason };
|
|
266
|
+
if (current.status === 'error')
|
|
267
|
+
return { ok: false, reason: 'The live Codex login could not be read.' };
|
|
268
|
+
let expected;
|
|
269
|
+
try {
|
|
270
|
+
expected = (0, codexCredentialStore_1.codexInstalledSnapshot)(current.snapshot, candidate);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return {
|
|
274
|
+
ok: false,
|
|
275
|
+
reason: 'Legacy credentials would replace unrelated MCP data; sign in again.',
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
(0, codexCredentialStore_1.writeLiveCodexCredentialStoreUnguarded)(expected);
|
|
280
|
+
if (!(0, codexCredentialStore_1.codexStoreMatches)((0, codexCredentialStore_1.readCodexCredentialStore)(), expected))
|
|
281
|
+
throw new Error('verification failed');
|
|
282
|
+
return { ok: true, installed: expected };
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return { ok: false, reason: 'The live Codex write did not verify.', attempted: expected };
|
|
286
|
+
}
|
|
160
287
|
}
|
|
161
288
|
function readMetadataFromAuthJson(codexHome) {
|
|
162
289
|
const identity = (0, codexAuth_1.readAuthIdentityFromRaw)((0, codexPaths_1.readFileOrNull)(path.join(codexHome, 'auth.json')));
|
|
@@ -212,7 +339,7 @@ function getCodexLoginStatus(codexHome) {
|
|
|
212
339
|
timeout: PROBE_TIMEOUT_MS,
|
|
213
340
|
killSignal: 'SIGKILL',
|
|
214
341
|
});
|
|
215
|
-
return parseCodexLoginStatusOutput(result.status, String(result.stdout ?? ''));
|
|
342
|
+
return parseCodexLoginStatusOutput(result.status, `${String(result.stdout ?? '')}\n${String(result.stderr ?? '')}`);
|
|
216
343
|
}
|
|
217
344
|
catch {
|
|
218
345
|
// Ignore missing CLI or spawn errors.
|
|
@@ -223,9 +350,11 @@ function getCodexLoginStatusAsync(codexHome) {
|
|
|
223
350
|
return new Promise((resolve) => {
|
|
224
351
|
try {
|
|
225
352
|
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) => {
|
|
353
|
+
(0, child_process_1.execFile)('codex', ['login', 'status'], { encoding: 'utf8', env, timeout: PROBE_TIMEOUT_MS, killSignal: 'SIGKILL' }, (error, stdout, stderr) => {
|
|
227
354
|
// A missing CLI, non-zero exit, or timeout all mean "not logged in".
|
|
228
|
-
resolve(error
|
|
355
|
+
resolve(error
|
|
356
|
+
? { loggedIn: false }
|
|
357
|
+
: parseCodexLoginStatusOutput(0, `${String(stdout ?? '')}\n${String(stderr ?? '')}`));
|
|
229
358
|
});
|
|
230
359
|
}
|
|
231
360
|
catch {
|
|
@@ -258,15 +387,13 @@ async function readCodexAccountMetadataAsync(codexHome) {
|
|
|
258
387
|
metadataFromLoginStatus(await getCodexLoginStatusAsync(codexHome)));
|
|
259
388
|
}
|
|
260
389
|
function hasCodexCredentialFiles(codexHome) {
|
|
261
|
-
return (
|
|
262
|
-
fs.existsSync(path.join(codexHome, '.credentials.json')));
|
|
390
|
+
return (0, codexCredentialStore_1.readCodexCredentialStore)(codexHome).status === 'ok';
|
|
263
391
|
}
|
|
264
392
|
function isCodexProfileAuthenticated(codexHome) {
|
|
265
|
-
return hasCodexCredentialFiles(codexHome)
|
|
393
|
+
return hasCodexCredentialFiles(codexHome);
|
|
266
394
|
}
|
|
267
|
-
/** {@link isCodexProfileAuthenticated} without blocking the event loop on the CLI probe. */
|
|
268
395
|
async function isCodexProfileAuthenticatedAsync(codexHome) {
|
|
269
|
-
return
|
|
396
|
+
return isCodexProfileAuthenticated(codexHome);
|
|
270
397
|
}
|
|
271
398
|
function ensureUniqueCodexLabel(label, excludeId) {
|
|
272
399
|
const normalized = label.trim().toLowerCase();
|
|
@@ -350,220 +477,190 @@ function getCodexExecutionEnv(baseEnv = process.env) {
|
|
|
350
477
|
CODEX_HOME: resolveSidekickCodexHome(),
|
|
351
478
|
};
|
|
352
479
|
}
|
|
480
|
+
// ── Prepare / finalize (add account) ─────────────────────────────────────
|
|
353
481
|
function keyringRefusal(mode) {
|
|
354
482
|
return {
|
|
355
483
|
success: false,
|
|
356
484
|
needsLogin: false,
|
|
357
|
-
error: `Codex
|
|
485
|
+
error: `Codex credential storage (${mode}) is not file-based. ${exports.CODEX_KEYRING_FIX_HINT}`,
|
|
358
486
|
};
|
|
359
487
|
}
|
|
360
|
-
function prepareCodexAccountCore(label) {
|
|
488
|
+
function prepareCodexAccountCore(label, importCurrent, scan) {
|
|
361
489
|
const trimmedLabel = label.trim();
|
|
362
|
-
if (!trimmedLabel)
|
|
363
|
-
return {
|
|
364
|
-
}
|
|
490
|
+
if (!trimmedLabel)
|
|
491
|
+
return { success: false, error: 'Codex accounts require a non-empty label.' };
|
|
365
492
|
const labelError = ensureUniqueCodexLabel(trimmedLabel);
|
|
366
|
-
if (labelError)
|
|
367
|
-
return {
|
|
368
|
-
}
|
|
493
|
+
if (labelError)
|
|
494
|
+
return { success: false, error: labelError };
|
|
369
495
|
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);
|
|
496
|
+
const mode = (0, codexPaths_1.getCodexCredentialStoreMode)(sourceHome);
|
|
497
|
+
if (mode !== 'file' && mode !== 'unknown')
|
|
498
|
+
return keyringRefusal(mode);
|
|
499
|
+
if ((0, codexCredentialStore_1.isSidekickCodexHome)(sourceHome))
|
|
500
|
+
return {
|
|
501
|
+
success: false,
|
|
502
|
+
error: 'Unset CODEX_HOME before adding an account; it names a Sidekick-owned home.',
|
|
503
|
+
};
|
|
504
|
+
const live = (0, codexCredentialStore_1.readCodexCredentialStore)();
|
|
505
|
+
if (importCurrent && live.status === 'ok') {
|
|
506
|
+
const sync = syncCodexLiveStateUnlocked('live-sync', { scan });
|
|
507
|
+
const profile = listCodexAccounts().find((p) => profileMatchesSnapshot(p, live.snapshot));
|
|
508
|
+
if (!profile)
|
|
413
509
|
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
|
-
},
|
|
510
|
+
success: false,
|
|
511
|
+
error: sync.warnings[0] ?? 'The live Codex login has no identifiable account.',
|
|
421
512
|
};
|
|
422
|
-
|
|
423
|
-
|
|
513
|
+
const manual = profile.metadata?.origin === 'live-sync' || profile.label === profile.email;
|
|
514
|
+
if (manual)
|
|
515
|
+
(0, accountRegistry_1.upsertSavedAccountProfile)({
|
|
516
|
+
...profile,
|
|
517
|
+
label: trimmedLabel,
|
|
518
|
+
metadata: { ...profile.metadata, origin: 'manual' },
|
|
519
|
+
});
|
|
520
|
+
return {
|
|
521
|
+
success: true,
|
|
522
|
+
profileId: profile.id,
|
|
523
|
+
codexHome: sourceHome,
|
|
524
|
+
needsLogin: false,
|
|
525
|
+
warning: sync.warnings.join(' ') ||
|
|
526
|
+
(!sync.registered
|
|
527
|
+
? `This Codex login is already saved as ${profile.label ?? profile.id}.`
|
|
528
|
+
: undefined),
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
if (importCurrent && (live.status === 'corrupt' || live.status === 'error')) {
|
|
532
|
+
return {
|
|
533
|
+
success: false,
|
|
534
|
+
error: 'The live Codex credential could not be read; retry after Codex finishes writing it.',
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
const profileId = (0, crypto_1.randomUUID)();
|
|
538
|
+
try {
|
|
539
|
+
writePendingProfile(profileId, { label: trimmedLabel, addedAt: new Date().toISOString() });
|
|
540
|
+
const codexHome = (0, codexPaths_1.getCodexLoginHome)(profileId);
|
|
541
|
+
copySourceCodexConfig(sourceHome, codexHome);
|
|
542
|
+
return { success: true, profileId, codexHome, needsLogin: true };
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
discardCodexLogin(profileId);
|
|
546
|
+
return { success: false, error: 'Could not prepare the isolated Codex login.' };
|
|
424
547
|
}
|
|
425
|
-
return { result: { success: true, profileId, codexHome, needsLogin: true } };
|
|
426
548
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
return
|
|
549
|
+
/** Legacy callers can import current; explicit sign-in passes importCurrent: false. */
|
|
550
|
+
function prepareCodexAccount(label, options = {}) {
|
|
551
|
+
const scan = options.importCurrent === false ? undefined : (0, processDetection_1.inspectRunningAccountConsumersSync)('codex');
|
|
552
|
+
try {
|
|
553
|
+
return withCodexAuthSwapLock(() => prepareCodexAccountCore(label, options.importCurrent ?? true, scan));
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
return { success: false, error: 'Could not acquire the Codex account-switch lock.' };
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
async function prepareCodexAccountAsync(label, options = {}) {
|
|
560
|
+
const scan = options.importCurrent === false ? undefined : await (0, processDetection_1.inspectRunningAccountConsumers)('codex');
|
|
561
|
+
try {
|
|
562
|
+
return withCodexAuthSwapLock(() => prepareCodexAccountCore(label, options.importCurrent ?? true, scan));
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
return { success: false, error: 'Could not acquire the Codex account-switch lock.' };
|
|
432
566
|
}
|
|
433
|
-
return step.result;
|
|
434
567
|
}
|
|
435
|
-
/**
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
if (
|
|
439
|
-
|
|
440
|
-
|
|
568
|
+
/** Removes only a pending login, never a registered canonical profile. */
|
|
569
|
+
function discardCodexLogin(id) {
|
|
570
|
+
fs.rmSync((0, codexPaths_1.getCodexLoginDir)(id), { recursive: true, force: true });
|
|
571
|
+
if (!listCodexAccounts().some((p) => p.id === id) &&
|
|
572
|
+
fs.existsSync((0, codexPaths_1.getCodexProfileStatePath)(id))) {
|
|
573
|
+
fs.rmSync((0, codexPaths_1.getCodexProfileDir)(id), { recursive: true, force: true });
|
|
441
574
|
}
|
|
442
|
-
return step.result;
|
|
443
575
|
}
|
|
444
|
-
function finalizeCodexAccountCore(profileId, opts,
|
|
576
|
+
function finalizeCodexAccountCore(profileId, opts, scan) {
|
|
445
577
|
const pending = readPendingProfile(profileId);
|
|
446
578
|
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' },
|
|
579
|
+
const saved = listCodexAccounts().find((p) => p.id === profileId);
|
|
580
|
+
if (!saved)
|
|
581
|
+
return { success: false, error: `Codex profile ${profileId} was not prepared.` };
|
|
582
|
+
return opts.activate === false
|
|
583
|
+
? { success: true, profileId }
|
|
584
|
+
: performCodexAuthSwapCore(saved, {}, { scan });
|
|
585
|
+
}
|
|
586
|
+
const home = (0, codexPaths_1.getCodexLoginHome)(profileId);
|
|
587
|
+
const read = (0, codexCredentialStore_1.readCodexCredentialStore)(home);
|
|
588
|
+
if (read.status !== 'ok')
|
|
589
|
+
return {
|
|
590
|
+
success: false,
|
|
591
|
+
error: 'Codex profile is not authenticated with readable file credentials yet.',
|
|
475
592
|
};
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
593
|
+
const identity = (0, codexAuth_1.readAuthIdentityFromRaw)(read.snapshot.authRaw);
|
|
594
|
+
const metadata = readCodexAccountMetadataFromFiles(home) ?? {};
|
|
595
|
+
const existing = identity ? findProfileForIdentity(identity, { fallbackToActive: false }) : null;
|
|
596
|
+
// Legacy pending homes must not become a saved profile before their credential is moved out.
|
|
597
|
+
const id = existing?.id ?? (home === (0, codexPaths_1.getCodexProfileHome)(profileId) ? (0, crypto_1.randomUUID)() : profileId);
|
|
481
598
|
const profile = {
|
|
482
|
-
|
|
599
|
+
...existing,
|
|
600
|
+
id,
|
|
483
601
|
providerId: 'codex',
|
|
484
|
-
label: pending.label,
|
|
485
|
-
email:
|
|
486
|
-
addedAt: pending.addedAt,
|
|
487
|
-
metadata: { ...
|
|
602
|
+
label: existing?.label ?? pending.label,
|
|
603
|
+
email: metadata.email ?? existing?.email,
|
|
604
|
+
addedAt: existing?.addedAt ?? pending.addedAt,
|
|
605
|
+
metadata: { ...existing?.metadata, ...metadata, origin: 'login' },
|
|
488
606
|
};
|
|
489
|
-
(0, accountRegistry_1.upsertSavedAccountProfile)(profile);
|
|
490
|
-
if (authRaw)
|
|
491
|
-
(0, accountHealth_1.writeCodexHealthSidecar)(profileId, authRaw, 'login');
|
|
492
607
|
try {
|
|
493
|
-
|
|
608
|
+
// Preserve first. No successful sign-in is lost if activation is blocked.
|
|
609
|
+
const previous = (0, codexCredentialStore_1.readCodexBackup)(id);
|
|
610
|
+
(0, codexCredentialStore_1.writeCodexBackup)(id, previous.status === 'ok' &&
|
|
611
|
+
(0, codexAuth_1.compareCodexAuthFreshness)(previous.snapshot.authRaw, read.snapshot.authRaw) > 0
|
|
612
|
+
? previous.snapshot
|
|
613
|
+
: read.snapshot);
|
|
614
|
+
(0, codexCredentialStore_1.removeCodexProfileCredentials)(home, read.snapshot);
|
|
615
|
+
(0, accountRegistry_1.upsertSavedAccountProfile)(profile);
|
|
616
|
+
(0, codexPaths_1.ensureCodexProfileDirs)(id);
|
|
617
|
+
copySourceCodexConfig(home, (0, codexPaths_1.getCodexProfileHome)(id));
|
|
618
|
+
storeCodexProfileAuthUnlocked(id, read.snapshot.authRaw, read.snapshot.legacyRaw, 'login');
|
|
494
619
|
}
|
|
495
620
|
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
621
|
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
|
-
},
|
|
622
|
+
success: false,
|
|
623
|
+
profileId: id,
|
|
624
|
+
error: 'Could not finish saving the Codex login; any verified private backup was kept.',
|
|
511
625
|
};
|
|
512
626
|
}
|
|
513
|
-
|
|
627
|
+
finally {
|
|
628
|
+
discardCodexLogin(profileId);
|
|
629
|
+
}
|
|
630
|
+
if (opts.activate === false)
|
|
631
|
+
return { success: true, profileId: id };
|
|
632
|
+
const result = performCodexAuthSwapCore(profile, {}, { scan });
|
|
633
|
+
return result.success
|
|
634
|
+
? { ...result, profileId: id }
|
|
635
|
+
: {
|
|
636
|
+
...result,
|
|
637
|
+
profileId: id,
|
|
638
|
+
needsLogin: false,
|
|
639
|
+
error: `The login was saved, but could not be activated: ${result.error}`,
|
|
640
|
+
};
|
|
514
641
|
}
|
|
515
642
|
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;
|
|
643
|
+
const scan = (0, processDetection_1.inspectRunningAccountConsumersSync)('codex');
|
|
644
|
+
try {
|
|
645
|
+
return withCodexAuthSwapLock(() => finalizeCodexAccountCore(profileId, opts, scan));
|
|
646
|
+
}
|
|
647
|
+
catch {
|
|
648
|
+
return { success: false, error: 'Could not finalize the Codex login.' };
|
|
649
|
+
}
|
|
528
650
|
}
|
|
529
|
-
/** {@link finalizeCodexAccount} without blocking the event loop on CLI probes. */
|
|
530
651
|
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) {
|
|
652
|
+
const scan = await (0, processDetection_1.inspectRunningAccountConsumers)('codex');
|
|
549
653
|
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;
|
|
654
|
+
return withCodexAuthSwapLock(() => finalizeCodexAccountCore(profileId, opts, scan));
|
|
562
655
|
}
|
|
563
656
|
catch {
|
|
564
|
-
return
|
|
657
|
+
return { success: false, error: 'Could not finalize the Codex login.' };
|
|
565
658
|
}
|
|
566
659
|
}
|
|
660
|
+
// ── Stash / identity matching ────────────────────────────────────────────
|
|
661
|
+
function getCodexStashDir() {
|
|
662
|
+
return path.join((0, accountRegistry_1.getAccountsDir)(), 'codex', 'stash');
|
|
663
|
+
}
|
|
567
664
|
function findProfileForIdentity(identity, options = {}) {
|
|
568
665
|
const profiles = listCodexAccounts();
|
|
569
666
|
if (identity?.workspaceId) {
|
|
@@ -572,7 +669,8 @@ function findProfileForIdentity(identity, options = {}) {
|
|
|
572
669
|
return byWorkspace;
|
|
573
670
|
}
|
|
574
671
|
if (identity?.email) {
|
|
575
|
-
const byEmail = profiles.find((profile) => (profile.email ?? profile.metadata?.email) === identity.email
|
|
672
|
+
const byEmail = profiles.find((profile) => (profile.email ?? profile.metadata?.email) === identity.email &&
|
|
673
|
+
(!identity.workspaceId || !profile.metadata?.workspaceId));
|
|
576
674
|
if (byEmail)
|
|
577
675
|
return byEmail;
|
|
578
676
|
}
|
|
@@ -597,468 +695,485 @@ function withCodexAuthSwapLock(operation) {
|
|
|
597
695
|
fs.mkdirSync(lockDir, { recursive: true, mode: 0o700 });
|
|
598
696
|
return (0, atomic_1.withFileLockSync)(path.join(lockDir, 'auth-swap.lock'), operation);
|
|
599
697
|
}
|
|
600
|
-
/**
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
* Returns the removed ids. Caller holds the swap lock.
|
|
605
|
-
*/
|
|
606
|
-
function dedupeCodexProfiles() {
|
|
698
|
+
/** Merge owned profiles only after preserving their newest login in a verified backup. */
|
|
699
|
+
function dedupeCodexProfiles(scan) {
|
|
700
|
+
if (consumerBlocker(scan))
|
|
701
|
+
return [];
|
|
607
702
|
const removed = [];
|
|
608
|
-
const profiles = listCodexAccounts();
|
|
609
703
|
const groups = new Map();
|
|
610
|
-
for (const profile of
|
|
704
|
+
for (const profile of listCodexAccounts()) {
|
|
611
705
|
const key = profile.metadata?.workspaceId
|
|
612
706
|
? `ws:${profile.metadata.workspaceId}`
|
|
613
|
-
:
|
|
614
|
-
? `email:${profile.email
|
|
707
|
+
: profile.email
|
|
708
|
+
? `email:${profile.email}`
|
|
615
709
|
: `id:${profile.id}`;
|
|
616
710
|
groups.set(key, [...(groups.get(key) ?? []), profile]);
|
|
617
711
|
}
|
|
618
712
|
for (const group of groups.values()) {
|
|
619
|
-
if (group.length < 2)
|
|
713
|
+
if (group.length < 2 || group.some((p) => (0, accountLease_1.isAccountLeaseHeld)('codex', p.id)))
|
|
620
714
|
continue;
|
|
621
715
|
const sorted = [...group].sort((a, b) => Date.parse(a.addedAt) - Date.parse(b.addedAt));
|
|
622
716
|
const keeper = sorted[0];
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
717
|
+
let freshest = readStoredCodexAccount(keeper.id);
|
|
718
|
+
for (const p of sorted.slice(1)) {
|
|
719
|
+
const candidate = readStoredCodexAccount(p.id);
|
|
720
|
+
if (candidate &&
|
|
721
|
+
(!freshest || (0, codexAuth_1.compareCodexAuthFreshness)(candidate.authRaw, freshest.authRaw) > 0))
|
|
722
|
+
freshest = candidate;
|
|
723
|
+
}
|
|
724
|
+
if (!freshest)
|
|
725
|
+
continue;
|
|
726
|
+
(0, codexCredentialStore_1.writeCodexBackup)(keeper.id, freshest);
|
|
627
727
|
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
|
-
}
|
|
728
|
+
const store = (0, codexCredentialStore_1.readCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(loser.id));
|
|
729
|
+
if (store.status === 'error' || store.status === 'corrupt')
|
|
730
|
+
continue;
|
|
731
|
+
if (store.status === 'ok')
|
|
732
|
+
(0, codexCredentialStore_1.removeCodexProfileCredentials)((0, codexPaths_1.getCodexProfileHome)(loser.id), store.snapshot);
|
|
733
|
+
const snapshot = (0, quotaSnapshots_1.readQuotaSnapshot)('codex', loser.id);
|
|
734
|
+
if (snapshot && !(0, quotaSnapshots_1.readQuotaSnapshot)('codex', keeper.id))
|
|
735
|
+
(0, quotaSnapshots_1.writeQuotaSnapshot)('codex', keeper.id, snapshot);
|
|
736
|
+
const archive = path.join(getCodexStashDir(), `dup-${loser.id}`);
|
|
737
|
+
fs.mkdirSync(path.dirname(archive), { recursive: true, mode: 0o700 });
|
|
738
|
+
if (fs.existsSync((0, codexPaths_1.getCodexProfileDir)(loser.id)))
|
|
739
|
+
fs.renameSync((0, codexPaths_1.getCodexProfileDir)(loser.id), archive);
|
|
740
|
+
if ((0, accountRegistry_1.getActiveSavedAccount)('codex')?.id === loser.id)
|
|
741
|
+
(0, accountRegistry_1.setActiveSavedAccount)('codex', keeper.id, { silent: true });
|
|
653
742
|
(0, accountRegistry_1.removeSavedAccountProfile)('codex', loser.id);
|
|
743
|
+
fs.rmSync((0, codexPaths_1.getCodexBackupPath)(loser.id), { force: true });
|
|
744
|
+
fs.rmSync((0, codexPaths_1.getCodexBackupPath)(loser.id, true), { force: true });
|
|
654
745
|
removed.push(loser.id);
|
|
655
|
-
if (activeId === loser.id)
|
|
656
|
-
(0, accountRegistry_1.setActiveSavedAccount)('codex', keeper.id, { silent: true });
|
|
657
|
-
}
|
|
658
|
-
if (freshestRaw && freshestRaw !== (0, codexPaths_1.readFileOrNull)(keeperAuthPath)) {
|
|
659
|
-
storeCodexProfileAuth(keeper.id, freshestRaw, null, 'migration');
|
|
660
746
|
}
|
|
661
|
-
|
|
662
|
-
(0, accountRegistry_1.upsertSavedAccountProfile)({
|
|
663
|
-
...keeper,
|
|
664
|
-
email: metadata.email ?? keeper.email,
|
|
665
|
-
metadata: { ...keeper.metadata, ...metadata },
|
|
666
|
-
});
|
|
747
|
+
storeCodexProfileAuthUnlocked(keeper.id, freshest.authRaw, freshest.legacyRaw, 'migration');
|
|
667
748
|
}
|
|
668
749
|
return removed;
|
|
669
750
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
751
|
+
function codexJournalPath() {
|
|
752
|
+
return path.join((0, accountRegistry_1.getAccountsDir)(), 'codex', 'switch-journal.json');
|
|
753
|
+
}
|
|
754
|
+
function loginHash(snapshot) {
|
|
755
|
+
return (0, codexCredentialStore_1.codexSnapshotFingerprint)((0, codexCredentialStore_1.codexLoginOnly)(snapshot));
|
|
756
|
+
}
|
|
757
|
+
function clearCodexJournal() {
|
|
758
|
+
fs.rmSync(codexJournalPath(), { force: true });
|
|
759
|
+
}
|
|
760
|
+
function hasPendingCodexRepair() {
|
|
761
|
+
if (fs.existsSync(codexJournalPath()))
|
|
762
|
+
return true;
|
|
763
|
+
const live = (0, codexCredentialStore_1.readCodexCredentialStore)();
|
|
764
|
+
if (live.status === 'error' || live.status === 'corrupt')
|
|
765
|
+
return true;
|
|
766
|
+
if (live.status !== 'ok')
|
|
767
|
+
return false;
|
|
768
|
+
return listCodexAccounts().some((p) => {
|
|
769
|
+
if (!profileMatchesSnapshot(p, live.snapshot))
|
|
770
|
+
return false;
|
|
771
|
+
const home = (0, codexCredentialStore_1.readCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(p.id));
|
|
772
|
+
const backup = (0, codexCredentialStore_1.readCodexBackup)(p.id);
|
|
773
|
+
return (home.status !== 'absent' ||
|
|
774
|
+
(backup.status === 'ok' &&
|
|
775
|
+
(0, codexAuth_1.compareCodexAuthFreshness)(backup.snapshot.authRaw, live.snapshot.authRaw) > 0));
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
function resolveCodexJournal(report, scan) {
|
|
779
|
+
if (!fs.existsSync(codexJournalPath()))
|
|
780
|
+
return;
|
|
781
|
+
let journal;
|
|
782
|
+
try {
|
|
783
|
+
journal = JSON.parse(fs.readFileSync(codexJournalPath(), 'utf8'));
|
|
784
|
+
if (!journal.to || !journal.fromHash || !journal.toHash)
|
|
785
|
+
throw new Error();
|
|
786
|
+
}
|
|
787
|
+
catch {
|
|
788
|
+
report.warnings.push('The Codex switch journal is unreadable; run accounts doctor.');
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
const live = (0, codexCredentialStore_1.readCodexCredentialStore)();
|
|
792
|
+
if (live.status === 'error' || live.status === 'corrupt') {
|
|
793
|
+
report.warnings.push('An interrupted Codex switch is pending; the live login could not be read.');
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
const blocker = consumerBlocker(scan);
|
|
797
|
+
if (blocker) {
|
|
798
|
+
report.warnings.push(blocker);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
const hash = loginHash(live.snapshot);
|
|
802
|
+
const to = listCodexAccounts().find((p) => p.id === journal.to);
|
|
803
|
+
if (hash === journal.toHash ||
|
|
804
|
+
(live.status === 'ok' && to && profileMatchesSnapshot(to, live.snapshot))) {
|
|
805
|
+
if (live.status !== 'ok')
|
|
806
|
+
return;
|
|
807
|
+
(0, codexCredentialStore_1.writeCodexBackup)(journal.to, live.snapshot);
|
|
808
|
+
const target = (0, codexCredentialStore_1.readCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(journal.to));
|
|
809
|
+
if (target.status === 'error' || target.status === 'corrupt')
|
|
810
|
+
throw new Error('The interrupted switch target is unreadable.');
|
|
811
|
+
if (target.status === 'ok')
|
|
812
|
+
(0, codexCredentialStore_1.removeCodexProfileCredentials)((0, codexPaths_1.getCodexProfileHome)(journal.to), target.snapshot);
|
|
813
|
+
if (journal.from) {
|
|
814
|
+
const backup = (0, codexCredentialStore_1.readCodexBackup)(journal.from);
|
|
815
|
+
if (backup.status === 'ok' && loginHash(backup.snapshot) === journal.fromHash) {
|
|
816
|
+
if ((0, codexCredentialStore_1.readCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(journal.from)).status === 'absent')
|
|
817
|
+
(0, codexCredentialStore_1.writeInactiveCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(journal.from), backup.snapshot);
|
|
818
|
+
(0, accountHealth_1.writeCodexHealthSidecar)(journal.from, backup.snapshot.authRaw, 'switch');
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
(0, accountRegistry_1.setActiveSavedAccount)('codex', journal.to, { silent: true });
|
|
822
|
+
report.journal = 'finished';
|
|
823
|
+
}
|
|
824
|
+
else if (hash === journal.fromHash) {
|
|
825
|
+
const target = (0, codexCredentialStore_1.readCodexBackup)(journal.to);
|
|
826
|
+
if (target.status === 'ok' && loginHash(target.snapshot) === journal.toHash) {
|
|
827
|
+
if ((0, codexCredentialStore_1.readCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(journal.to)).status === 'absent')
|
|
828
|
+
(0, codexCredentialStore_1.writeInactiveCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(journal.to), target.snapshot);
|
|
829
|
+
}
|
|
830
|
+
if (journal.from) {
|
|
831
|
+
const outgoing = (0, codexCredentialStore_1.readCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(journal.from));
|
|
832
|
+
if (outgoing.status === 'ok')
|
|
833
|
+
(0, codexCredentialStore_1.removeCodexProfileCredentials)((0, codexPaths_1.getCodexProfileHome)(journal.from), outgoing.snapshot);
|
|
834
|
+
}
|
|
835
|
+
(0, accountRegistry_1.setActiveSavedAccount)('codex', journal.from, { silent: true });
|
|
836
|
+
report.journal = 'rolled-back';
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
report.journal = 'cleared';
|
|
840
|
+
report.warnings.push('The live Codex login matches neither side of the interrupted switch; it was kept. Private backups are available for inspection.');
|
|
841
|
+
}
|
|
842
|
+
clearCodexJournal();
|
|
843
|
+
}
|
|
844
|
+
/** No subprocesses inside this function: hosts resolve process scans before taking the lock. */
|
|
676
845
|
function syncCodexLiveStateUnlocked(source = 'live-sync', probes = {}) {
|
|
677
846
|
const report = (0, accountSyncTypes_1.emptyProviderSyncReport)();
|
|
678
847
|
try {
|
|
679
|
-
const
|
|
848
|
+
const mode = (0, codexPaths_1.getCodexCredentialStoreMode)();
|
|
849
|
+
if (mode !== 'file' && mode !== 'unknown') {
|
|
850
|
+
report.skipped = 'keyring';
|
|
851
|
+
report.warnings.push(exports.CODEX_KEYRING_FIX_HINT);
|
|
852
|
+
return report;
|
|
853
|
+
}
|
|
854
|
+
if ((0, codexCredentialStore_1.isSidekickCodexHome)((0, codexPaths_1.getSystemCodexHome)())) {
|
|
855
|
+
report.skipped = 'managed-home';
|
|
856
|
+
report.warnings.push('Unset CODEX_HOME before syncing; it points at a Sidekick-owned home.');
|
|
857
|
+
return report;
|
|
858
|
+
}
|
|
859
|
+
resolveCodexJournal(report, probes.scan);
|
|
860
|
+
let live = (0, codexCredentialStore_1.readCodexCredentialStore)();
|
|
861
|
+
if (live.status === 'error' || live.status === 'corrupt') {
|
|
862
|
+
report.skipped = 'unreadable';
|
|
863
|
+
report.warnings.push('The live Codex login could not be read; nothing was replaced.');
|
|
864
|
+
return report;
|
|
865
|
+
}
|
|
866
|
+
if (live.status === 'absent') {
|
|
867
|
+
report.skipped = 'logged-out';
|
|
868
|
+
return report;
|
|
869
|
+
}
|
|
870
|
+
const merged = dedupeCodexProfiles(probes.scan);
|
|
680
871
|
if (merged.length)
|
|
681
872
|
report.merged = merged;
|
|
682
|
-
const
|
|
683
|
-
const
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
report.
|
|
691
|
-
|
|
692
|
-
else {
|
|
693
|
-
report.skipped = 'logged-out';
|
|
873
|
+
const identity = (0, codexAuth_1.readAuthIdentityFromRaw)(live.snapshot.authRaw);
|
|
874
|
+
const initialSnapshot = live.snapshot;
|
|
875
|
+
let profile = listCodexAccounts().find((p) => profileMatchesSnapshot(p, initialSnapshot));
|
|
876
|
+
if (!profile) {
|
|
877
|
+
if (!identity?.email &&
|
|
878
|
+
!identity?.workspaceId &&
|
|
879
|
+
identity?.authMode !== 'api-key' &&
|
|
880
|
+
!(0, codexCredentialStore_1.isLegacyCodexLogin)(live.snapshot.legacyRaw)) {
|
|
881
|
+
report.skipped = 'no-identity';
|
|
882
|
+
return report;
|
|
694
883
|
}
|
|
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
884
|
profile = {
|
|
702
|
-
id:
|
|
885
|
+
id: (0, crypto_1.randomUUID)(),
|
|
703
886
|
providerId: 'codex',
|
|
704
|
-
label: identity
|
|
705
|
-
email: identity
|
|
887
|
+
label: identity?.email ?? identity?.workspaceId ?? 'Codex API key',
|
|
888
|
+
email: identity?.email,
|
|
706
889
|
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
|
-
},
|
|
890
|
+
metadata: { ...identity, authMode: identity?.authMode ?? 'api-key', origin: 'live-sync' },
|
|
714
891
|
};
|
|
715
|
-
|
|
716
|
-
|
|
892
|
+
// API-key identities require their verified bytes before registry matching can work.
|
|
893
|
+
(0, codexCredentialStore_1.writeCodexBackup)(profile.id, live.snapshot);
|
|
717
894
|
(0, accountRegistry_1.upsertSavedAccountProfile)(profile);
|
|
718
|
-
report.registered = { id:
|
|
895
|
+
report.registered = { id: profile.id, email: profile.email };
|
|
719
896
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
897
|
+
const home = (0, codexPaths_1.getCodexProfileHome)(profile.id);
|
|
898
|
+
(0, codexPaths_1.ensureCodexProfileDirs)(profile.id);
|
|
899
|
+
if (!fs.existsSync(path.join(home, 'config.toml')))
|
|
900
|
+
copySourceCodexConfig((0, codexPaths_1.getSystemCodexHome)(), home);
|
|
901
|
+
const duplicate = (0, codexCredentialStore_1.readCodexCredentialStore)(home);
|
|
902
|
+
const stored = readStoredCodexAccount(profile.id);
|
|
903
|
+
if (stored && (0, codexAuth_1.compareCodexAuthFreshness)(stored.authRaw, live.snapshot.authRaw) > 0) {
|
|
904
|
+
const install = installIntoLiveCodexStore(stored, profile.id, 'repair', {
|
|
905
|
+
before: live,
|
|
906
|
+
liveAccountId: profile.id,
|
|
907
|
+
scan: probes.scan,
|
|
908
|
+
});
|
|
909
|
+
if (install.ok) {
|
|
910
|
+
live = { status: 'ok', snapshot: install.installed };
|
|
911
|
+
report.repaired = profile.id;
|
|
726
912
|
}
|
|
727
913
|
else {
|
|
728
|
-
report.
|
|
729
|
-
return report;
|
|
914
|
+
report.warnings.push(`A newer Codex login is saved privately; repair is pending. ${install.reason}`);
|
|
730
915
|
}
|
|
731
916
|
}
|
|
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}`);
|
|
917
|
+
const backup = (0, codexCredentialStore_1.readCodexBackup)(profile.id);
|
|
918
|
+
if (backup.status !== 'ok' ||
|
|
919
|
+
(0, codexAuth_1.compareCodexAuthFreshness)(backup.snapshot.authRaw, live.snapshot.authRaw) <= 0) {
|
|
920
|
+
(0, codexCredentialStore_1.writeCodexBackup)(profile.id, live.snapshot);
|
|
921
|
+
report.folded = profile.id;
|
|
922
|
+
}
|
|
923
|
+
(0, accountHealth_1.writeCodexHealthSidecar)(profile.id, live.snapshot.authRaw, source);
|
|
924
|
+
if (duplicate.status === 'ok') {
|
|
925
|
+
const blocker = consumerBlocker(probes.scan);
|
|
926
|
+
// Preserve a newer copy even when its promotion must wait.
|
|
927
|
+
if ((0, codexAuth_1.compareCodexAuthFreshness)(duplicate.snapshot.authRaw, live.snapshot.authRaw) > 0)
|
|
928
|
+
(0, codexCredentialStore_1.writeCodexBackup)(profile.id, duplicate.snapshot);
|
|
929
|
+
if (blocker || (0, accountLease_1.isAccountLeaseHeld)('codex', profile.id))
|
|
930
|
+
report.warnings.push(blocker ?? 'The profile is leased; duplicate cleanup is pending.');
|
|
931
|
+
else {
|
|
932
|
+
(0, codexCredentialStore_1.removeCodexProfileCredentials)(home, duplicate.snapshot);
|
|
933
|
+
report.demoted = profile.id;
|
|
754
934
|
}
|
|
755
935
|
}
|
|
756
|
-
|
|
757
|
-
|
|
936
|
+
else if (duplicate.status === 'corrupt' || duplicate.status === 'error') {
|
|
937
|
+
report.warnings.push('The live account has an unreadable profile credential; run accounts doctor.');
|
|
938
|
+
}
|
|
939
|
+
if ((0, accountRegistry_1.getActiveSavedAccount)('codex')?.id !== profile.id) {
|
|
758
940
|
(0, accountRegistry_1.setActiveSavedAccount)('codex', profile.id, { silent: true });
|
|
759
941
|
report.repointed = profile.id;
|
|
760
942
|
}
|
|
761
943
|
}
|
|
762
|
-
catch
|
|
763
|
-
report.warnings.push(
|
|
944
|
+
catch {
|
|
945
|
+
report.warnings.push('Codex account sync could not finish; verified credentials were preserved for the next retry.');
|
|
764
946
|
}
|
|
765
947
|
return report;
|
|
766
948
|
}
|
|
767
|
-
/** {@link syncCodexLiveStateUnlocked} under the swap lock. */
|
|
768
949
|
function syncCodexLiveState(source = 'live-sync') {
|
|
950
|
+
const scan = (0, processDetection_1.inspectRunningAccountConsumersSync)('codex');
|
|
769
951
|
try {
|
|
770
|
-
return withCodexAuthSwapLock(() => syncCodexLiveStateUnlocked(source));
|
|
952
|
+
return withCodexAuthSwapLock(() => syncCodexLiveStateUnlocked(source, { scan }));
|
|
771
953
|
}
|
|
772
|
-
catch
|
|
773
|
-
return { warnings: [
|
|
954
|
+
catch {
|
|
955
|
+
return { warnings: ['Could not acquire the Codex account-switch lock.'] };
|
|
774
956
|
}
|
|
775
957
|
}
|
|
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 = [];
|
|
958
|
+
async function syncCodexLiveStateAsync(source = 'live-sync') {
|
|
959
|
+
const scan = await (0, processDetection_1.inspectRunningAccountConsumers)('codex');
|
|
783
960
|
try {
|
|
784
|
-
|
|
961
|
+
return withCodexAuthSwapLock(() => syncCodexLiveStateUnlocked(source, { scan }));
|
|
785
962
|
}
|
|
786
963
|
catch {
|
|
787
|
-
return
|
|
964
|
+
return { warnings: ['Could not acquire the Codex account-switch lock.'] };
|
|
788
965
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
966
|
+
}
|
|
967
|
+
/** Only abandoned Sidekick-owned login homes; authenticated copies are preserved privately first. */
|
|
968
|
+
function cleanupAbandonedCodexLogins(olderThanMs = 24 * 60 * 60 * 1000, now = Date.now(), scan) {
|
|
969
|
+
const removed = [];
|
|
970
|
+
if (consumerBlocker(scan ?? (0, processDetection_1.inspectRunningAccountConsumersSync)('codex')))
|
|
971
|
+
return removed;
|
|
972
|
+
withCodexAuthSwapLock(() => {
|
|
973
|
+
for (const root of [(0, codexPaths_1.getCodexLoginsDir)(), (0, codexPaths_1.getCodexProfilesDir)()]) {
|
|
974
|
+
if (!fs.existsSync(root))
|
|
975
|
+
continue;
|
|
976
|
+
for (const id of fs.readdirSync(root)) {
|
|
977
|
+
if (listCodexAccounts().some((p) => p.id === id))
|
|
978
|
+
continue;
|
|
979
|
+
const pending = readPendingProfile(id);
|
|
980
|
+
if (!pending || now - Date.parse(pending.addedAt) < olderThanMs)
|
|
981
|
+
continue;
|
|
982
|
+
const home = (0, codexPaths_1.getCodexLoginHome)(id);
|
|
983
|
+
const read = (0, codexCredentialStore_1.readCodexCredentialStore)(home);
|
|
984
|
+
if (read.status === 'error' || read.status === 'corrupt')
|
|
985
|
+
continue;
|
|
986
|
+
if (read.status === 'ok') {
|
|
987
|
+
const identity = (0, codexAuth_1.readAuthIdentityFromRaw)(read.snapshot.authRaw);
|
|
988
|
+
const owner = identity
|
|
989
|
+
? findProfileForIdentity(identity, { fallbackToActive: false })
|
|
990
|
+
: null;
|
|
991
|
+
if (!owner)
|
|
992
|
+
continue; // An unregistered authenticated login needs a human decision.
|
|
993
|
+
const saved = readStoredCodexAccount(owner.id);
|
|
994
|
+
(0, codexCredentialStore_1.writeCodexBackup)(owner.id, saved && (0, codexAuth_1.compareCodexAuthFreshness)(saved.authRaw, read.snapshot.authRaw) >= 0
|
|
995
|
+
? saved
|
|
996
|
+
: read.snapshot);
|
|
997
|
+
}
|
|
998
|
+
discardCodexLogin(id);
|
|
999
|
+
removed.push(id);
|
|
1000
|
+
}
|
|
808
1001
|
}
|
|
809
|
-
}
|
|
1002
|
+
});
|
|
810
1003
|
return removed;
|
|
811
1004
|
}
|
|
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
1005
|
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
1006
|
const base = {
|
|
878
1007
|
success: true,
|
|
879
1008
|
provider: 'codex',
|
|
880
1009
|
accountId: target.id,
|
|
881
|
-
previousAccountId,
|
|
1010
|
+
previousAccountId: (0, accountRegistry_1.getActiveSavedAccount)('codex')?.id ?? null,
|
|
882
1011
|
verified: false,
|
|
883
1012
|
verification: 'none',
|
|
884
1013
|
warnings: [],
|
|
885
1014
|
hints: [],
|
|
886
|
-
runningConsumers: probes.consumers,
|
|
1015
|
+
runningConsumers: probes.scan.consumers,
|
|
887
1016
|
email: target.email ?? target.metadata?.email,
|
|
888
1017
|
};
|
|
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);
|
|
1018
|
+
const fail = (error, extra = {}) => (0, accountSwitch_1.finishSwitchResult)({ ...base, success: false, error, ...extra });
|
|
1019
|
+
const mode = (0, codexPaths_1.getCodexCredentialStoreMode)();
|
|
1020
|
+
if (mode !== 'file' && mode !== 'unknown')
|
|
1021
|
+
return fail(exports.CODEX_KEYRING_FIX_HINT);
|
|
1022
|
+
if ((0, codexCredentialStore_1.isSidekickCodexHome)((0, codexPaths_1.getSystemCodexHome)()))
|
|
1023
|
+
return fail('CODEX_HOME points at a Sidekick-owned home; unset it before switching.');
|
|
1024
|
+
let before = (0, codexCredentialStore_1.readCodexCredentialStore)();
|
|
1025
|
+
if (before.status === 'error' || before.status === 'corrupt')
|
|
1026
|
+
return fail('The live Codex login could not be read; it was left untouched.');
|
|
1027
|
+
const saved = readStoredCodexAccount(target.id);
|
|
1028
|
+
if (before.status === 'ok' && profileMatchesSnapshot(target, before.snapshot)) {
|
|
1029
|
+
if (saved && (0, codexAuth_1.compareCodexAuthFreshness)(saved.authRaw, before.snapshot.authRaw) > 0) {
|
|
1030
|
+
const result = installIntoLiveCodexStore(saved, target.id, 'relogin', {
|
|
1031
|
+
before,
|
|
1032
|
+
liveAccountId: target.id,
|
|
1033
|
+
scan: probes.scan,
|
|
1034
|
+
});
|
|
1035
|
+
if (!result.ok)
|
|
1036
|
+
return fail(result.reason);
|
|
1037
|
+
}
|
|
1038
|
+
const sync = syncCodexLiveStateUnlocked('switch', probes);
|
|
959
1039
|
return (0, accountSwitch_1.finishSwitchResult)({
|
|
960
1040
|
...base,
|
|
961
1041
|
alreadyActive: true,
|
|
962
1042
|
verified: true,
|
|
963
1043
|
verification: 'store',
|
|
1044
|
+
warnings: sync.warnings,
|
|
964
1045
|
health: (0, accountHealth_1.getAccountHealth)('codex', target.id),
|
|
965
|
-
hints: [
|
|
1046
|
+
hints: ['That account is already the live Codex login.'],
|
|
966
1047
|
});
|
|
967
1048
|
}
|
|
968
|
-
|
|
1049
|
+
const blocker = consumerBlocker(probes.scan);
|
|
1050
|
+
if (blocker)
|
|
1051
|
+
return fail(blocker);
|
|
1052
|
+
if ((0, accountLease_1.isAccountLeaseHeld)('codex', target.id))
|
|
1053
|
+
return fail('Keep-alive is refreshing this account; retry after it finishes.');
|
|
1054
|
+
const sync = syncCodexLiveStateUnlocked('switch', probes);
|
|
1055
|
+
base.warnings.push(...sync.warnings);
|
|
1056
|
+
if (fs.existsSync(codexJournalPath()))
|
|
1057
|
+
return fail('An interrupted Codex switch still needs recovery; run accounts doctor.');
|
|
1058
|
+
before = (0, codexCredentialStore_1.readCodexCredentialStore)();
|
|
1059
|
+
if (before.status === 'error' || before.status === 'corrupt')
|
|
1060
|
+
return fail('The live Codex credential could not be read.');
|
|
1061
|
+
const beforeSnapshot = before.snapshot;
|
|
1062
|
+
const outgoing = before.status === 'ok'
|
|
1063
|
+
? listCodexAccounts().find((p) => profileMatchesSnapshot(p, beforeSnapshot))
|
|
1064
|
+
: null;
|
|
1065
|
+
if (before.status === 'ok' && !outgoing)
|
|
1066
|
+
return fail('The live login has no unambiguous account identity to save it under; sign in with Codex and retry.');
|
|
1067
|
+
base.previousAccountId = outgoing?.id ?? null;
|
|
1068
|
+
if (outgoing?.id === target.id)
|
|
1069
|
+
return fail('The live account changed during the switch; retry.');
|
|
1070
|
+
const targetHome = (0, codexPaths_1.getCodexProfileHome)(target.id);
|
|
1071
|
+
const targetBefore = (0, codexCredentialStore_1.readCodexCredentialStore)(targetHome);
|
|
1072
|
+
if (targetBefore.status === 'error' || targetBefore.status === 'corrupt')
|
|
1073
|
+
return fail('The target profile is unreadable; the switch was deferred.');
|
|
1074
|
+
const candidate = readStoredCodexAccount(target.id);
|
|
1075
|
+
if (!candidate)
|
|
1076
|
+
return fail('No readable stored credentials for this account; sign in again.', {
|
|
1077
|
+
needsLogin: true,
|
|
1078
|
+
});
|
|
1079
|
+
const candidateIdentity = (0, codexAuth_1.readAuthIdentityFromRaw)(candidate.authRaw);
|
|
1080
|
+
if (candidateIdentity?.authMode === 'chatgpt' &&
|
|
1081
|
+
!(0, codexAuth_1.identitiesMatch)(candidateIdentity, profileIdentity(target))) {
|
|
1082
|
+
return fail('The saved Codex credential does not belong to the selected account.');
|
|
1083
|
+
}
|
|
1084
|
+
const liveFp = (0, codexAuth_1.codexRefreshFingerprint)(beforeSnapshot.authRaw);
|
|
1085
|
+
if (liveFp && liveFp === (0, codexAuth_1.codexRefreshFingerprint)(candidate.authRaw))
|
|
1086
|
+
return fail('The target is another copy of the live refresh token; sign in again.');
|
|
969
1087
|
const health = (0, accountHealth_1.getAccountHealth)('codex', target.id, { probe: 'store' });
|
|
970
1088
|
base.health = health;
|
|
971
|
-
if ((health.state === 'expired' || health.state === 'missing') && !options.force)
|
|
972
|
-
return (
|
|
973
|
-
...base,
|
|
974
|
-
success: false,
|
|
1089
|
+
if ((health.state === 'expired' || health.state === 'missing') && !options.force)
|
|
1090
|
+
return fail('Stored credentials have expired or are missing; sign in again.', {
|
|
975
1091
|
needsLogin: true,
|
|
976
|
-
|
|
1092
|
+
});
|
|
1093
|
+
const refreshed = (0, codexAuth_1.readLastRefresh)(candidate.authRaw);
|
|
1094
|
+
if (refreshed !== null && Date.now() - refreshed > codexAuth_1.STALE_AUTH_THRESHOLD_MS)
|
|
1095
|
+
base.warnings.push('The stored Codex login has not been refreshed in over 8 days.');
|
|
1096
|
+
// Capture and verify both sides before removing either usable copy. Mandatory even with force.
|
|
1097
|
+
try {
|
|
1098
|
+
if (outgoing)
|
|
1099
|
+
(0, codexCredentialStore_1.writeCodexBackup)(outgoing.id, beforeSnapshot);
|
|
1100
|
+
(0, codexCredentialStore_1.writeCodexBackup)(target.id, candidate);
|
|
1101
|
+
(0, atomic_1.atomicWriteJsonSync)(codexJournalPath(), {
|
|
1102
|
+
from: outgoing?.id ?? null,
|
|
1103
|
+
to: target.id,
|
|
1104
|
+
fromHash: loginHash(beforeSnapshot),
|
|
1105
|
+
toHash: loginHash(candidate),
|
|
1106
|
+
at: new Date().toISOString(),
|
|
977
1107
|
});
|
|
978
1108
|
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1109
|
+
catch {
|
|
1110
|
+
return fail('The outgoing login or switch journal could not be saved and verified; live was left untouched.');
|
|
1111
|
+
}
|
|
1112
|
+
if (!(0, codexCredentialStore_1.codexStoreMatches)((0, codexCredentialStore_1.readCodexCredentialStore)(), beforeSnapshot))
|
|
1113
|
+
return fail('The live login changed after capture; the switch was deferred.');
|
|
1114
|
+
const profileStore = (0, codexCredentialStore_1.readCodexCredentialStore)(targetHome);
|
|
1115
|
+
if (!(0, codexCredentialStore_1.codexStoreMatches)(profileStore, targetBefore.snapshot))
|
|
1116
|
+
return fail('The target credential changed after selection; the switch was deferred.');
|
|
1117
|
+
if (profileStore.status === 'error' || profileStore.status === 'corrupt')
|
|
1118
|
+
return fail('The target profile is unreadable; the switch was deferred.');
|
|
1119
|
+
try {
|
|
1120
|
+
if (profileStore.status === 'ok')
|
|
1121
|
+
(0, codexCredentialStore_1.removeCodexProfileCredentials)(targetHome, profileStore.snapshot);
|
|
982
1122
|
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1123
|
+
catch {
|
|
1124
|
+
return fail('The target copy could not be removed safely; the switch was deferred.');
|
|
1125
|
+
}
|
|
1126
|
+
const install = installIntoLiveCodexStore(candidate, target.id, 'switch', {
|
|
1127
|
+
before,
|
|
1128
|
+
liveAccountId: outgoing?.id ?? null,
|
|
1129
|
+
capturedOutgoing: Boolean(outgoing),
|
|
1130
|
+
scan: probes.scan,
|
|
1131
|
+
});
|
|
1132
|
+
const recover = (written) => {
|
|
1133
|
+
if (written) {
|
|
1134
|
+
installIntoLiveCodexStore(beforeSnapshot, outgoing?.id ?? target.id, 'rollback', {
|
|
1135
|
+
before: (0, codexCredentialStore_1.readCodexCredentialStore)(),
|
|
1136
|
+
liveAccountId: target.id,
|
|
1137
|
+
written,
|
|
1138
|
+
scan: probes.scan,
|
|
1139
|
+
});
|
|
996
1140
|
}
|
|
1141
|
+
// Journal reconciliation never overwrites live and never restores a copy of its identity.
|
|
1142
|
+
resolveCodexJournal((0, accountSyncTypes_1.emptyProviderSyncReport)(), probes.scan);
|
|
1143
|
+
};
|
|
1144
|
+
if (!install.ok) {
|
|
997
1145
|
try {
|
|
998
|
-
(
|
|
1146
|
+
recover(install.attempted);
|
|
999
1147
|
}
|
|
1000
1148
|
catch {
|
|
1001
|
-
/*
|
|
1149
|
+
/* journal and private backups survive */
|
|
1002
1150
|
}
|
|
1003
|
-
|
|
1004
|
-
|
|
1151
|
+
return fail(`${install.reason} Verified private backups were kept; run accounts doctor if recovery is pending.`, { verification: 'failed' });
|
|
1152
|
+
}
|
|
1005
1153
|
try {
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
fs.rmSync(liveLegacyPath, { force: true });
|
|
1154
|
+
(0, accountRegistry_1.setActiveSavedAccount)('codex', target.id);
|
|
1155
|
+
}
|
|
1156
|
+
catch {
|
|
1157
|
+
try {
|
|
1158
|
+
recover(install.installed);
|
|
1012
1159
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
if (liveAuthRaw)
|
|
1016
|
-
fs.rmSync(liveAuthPath, { force: true });
|
|
1160
|
+
catch {
|
|
1161
|
+
/* journal survives */
|
|
1017
1162
|
}
|
|
1163
|
+
return fail('The account registry could not be updated; recovery retained the verified backups.');
|
|
1018
1164
|
}
|
|
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
1165
|
try {
|
|
1049
|
-
|
|
1166
|
+
if (outgoing)
|
|
1167
|
+
storeCodexProfileAuthUnlocked(outgoing.id, beforeSnapshot.authRaw, beforeSnapshot.legacyRaw, 'switch');
|
|
1168
|
+
(0, accountHealth_1.writeCodexHealthSidecar)(target.id, candidate.authRaw, 'switch');
|
|
1169
|
+
clearCodexJournal();
|
|
1050
1170
|
}
|
|
1051
|
-
catch
|
|
1052
|
-
|
|
1053
|
-
return (0, accountSwitch_1.finishSwitchResult)({
|
|
1054
|
-
...base,
|
|
1055
|
-
success: false,
|
|
1056
|
-
error: `Failed to update account registry: ${err}`,
|
|
1057
|
-
});
|
|
1171
|
+
catch {
|
|
1172
|
+
base.warnings.push('The live switch succeeded; profile cleanup is pending and will retry on sync.');
|
|
1058
1173
|
}
|
|
1059
1174
|
let undoToken;
|
|
1060
1175
|
try {
|
|
1061
|
-
undoToken = (0, accountSwitch_1.writeLastSwitch)('codex',
|
|
1176
|
+
undoToken = (0, accountSwitch_1.writeLastSwitch)('codex', outgoing?.id ?? null, target.id).token;
|
|
1062
1177
|
}
|
|
1063
1178
|
catch {
|
|
1064
1179
|
base.warnings.push('The switch succeeded but could not be recorded for undo.');
|
|
@@ -1068,123 +1183,174 @@ function performCodexAuthSwapCore(target, options, probes) {
|
|
|
1068
1183
|
verified: true,
|
|
1069
1184
|
verification: 'store',
|
|
1070
1185
|
undoToken,
|
|
1071
|
-
hints: [`New
|
|
1186
|
+
hints: [`New Codex sessions use ${target.label ?? target.email ?? target.id}.`],
|
|
1072
1187
|
});
|
|
1073
1188
|
}
|
|
1074
1189
|
function switchToCodexAccount(profileId, options = {}) {
|
|
1075
|
-
const target = listCodexAccounts().find((
|
|
1076
|
-
if (!target)
|
|
1190
|
+
const target = listCodexAccounts().find((p) => p.id === profileId);
|
|
1191
|
+
if (!target)
|
|
1077
1192
|
return (0, accountSwitch_1.switchFailure)('codex', profileId, `Codex account ${profileId} not found.`);
|
|
1193
|
+
const scan = (0, processDetection_1.inspectRunningAccountConsumersSync)('codex');
|
|
1194
|
+
try {
|
|
1195
|
+
return withCodexAuthSwapLock(() => performCodexAuthSwapCore(target, options, { scan }));
|
|
1196
|
+
}
|
|
1197
|
+
catch {
|
|
1198
|
+
return (0, accountSwitch_1.switchFailure)('codex', profileId, 'The Codex switch could not finish; private backups and any pending journal were kept.');
|
|
1078
1199
|
}
|
|
1079
|
-
return performCodexAuthSwap(target, options);
|
|
1080
1200
|
}
|
|
1081
|
-
/** {@link switchToCodexAccount} without blocking the event loop on CLI probes. */
|
|
1082
1201
|
async function switchToCodexAccountAsync(profileId, options = {}) {
|
|
1083
|
-
const target = listCodexAccounts().find((
|
|
1084
|
-
if (!target)
|
|
1202
|
+
const target = listCodexAccounts().find((p) => p.id === profileId);
|
|
1203
|
+
if (!target)
|
|
1085
1204
|
return (0, accountSwitch_1.switchFailure)('codex', profileId, `Codex account ${profileId} not found.`);
|
|
1205
|
+
const scan = await (0, processDetection_1.inspectRunningAccountConsumers)('codex');
|
|
1206
|
+
let result;
|
|
1207
|
+
try {
|
|
1208
|
+
result = withCodexAuthSwapLock(() => performCodexAuthSwapCore(target, options, { scan }));
|
|
1086
1209
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1210
|
+
catch {
|
|
1211
|
+
return (0, accountSwitch_1.switchFailure)('codex', profileId, 'The Codex switch could not finish; private backups and any pending journal were kept.');
|
|
1212
|
+
}
|
|
1213
|
+
if (result.success && options.verifyWithCli && !result.alreadyActive) {
|
|
1214
|
+
const status = await getCodexLoginStatusAsync((0, codexPaths_1.getSystemCodexHome)());
|
|
1215
|
+
if (status.loggedIn)
|
|
1216
|
+
result.verification = 'cli';
|
|
1217
|
+
else
|
|
1218
|
+
result = (0, accountSwitch_1.finishSwitchResult)({
|
|
1219
|
+
...result,
|
|
1220
|
+
warnings: [
|
|
1221
|
+
...result.warnings,
|
|
1222
|
+
'Codex could not read the installed login; sign in again if prompted.',
|
|
1223
|
+
],
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
return result;
|
|
1099
1227
|
}
|
|
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.
|
|
1228
|
+
/** Legacy migration entry points now use the same guarded reconciliation as ordinary sync. */
|
|
1103
1229
|
function reconcileCodexAuthState() {
|
|
1230
|
+
syncCodexLiveState('migration');
|
|
1231
|
+
}
|
|
1232
|
+
async function reconcileCodexAuthStateAsync() {
|
|
1233
|
+
await syncCodexLiveStateAsync('migration');
|
|
1234
|
+
}
|
|
1235
|
+
function codexKeepAliveBlockerUnlocked(id) {
|
|
1236
|
+
if ((0, codexCredentialStore_1.isSidekickCodexHome)((0, codexPaths_1.getSystemCodexHome)()))
|
|
1237
|
+
return 'unset CODEX_HOME before managing accounts; it names a Sidekick-owned home';
|
|
1238
|
+
const mode = (0, codexPaths_1.getCodexCredentialStoreMode)();
|
|
1239
|
+
if (mode !== 'file' && mode !== 'unknown')
|
|
1240
|
+
return 'live credential storage is not readable file mode';
|
|
1241
|
+
const profile = listCodexAccounts().find((p) => p.id === id);
|
|
1242
|
+
if (!profile)
|
|
1243
|
+
return 'account not found';
|
|
1244
|
+
const live = (0, codexCredentialStore_1.readCodexCredentialStore)();
|
|
1245
|
+
if (live.status === 'error' || live.status === 'corrupt')
|
|
1246
|
+
return 'the live login could not be read';
|
|
1247
|
+
if (live.status === 'ok' && profileMatchesSnapshot(profile, live.snapshot))
|
|
1248
|
+
return 'live account; the CLI refreshes it in normal use';
|
|
1249
|
+
const store = (0, codexCredentialStore_1.readCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(id));
|
|
1250
|
+
if (store.status !== 'ok')
|
|
1251
|
+
return 'no readable profile copy to refresh (keep-alive never restores backups)';
|
|
1252
|
+
const fp = (0, codexAuth_1.codexRefreshFingerprint)(store.snapshot.authRaw);
|
|
1253
|
+
if (!fp)
|
|
1254
|
+
return 'no refreshable ChatGPT login';
|
|
1255
|
+
if (live.status === 'ok' && fp === (0, codexAuth_1.codexRefreshFingerprint)(live.snapshot.authRaw))
|
|
1256
|
+
return 'same refresh token as the live login';
|
|
1257
|
+
if ((0, accountHealth_1.readHealthSidecar)('codex', id)?.source === 'live-sync')
|
|
1258
|
+
return 'copied from the live login; waits for a switch away from it';
|
|
1259
|
+
const backup = (0, codexCredentialStore_1.readCodexBackup)(id);
|
|
1260
|
+
if (backup.status === 'error' || backup.status === 'corrupt')
|
|
1261
|
+
return 'its private backup could not be read';
|
|
1262
|
+
if (backup.status === 'ok' &&
|
|
1263
|
+
(0, codexAuth_1.compareCodexAuthFreshness)(backup.snapshot.authRaw, store.snapshot.authRaw) > 0)
|
|
1264
|
+
return 'its saved backup is newer than the profile copy';
|
|
1265
|
+
return null;
|
|
1266
|
+
}
|
|
1267
|
+
function getCodexKeepAliveBlocker(id) {
|
|
1104
1268
|
try {
|
|
1105
|
-
|
|
1106
|
-
? getCodexLoginStatus((0, codexPaths_1.getSystemCodexHome)()).loggedIn
|
|
1107
|
-
: false;
|
|
1108
|
-
withCodexAuthSwapLock(() => reconcileCodexAuthStateCore({ systemKeyringLoggedIn }));
|
|
1269
|
+
return withCodexAuthSwapLock(() => codexKeepAliveBlockerUnlocked(id));
|
|
1109
1270
|
}
|
|
1110
1271
|
catch {
|
|
1111
|
-
|
|
1272
|
+
return 'could not acquire the account-switch lock';
|
|
1112
1273
|
}
|
|
1113
1274
|
}
|
|
1114
|
-
|
|
1115
|
-
async function reconcileCodexAuthStateAsync() {
|
|
1275
|
+
function beginCodexKeepAlive(id, leaseMs) {
|
|
1116
1276
|
try {
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1277
|
+
return withCodexAuthSwapLock(() => {
|
|
1278
|
+
const reason = codexKeepAliveBlockerUnlocked(id);
|
|
1279
|
+
if (reason)
|
|
1280
|
+
return { ok: false, reason };
|
|
1281
|
+
return (0, accountLease_1.acquireAccountLease)('codex', id, leaseMs)
|
|
1282
|
+
? { ok: true }
|
|
1283
|
+
: { ok: false, reason: 'another keep-alive run holds this account' };
|
|
1284
|
+
});
|
|
1121
1285
|
}
|
|
1122
1286
|
catch {
|
|
1123
|
-
|
|
1287
|
+
return { ok: false, reason: 'could not acquire the account-switch lock' };
|
|
1124
1288
|
}
|
|
1125
1289
|
}
|
|
1126
|
-
function
|
|
1290
|
+
function finishCodexKeepAlive(id) {
|
|
1127
1291
|
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);
|
|
1292
|
+
withCodexAuthSwapLock(() => {
|
|
1293
|
+
const profile = listCodexAccounts().find((p) => p.id === id);
|
|
1294
|
+
const store = (0, codexCredentialStore_1.readCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(id));
|
|
1295
|
+
if (!profile || isLiveCodexProfile(profile) || store.status !== 'ok')
|
|
1296
|
+
return;
|
|
1297
|
+
if (!(0, codexAuth_1.identitiesMatch)((0, codexAuth_1.readAuthIdentityFromRaw)(store.snapshot.authRaw), profileIdentity(profile)))
|
|
1298
|
+
return;
|
|
1299
|
+
const backup = (0, codexCredentialStore_1.readCodexBackup)(id);
|
|
1300
|
+
if (backup.status === 'ok' &&
|
|
1301
|
+
(0, codexAuth_1.compareCodexAuthFreshness)(backup.snapshot.authRaw, store.snapshot.authRaw) > 0)
|
|
1302
|
+
return;
|
|
1303
|
+
(0, codexCredentialStore_1.writeCodexBackup)(id, store.snapshot);
|
|
1304
|
+
(0, accountHealth_1.writeCodexHealthSidecar)(id, store.snapshot.authRaw, 'keepalive');
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
finally {
|
|
1308
|
+
(0, accountLease_1.releaseAccountLease)('codex', id);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
function restoreCodexProfileFromBackup(id, options = {}) {
|
|
1312
|
+
try {
|
|
1313
|
+
return withCodexAuthSwapLock(() => {
|
|
1314
|
+
if ((0, codexCredentialStore_1.isSidekickCodexHome)((0, codexPaths_1.getSystemCodexHome)()))
|
|
1315
|
+
return 'none';
|
|
1316
|
+
const profile = listCodexAccounts().find((p) => p.id === id);
|
|
1317
|
+
if (!profile)
|
|
1318
|
+
return 'none';
|
|
1319
|
+
const live = (0, codexCredentialStore_1.readCodexCredentialStore)();
|
|
1320
|
+
if (live.status === 'error' || live.status === 'corrupt')
|
|
1321
|
+
return 'none';
|
|
1322
|
+
if (isLiveCodexProfile(profile))
|
|
1323
|
+
return 'live';
|
|
1324
|
+
const home = (0, codexPaths_1.getCodexProfileHome)(id);
|
|
1325
|
+
const store = (0, codexCredentialStore_1.readCodexCredentialStore)(home);
|
|
1326
|
+
if (store.status === 'ok') {
|
|
1327
|
+
const fp = (0, codexAuth_1.codexRefreshFingerprint)(store.snapshot.authRaw);
|
|
1328
|
+
if (fp && live.status === 'ok' && fp === (0, codexAuth_1.codexRefreshFingerprint)(live.snapshot.authRaw))
|
|
1329
|
+
return 'none';
|
|
1330
|
+
if (!(0, codexAuth_1.identitiesMatch)((0, codexAuth_1.readAuthIdentityFromRaw)(store.snapshot.authRaw), profileIdentity(profile)) &&
|
|
1331
|
+
(0, codexAuth_1.readAuthIdentityFromRaw)(store.snapshot.authRaw)?.authMode !== 'api-key' &&
|
|
1332
|
+
!(0, codexCredentialStore_1.isLegacyCodexLogin)(store.snapshot.legacyRaw))
|
|
1333
|
+
return 'none';
|
|
1334
|
+
const backup = (0, codexCredentialStore_1.readCodexBackup)(id);
|
|
1335
|
+
if (backup.status === 'ok' &&
|
|
1336
|
+
(0, codexAuth_1.compareCodexAuthFreshness)(backup.snapshot.authRaw, store.snapshot.authRaw) > 0)
|
|
1337
|
+
return 'none';
|
|
1338
|
+
return 'present';
|
|
1182
1339
|
}
|
|
1183
|
-
|
|
1184
|
-
|
|
1340
|
+
if (store.status !== 'absent' || options.restore === false)
|
|
1341
|
+
return 'none';
|
|
1342
|
+
const backup = (0, codexCredentialStore_1.readCodexBackup)(id);
|
|
1343
|
+
if (backup.status !== 'ok')
|
|
1344
|
+
return 'none';
|
|
1345
|
+
const fp = (0, codexAuth_1.codexRefreshFingerprint)(backup.snapshot.authRaw);
|
|
1346
|
+
if (fp && live.status === 'ok' && fp === (0, codexAuth_1.codexRefreshFingerprint)(live.snapshot.authRaw))
|
|
1347
|
+
return 'none';
|
|
1348
|
+
(0, codexCredentialStore_1.writeInactiveCodexCredentialStore)(home, backup.snapshot);
|
|
1349
|
+
return 'restored';
|
|
1350
|
+
});
|
|
1185
1351
|
}
|
|
1186
1352
|
catch {
|
|
1187
|
-
|
|
1353
|
+
return 'none';
|
|
1188
1354
|
}
|
|
1189
1355
|
}
|
|
1190
1356
|
function removeCodexAccount(profileId) {
|
|
@@ -1193,5 +1359,10 @@ function removeCodexAccount(profileId) {
|
|
|
1193
1359
|
return { success: false, error: `Codex account ${profileId} not found.` };
|
|
1194
1360
|
}
|
|
1195
1361
|
fs.rmSync((0, codexPaths_1.getCodexProfileDir)(profileId), { recursive: true, force: true });
|
|
1362
|
+
fs.rmSync((0, codexPaths_1.getCodexBackupPath)(profileId), { force: true });
|
|
1363
|
+
fs.rmSync((0, codexPaths_1.getCodexBackupPath)(profileId, true), { force: true });
|
|
1196
1364
|
return { success: true };
|
|
1197
1365
|
}
|
|
1366
|
+
async function cleanupAbandonedCodexLoginsAsync(olderThanMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
1367
|
+
return cleanupAbandonedCodexLogins(olderThanMs, now, await (0, processDetection_1.inspectRunningAccountConsumers)('codex'));
|
|
1368
|
+
}
|