sidekick-shared 0.27.5 → 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 +30 -0
- package/README.md +35 -34
- 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 +14 -4
- package/dist/index.js +29 -6
- 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/processDetection.d.ts +10 -3
- package/dist/processDetection.js +44 -11
- 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/terminalSync.d.ts +12 -2
- package/dist/terminalSync.js +27 -2
- package/package.json +1 -1
package/dist/claudeProfiles.js
CHANGED
|
@@ -38,8 +38,10 @@ exports.getDefaultClaudeHome = getDefaultClaudeHome;
|
|
|
38
38
|
exports.isDefaultClaudeHome = isDefaultClaudeHome;
|
|
39
39
|
exports.getLiveClaudeHome = getLiveClaudeHome;
|
|
40
40
|
exports.getClaudeProfilesDir = getClaudeProfilesDir;
|
|
41
|
+
exports.isLiveClaudeHomeSidekickProfile = isLiveClaudeHomeSidekickProfile;
|
|
41
42
|
exports.getClaudeProfileDir = getClaudeProfileDir;
|
|
42
43
|
exports.getClaudeProfileHome = getClaudeProfileHome;
|
|
44
|
+
exports.getClaudeCredentialBackupPath = getClaudeCredentialBackupPath;
|
|
43
45
|
exports.ensureClaudeProfileDirs = ensureClaudeProfileDirs;
|
|
44
46
|
exports.claudeKeychainSuffix = claudeKeychainSuffix;
|
|
45
47
|
exports.claudeKeychainService = claudeKeychainService;
|
|
@@ -84,12 +86,32 @@ function getLiveClaudeHome() {
|
|
|
84
86
|
function getClaudeProfilesDir() {
|
|
85
87
|
return path.join((0, accountRegistry_1.getAccountsDir)(), 'claude', 'profiles');
|
|
86
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* True when the live home is one of sidekick's own profile homes: a shell
|
|
91
|
+
* that eval'd `sidekick accounts env` or runs inside `sidekick accounts shell`.
|
|
92
|
+
* That profile's store then reads as the live store, so sync, re-login, and
|
|
93
|
+
* switching must not treat it as live: they would delete the shell's only
|
|
94
|
+
* usable copy and re-point the active account at it.
|
|
95
|
+
*/
|
|
96
|
+
function isLiveClaudeHomeSidekickProfile() {
|
|
97
|
+
const liveHome = path.resolve(getLiveClaudeHome());
|
|
98
|
+
const profilesDir = path.resolve(getClaudeProfilesDir());
|
|
99
|
+
return liveHome === profilesDir || liveHome.startsWith(profilesDir + path.sep);
|
|
100
|
+
}
|
|
87
101
|
function getClaudeProfileDir(uuid) {
|
|
88
102
|
return path.join(getClaudeProfilesDir(), uuid);
|
|
89
103
|
}
|
|
90
104
|
function getClaudeProfileHome(uuid) {
|
|
91
105
|
return path.join(getClaudeProfileDir(uuid), 'home');
|
|
92
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Sidekick's private backup of a profile's credential. Claude Code never reads
|
|
109
|
+
* it, so it is not a CLI-usable copy: this is where the live account's
|
|
110
|
+
* credential is mirrored while its profile home stays empty.
|
|
111
|
+
*/
|
|
112
|
+
function getClaudeCredentialBackupPath(uuid) {
|
|
113
|
+
return path.join((0, accountRegistry_1.getAccountsDir)(), 'credentials', `${uuid}.credentials.json`);
|
|
114
|
+
}
|
|
93
115
|
function ensureClaudeProfileDirs(uuid) {
|
|
94
116
|
fs.mkdirSync(getClaudeProfileHome(uuid), { recursive: true, mode: 0o700 });
|
|
95
117
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Claude Code treats a lock older than this as abandoned (proper-lockfile `stale`). */
|
|
2
|
+
export declare const CLAUDE_REFRESH_LOCK_STALE_MS = 60000;
|
|
3
|
+
/** Claude Code is refreshing a login in one of the homes; retry shortly. */
|
|
4
|
+
export declare class ClaudeRefreshLockBusyError extends Error {
|
|
5
|
+
constructor(lockPath: string);
|
|
6
|
+
}
|
|
7
|
+
export declare function claudeRefreshLockPaths(home: string): string[];
|
|
8
|
+
/**
|
|
9
|
+
* Run `operation` holding the refresh locks of every existing home (a home
|
|
10
|
+
* that does not exist has no CLI using it). Throws
|
|
11
|
+
* {@link ClaudeRefreshLockBusyError} when a lock stays held past `waitMs`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function withClaudeRefreshLocksSync<T>(homes: string[], operation: () => T, options?: {
|
|
14
|
+
waitMs?: number;
|
|
15
|
+
}): T;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ClaudeRefreshLockBusyError = exports.CLAUDE_REFRESH_LOCK_STALE_MS = void 0;
|
|
37
|
+
exports.claudeRefreshLockPaths = claudeRefreshLockPaths;
|
|
38
|
+
exports.withClaudeRefreshLocksSync = withClaudeRefreshLocksSync;
|
|
39
|
+
/**
|
|
40
|
+
* Borrow Claude Code's own token-refresh lock while Sidekick reads and
|
|
41
|
+
* replaces a home's credential.
|
|
42
|
+
*
|
|
43
|
+
* Claude Code 2.1.x serialises refreshes per config directory with a
|
|
44
|
+
* proper-lockfile directory lock at `<configDir>/.oauth_refresh.lock` (stale
|
|
45
|
+
* after 60 s) plus a legacy `<realpath(configDir)>.lock`. Under that lock it
|
|
46
|
+
* re-reads the store before refreshing and adopts a token someone else wrote,
|
|
47
|
+
* so holding both locks keeps a refresh (which rotates the refresh token) out
|
|
48
|
+
* of the window between Sidekick's read and its write.
|
|
49
|
+
*
|
|
50
|
+
* Best effort by construction: this layers on Claude Code internals. If they
|
|
51
|
+
* change we lose only the exclusion; the callers' fingerprint re-checks still
|
|
52
|
+
* catch a store that moved. Held briefly (a few `security` calls), well under
|
|
53
|
+
* the 60 s stale window, and released on every exit.
|
|
54
|
+
*/
|
|
55
|
+
const fs = __importStar(require("fs"));
|
|
56
|
+
const path = __importStar(require("path"));
|
|
57
|
+
/** Claude Code treats a lock older than this as abandoned (proper-lockfile `stale`). */
|
|
58
|
+
exports.CLAUDE_REFRESH_LOCK_STALE_MS = 60_000;
|
|
59
|
+
const DEFAULT_WAIT_MS = 5_000;
|
|
60
|
+
const POLL_MS = 50;
|
|
61
|
+
const sleepView = new Int32Array(new SharedArrayBuffer(4));
|
|
62
|
+
/** Claude Code is refreshing a login in one of the homes; retry shortly. */
|
|
63
|
+
class ClaudeRefreshLockBusyError extends Error {
|
|
64
|
+
constructor(lockPath) {
|
|
65
|
+
super(`Claude Code is refreshing a login (${lockPath} is held); try again in a few seconds.`);
|
|
66
|
+
this.name = 'ClaudeRefreshLockBusyError';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.ClaudeRefreshLockBusyError = ClaudeRefreshLockBusyError;
|
|
70
|
+
function claudeRefreshLockPaths(home) {
|
|
71
|
+
let real = path.resolve(home);
|
|
72
|
+
try {
|
|
73
|
+
real = fs.realpathSync(home);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
/* keep the resolved path */
|
|
77
|
+
}
|
|
78
|
+
return [path.join(home, '.oauth_refresh.lock'), `${real}.lock`];
|
|
79
|
+
}
|
|
80
|
+
function tryAcquire(lockPath, now) {
|
|
81
|
+
try {
|
|
82
|
+
fs.mkdirSync(lockPath);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error.code !== 'EEXIST')
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const stat = fs.statSync(lockPath);
|
|
91
|
+
if (now - stat.mtimeMs > exports.CLAUDE_REFRESH_LOCK_STALE_MS) {
|
|
92
|
+
// proper-lockfile's own recovery: an abandoned lock is removed and retaken.
|
|
93
|
+
fs.rmSync(lockPath, { recursive: true, force: true });
|
|
94
|
+
fs.mkdirSync(lockPath);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
/* released or retaken between checks; poll again */
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Run `operation` holding the refresh locks of every existing home (a home
|
|
105
|
+
* that does not exist has no CLI using it). Throws
|
|
106
|
+
* {@link ClaudeRefreshLockBusyError} when a lock stays held past `waitMs`.
|
|
107
|
+
*/
|
|
108
|
+
function withClaudeRefreshLocksSync(homes, operation, options = {}) {
|
|
109
|
+
const waitMs = options.waitMs ?? DEFAULT_WAIT_MS;
|
|
110
|
+
const lockPaths = [
|
|
111
|
+
...new Set(homes
|
|
112
|
+
.filter((home) => {
|
|
113
|
+
try {
|
|
114
|
+
return fs.statSync(home).isDirectory();
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
.flatMap(claudeRefreshLockPaths)),
|
|
121
|
+
];
|
|
122
|
+
const held = [];
|
|
123
|
+
try {
|
|
124
|
+
const deadline = Date.now() + waitMs;
|
|
125
|
+
for (const lockPath of lockPaths) {
|
|
126
|
+
while (!tryAcquire(lockPath, Date.now())) {
|
|
127
|
+
if (Date.now() >= deadline)
|
|
128
|
+
throw new ClaudeRefreshLockBusyError(lockPath);
|
|
129
|
+
Atomics.wait(sleepView, 0, 0, POLL_MS);
|
|
130
|
+
}
|
|
131
|
+
held.push(lockPath);
|
|
132
|
+
}
|
|
133
|
+
return operation();
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
for (const lockPath of held.reverse()) {
|
|
137
|
+
try {
|
|
138
|
+
fs.rmSync(lockPath, { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
/* Claude Code recovers a stale lock after 60 s */
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
package/dist/codexAuth.d.ts
CHANGED
|
@@ -30,3 +30,7 @@ export declare function identitiesMatch(a: {
|
|
|
30
30
|
email?: string;
|
|
31
31
|
workspaceId?: string;
|
|
32
32
|
} | null | undefined): boolean;
|
|
33
|
+
/** Non-secret chain-link identifier; raw token values never belong in diagnostics. */
|
|
34
|
+
export declare function codexRefreshFingerprint(raw: string | null): string | null;
|
|
35
|
+
/** Compare evidence, never filesystem timestamps. Incomparable credentials tie. */
|
|
36
|
+
export declare function compareCodexAuthFreshness(a: string | null, b: string | null): number;
|
package/dist/codexAuth.js
CHANGED
|
@@ -40,11 +40,14 @@ exports.readAuthIdentityFromRaw = readAuthIdentityFromRaw;
|
|
|
40
40
|
exports.readLastRefresh = readLastRefresh;
|
|
41
41
|
exports.readAccessTokenExpiry = readAccessTokenExpiry;
|
|
42
42
|
exports.identitiesMatch = identitiesMatch;
|
|
43
|
+
exports.codexRefreshFingerprint = codexRefreshFingerprint;
|
|
44
|
+
exports.compareCodexAuthFreshness = compareCodexAuthFreshness;
|
|
43
45
|
/**
|
|
44
46
|
* Pure helpers over the Codex CLI `auth.json` payload. No filesystem access
|
|
45
47
|
* beyond an optional mtime fallback, no child processes.
|
|
46
48
|
*/
|
|
47
49
|
const fs = __importStar(require("fs"));
|
|
50
|
+
const crypto_1 = require("crypto");
|
|
48
51
|
// Codex refreshes OAuth tokens at most every 8 days; a stored refresh token
|
|
49
52
|
// older than that may already be rejected by the auth server.
|
|
50
53
|
exports.STALE_AUTH_THRESHOLD_MS = 8 * 24 * 60 * 60 * 1000;
|
|
@@ -125,3 +128,18 @@ function identitiesMatch(a, b) {
|
|
|
125
128
|
return a.workspaceId === b.workspaceId;
|
|
126
129
|
return Boolean(a.email && b.email && a.email === b.email);
|
|
127
130
|
}
|
|
131
|
+
/** Non-secret chain-link identifier; raw token values never belong in diagnostics. */
|
|
132
|
+
function codexRefreshFingerprint(raw) {
|
|
133
|
+
const token = parseAuthJson(raw)?.tokens?.refresh_token;
|
|
134
|
+
return typeof token === 'string' && token
|
|
135
|
+
? (0, crypto_1.createHash)('sha256').update(token).digest('hex').slice(0, 16)
|
|
136
|
+
: null;
|
|
137
|
+
}
|
|
138
|
+
/** Compare evidence, never filesystem timestamps. Incomparable credentials tie. */
|
|
139
|
+
function compareCodexAuthFreshness(a, b) {
|
|
140
|
+
const ar = readLastRefresh(a), br = readLastRefresh(b);
|
|
141
|
+
if (ar !== null && br !== null && ar !== br)
|
|
142
|
+
return ar > br ? 1 : -1;
|
|
143
|
+
const ae = readAccessTokenExpiry(a), be = readAccessTokenExpiry(b);
|
|
144
|
+
return ae !== null && be !== null && ae !== be ? (ae > be ? 1 : -1) : 0;
|
|
145
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ProviderSyncReport } from './accountSyncTypes';
|
|
2
|
+
export interface CodexCredentialFinding {
|
|
3
|
+
kind: 'live-duplicate' | 'stale-stash' | 'orphan' | 'corrupt' | 'unreadable' | 'pending-switch';
|
|
4
|
+
configDir: string;
|
|
5
|
+
/** Present for old timestamped stash files. */
|
|
6
|
+
file?: string;
|
|
7
|
+
ownership: 'profile-home' | 'login-home' | 'stash' | 'live' | 'journal' | 'backup';
|
|
8
|
+
accountId?: string;
|
|
9
|
+
email?: string;
|
|
10
|
+
fingerprint?: string;
|
|
11
|
+
detail: string;
|
|
12
|
+
action: string;
|
|
13
|
+
}
|
|
14
|
+
export interface CodexCredentialInspection {
|
|
15
|
+
findings: CodexCredentialFinding[];
|
|
16
|
+
homesChecked: number;
|
|
17
|
+
}
|
|
18
|
+
export interface CodexCredentialRepairResult extends CodexCredentialInspection {
|
|
19
|
+
dryRun: boolean;
|
|
20
|
+
repaired: Array<{
|
|
21
|
+
finding: CodexCredentialFinding;
|
|
22
|
+
ok: boolean;
|
|
23
|
+
error?: string;
|
|
24
|
+
}>;
|
|
25
|
+
sync?: ProviderSyncReport;
|
|
26
|
+
}
|
|
27
|
+
export declare function inspectCodexCredentialStores(options?: {
|
|
28
|
+
now?: number;
|
|
29
|
+
}): CodexCredentialInspection;
|
|
30
|
+
export declare function repairCodexCredentialStores(options?: {
|
|
31
|
+
now?: number;
|
|
32
|
+
dryRun?: boolean;
|
|
33
|
+
}): CodexCredentialRepairResult;
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.inspectCodexCredentialStores = inspectCodexCredentialStores;
|
|
37
|
+
exports.repairCodexCredentialStores = repairCodexCredentialStores;
|
|
38
|
+
/** Inspection is read-only. Repairs touch only credential paths Sidekick owns. */
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const accountRegistry_1 = require("./accountRegistry");
|
|
42
|
+
const codexAuth_1 = require("./codexAuth");
|
|
43
|
+
const codexPaths_1 = require("./codexPaths");
|
|
44
|
+
const codexCredentialStore_1 = require("./codexCredentialStore");
|
|
45
|
+
const accountLease_1 = require("./accountLease");
|
|
46
|
+
const processDetection_1 = require("./processDetection");
|
|
47
|
+
const codexProfiles_1 = require("./codexProfiles");
|
|
48
|
+
function within(child, root) {
|
|
49
|
+
const resolved = (0, codexCredentialStore_1.canonicalCodexPath)(child), base = (0, codexCredentialStore_1.canonicalCodexPath)(root);
|
|
50
|
+
return resolved === base || resolved.startsWith(base + path.sep);
|
|
51
|
+
}
|
|
52
|
+
function ownedDirectories(root) {
|
|
53
|
+
try {
|
|
54
|
+
return fs
|
|
55
|
+
.readdirSync(root, { withFileTypes: true })
|
|
56
|
+
.filter((e) => e.isDirectory() && !e.isSymbolicLink())
|
|
57
|
+
.map((e) => path.join(root, e.name))
|
|
58
|
+
.filter((dir) => within(dir, root));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function readFinding(f) {
|
|
65
|
+
if (!f.file)
|
|
66
|
+
return (0, codexCredentialStore_1.readCodexCredentialStore)(f.configDir);
|
|
67
|
+
try {
|
|
68
|
+
const raw = fs.readFileSync(f.file, 'utf8');
|
|
69
|
+
const legacy = path.basename(f.file).startsWith('credentials-');
|
|
70
|
+
const snapshot = { authRaw: legacy ? null : raw, legacyRaw: legacy ? raw : null };
|
|
71
|
+
return {
|
|
72
|
+
status: (legacy ? (0, codexCredentialStore_1.isLegacyCodexLogin)(raw) : (0, codexCredentialStore_1.isCodexLogin)(raw)) ? 'ok' : 'corrupt',
|
|
73
|
+
snapshot,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
return error.code === 'ENOENT'
|
|
78
|
+
? { status: 'absent', snapshot: { authRaw: null, legacyRaw: null } }
|
|
79
|
+
: { status: 'error' };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function inspectCodexCredentialStores(options = {}) {
|
|
83
|
+
const now = options.now ?? Date.now();
|
|
84
|
+
const findings = [];
|
|
85
|
+
const live = (0, codexCredentialStore_1.readCodexCredentialStore)();
|
|
86
|
+
if (live.status === 'corrupt' || live.status === 'error')
|
|
87
|
+
findings.push({
|
|
88
|
+
kind: live.status === 'error' ? 'unreadable' : 'corrupt',
|
|
89
|
+
configDir: (0, codexPaths_1.getSystemCodexHome)(),
|
|
90
|
+
ownership: 'live',
|
|
91
|
+
detail: 'The live Codex login could not be read; it may be mid-write.',
|
|
92
|
+
action: 'Close Codex and inspect again; live data is never deleted by doctor.',
|
|
93
|
+
});
|
|
94
|
+
const profiles = (0, accountRegistry_1.listSavedAccountProfiles)('codex');
|
|
95
|
+
for (const profile of profiles) {
|
|
96
|
+
const backup = (0, codexCredentialStore_1.readCodexBackup)(profile.id);
|
|
97
|
+
if (backup.status === 'error' || backup.status === 'corrupt')
|
|
98
|
+
findings.push({
|
|
99
|
+
kind: backup.status === 'error' ? 'unreadable' : 'corrupt',
|
|
100
|
+
ownership: 'backup',
|
|
101
|
+
accountId: profile.id,
|
|
102
|
+
email: profile.email,
|
|
103
|
+
configDir: path.dirname((0, codexPaths_1.getCodexBackupPath)(profile.id)),
|
|
104
|
+
file: (0, codexPaths_1.getCodexBackupPath)(profile.id),
|
|
105
|
+
detail: 'The private Codex backup could not be read safely.',
|
|
106
|
+
action: 'Inspect the backup or sign in again; unreadable credentials are preserved.',
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const candidates = [];
|
|
110
|
+
for (const root of [(0, codexPaths_1.getCodexProfilesDir)(), (0, codexPaths_1.getCodexLoginsDir)()]) {
|
|
111
|
+
for (const dir of ownedDirectories(root)) {
|
|
112
|
+
const pending = path.join(dir, 'profile.json');
|
|
113
|
+
if (fs.existsSync(pending)) {
|
|
114
|
+
try {
|
|
115
|
+
const at = Date.parse(JSON.parse(fs.readFileSync(pending, 'utf8')).addedAt);
|
|
116
|
+
if (!Number.isFinite(at) || now - at < 24 * 60 * 60 * 1000)
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const home = path.join(dir, 'codex-home');
|
|
124
|
+
if (within(home, root) &&
|
|
125
|
+
(0, codexCredentialStore_1.canonicalCodexPath)(home) !== (0, codexCredentialStore_1.canonicalCodexPath)((0, codexPaths_1.getSystemCodexHome)())) {
|
|
126
|
+
candidates.push({
|
|
127
|
+
configDir: home,
|
|
128
|
+
ownership: root === (0, codexPaths_1.getCodexProfilesDir)() ? 'profile-home' : 'login-home',
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const stash = path.join((0, accountRegistry_1.getAccountsDir)(), 'codex', 'stash');
|
|
134
|
+
for (const dir of ownedDirectories(stash)) {
|
|
135
|
+
const home = path.join(dir, 'codex-home');
|
|
136
|
+
if (within(home, stash))
|
|
137
|
+
candidates.push({ configDir: home, ownership: 'stash' });
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
for (const entry of fs.readdirSync(stash, { withFileTypes: true })) {
|
|
141
|
+
if (entry.isFile() && /^(auth|credentials)-.*\.json$/.test(entry.name))
|
|
142
|
+
candidates.push({
|
|
143
|
+
configDir: stash,
|
|
144
|
+
file: path.join(stash, entry.name),
|
|
145
|
+
ownership: 'stash',
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
/* no old stash */
|
|
151
|
+
}
|
|
152
|
+
for (const candidate of candidates) {
|
|
153
|
+
const store = readFinding(candidate);
|
|
154
|
+
if (store.status === 'absent')
|
|
155
|
+
continue;
|
|
156
|
+
const identity = store.status === 'ok' ? (0, codexAuth_1.readAuthIdentityFromRaw)(store.snapshot.authRaw) : null;
|
|
157
|
+
const profile = profiles.find((p) => (0, codexAuth_1.identitiesMatch)(identity, {
|
|
158
|
+
email: p.email ?? p.metadata?.email,
|
|
159
|
+
workspaceId: p.metadata?.workspaceId,
|
|
160
|
+
})) ??
|
|
161
|
+
profiles.find((p) => (0, codexCredentialStore_1.canonicalCodexPath)((0, codexPaths_1.getCodexProfileHome)(p.id)) === (0, codexCredentialStore_1.canonicalCodexPath)(candidate.configDir));
|
|
162
|
+
const base = { ...candidate, accountId: profile?.id, email: identity?.email ?? profile?.email };
|
|
163
|
+
if (store.status === 'error' || store.status === 'corrupt') {
|
|
164
|
+
findings.push({
|
|
165
|
+
...base,
|
|
166
|
+
kind: store.status === 'error' ? 'unreadable' : 'corrupt',
|
|
167
|
+
detail: 'This credential store could not be read safely.',
|
|
168
|
+
action: 'Inspect or sign in again; unreadable credentials are preserved.',
|
|
169
|
+
});
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const fingerprint = (0, codexCredentialStore_1.codexSnapshotFingerprint)(store.snapshot);
|
|
173
|
+
const liveDuplicate = live.status === 'ok' &&
|
|
174
|
+
((0, codexAuth_1.identitiesMatch)(identity, (0, codexAuth_1.readAuthIdentityFromRaw)(live.snapshot.authRaw)) ||
|
|
175
|
+
fingerprint === (0, codexCredentialStore_1.codexSnapshotFingerprint)(live.snapshot));
|
|
176
|
+
if (candidate.ownership === 'stash')
|
|
177
|
+
findings.push({
|
|
178
|
+
...base,
|
|
179
|
+
fingerprint,
|
|
180
|
+
kind: 'stale-stash',
|
|
181
|
+
detail: 'An old stash holds another account credential.',
|
|
182
|
+
action: 'Preserve the freshest known login privately, then remove this credential.',
|
|
183
|
+
});
|
|
184
|
+
else if (liveDuplicate)
|
|
185
|
+
findings.push({
|
|
186
|
+
...base,
|
|
187
|
+
fingerprint,
|
|
188
|
+
kind: 'live-duplicate',
|
|
189
|
+
detail: 'This home holds a second usable copy of the live Codex login.',
|
|
190
|
+
action: 'Repair a newer login first, then remove the duplicate when Codex is stopped.',
|
|
191
|
+
});
|
|
192
|
+
else if (!profile || candidate.ownership === 'login-home')
|
|
193
|
+
findings.push({
|
|
194
|
+
...base,
|
|
195
|
+
fingerprint,
|
|
196
|
+
kind: 'orphan',
|
|
197
|
+
detail: 'An abandoned home holds a login outside a saved profile.',
|
|
198
|
+
action: 'Preserve it under its known account before removing it; unknown identities need manual review.',
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
if (fs.existsSync(path.join((0, accountRegistry_1.getAccountsDir)(), 'codex', 'switch-journal.json')))
|
|
202
|
+
findings.push({
|
|
203
|
+
kind: 'pending-switch',
|
|
204
|
+
ownership: 'journal',
|
|
205
|
+
configDir: path.join((0, accountRegistry_1.getAccountsDir)(), 'codex'),
|
|
206
|
+
detail: 'An interrupted account switch needs reconciliation.',
|
|
207
|
+
action: 'Close Codex and finish recovery from the live fingerprint.',
|
|
208
|
+
});
|
|
209
|
+
return { findings, homesChecked: candidates.length + 1 };
|
|
210
|
+
}
|
|
211
|
+
function repairCodexCredentialStores(options = {}) {
|
|
212
|
+
const inspection = inspectCodexCredentialStores(options);
|
|
213
|
+
const result = {
|
|
214
|
+
...inspection,
|
|
215
|
+
dryRun: options.dryRun ?? true,
|
|
216
|
+
repaired: [],
|
|
217
|
+
};
|
|
218
|
+
if (result.dryRun)
|
|
219
|
+
return result;
|
|
220
|
+
const scan = (0, processDetection_1.inspectRunningAccountConsumersSync)('codex');
|
|
221
|
+
const mode = (0, codexPaths_1.getCodexCredentialStoreMode)();
|
|
222
|
+
if (!scan.ok || scan.consumers.length || (mode !== 'file' && mode !== 'unknown')) {
|
|
223
|
+
result.repaired = inspection.findings.map((finding) => ({
|
|
224
|
+
finding,
|
|
225
|
+
ok: false,
|
|
226
|
+
error: 'Repair requires stopped Codex consumers, a successful process scan, and readable file-mode configuration.',
|
|
227
|
+
}));
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
(0, codexProfiles_1.withCodexAuthSwapLock)(() => {
|
|
231
|
+
result.sync = (0, codexProfiles_1.syncCodexLiveStateUnlocked)('migration', { scan });
|
|
232
|
+
for (const finding of inspection.findings) {
|
|
233
|
+
let error;
|
|
234
|
+
try {
|
|
235
|
+
if (finding.kind === 'pending-switch') {
|
|
236
|
+
if (fs.existsSync(path.join(finding.configDir, 'switch-journal.json')))
|
|
237
|
+
throw new Error();
|
|
238
|
+
}
|
|
239
|
+
else if (finding.kind === 'live-duplicate' && finding.ownership === 'profile-home') {
|
|
240
|
+
if (readFinding(finding).status !== 'absent')
|
|
241
|
+
throw new Error();
|
|
242
|
+
}
|
|
243
|
+
else if (finding.kind === 'stale-stash' ||
|
|
244
|
+
finding.kind === 'orphan' ||
|
|
245
|
+
finding.kind === 'live-duplicate') {
|
|
246
|
+
if (!within(finding.file ?? finding.configDir, path.join((0, accountRegistry_1.getAccountsDir)(), 'codex')) ||
|
|
247
|
+
(0, codexCredentialStore_1.canonicalCodexPath)(finding.configDir) === (0, codexCredentialStore_1.canonicalCodexPath)((0, codexPaths_1.getSystemCodexHome)()))
|
|
248
|
+
throw new Error();
|
|
249
|
+
const current = readFinding(finding);
|
|
250
|
+
if (current.status === 'absent') {
|
|
251
|
+
result.repaired.push({ finding, ok: true });
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (current.status !== 'ok' ||
|
|
255
|
+
(0, codexCredentialStore_1.codexSnapshotFingerprint)(current.snapshot) !== finding.fingerprint ||
|
|
256
|
+
!finding.accountId ||
|
|
257
|
+
(0, accountLease_1.isAccountLeaseHeld)('codex', finding.accountId))
|
|
258
|
+
throw new Error();
|
|
259
|
+
const backup = (0, codexCredentialStore_1.readCodexBackup)(finding.accountId);
|
|
260
|
+
if (backup.status === 'ok' &&
|
|
261
|
+
(0, codexAuth_1.compareCodexAuthFreshness)(backup.snapshot.authRaw, current.snapshot.authRaw) === 0 &&
|
|
262
|
+
(0, codexCredentialStore_1.codexSnapshotFingerprint)(backup.snapshot) !==
|
|
263
|
+
(0, codexCredentialStore_1.codexSnapshotFingerprint)(current.snapshot) &&
|
|
264
|
+
(!(0, codexAuth_1.codexRefreshFingerprint)(backup.snapshot.authRaw) ||
|
|
265
|
+
(0, codexAuth_1.codexRefreshFingerprint)(backup.snapshot.authRaw) !==
|
|
266
|
+
(0, codexAuth_1.codexRefreshFingerprint)(current.snapshot.authRaw))) {
|
|
267
|
+
throw new Error(); // Two incomparable chains: deletion would guess which is usable.
|
|
268
|
+
}
|
|
269
|
+
const chosen = backup.status === 'ok' &&
|
|
270
|
+
(0, codexAuth_1.compareCodexAuthFreshness)(backup.snapshot.authRaw, current.snapshot.authRaw) >= 0
|
|
271
|
+
? backup.snapshot
|
|
272
|
+
: current.snapshot;
|
|
273
|
+
(0, codexCredentialStore_1.writeCodexBackup)(finding.accountId, chosen);
|
|
274
|
+
const live = (0, codexCredentialStore_1.readCodexCredentialStore)();
|
|
275
|
+
if (live.status === 'ok' &&
|
|
276
|
+
(0, codexAuth_1.identitiesMatch)((0, codexAuth_1.readAuthIdentityFromRaw)(live.snapshot.authRaw), (0, codexAuth_1.readAuthIdentityFromRaw)(chosen.authRaw)) &&
|
|
277
|
+
(0, codexAuth_1.compareCodexAuthFreshness)(chosen.authRaw, live.snapshot.authRaw) > 0) {
|
|
278
|
+
result.sync = (0, codexProfiles_1.syncCodexLiveStateUnlocked)('migration', { scan });
|
|
279
|
+
const repairedLive = (0, codexCredentialStore_1.readCodexCredentialStore)();
|
|
280
|
+
if (repairedLive.status !== 'ok' ||
|
|
281
|
+
(0, codexAuth_1.compareCodexAuthFreshness)(repairedLive.snapshot.authRaw, chosen.authRaw) < 0)
|
|
282
|
+
throw new Error();
|
|
283
|
+
}
|
|
284
|
+
if (finding.file)
|
|
285
|
+
fs.rmSync(finding.file);
|
|
286
|
+
else
|
|
287
|
+
(0, codexCredentialStore_1.removeCodexProfileCredentials)(finding.configDir, current.snapshot);
|
|
288
|
+
if (finding.ownership === 'login-home')
|
|
289
|
+
fs.rmSync(path.dirname(finding.configDir), { recursive: true, force: true });
|
|
290
|
+
}
|
|
291
|
+
else
|
|
292
|
+
throw new Error();
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
error =
|
|
296
|
+
'Preserved for review: unreadable, ambiguous, changed, leased, or not safely repairable yet.';
|
|
297
|
+
}
|
|
298
|
+
result.repaired.push({ finding, ok: !error, error });
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface CodexAuthSnapshot {
|
|
2
|
+
authRaw: string | null;
|
|
3
|
+
/** Includes unrelated MCP data when reading a home; writers preserve it. */
|
|
4
|
+
legacyRaw: string | null;
|
|
5
|
+
}
|
|
6
|
+
export type CodexCredentialStoreRead = {
|
|
7
|
+
status: 'ok' | 'absent' | 'corrupt';
|
|
8
|
+
snapshot: CodexAuthSnapshot;
|
|
9
|
+
} | {
|
|
10
|
+
status: 'error';
|
|
11
|
+
};
|
|
12
|
+
export declare function isLegacyCodexLogin(raw: string | null): boolean;
|
|
13
|
+
export declare function isCodexLogin(raw: string | null): boolean;
|
|
14
|
+
export declare function codexLoginOnly(snapshot: CodexAuthSnapshot): CodexAuthSnapshot;
|
|
15
|
+
export declare function readCodexCredentialStore(home?: string): CodexCredentialStoreRead;
|
|
16
|
+
export declare function readCodexBackup(id: string): CodexCredentialStoreRead;
|
|
17
|
+
export declare function codexSnapshotFingerprint(snapshot: CodexAuthSnapshot): string;
|
|
18
|
+
/** The whole snapshot, including absence, must agree before destructive operations. */
|
|
19
|
+
export declare function codexStoreMatches(read: CodexCredentialStoreRead, snapshot: CodexAuthSnapshot): boolean;
|
|
20
|
+
export declare function writeCodexBackup(id: string, snapshot: CodexAuthSnapshot): void;
|
|
21
|
+
/** Resolve existing parents too, so nonexistent children of a symlink cannot bypass ownership. */
|
|
22
|
+
export declare function canonicalCodexPath(value: string): string;
|
|
23
|
+
export declare function isSidekickCodexHome(home: string): boolean;
|
|
24
|
+
/** Install only account data; an unrelated MCP credential file is never copied or deleted. */
|
|
25
|
+
export declare function codexInstalledSnapshot(before: CodexAuthSnapshot, candidate: CodexAuthSnapshot): CodexAuthSnapshot;
|
|
26
|
+
export declare function writeInactiveCodexCredentialStore(home: string, candidate: CodexAuthSnapshot): void;
|
|
27
|
+
/** Internal capability: only the guarded installer in codexProfiles may call this. */
|
|
28
|
+
export declare function writeLiveCodexCredentialStoreUnguarded(snapshot: CodexAuthSnapshot): void;
|
|
29
|
+
export declare function removeCodexProfileCredentials(home: string, expected: CodexAuthSnapshot): void;
|