sidekick-shared 0.26.5 → 0.26.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 +35 -0
- package/README.md +56 -51
- package/dist/accountChanges.js +21 -2
- package/dist/accountHealth.d.ts +72 -0
- package/dist/accountHealth.js +286 -0
- package/dist/accountLaunch.d.ts +30 -0
- package/dist/accountLaunch.js +124 -0
- package/dist/accountManager.d.ts +33 -8
- package/dist/accountManager.js +183 -50
- package/dist/accountRegistry.d.ts +10 -0
- package/dist/accountRegistry.js +42 -22
- package/dist/accountSwitch.d.ts +43 -0
- package/dist/accountSwitch.js +115 -0
- package/dist/accountSync.d.ts +30 -0
- package/dist/accountSync.js +180 -0
- package/dist/accountSyncTypes.d.ts +25 -0
- package/dist/accountSyncTypes.js +7 -0
- package/dist/accounts.d.ts +58 -6
- package/dist/accounts.js +408 -189
- package/dist/claudeCredentials.d.ts +38 -0
- package/dist/claudeCredentials.js +74 -0
- package/dist/claudeProfiles.d.ts +40 -0
- package/dist/claudeProfiles.js +102 -10
- package/dist/codexAuth.d.ts +32 -0
- package/dist/codexAuth.js +127 -0
- package/dist/codexPaths.d.ts +21 -0
- package/dist/codexPaths.js +117 -0
- package/dist/codexProfiles.d.ts +58 -5
- package/dist/codexProfiles.js +574 -289
- package/dist/credentialIO.d.ts +12 -5
- package/dist/credentialIO.js +63 -29
- package/dist/credentials.d.ts +8 -0
- package/dist/credentials.js +28 -32
- package/dist/ensureDefaultAccounts.d.ts +8 -0
- package/dist/ensureDefaultAccounts.js +42 -82
- package/dist/index.d.ts +27 -10
- package/dist/index.js +61 -9
- package/dist/keepAlive.d.ts +44 -0
- package/dist/keepAlive.js +106 -0
- package/dist/processDetection.d.ts +32 -0
- package/dist/processDetection.js +210 -0
- package/dist/schemas/accountManager.d.ts +283 -1
- package/dist/schemas/accountManager.js +122 -1
- package/dist/schemas/assistantTurn.d.ts +2 -2
- package/dist/schemas/index.d.ts +6 -1
- package/dist/schemas/index.js +14 -1
- package/dist/schemas/observedSessionV1.d.ts +21 -21
- package/dist/schemas/quota.d.ts +18 -18
- package/dist/schemas/quotaHistory.d.ts +1 -1
- package/dist/schemas/stateFile.d.ts +5 -5
- package/dist/terminalSync.d.ts +5 -5
- package/dist/terminalSync.js +7 -112
- package/dist/writers/atomic.d.ts +2 -0
- package/dist/writers/atomic.js +49 -2
- package/package.json +1 -1
package/dist/codexProfiles.js
CHANGED
|
@@ -33,10 +33,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.getSystemCodexHome =
|
|
36
|
+
exports.CODEX_KEYRING_FIX_HINT = exports.getCodexCredentialStoreMode = exports.getSystemCodexHome = exports.getCodexProfileHome = exports.getCodexProfilesDir = void 0;
|
|
37
37
|
exports.getCodexMonitoringHomes = getCodexMonitoringHomes;
|
|
38
|
-
exports.
|
|
39
|
-
exports.
|
|
38
|
+
exports.storeCodexProfileAuth = storeCodexProfileAuth;
|
|
39
|
+
exports.parseCodexLoginStatusOutput = parseCodexLoginStatusOutput;
|
|
40
|
+
exports.getCodexLoginStatus = getCodexLoginStatus;
|
|
41
|
+
exports.getCodexLoginStatusAsync = getCodexLoginStatusAsync;
|
|
40
42
|
exports.readCodexAccountMetadata = readCodexAccountMetadata;
|
|
41
43
|
exports.readCodexAccountMetadataAsync = readCodexAccountMetadataAsync;
|
|
42
44
|
exports.isCodexProfileAuthenticated = isCodexProfileAuthenticated;
|
|
@@ -50,13 +52,17 @@ exports.prepareCodexAccount = prepareCodexAccount;
|
|
|
50
52
|
exports.prepareCodexAccountAsync = prepareCodexAccountAsync;
|
|
51
53
|
exports.finalizeCodexAccount = finalizeCodexAccount;
|
|
52
54
|
exports.finalizeCodexAccountAsync = finalizeCodexAccountAsync;
|
|
55
|
+
exports.withCodexAuthSwapLock = withCodexAuthSwapLock;
|
|
56
|
+
exports.dedupeCodexProfiles = dedupeCodexProfiles;
|
|
57
|
+
exports.syncCodexLiveStateUnlocked = syncCodexLiveStateUnlocked;
|
|
58
|
+
exports.syncCodexLiveState = syncCodexLiveState;
|
|
59
|
+
exports.cleanupAbandonedCodexLogins = cleanupAbandonedCodexLogins;
|
|
53
60
|
exports.switchToCodexAccount = switchToCodexAccount;
|
|
54
61
|
exports.switchToCodexAccountAsync = switchToCodexAccountAsync;
|
|
55
62
|
exports.reconcileCodexAuthState = reconcileCodexAuthState;
|
|
56
63
|
exports.reconcileCodexAuthStateAsync = reconcileCodexAuthStateAsync;
|
|
57
64
|
exports.removeCodexAccount = removeCodexAccount;
|
|
58
65
|
const fs = __importStar(require("fs"));
|
|
59
|
-
const os = __importStar(require("os"));
|
|
60
66
|
const path = __importStar(require("path"));
|
|
61
67
|
const child_process_1 = require("child_process");
|
|
62
68
|
const crypto_1 = require("crypto");
|
|
@@ -65,16 +71,19 @@ const accountRegistry_1 = require("./accountRegistry");
|
|
|
65
71
|
// re-serializing would drop fields added by newer codex versions, and the
|
|
66
72
|
// rotated refresh token inside is only valid in its freshest form.
|
|
67
73
|
const atomic_1 = require("./writers/atomic");
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
74
|
+
const codexAuth_1 = require("./codexAuth");
|
|
75
|
+
const codexPaths_1 = require("./codexPaths");
|
|
76
|
+
const accountHealth_1 = require("./accountHealth");
|
|
77
|
+
const accountSwitch_1 = require("./accountSwitch");
|
|
78
|
+
const accountSyncTypes_1 = require("./accountSyncTypes");
|
|
79
|
+
const processDetection_1 = require("./processDetection");
|
|
80
|
+
const quotaSnapshots_1 = require("./quotaSnapshots");
|
|
81
|
+
var codexPaths_2 = require("./codexPaths");
|
|
82
|
+
Object.defineProperty(exports, "getCodexProfilesDir", { enumerable: true, get: function () { return codexPaths_2.getCodexProfilesDir; } });
|
|
83
|
+
Object.defineProperty(exports, "getCodexProfileHome", { enumerable: true, get: function () { return codexPaths_2.getCodexProfileHome; } });
|
|
84
|
+
Object.defineProperty(exports, "getSystemCodexHome", { enumerable: true, get: function () { return codexPaths_2.getSystemCodexHome; } });
|
|
85
|
+
Object.defineProperty(exports, "getCodexCredentialStoreMode", { enumerable: true, get: function () { return codexPaths_2.getCodexCredentialStoreMode; } });
|
|
86
|
+
exports.CODEX_KEYRING_FIX_HINT = 'Set `cli_auth_credentials_store = "file"` in ~/.codex/config.toml and run `codex login` again.';
|
|
78
87
|
function dedupePaths(paths) {
|
|
79
88
|
const seen = new Set();
|
|
80
89
|
const unique = [];
|
|
@@ -87,58 +96,32 @@ function dedupePaths(paths) {
|
|
|
87
96
|
}
|
|
88
97
|
return unique;
|
|
89
98
|
}
|
|
90
|
-
function getSystemCodexHome() {
|
|
91
|
-
return getExplicitCodexHome() ?? getDefaultSystemCodexHome();
|
|
92
|
-
}
|
|
93
99
|
function getCodexMonitoringHomes() {
|
|
94
|
-
const explicitHome = getExplicitCodexHome();
|
|
100
|
+
const explicitHome = (0, codexPaths_1.getExplicitCodexHome)();
|
|
95
101
|
if (explicitHome)
|
|
96
102
|
return [explicitHome];
|
|
97
103
|
// The system home is the single live home; profile homes only matter for
|
|
98
104
|
// sessions recorded back when they doubled as live CODEX_HOMEs.
|
|
99
|
-
const homes = [getDefaultSystemCodexHome()];
|
|
105
|
+
const homes = [(0, codexPaths_1.getDefaultSystemCodexHome)()];
|
|
100
106
|
for (const profile of listCodexAccounts()) {
|
|
101
|
-
const profileHome = getCodexProfileHome(profile.id);
|
|
107
|
+
const profileHome = (0, codexPaths_1.getCodexProfileHome)(profile.id);
|
|
102
108
|
if (fs.existsSync(path.join(profileHome, 'sessions'))) {
|
|
103
109
|
homes.push(profileHome);
|
|
104
110
|
}
|
|
105
111
|
}
|
|
106
112
|
return dedupePaths(homes);
|
|
107
113
|
}
|
|
108
|
-
function getCodexProfilesDir() {
|
|
109
|
-
return path.join((0, accountRegistry_1.getAccountsDir)(), 'codex', 'profiles');
|
|
110
|
-
}
|
|
111
|
-
function getCodexProfileDir(profileId) {
|
|
112
|
-
return path.join(getCodexProfilesDir(), profileId);
|
|
113
|
-
}
|
|
114
|
-
function getCodexProfileHome(profileId) {
|
|
115
|
-
return path.join(getCodexProfileDir(profileId), 'codex-home');
|
|
116
|
-
}
|
|
117
|
-
function getCodexProfileStatePath(profileId) {
|
|
118
|
-
return path.join(getCodexProfileDir(profileId), 'profile.json');
|
|
119
|
-
}
|
|
120
|
-
function ensureCodexProfileDirs(profileId) {
|
|
121
|
-
fs.mkdirSync(getCodexProfileHome(profileId), { recursive: true, mode: 0o700 });
|
|
122
|
-
}
|
|
123
|
-
function readFileOrNull(filePath) {
|
|
124
|
-
try {
|
|
125
|
-
return fs.readFileSync(filePath, 'utf8');
|
|
126
|
-
}
|
|
127
|
-
catch {
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
114
|
function readPendingProfile(profileId) {
|
|
132
115
|
try {
|
|
133
|
-
return JSON.parse(fs.readFileSync(getCodexProfileStatePath(profileId), 'utf8'));
|
|
116
|
+
return JSON.parse(fs.readFileSync((0, codexPaths_1.getCodexProfileStatePath)(profileId), 'utf8'));
|
|
134
117
|
}
|
|
135
118
|
catch {
|
|
136
119
|
return null;
|
|
137
120
|
}
|
|
138
121
|
}
|
|
139
122
|
function writePendingProfile(profileId, pending) {
|
|
140
|
-
ensureCodexProfileDirs(profileId);
|
|
141
|
-
(0, atomic_1.atomicWriteJsonSync)(getCodexProfileStatePath(profileId), pending);
|
|
123
|
+
(0, codexPaths_1.ensureCodexProfileDirs)(profileId);
|
|
124
|
+
(0, atomic_1.atomicWriteJsonSync)((0, codexPaths_1.getCodexProfileStatePath)(profileId), pending);
|
|
142
125
|
}
|
|
143
126
|
function copyIfExists(source, destination) {
|
|
144
127
|
if (!fs.existsSync(source))
|
|
@@ -147,75 +130,36 @@ function copyIfExists(source, destination) {
|
|
|
147
130
|
fs.copyFileSync(source, destination);
|
|
148
131
|
return true;
|
|
149
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Copy the live `config.toml` into a profile home, forcing file-based
|
|
135
|
+
* credential storage so an isolated `codex login` writes `auth.json` (a
|
|
136
|
+
* keyring entry keyed to the profile home could never be swapped).
|
|
137
|
+
*/
|
|
150
138
|
function copySourceCodexConfig(sourceHome, targetHome) {
|
|
151
|
-
|
|
139
|
+
const source = (0, codexPaths_1.readFileOrNull)(path.join(sourceHome, 'config.toml')) ?? '';
|
|
140
|
+
(0, atomic_1.atomicWriteFileSync)(path.join(targetHome, 'config.toml'), (0, codexPaths_1.forceFileCredentialStore)(source));
|
|
152
141
|
}
|
|
153
142
|
function importCurrentCodexAuth(sourceHome, targetHome) {
|
|
154
143
|
const authCopied = copyIfExists(path.join(sourceHome, 'auth.json'), path.join(targetHome, 'auth.json'));
|
|
155
144
|
const legacyCredsCopied = copyIfExists(path.join(sourceHome, '.credentials.json'), path.join(targetHome, '.credentials.json'));
|
|
156
145
|
return authCopied || legacyCredsCopied;
|
|
157
146
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (!raw)
|
|
172
|
-
return null;
|
|
173
|
-
try {
|
|
174
|
-
return JSON.parse(raw);
|
|
175
|
-
}
|
|
176
|
-
catch {
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
function readAuthIdentityFromRaw(raw) {
|
|
181
|
-
const parsed = parseAuthJson(raw);
|
|
182
|
-
if (!parsed)
|
|
183
|
-
return null;
|
|
184
|
-
const idToken = parsed.tokens?.id_token;
|
|
185
|
-
const claims = idToken ? parseJwtPayload(idToken) : null;
|
|
186
|
-
const profileClaims = claims?.['https://api.openai.com/profile'];
|
|
187
|
-
const authClaims = claims?.['https://api.openai.com/auth'];
|
|
188
|
-
const email = typeof claims?.email === 'string'
|
|
189
|
-
? claims.email
|
|
190
|
-
: typeof profileClaims?.email === 'string'
|
|
191
|
-
? profileClaims.email
|
|
192
|
-
: undefined;
|
|
193
|
-
const workspaceId = typeof authClaims?.chatgpt_account_id === 'string'
|
|
194
|
-
? authClaims.chatgpt_account_id
|
|
195
|
-
: parsed.tokens?.account_id;
|
|
196
|
-
const planType = typeof authClaims?.chatgpt_plan_type === 'string' ? authClaims.chatgpt_plan_type : undefined;
|
|
197
|
-
const authMode = parsed.OPENAI_API_KEY || parsed.auth_mode === 'api_key' ? 'api-key' : 'chatgpt';
|
|
198
|
-
return { email, workspaceId, planType, authMode };
|
|
199
|
-
}
|
|
200
|
-
function readLastRefresh(raw, fallbackPath) {
|
|
201
|
-
const parsed = parseAuthJson(raw);
|
|
202
|
-
if (parsed?.last_refresh) {
|
|
203
|
-
const ts = Date.parse(parsed.last_refresh);
|
|
204
|
-
if (!Number.isNaN(ts))
|
|
205
|
-
return ts;
|
|
206
|
-
}
|
|
207
|
-
if (fallbackPath) {
|
|
208
|
-
try {
|
|
209
|
-
return fs.statSync(fallbackPath).mtimeMs;
|
|
210
|
-
}
|
|
211
|
-
catch {
|
|
212
|
-
/* fall through */
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
return null;
|
|
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
|
+
function storeCodexProfileAuth(profileId, authRaw, legacyRaw, source) {
|
|
153
|
+
const profileHome = (0, codexPaths_1.getCodexProfileHome)(profileId);
|
|
154
|
+
if (authRaw)
|
|
155
|
+
(0, atomic_1.atomicWriteFileSync)(path.join(profileHome, 'auth.json'), authRaw);
|
|
156
|
+
if (legacyRaw)
|
|
157
|
+
(0, atomic_1.atomicWriteFileSync)(path.join(profileHome, '.credentials.json'), legacyRaw);
|
|
158
|
+
if (authRaw)
|
|
159
|
+
(0, accountHealth_1.writeCodexHealthSidecar)(profileId, authRaw, source);
|
|
216
160
|
}
|
|
217
161
|
function readMetadataFromAuthJson(codexHome) {
|
|
218
|
-
const identity = readAuthIdentityFromRaw(readFileOrNull(path.join(codexHome, 'auth.json')));
|
|
162
|
+
const identity = (0, codexAuth_1.readAuthIdentityFromRaw)((0, codexPaths_1.readFileOrNull)(path.join(codexHome, 'auth.json')));
|
|
219
163
|
if (!identity)
|
|
220
164
|
return {};
|
|
221
165
|
return {
|
|
@@ -289,33 +233,6 @@ function getCodexLoginStatusAsync(codexHome) {
|
|
|
289
233
|
}
|
|
290
234
|
});
|
|
291
235
|
}
|
|
292
|
-
/** Sync sibling of {@link detectRunningCodexProcessAsync}; see the event-loop caveat above. */
|
|
293
|
-
function detectRunningCodexProcess() {
|
|
294
|
-
if (process.platform === 'win32')
|
|
295
|
-
return false;
|
|
296
|
-
try {
|
|
297
|
-
return ((0, child_process_1.spawnSync)('pgrep', ['-x', 'codex'], {
|
|
298
|
-
encoding: 'utf8',
|
|
299
|
-
timeout: PROBE_TIMEOUT_MS,
|
|
300
|
-
killSignal: 'SIGKILL',
|
|
301
|
-
}).status === 0);
|
|
302
|
-
}
|
|
303
|
-
catch {
|
|
304
|
-
return false;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
function detectRunningCodexProcessAsync() {
|
|
308
|
-
if (process.platform === 'win32')
|
|
309
|
-
return Promise.resolve(false);
|
|
310
|
-
return new Promise((resolve) => {
|
|
311
|
-
try {
|
|
312
|
-
(0, child_process_1.execFile)('pgrep', ['-x', 'codex'], { encoding: 'utf8', timeout: PROBE_TIMEOUT_MS, killSignal: 'SIGKILL' }, (error) => resolve(!error));
|
|
313
|
-
}
|
|
314
|
-
catch {
|
|
315
|
-
resolve(false);
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
236
|
/** File-based metadata only — never spawns the codex CLI. */
|
|
320
237
|
function readCodexAccountMetadataFromFiles(codexHome) {
|
|
321
238
|
const fromAuth = readMetadataFromAuthJson(codexHome);
|
|
@@ -365,7 +282,13 @@ function getActiveCodexAccount() {
|
|
|
365
282
|
function resolveSidekickCodexHome() {
|
|
366
283
|
// Account switching swaps auth.json inside the system home, so the system
|
|
367
284
|
// home (or an explicit CODEX_HOME) is always the single live home.
|
|
368
|
-
return getSystemCodexHome();
|
|
285
|
+
return (0, codexPaths_1.getSystemCodexHome)();
|
|
286
|
+
}
|
|
287
|
+
function profileIdentity(profile) {
|
|
288
|
+
return {
|
|
289
|
+
email: profile.email ?? profile.metadata?.email,
|
|
290
|
+
workspaceId: profile.metadata?.workspaceId,
|
|
291
|
+
};
|
|
369
292
|
}
|
|
370
293
|
/**
|
|
371
294
|
* Resolves the *currently logged-in* Codex account for display, preferring the
|
|
@@ -382,7 +305,7 @@ function resolveSidekickCodexHome() {
|
|
|
382
305
|
*/
|
|
383
306
|
function resolveActiveCodexAccount(options = {}) {
|
|
384
307
|
const selfHeal = options.selfHeal ?? true;
|
|
385
|
-
const identity = readAuthIdentityFromRaw(readFileOrNull(path.join(resolveSidekickCodexHome(), 'auth.json')));
|
|
308
|
+
const identity = (0, codexAuth_1.readAuthIdentityFromRaw)((0, codexPaths_1.readFileOrNull)(path.join(resolveSidekickCodexHome(), 'auth.json')));
|
|
386
309
|
if (identity && (identity.email || identity.workspaceId)) {
|
|
387
310
|
const match = findProfileForIdentity(identity);
|
|
388
311
|
if (match) {
|
|
@@ -393,7 +316,7 @@ function resolveActiveCodexAccount(options = {}) {
|
|
|
393
316
|
// disk) must never break display, extension activation, or the quota
|
|
394
317
|
// watcher's hot path. We still return the correct live identity below.
|
|
395
318
|
try {
|
|
396
|
-
(0, accountRegistry_1.setActiveSavedAccount)('codex', match.id);
|
|
319
|
+
(0, accountRegistry_1.setActiveSavedAccount)('codex', match.id, { silent: true });
|
|
397
320
|
}
|
|
398
321
|
catch {
|
|
399
322
|
/* keep going with the live identity */
|
|
@@ -427,6 +350,13 @@ function getCodexExecutionEnv(baseEnv = process.env) {
|
|
|
427
350
|
CODEX_HOME: resolveSidekickCodexHome(),
|
|
428
351
|
};
|
|
429
352
|
}
|
|
353
|
+
function keyringRefusal(mode) {
|
|
354
|
+
return {
|
|
355
|
+
success: false,
|
|
356
|
+
needsLogin: false,
|
|
357
|
+
error: `Codex stores credentials in the OS keyring (cli_auth_credentials_store = "${mode}"); sidekick can only switch file-based logins. ${exports.CODEX_KEYRING_FIX_HINT}`,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
430
360
|
function prepareCodexAccountCore(label) {
|
|
431
361
|
const trimmedLabel = label.trim();
|
|
432
362
|
if (!trimmedLabel) {
|
|
@@ -436,17 +366,60 @@ function prepareCodexAccountCore(label) {
|
|
|
436
366
|
if (labelError) {
|
|
437
367
|
return { result: { success: false, error: labelError } };
|
|
438
368
|
}
|
|
369
|
+
const sourceHome = (0, codexPaths_1.getSystemCodexHome)();
|
|
370
|
+
const storeMode = (0, codexPaths_1.getCodexCredentialStoreMode)(sourceHome);
|
|
371
|
+
const liveAuthPresent = hasCodexCredentialFiles(sourceHome);
|
|
372
|
+
if (storeMode === 'keyring' || (storeMode === 'auto' && !liveAuthPresent)) {
|
|
373
|
+
return { result: keyringRefusal(storeMode) };
|
|
374
|
+
}
|
|
439
375
|
const profileId = (0, crypto_1.randomUUID)();
|
|
440
|
-
const codexHome = getCodexProfileHome(profileId);
|
|
441
|
-
ensureCodexProfileDirs(profileId);
|
|
376
|
+
const codexHome = (0, codexPaths_1.getCodexProfileHome)(profileId);
|
|
377
|
+
(0, codexPaths_1.ensureCodexProfileDirs)(profileId);
|
|
442
378
|
writePendingProfile(profileId, {
|
|
443
379
|
label: trimmedLabel,
|
|
444
380
|
addedAt: new Date().toISOString(),
|
|
445
381
|
});
|
|
446
|
-
const sourceHome = getSystemCodexHome();
|
|
447
382
|
copySourceCodexConfig(sourceHome, codexHome);
|
|
448
383
|
const imported = importCurrentCodexAuth(sourceHome, codexHome);
|
|
449
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);
|
|
413
|
+
return {
|
|
414
|
+
result: {
|
|
415
|
+
success: true,
|
|
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
|
+
},
|
|
421
|
+
};
|
|
422
|
+
}
|
|
450
423
|
return { finalize: { profileId, codexHome } };
|
|
451
424
|
}
|
|
452
425
|
return { result: { success: true, profileId, codexHome, needsLogin: true } };
|
|
@@ -471,23 +444,59 @@ async function prepareCodexAccountAsync(label) {
|
|
|
471
444
|
function finalizeCodexAccountCore(profileId, opts, probes) {
|
|
472
445
|
const pending = readPendingProfile(profileId);
|
|
473
446
|
if (!pending) {
|
|
474
|
-
|
|
447
|
+
// Already finalized: `prepareCodexAccount` folds a live login that is
|
|
448
|
+
// already saved into its profile and reports `needsLogin: false`, and the
|
|
449
|
+
// login flow then finalizes that profile id. Activate it if asked.
|
|
450
|
+
const saved = listCodexAccounts().find((profile) => profile.id === profileId);
|
|
451
|
+
if (!saved) {
|
|
452
|
+
return { result: { success: false, error: `Codex profile ${profileId} was not prepared.` } };
|
|
453
|
+
}
|
|
454
|
+
if (opts.activate === false)
|
|
455
|
+
return { result: { success: true, profileId } };
|
|
456
|
+
return { swap: saved };
|
|
475
457
|
}
|
|
476
|
-
const codexHome = getCodexProfileHome(profileId);
|
|
458
|
+
const codexHome = (0, codexPaths_1.getCodexProfileHome)(profileId);
|
|
477
459
|
if (!probes.authenticated) {
|
|
478
460
|
return { result: { success: false, error: 'Codex profile is not authenticated yet.' } };
|
|
479
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' },
|
|
475
|
+
};
|
|
476
|
+
(0, accountRegistry_1.upsertSavedAccountProfile)(merged);
|
|
477
|
+
if (opts.activate === false)
|
|
478
|
+
return { result: { success: true, profileId: existing.id } };
|
|
479
|
+
return { swap: merged };
|
|
480
|
+
}
|
|
480
481
|
const profile = {
|
|
481
482
|
id: profileId,
|
|
482
483
|
providerId: 'codex',
|
|
483
484
|
label: pending.label,
|
|
484
485
|
email: probes.metadata.email,
|
|
485
486
|
addedAt: pending.addedAt,
|
|
486
|
-
metadata: probes.metadata,
|
|
487
|
+
metadata: { ...probes.metadata, origin: 'login' },
|
|
487
488
|
};
|
|
488
489
|
(0, accountRegistry_1.upsertSavedAccountProfile)(profile);
|
|
490
|
+
if (authRaw)
|
|
491
|
+
(0, accountHealth_1.writeCodexHealthSidecar)(profileId, authRaw, 'login');
|
|
492
|
+
try {
|
|
493
|
+
fs.rmSync((0, codexPaths_1.getCodexProfileStatePath)(profileId), { force: true });
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
/* the pending marker is advisory */
|
|
497
|
+
}
|
|
489
498
|
if (opts.activate === false) {
|
|
490
|
-
return { result: { success: true } };
|
|
499
|
+
return { result: { success: true, profileId } };
|
|
491
500
|
}
|
|
492
501
|
if (!hasCodexCredentialFiles(codexHome)) {
|
|
493
502
|
// Authenticated via the OS keyring — there are no credential files to
|
|
@@ -496,7 +505,8 @@ function finalizeCodexAccountCore(profileId, opts, probes) {
|
|
|
496
505
|
return {
|
|
497
506
|
result: {
|
|
498
507
|
success: true,
|
|
499
|
-
|
|
508
|
+
profileId,
|
|
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}`,
|
|
500
510
|
},
|
|
501
511
|
};
|
|
502
512
|
}
|
|
@@ -504,29 +514,34 @@ function finalizeCodexAccountCore(profileId, opts, probes) {
|
|
|
504
514
|
}
|
|
505
515
|
function finalizeCodexAccount(profileId, opts = {}) {
|
|
506
516
|
if (!readPendingProfile(profileId)) {
|
|
507
|
-
|
|
517
|
+
// Already finalized: the core activates a saved profile without probing.
|
|
518
|
+
const step = finalizeCodexAccountCore(profileId, opts, { authenticated: false, metadata: {} });
|
|
519
|
+
return 'swap' in step ? performCodexAuthSwap(step.swap, {}) : step.result;
|
|
508
520
|
}
|
|
509
|
-
const codexHome = getCodexProfileHome(profileId);
|
|
521
|
+
const codexHome = (0, codexPaths_1.getCodexProfileHome)(profileId);
|
|
510
522
|
const authenticated = isCodexProfileAuthenticated(codexHome);
|
|
511
523
|
const step = finalizeCodexAccountCore(profileId, opts, {
|
|
512
524
|
authenticated,
|
|
513
525
|
metadata: authenticated ? readCodexAccountMetadata(codexHome) : {},
|
|
514
526
|
});
|
|
515
|
-
return 'swap' in step ? performCodexAuthSwap(step.swap) : step.result;
|
|
527
|
+
return 'swap' in step ? performCodexAuthSwap(step.swap, {}) : step.result;
|
|
516
528
|
}
|
|
517
529
|
/** {@link finalizeCodexAccount} without blocking the event loop on CLI probes. */
|
|
518
530
|
async function finalizeCodexAccountAsync(profileId, opts = {}) {
|
|
519
531
|
if (!readPendingProfile(profileId)) {
|
|
520
|
-
|
|
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;
|
|
521
535
|
}
|
|
522
|
-
const codexHome = getCodexProfileHome(profileId);
|
|
536
|
+
const codexHome = (0, codexPaths_1.getCodexProfileHome)(profileId);
|
|
523
537
|
const authenticated = await isCodexProfileAuthenticatedAsync(codexHome);
|
|
524
538
|
const step = finalizeCodexAccountCore(profileId, opts, {
|
|
525
539
|
authenticated,
|
|
526
540
|
metadata: authenticated ? await readCodexAccountMetadataAsync(codexHome) : {},
|
|
527
541
|
});
|
|
528
|
-
return 'swap' in step ? performCodexAuthSwapAsync(step.swap) : step.result;
|
|
542
|
+
return 'swap' in step ? performCodexAuthSwapAsync(step.swap, {}) : step.result;
|
|
529
543
|
}
|
|
544
|
+
// ── Stash / identity matching ────────────────────────────────────────────
|
|
530
545
|
function getCodexStashDir() {
|
|
531
546
|
return path.join((0, accountRegistry_1.getAccountsDir)(), 'codex', 'stash');
|
|
532
547
|
}
|
|
@@ -549,7 +564,7 @@ function stashLiveCodexAuth(liveAuthRaw, liveLegacyRaw) {
|
|
|
549
564
|
return null;
|
|
550
565
|
}
|
|
551
566
|
}
|
|
552
|
-
function findProfileForIdentity(identity) {
|
|
567
|
+
function findProfileForIdentity(identity, options = {}) {
|
|
553
568
|
const profiles = listCodexAccounts();
|
|
554
569
|
if (identity?.workspaceId) {
|
|
555
570
|
const byWorkspace = profiles.find((profile) => profile.metadata?.workspaceId === identity.workspaceId);
|
|
@@ -561,67 +576,238 @@ function findProfileForIdentity(identity) {
|
|
|
561
576
|
if (byEmail)
|
|
562
577
|
return byEmail;
|
|
563
578
|
}
|
|
564
|
-
if (!identity?.workspaceId && !identity?.email) {
|
|
579
|
+
if (!identity?.workspaceId && !identity?.email && options.fallbackToActive !== false) {
|
|
565
580
|
// API-key auth or unparseable tokens carry no identity; assume the live
|
|
566
581
|
// file belongs to whichever account the registry says is active.
|
|
567
582
|
return getActiveCodexAccount();
|
|
568
583
|
}
|
|
569
584
|
return null;
|
|
570
585
|
}
|
|
571
|
-
//
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
586
|
+
// ── Live-state sync ──────────────────────────────────────────────────────
|
|
587
|
+
/**
|
|
588
|
+
* Serializes live-auth mutations across processes. Codex rotates refresh
|
|
589
|
+
* tokens, so two interleaved swaps stashing and restoring auth.json can
|
|
590
|
+
* resurrect a stale token and permanently invalidate the login. Lock ordering:
|
|
591
|
+
* this lock is taken first and the registry lock (inside setActiveSavedAccount
|
|
592
|
+
* / upsertSavedAccountProfile) inside it — never the reverse. Never spawn a
|
|
593
|
+
* child process while holding it.
|
|
594
|
+
*/
|
|
595
|
+
function withCodexAuthSwapLock(operation) {
|
|
596
|
+
const lockDir = path.join((0, accountRegistry_1.getAccountsDir)(), 'codex');
|
|
597
|
+
fs.mkdirSync(lockDir, { recursive: true, mode: 0o700 });
|
|
598
|
+
return (0, atomic_1.withFileLockSync)(path.join(lockDir, 'auth-swap.lock'), operation);
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Merge duplicate Codex profiles (same workspace, else same email). The oldest
|
|
602
|
+
* profile keeps its id and label, receives the freshest `auth.json` and the
|
|
603
|
+
* quota snapshot, and the active pointer; losers are stashed, never deleted.
|
|
604
|
+
* Returns the removed ids. Caller holds the swap lock.
|
|
605
|
+
*/
|
|
606
|
+
function dedupeCodexProfiles() {
|
|
607
|
+
const removed = [];
|
|
608
|
+
const profiles = listCodexAccounts();
|
|
609
|
+
const groups = new Map();
|
|
610
|
+
for (const profile of profiles) {
|
|
611
|
+
const key = profile.metadata?.workspaceId
|
|
612
|
+
? `ws:${profile.metadata.workspaceId}`
|
|
613
|
+
: (profile.email ?? profile.metadata?.email)
|
|
614
|
+
? `email:${profile.email ?? profile.metadata?.email}`
|
|
615
|
+
: `id:${profile.id}`;
|
|
616
|
+
groups.set(key, [...(groups.get(key) ?? []), profile]);
|
|
617
|
+
}
|
|
618
|
+
for (const group of groups.values()) {
|
|
619
|
+
if (group.length < 2)
|
|
620
|
+
continue;
|
|
621
|
+
const sorted = [...group].sort((a, b) => Date.parse(a.addedAt) - Date.parse(b.addedAt));
|
|
622
|
+
const keeper = sorted[0];
|
|
623
|
+
const keeperAuthPath = path.join((0, codexPaths_1.getCodexProfileHome)(keeper.id), 'auth.json');
|
|
624
|
+
let freshestRaw = (0, codexPaths_1.readFileOrNull)(keeperAuthPath);
|
|
625
|
+
let freshestAt = (0, codexAuth_1.readLastRefresh)(freshestRaw, keeperAuthPath) ?? -1;
|
|
626
|
+
const activeId = (0, accountRegistry_1.getActiveSavedAccount)('codex')?.id;
|
|
627
|
+
for (const loser of sorted.slice(1)) {
|
|
628
|
+
const loserAuthPath = path.join((0, codexPaths_1.getCodexProfileHome)(loser.id), 'auth.json');
|
|
629
|
+
const loserRaw = (0, codexPaths_1.readFileOrNull)(loserAuthPath);
|
|
630
|
+
const loserAt = (0, codexAuth_1.readLastRefresh)(loserRaw, loserAuthPath) ?? -1;
|
|
631
|
+
if (loserRaw && loserAt > freshestAt) {
|
|
632
|
+
freshestRaw = loserRaw;
|
|
633
|
+
freshestAt = loserAt;
|
|
634
|
+
}
|
|
635
|
+
try {
|
|
636
|
+
if (!(0, quotaSnapshots_1.readQuotaSnapshot)('codex', keeper.id)) {
|
|
637
|
+
const snapshot = (0, quotaSnapshots_1.readQuotaSnapshot)('codex', loser.id);
|
|
638
|
+
if (snapshot)
|
|
639
|
+
(0, quotaSnapshots_1.writeQuotaSnapshot)('codex', keeper.id, snapshot);
|
|
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
|
+
}
|
|
653
|
+
(0, accountRegistry_1.removeSavedAccountProfile)('codex', loser.id);
|
|
654
|
+
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
|
+
}
|
|
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
|
+
});
|
|
667
|
+
}
|
|
668
|
+
return removed;
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Fold the live `auth.json` into the saved profiles: register an unknown login
|
|
672
|
+
* (label = email), refresh the matching profile's backup when the live file is
|
|
673
|
+
* newer, and re-point the active pointer at it. Caller holds the swap lock.
|
|
674
|
+
* Never throws; problems land in `warnings`.
|
|
675
|
+
*/
|
|
676
|
+
function syncCodexLiveStateUnlocked(source = 'live-sync', probes = {}) {
|
|
677
|
+
const report = (0, accountSyncTypes_1.emptyProviderSyncReport)();
|
|
578
678
|
try {
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
679
|
+
const merged = dedupeCodexProfiles();
|
|
680
|
+
if (merged.length)
|
|
681
|
+
report.merged = merged;
|
|
682
|
+
const systemHome = (0, codexPaths_1.getSystemCodexHome)();
|
|
683
|
+
const liveAuthPath = path.join(systemHome, 'auth.json');
|
|
684
|
+
const liveAuthRaw = (0, codexPaths_1.readFileOrNull)(liveAuthPath);
|
|
685
|
+
const liveLegacyRaw = (0, codexPaths_1.readFileOrNull)(path.join(systemHome, '.credentials.json'));
|
|
686
|
+
if (!liveAuthRaw && !liveLegacyRaw) {
|
|
687
|
+
const mode = (0, codexPaths_1.getCodexCredentialStoreMode)(systemHome);
|
|
688
|
+
if (probes.systemKeyringLoggedIn || mode === 'keyring' || mode === 'auto') {
|
|
689
|
+
report.skipped = 'keyring';
|
|
690
|
+
report.warnings.push(`Codex keeps its login in the OS keyring; sidekick cannot switch it. ${exports.CODEX_KEYRING_FIX_HINT}`);
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
report.skipped = 'logged-out';
|
|
694
|
+
}
|
|
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
|
+
profile = {
|
|
702
|
+
id: profileId,
|
|
703
|
+
providerId: 'codex',
|
|
704
|
+
label: identity.email ?? identity.workspaceId,
|
|
705
|
+
email: identity.email,
|
|
706
|
+
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
|
+
},
|
|
588
714
|
};
|
|
715
|
+
(0, codexPaths_1.ensureCodexProfileDirs)(profileId);
|
|
716
|
+
copySourceCodexConfig(systemHome, (0, codexPaths_1.getCodexProfileHome)(profileId));
|
|
717
|
+
(0, accountRegistry_1.upsertSavedAccountProfile)(profile);
|
|
718
|
+
report.registered = { id: profileId, email: identity.email };
|
|
589
719
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
(
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
email: metadata.email ?? profile.email,
|
|
602
|
-
metadata: { ...profile.metadata, ...metadata },
|
|
603
|
-
});
|
|
720
|
+
else if (!profile) {
|
|
721
|
+
// API-key auth or an unparseable token: fold into the active profile
|
|
722
|
+
// only when it is itself an api-key login; otherwise stash.
|
|
723
|
+
const active = getActiveCodexAccount();
|
|
724
|
+
if (active && active.metadata?.authMode === 'api-key') {
|
|
725
|
+
profile = active;
|
|
726
|
+
}
|
|
727
|
+
else {
|
|
728
|
+
report.skipped = 'no-identity';
|
|
729
|
+
return report;
|
|
730
|
+
}
|
|
604
731
|
}
|
|
605
|
-
|
|
606
|
-
|
|
732
|
+
// Codex rotates the refresh token in place, so for the same identity the
|
|
733
|
+
// live file is the freshest copy unless the profile was refreshed on its
|
|
734
|
+
// own (keep-alive, isolated launch) and both files say when.
|
|
735
|
+
const profileAuthPath = path.join((0, codexPaths_1.getCodexProfileHome)(profile.id), 'auth.json');
|
|
736
|
+
const storedRaw = (0, codexPaths_1.readFileOrNull)(profileAuthPath);
|
|
737
|
+
const storedRefresh = (0, codexAuth_1.readLastRefresh)(storedRaw);
|
|
738
|
+
const liveRefresh = (0, codexAuth_1.readLastRefresh)(liveAuthRaw);
|
|
739
|
+
const storedIsNewer = storedRefresh !== null && liveRefresh !== null && storedRefresh > liveRefresh;
|
|
740
|
+
const newer = liveAuthRaw !== storedRaw && !storedIsNewer;
|
|
741
|
+
if (newer) {
|
|
742
|
+
try {
|
|
743
|
+
storeCodexProfileAuth(profile.id, liveAuthRaw, liveLegacyRaw, source);
|
|
744
|
+
const metadata = readCodexAccountMetadataFromFiles((0, codexPaths_1.getCodexProfileHome)(profile.id)) ?? {};
|
|
745
|
+
(0, accountRegistry_1.upsertSavedAccountProfile)({
|
|
746
|
+
...profile,
|
|
747
|
+
email: metadata.email ?? profile.email,
|
|
748
|
+
metadata: { ...profile.metadata, ...metadata },
|
|
749
|
+
});
|
|
750
|
+
report.folded = profile.id;
|
|
751
|
+
}
|
|
752
|
+
catch (err) {
|
|
753
|
+
report.warnings.push(`Could not back up the live Codex credentials: ${err}`);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
const active = (0, accountRegistry_1.getActiveSavedAccount)('codex');
|
|
757
|
+
if (!active || active.id !== profile.id) {
|
|
758
|
+
(0, accountRegistry_1.setActiveSavedAccount)('codex', profile.id, { silent: true });
|
|
759
|
+
report.repointed = profile.id;
|
|
607
760
|
}
|
|
608
|
-
return { syncedProfileId: profile.id };
|
|
609
761
|
}
|
|
610
762
|
catch (err) {
|
|
611
|
-
|
|
763
|
+
report.warnings.push(`Codex account sync failed: ${err}`);
|
|
764
|
+
}
|
|
765
|
+
return report;
|
|
766
|
+
}
|
|
767
|
+
/** {@link syncCodexLiveStateUnlocked} under the swap lock. */
|
|
768
|
+
function syncCodexLiveState(source = 'live-sync') {
|
|
769
|
+
try {
|
|
770
|
+
return withCodexAuthSwapLock(() => syncCodexLiveStateUnlocked(source));
|
|
771
|
+
}
|
|
772
|
+
catch (err) {
|
|
773
|
+
return { warnings: [`Could not acquire the account-switch lock: ${err}`] };
|
|
612
774
|
}
|
|
613
775
|
}
|
|
614
776
|
/**
|
|
615
|
-
*
|
|
616
|
-
*
|
|
617
|
-
* resurrect a stale token and permanently invalidate the login. Lock ordering:
|
|
618
|
-
* this lock is taken first and the registry lock (inside setActiveSavedAccount
|
|
619
|
-
* / upsertSavedAccountProfile) inside it — never the reverse.
|
|
777
|
+
* Remove isolated login homes that never authenticated once they are older
|
|
778
|
+
* than `olderThanMs`. Returns the removed profile ids.
|
|
620
779
|
*/
|
|
621
|
-
function
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
|
|
780
|
+
function cleanupAbandonedCodexLogins(olderThanMs = 6 * 60 * 60 * 1000, now = Date.now()) {
|
|
781
|
+
const removed = [];
|
|
782
|
+
let entries = [];
|
|
783
|
+
try {
|
|
784
|
+
entries = fs.readdirSync((0, codexPaths_1.getCodexProfilesDir)());
|
|
785
|
+
}
|
|
786
|
+
catch {
|
|
787
|
+
return removed;
|
|
788
|
+
}
|
|
789
|
+
const registered = new Set(listCodexAccounts().map((p) => p.id));
|
|
790
|
+
for (const profileId of entries) {
|
|
791
|
+
if (registered.has(profileId))
|
|
792
|
+
continue;
|
|
793
|
+
const pending = readPendingProfile(profileId);
|
|
794
|
+
if (!pending)
|
|
795
|
+
continue;
|
|
796
|
+
if (hasCodexCredentialFiles((0, codexPaths_1.getCodexProfileHome)(profileId)))
|
|
797
|
+
continue;
|
|
798
|
+
const addedAt = Date.parse(pending.addedAt);
|
|
799
|
+
const age = Number.isNaN(addedAt) ? Number.POSITIVE_INFINITY : now - addedAt;
|
|
800
|
+
if (age < olderThanMs)
|
|
801
|
+
continue;
|
|
802
|
+
try {
|
|
803
|
+
fs.rmSync((0, codexPaths_1.getCodexProfileDir)(profileId), { recursive: true, force: true });
|
|
804
|
+
removed.push(profileId);
|
|
805
|
+
}
|
|
806
|
+
catch {
|
|
807
|
+
/* best effort */
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return removed;
|
|
625
811
|
}
|
|
626
812
|
/**
|
|
627
813
|
* CLI probes are resolved before the auth-swap lock is taken — never spawn a
|
|
@@ -630,118 +816,170 @@ function withCodexAuthSwapLock(operation) {
|
|
|
630
816
|
* absence.
|
|
631
817
|
*/
|
|
632
818
|
function resolveCodexSwapProbesSync() {
|
|
633
|
-
const systemHome = getSystemCodexHome();
|
|
819
|
+
const systemHome = (0, codexPaths_1.getSystemCodexHome)();
|
|
634
820
|
return {
|
|
635
|
-
|
|
821
|
+
consumers: (0, processDetection_1.detectRunningAccountConsumersSync)('codex'),
|
|
636
822
|
systemKeyringLoggedIn: hasCodexCredentialFiles(systemHome)
|
|
637
823
|
? false
|
|
638
824
|
: getCodexLoginStatus(systemHome).loggedIn,
|
|
639
825
|
};
|
|
640
826
|
}
|
|
641
827
|
async function resolveCodexSwapProbesAsync() {
|
|
642
|
-
const systemHome = getSystemCodexHome();
|
|
828
|
+
const systemHome = (0, codexPaths_1.getSystemCodexHome)();
|
|
643
829
|
return {
|
|
644
|
-
|
|
830
|
+
consumers: await (0, processDetection_1.detectRunningAccountConsumers)('codex'),
|
|
645
831
|
systemKeyringLoggedIn: hasCodexCredentialFiles(systemHome)
|
|
646
832
|
? false
|
|
647
833
|
: (await getCodexLoginStatusAsync(systemHome)).loggedIn,
|
|
648
834
|
};
|
|
649
835
|
}
|
|
650
|
-
function performCodexAuthSwap(target) {
|
|
836
|
+
function performCodexAuthSwap(target, options) {
|
|
651
837
|
const probes = resolveCodexSwapProbesSync();
|
|
652
838
|
try {
|
|
653
|
-
return withCodexAuthSwapLock(() => performCodexAuthSwapCore(target, probes));
|
|
839
|
+
return withCodexAuthSwapLock(() => performCodexAuthSwapCore(target, options, probes));
|
|
654
840
|
}
|
|
655
841
|
catch (err) {
|
|
656
842
|
// Keep the non-throwing result contract when the lock cannot be acquired.
|
|
657
|
-
return
|
|
843
|
+
return (0, accountSwitch_1.switchFailure)('codex', target.id, `Could not acquire the account-switch lock: ${err}`);
|
|
658
844
|
}
|
|
659
845
|
}
|
|
660
|
-
async function performCodexAuthSwapAsync(target) {
|
|
846
|
+
async function performCodexAuthSwapAsync(target, options) {
|
|
661
847
|
const probes = await resolveCodexSwapProbesAsync();
|
|
848
|
+
let result;
|
|
662
849
|
try {
|
|
663
850
|
// The sync lock is fine here: probes are pre-resolved, so the critical
|
|
664
851
|
// section is only fast local-filesystem work.
|
|
665
|
-
|
|
852
|
+
result = withCodexAuthSwapLock(() => performCodexAuthSwapCore(target, options, probes));
|
|
666
853
|
}
|
|
667
854
|
catch (err) {
|
|
668
|
-
return
|
|
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
|
+
});
|
|
669
868
|
}
|
|
869
|
+
return result;
|
|
670
870
|
}
|
|
671
|
-
function performCodexAuthSwapCore(target, probes) {
|
|
672
|
-
const systemHome = getSystemCodexHome();
|
|
871
|
+
function performCodexAuthSwapCore(target, options, probes) {
|
|
872
|
+
const systemHome = (0, codexPaths_1.getSystemCodexHome)();
|
|
673
873
|
const liveAuthPath = path.join(systemHome, 'auth.json');
|
|
674
874
|
const liveLegacyPath = path.join(systemHome, '.credentials.json');
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
875
|
+
const targetName = target.label ?? target.email ?? target.id;
|
|
876
|
+
let previousAccountId = (0, accountRegistry_1.getActiveSavedAccount)('codex')?.id ?? null;
|
|
877
|
+
const base = {
|
|
878
|
+
success: true,
|
|
879
|
+
provider: 'codex',
|
|
880
|
+
accountId: target.id,
|
|
881
|
+
previousAccountId,
|
|
882
|
+
verified: false,
|
|
883
|
+
verification: 'none',
|
|
884
|
+
warnings: [],
|
|
885
|
+
hints: [],
|
|
886
|
+
runningConsumers: probes.consumers,
|
|
887
|
+
email: target.email ?? target.metadata?.email,
|
|
888
|
+
};
|
|
889
|
+
const profilesDir = path.resolve((0, codexPaths_1.getCodexProfilesDir)());
|
|
890
|
+
const resolvedHome = path.resolve(systemHome);
|
|
891
|
+
if (resolvedHome === profilesDir || resolvedHome.startsWith(profilesDir + path.sep)) {
|
|
892
|
+
return (0, accountSwitch_1.finishSwitchResult)({
|
|
893
|
+
...base,
|
|
679
894
|
success: false,
|
|
680
|
-
error:
|
|
681
|
-
};
|
|
895
|
+
error: `CODEX_HOME points at a sidekick profile home (${systemHome}); unset it before switching accounts.`,
|
|
896
|
+
});
|
|
682
897
|
}
|
|
683
|
-
const
|
|
898
|
+
const liveBefore = (0, codexPaths_1.readFileOrNull)(liveAuthPath);
|
|
899
|
+
const liveLegacyBefore = (0, codexPaths_1.readFileOrNull)(liveLegacyPath);
|
|
900
|
+
if (!liveBefore && !liveLegacyBefore && probes.systemKeyringLoggedIn) {
|
|
901
|
+
return (0, accountSwitch_1.finishSwitchResult)({
|
|
902
|
+
...base,
|
|
903
|
+
success: false,
|
|
904
|
+
error: `Codex stores credentials in the OS keyring; file-based account switching is not supported. ${exports.CODEX_KEYRING_FIX_HINT}`,
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
// Phase 1: fold the live (freshest, rotated) credentials back into their
|
|
908
|
+
// profile, registering the login if sidekick has never seen it.
|
|
909
|
+
const sync = syncCodexLiveStateUnlocked('switch', {
|
|
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);
|
|
684
927
|
const targetAuthPath = path.join(profileHome, 'auth.json');
|
|
685
|
-
const targetAuthRaw = readFileOrNull(targetAuthPath);
|
|
686
|
-
const targetLegacyRaw = readFileOrNull(path.join(profileHome, '.credentials.json'));
|
|
687
|
-
const targetName = target.label ?? target.email ?? target.id;
|
|
928
|
+
const targetAuthRaw = (0, codexPaths_1.readFileOrNull)(targetAuthPath);
|
|
929
|
+
const targetLegacyRaw = (0, codexPaths_1.readFileOrNull)(path.join(profileHome, '.credentials.json'));
|
|
688
930
|
if (!targetAuthRaw && !targetLegacyRaw) {
|
|
689
|
-
return {
|
|
931
|
+
return (0, accountSwitch_1.finishSwitchResult)({
|
|
932
|
+
...base,
|
|
690
933
|
success: false,
|
|
691
|
-
|
|
692
|
-
|
|
934
|
+
needsLogin: true,
|
|
935
|
+
error: `No stored credentials for "${targetName}". Sign in again to this account.`,
|
|
936
|
+
});
|
|
693
937
|
}
|
|
694
|
-
if (targetAuthRaw && !parseAuthJson(targetAuthRaw)) {
|
|
695
|
-
return {
|
|
938
|
+
if (targetAuthRaw && !(0, codexAuth_1.parseAuthJson)(targetAuthRaw)) {
|
|
939
|
+
return (0, accountSwitch_1.finishSwitchResult)({
|
|
940
|
+
...base,
|
|
696
941
|
success: false,
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
const warnings = [];
|
|
701
|
-
if (probes.codexRunning) {
|
|
702
|
-
warnings.push('A codex process appears to be running; restart codex sessions so they pick up the switched account.');
|
|
942
|
+
needsLogin: true,
|
|
943
|
+
error: `Stored credentials for "${targetName}" are corrupted. Sign in again to this account.`,
|
|
944
|
+
});
|
|
703
945
|
}
|
|
704
946
|
// If the live file already belongs to the target account it is the freshest
|
|
705
947
|
// copy (rotated refresh token included) — never replace it with a staler
|
|
706
|
-
// backup, which would permanently invalidate the login.
|
|
707
|
-
|
|
708
|
-
const
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
const liveMatchesTarget = Boolean((liveIdentity?.workspaceId &&
|
|
713
|
-
targetWorkspaceId &&
|
|
714
|
-
liveIdentity.workspaceId === targetWorkspaceId) ||
|
|
715
|
-
(liveIdentity?.email && targetEmail && liveIdentity.email === targetEmail) ||
|
|
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
|
+
}) ||
|
|
716
954
|
(liveAuthRaw !== null && liveAuthRaw === targetAuthRaw) ||
|
|
717
955
|
(!liveAuthRaw && !targetAuthRaw && liveLegacyRaw !== null && liveLegacyRaw === targetLegacyRaw));
|
|
718
956
|
if (liveMatchesTarget) {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
(0,
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
957
|
+
if (previousAccountId !== target.id)
|
|
958
|
+
(0, accountRegistry_1.setActiveSavedAccount)('codex', target.id);
|
|
959
|
+
return (0, accountSwitch_1.finishSwitchResult)({
|
|
960
|
+
...base,
|
|
961
|
+
alreadyActive: true,
|
|
962
|
+
verified: true,
|
|
963
|
+
verification: 'store',
|
|
964
|
+
health: (0, accountHealth_1.getAccountHealth)('codex', target.id),
|
|
965
|
+
hints: [`${targetName} is already the active Codex account.`],
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
// Preflight: warn about stale backups; refuse ones known to be dead.
|
|
969
|
+
const health = (0, accountHealth_1.getAccountHealth)('codex', target.id, { probe: 'store' });
|
|
970
|
+
base.health = health;
|
|
971
|
+
if ((health.state === 'expired' || health.state === 'missing') && !options.force) {
|
|
972
|
+
return (0, accountSwitch_1.finishSwitchResult)({
|
|
973
|
+
...base,
|
|
974
|
+
success: false,
|
|
975
|
+
needsLogin: true,
|
|
976
|
+
error: `Stored credentials for "${targetName}" have expired; sign in again.`,
|
|
977
|
+
});
|
|
737
978
|
}
|
|
738
|
-
const targetLastRefresh = readLastRefresh(targetAuthRaw, targetAuthPath);
|
|
739
|
-
if (targetLastRefresh !== null && Date.now() - targetLastRefresh > STALE_AUTH_THRESHOLD_MS) {
|
|
740
|
-
warnings.push(`Stored credentials for "${targetName}" have not been refreshed in over 8 days; codex may ask you to log in again.`);
|
|
979
|
+
const targetLastRefresh = (0, codexAuth_1.readLastRefresh)(targetAuthRaw, targetAuthPath);
|
|
980
|
+
if (targetLastRefresh !== null && Date.now() - targetLastRefresh > codexAuth_1.STALE_AUTH_THRESHOLD_MS) {
|
|
981
|
+
base.warnings.push(`Stored credentials for "${targetName}" have not been refreshed in over 8 days; codex may ask you to log in again.`);
|
|
741
982
|
}
|
|
742
|
-
const syncBack = syncBackLiveCodexAuth(liveAuthRaw, liveLegacyRaw);
|
|
743
|
-
if (syncBack.warning)
|
|
744
|
-
warnings.push(syncBack.warning);
|
|
745
983
|
const restoreLiveFiles = () => {
|
|
746
984
|
try {
|
|
747
985
|
if (liveAuthRaw)
|
|
@@ -756,7 +994,14 @@ function performCodexAuthSwapCore(target, probes) {
|
|
|
756
994
|
catch {
|
|
757
995
|
/* rollback is best-effort */
|
|
758
996
|
}
|
|
997
|
+
try {
|
|
998
|
+
(0, accountRegistry_1.setActiveSavedAccount)('codex', previousAccountId, { silent: true });
|
|
999
|
+
}
|
|
1000
|
+
catch {
|
|
1001
|
+
/* rollback is best-effort */
|
|
1002
|
+
}
|
|
759
1003
|
};
|
|
1004
|
+
// Install.
|
|
760
1005
|
try {
|
|
761
1006
|
if (targetAuthRaw) {
|
|
762
1007
|
(0, atomic_1.atomicWriteFileSync)(liveAuthPath, targetAuthRaw);
|
|
@@ -773,32 +1018,75 @@ function performCodexAuthSwapCore(target, probes) {
|
|
|
773
1018
|
}
|
|
774
1019
|
catch (err) {
|
|
775
1020
|
restoreLiveFiles();
|
|
776
|
-
return
|
|
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
|
+
});
|
|
777
1046
|
}
|
|
1047
|
+
// Pointer last.
|
|
778
1048
|
try {
|
|
779
1049
|
(0, accountRegistry_1.setActiveSavedAccount)('codex', target.id);
|
|
780
1050
|
}
|
|
781
1051
|
catch (err) {
|
|
782
1052
|
restoreLiveFiles();
|
|
783
|
-
return
|
|
1053
|
+
return (0, accountSwitch_1.finishSwitchResult)({
|
|
1054
|
+
...base,
|
|
1055
|
+
success: false,
|
|
1056
|
+
error: `Failed to update account registry: ${err}`,
|
|
1057
|
+
});
|
|
784
1058
|
}
|
|
785
|
-
|
|
1059
|
+
let undoToken;
|
|
1060
|
+
try {
|
|
1061
|
+
undoToken = (0, accountSwitch_1.writeLastSwitch)('codex', previousAccountId, target.id).token;
|
|
1062
|
+
}
|
|
1063
|
+
catch {
|
|
1064
|
+
base.warnings.push('The switch succeeded but could not be recorded for undo.');
|
|
1065
|
+
}
|
|
1066
|
+
return (0, accountSwitch_1.finishSwitchResult)({
|
|
1067
|
+
...base,
|
|
1068
|
+
verified: true,
|
|
1069
|
+
verification: 'store',
|
|
1070
|
+
undoToken,
|
|
1071
|
+
hints: [`New codex sessions use ${target.email ?? targetName}.`],
|
|
1072
|
+
});
|
|
786
1073
|
}
|
|
787
|
-
function switchToCodexAccount(profileId) {
|
|
1074
|
+
function switchToCodexAccount(profileId, options = {}) {
|
|
788
1075
|
const target = listCodexAccounts().find((account) => account.id === profileId);
|
|
789
1076
|
if (!target) {
|
|
790
|
-
return
|
|
1077
|
+
return (0, accountSwitch_1.switchFailure)('codex', profileId, `Codex account ${profileId} not found.`);
|
|
791
1078
|
}
|
|
792
|
-
return performCodexAuthSwap(target);
|
|
1079
|
+
return performCodexAuthSwap(target, options);
|
|
793
1080
|
}
|
|
794
1081
|
/** {@link switchToCodexAccount} without blocking the event loop on CLI probes. */
|
|
795
|
-
async function switchToCodexAccountAsync(profileId) {
|
|
1082
|
+
async function switchToCodexAccountAsync(profileId, options = {}) {
|
|
796
1083
|
const target = listCodexAccounts().find((account) => account.id === profileId);
|
|
797
1084
|
if (!target) {
|
|
798
|
-
return
|
|
1085
|
+
return (0, accountSwitch_1.switchFailure)('codex', profileId, `Codex account ${profileId} not found.`);
|
|
799
1086
|
}
|
|
800
|
-
return performCodexAuthSwapAsync(target);
|
|
1087
|
+
return performCodexAuthSwapAsync(target, options);
|
|
801
1088
|
}
|
|
1089
|
+
// ── One-time migration ───────────────────────────────────────────────────
|
|
802
1090
|
/**
|
|
803
1091
|
* The reconcile flow consults the keyring probe only when the migration has
|
|
804
1092
|
* not run yet and the system home has no auth.json; skip the spawn otherwise.
|
|
@@ -807,7 +1095,7 @@ function needsReconcileKeyringProbe() {
|
|
|
807
1095
|
const markerPath = path.join((0, accountRegistry_1.getAccountsDir)(), 'codex', '.live-auth-migrated-v1');
|
|
808
1096
|
if (fs.existsSync(markerPath))
|
|
809
1097
|
return false;
|
|
810
|
-
return !fs.existsSync(path.join(getSystemCodexHome(), 'auth.json'));
|
|
1098
|
+
return !fs.existsSync(path.join((0, codexPaths_1.getSystemCodexHome)(), 'auth.json'));
|
|
811
1099
|
}
|
|
812
1100
|
// One-time migration for installs created when profile homes doubled as live
|
|
813
1101
|
// CODEX_HOMEs: the active profile's auth.json may hold a fresher rotated
|
|
@@ -815,7 +1103,7 @@ function needsReconcileKeyringProbe() {
|
|
|
815
1103
|
function reconcileCodexAuthState() {
|
|
816
1104
|
try {
|
|
817
1105
|
const systemKeyringLoggedIn = needsReconcileKeyringProbe()
|
|
818
|
-
? getCodexLoginStatus(getSystemCodexHome()).loggedIn
|
|
1106
|
+
? getCodexLoginStatus((0, codexPaths_1.getSystemCodexHome)()).loggedIn
|
|
819
1107
|
: false;
|
|
820
1108
|
withCodexAuthSwapLock(() => reconcileCodexAuthStateCore({ systemKeyringLoggedIn }));
|
|
821
1109
|
}
|
|
@@ -827,7 +1115,7 @@ function reconcileCodexAuthState() {
|
|
|
827
1115
|
async function reconcileCodexAuthStateAsync() {
|
|
828
1116
|
try {
|
|
829
1117
|
const systemKeyringLoggedIn = needsReconcileKeyringProbe()
|
|
830
|
-
? (await getCodexLoginStatusAsync(getSystemCodexHome())).loggedIn
|
|
1118
|
+
? (await getCodexLoginStatusAsync((0, codexPaths_1.getSystemCodexHome)())).loggedIn
|
|
831
1119
|
: false;
|
|
832
1120
|
withCodexAuthSwapLock(() => reconcileCodexAuthStateCore({ systemKeyringLoggedIn }));
|
|
833
1121
|
}
|
|
@@ -846,16 +1134,16 @@ function reconcileCodexAuthStateCore(probes) {
|
|
|
846
1134
|
writeMarker();
|
|
847
1135
|
return;
|
|
848
1136
|
}
|
|
849
|
-
const profileHome = getCodexProfileHome(active.id);
|
|
1137
|
+
const profileHome = (0, codexPaths_1.getCodexProfileHome)(active.id);
|
|
850
1138
|
const profileAuthPath = path.join(profileHome, 'auth.json');
|
|
851
|
-
const profileAuthRaw = readFileOrNull(profileAuthPath);
|
|
1139
|
+
const profileAuthRaw = (0, codexPaths_1.readFileOrNull)(profileAuthPath);
|
|
852
1140
|
if (!profileAuthRaw) {
|
|
853
1141
|
writeMarker();
|
|
854
1142
|
return;
|
|
855
1143
|
}
|
|
856
|
-
const systemHome = getSystemCodexHome();
|
|
1144
|
+
const systemHome = (0, codexPaths_1.getSystemCodexHome)();
|
|
857
1145
|
const liveAuthPath = path.join(systemHome, 'auth.json');
|
|
858
|
-
const liveAuthRaw = readFileOrNull(liveAuthPath);
|
|
1146
|
+
const liveAuthRaw = (0, codexPaths_1.readFileOrNull)(liveAuthPath);
|
|
859
1147
|
if (!liveAuthRaw) {
|
|
860
1148
|
// No live credentials (account was added via isolated login and never
|
|
861
1149
|
// promoted). Promote the active profile's copy unless codex is logged
|
|
@@ -867,15 +1155,12 @@ function reconcileCodexAuthStateCore(probes) {
|
|
|
867
1155
|
writeMarker();
|
|
868
1156
|
return;
|
|
869
1157
|
}
|
|
870
|
-
const liveIdentity = readAuthIdentityFromRaw(liveAuthRaw);
|
|
871
|
-
const
|
|
872
|
-
const sameIdentity =
|
|
873
|
-
profileIdentity?.workspaceId &&
|
|
874
|
-
liveIdentity.workspaceId === profileIdentity.workspaceId) ||
|
|
875
|
-
(liveIdentity?.email && liveIdentity.email === profileIdentity?.email));
|
|
1158
|
+
const liveIdentity = (0, codexAuth_1.readAuthIdentityFromRaw)(liveAuthRaw);
|
|
1159
|
+
const profileIdentityValue = (0, codexAuth_1.readAuthIdentityFromRaw)(profileAuthRaw);
|
|
1160
|
+
const sameIdentity = (0, codexAuth_1.identitiesMatch)(liveIdentity, profileIdentityValue);
|
|
876
1161
|
if (sameIdentity) {
|
|
877
|
-
const liveRefresh = readLastRefresh(liveAuthRaw, liveAuthPath);
|
|
878
|
-
const profileRefresh = readLastRefresh(profileAuthRaw, profileAuthPath);
|
|
1162
|
+
const liveRefresh = (0, codexAuth_1.readLastRefresh)(liveAuthRaw, liveAuthPath);
|
|
1163
|
+
const profileRefresh = (0, codexAuth_1.readLastRefresh)(profileAuthRaw, profileAuthPath);
|
|
879
1164
|
if (profileRefresh !== null && (liveRefresh === null || profileRefresh > liveRefresh)) {
|
|
880
1165
|
// The profile copy was the live home under the old model and holds
|
|
881
1166
|
// the valid rotated refresh token — promote it.
|
|
@@ -883,7 +1168,7 @@ function reconcileCodexAuthStateCore(probes) {
|
|
|
883
1168
|
(0, atomic_1.atomicWriteFileSync)(liveAuthPath, profileAuthRaw);
|
|
884
1169
|
}
|
|
885
1170
|
else {
|
|
886
|
-
(
|
|
1171
|
+
storeCodexProfileAuth(active.id, liveAuthRaw, null, 'migration');
|
|
887
1172
|
}
|
|
888
1173
|
}
|
|
889
1174
|
else {
|
|
@@ -892,7 +1177,7 @@ function reconcileCodexAuthStateCore(probes) {
|
|
|
892
1177
|
// one, and refresh its backup.
|
|
893
1178
|
const matching = findProfileForIdentity(liveIdentity);
|
|
894
1179
|
if (matching && matching.id !== active.id) {
|
|
895
|
-
(
|
|
1180
|
+
storeCodexProfileAuth(matching.id, liveAuthRaw, null, 'migration');
|
|
896
1181
|
(0, accountRegistry_1.setActiveSavedAccount)('codex', matching.id);
|
|
897
1182
|
}
|
|
898
1183
|
}
|
|
@@ -907,6 +1192,6 @@ function removeCodexAccount(profileId) {
|
|
|
907
1192
|
if (!removed) {
|
|
908
1193
|
return { success: false, error: `Codex account ${profileId} not found.` };
|
|
909
1194
|
}
|
|
910
|
-
fs.rmSync(getCodexProfileDir(profileId), { recursive: true, force: true });
|
|
1195
|
+
fs.rmSync((0, codexPaths_1.getCodexProfileDir)(profileId), { recursive: true, force: true });
|
|
911
1196
|
return { success: true };
|
|
912
1197
|
}
|