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
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers over the Claude Code credential blob (`claudeAiOauth`). No I/O.
|
|
3
|
+
*/
|
|
4
|
+
export interface ClaudeOauthCredentials {
|
|
5
|
+
accessToken: string;
|
|
6
|
+
refreshToken?: string;
|
|
7
|
+
/** Access token expiry, ms since epoch (~8 h after issue). */
|
|
8
|
+
expiresAt?: number;
|
|
9
|
+
/** Refresh token expiry, ms since epoch (~3 weeks after issue on current builds). */
|
|
10
|
+
refreshTokenExpiresAt?: number;
|
|
11
|
+
scopes?: string[];
|
|
12
|
+
subscriptionType?: string;
|
|
13
|
+
rateLimitTier?: string;
|
|
14
|
+
}
|
|
15
|
+
/** Lifetime of a Claude Code access token. */
|
|
16
|
+
export declare const CLAUDE_ACCESS_TOKEN_LIFETIME_MS: number;
|
|
17
|
+
/**
|
|
18
|
+
* Refresh-token lifetime observed on Claude Code 2.1.x. Used only to estimate
|
|
19
|
+
* the refresh expiry of snapshots captured before the CLI recorded it.
|
|
20
|
+
*/
|
|
21
|
+
export declare const CLAUDE_REFRESH_TOKEN_LIFETIME_ESTIMATE_MS: number;
|
|
22
|
+
/** Parse the stored blob, tolerating the `mcpOAuth` sibling and unknown keys. */
|
|
23
|
+
export declare function parseClaudeCredentialBlob(blob: unknown): ClaudeOauthCredentials | null;
|
|
24
|
+
/**
|
|
25
|
+
* Monotonic freshness clock: every refresh issues a new access token with a
|
|
26
|
+
* later `expiresAt`, so the larger value is the newer credential.
|
|
27
|
+
*/
|
|
28
|
+
export declare function claudeCredentialFreshness(credentials: ClaudeOauthCredentials | null): number;
|
|
29
|
+
/** True when `candidate` is strictly newer than `existing`. */
|
|
30
|
+
export declare function isNewerClaudeCredential(candidate: ClaudeOauthCredentials | null, existing: ClaudeOauthCredentials | null): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* When the refresh-token expiry was recorded, use it; otherwise estimate it
|
|
33
|
+
* from the access-token issue time. Returns `estimated: true` for the latter.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveClaudeRefreshExpiry(credentials: ClaudeOauthCredentials | null): {
|
|
36
|
+
refreshExpiresAt: number;
|
|
37
|
+
estimated: boolean;
|
|
38
|
+
} | null;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Pure helpers over the Claude Code credential blob (`claudeAiOauth`). No I/O.
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.CLAUDE_REFRESH_TOKEN_LIFETIME_ESTIMATE_MS = exports.CLAUDE_ACCESS_TOKEN_LIFETIME_MS = void 0;
|
|
7
|
+
exports.parseClaudeCredentialBlob = parseClaudeCredentialBlob;
|
|
8
|
+
exports.claudeCredentialFreshness = claudeCredentialFreshness;
|
|
9
|
+
exports.isNewerClaudeCredential = isNewerClaudeCredential;
|
|
10
|
+
exports.resolveClaudeRefreshExpiry = resolveClaudeRefreshExpiry;
|
|
11
|
+
/** Lifetime of a Claude Code access token. */
|
|
12
|
+
exports.CLAUDE_ACCESS_TOKEN_LIFETIME_MS = 8 * 60 * 60 * 1000;
|
|
13
|
+
/**
|
|
14
|
+
* Refresh-token lifetime observed on Claude Code 2.1.x. Used only to estimate
|
|
15
|
+
* the refresh expiry of snapshots captured before the CLI recorded it.
|
|
16
|
+
*/
|
|
17
|
+
exports.CLAUDE_REFRESH_TOKEN_LIFETIME_ESTIMATE_MS = 21 * 24 * 60 * 60 * 1000;
|
|
18
|
+
function finiteNumber(value) {
|
|
19
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
20
|
+
}
|
|
21
|
+
/** Parse the stored blob, tolerating the `mcpOAuth` sibling and unknown keys. */
|
|
22
|
+
function parseClaudeCredentialBlob(blob) {
|
|
23
|
+
if (!blob || typeof blob !== 'object')
|
|
24
|
+
return null;
|
|
25
|
+
const oauth = blob.claudeAiOauth;
|
|
26
|
+
if (!oauth || typeof oauth !== 'object')
|
|
27
|
+
return null;
|
|
28
|
+
const record = oauth;
|
|
29
|
+
if (typeof record.accessToken !== 'string' || !record.accessToken)
|
|
30
|
+
return null;
|
|
31
|
+
return {
|
|
32
|
+
accessToken: record.accessToken,
|
|
33
|
+
refreshToken: typeof record.refreshToken === 'string' ? record.refreshToken : undefined,
|
|
34
|
+
expiresAt: finiteNumber(record.expiresAt),
|
|
35
|
+
refreshTokenExpiresAt: finiteNumber(record.refreshTokenExpiresAt),
|
|
36
|
+
scopes: Array.isArray(record.scopes)
|
|
37
|
+
? record.scopes.filter((scope) => typeof scope === 'string')
|
|
38
|
+
: undefined,
|
|
39
|
+
subscriptionType: typeof record.subscriptionType === 'string' ? record.subscriptionType : undefined,
|
|
40
|
+
rateLimitTier: typeof record.rateLimitTier === 'string' ? record.rateLimitTier : undefined,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Monotonic freshness clock: every refresh issues a new access token with a
|
|
45
|
+
* later `expiresAt`, so the larger value is the newer credential.
|
|
46
|
+
*/
|
|
47
|
+
function claudeCredentialFreshness(credentials) {
|
|
48
|
+
if (!credentials)
|
|
49
|
+
return 0;
|
|
50
|
+
return credentials.expiresAt ?? credentials.refreshTokenExpiresAt ?? 0;
|
|
51
|
+
}
|
|
52
|
+
/** True when `candidate` is strictly newer than `existing`. */
|
|
53
|
+
function isNewerClaudeCredential(candidate, existing) {
|
|
54
|
+
return claudeCredentialFreshness(candidate) > claudeCredentialFreshness(existing);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* When the refresh-token expiry was recorded, use it; otherwise estimate it
|
|
58
|
+
* from the access-token issue time. Returns `estimated: true` for the latter.
|
|
59
|
+
*/
|
|
60
|
+
function resolveClaudeRefreshExpiry(credentials) {
|
|
61
|
+
if (!credentials)
|
|
62
|
+
return null;
|
|
63
|
+
if (credentials.refreshTokenExpiresAt !== undefined) {
|
|
64
|
+
return { refreshExpiresAt: credentials.refreshTokenExpiresAt, estimated: false };
|
|
65
|
+
}
|
|
66
|
+
if (credentials.expiresAt !== undefined) {
|
|
67
|
+
const issuedAt = credentials.expiresAt - exports.CLAUDE_ACCESS_TOKEN_LIFETIME_MS;
|
|
68
|
+
return {
|
|
69
|
+
refreshExpiresAt: issuedAt + exports.CLAUDE_REFRESH_TOKEN_LIFETIME_ESTIMATE_MS,
|
|
70
|
+
estimated: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
package/dist/claudeProfiles.d.ts
CHANGED
|
@@ -1,12 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code honours this variable ahead of `CLAUDE_CONFIG_DIR` when it
|
|
3
|
+
* derives the keychain item name. Any `claude` child sidekick spawns must not
|
|
4
|
+
* inherit it, or the login would land in an item nobody else reads.
|
|
5
|
+
*/
|
|
6
|
+
export declare const CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV = "CLAUDE_SECURESTORAGE_CONFIG_DIR";
|
|
1
7
|
export interface ClaudeProfileIdentity {
|
|
2
8
|
email: string;
|
|
3
9
|
uuid: string;
|
|
4
10
|
}
|
|
11
|
+
export declare function getDefaultClaudeHome(): string;
|
|
12
|
+
/** True when this process runs against the default `~/.claude` home. */
|
|
13
|
+
export declare function isDefaultClaudeHome(): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The home the `claude` CLI reads and writes for this process: `CLAUDE_CONFIG_DIR`
|
|
16
|
+
* when set, else `~/.claude`. Every "live" credential or identity read goes
|
|
17
|
+
* through this so sidekick follows the same home the CLI does.
|
|
18
|
+
*/
|
|
19
|
+
export declare function getLiveClaudeHome(): string;
|
|
5
20
|
export declare function getClaudeProfilesDir(): string;
|
|
21
|
+
export declare function getClaudeProfileDir(uuid: string): string;
|
|
6
22
|
export declare function getClaudeProfileHome(uuid: string): string;
|
|
7
23
|
export declare function ensureClaudeProfileDirs(uuid: string): void;
|
|
24
|
+
/**
|
|
25
|
+
* Claude Code hashes the NFC-normalised config directory; macOS hands paths
|
|
26
|
+
* back decomposed (NFD), so a home directory with a non-ASCII name would
|
|
27
|
+
* otherwise hash differently from the item the CLI wrote.
|
|
28
|
+
*/
|
|
8
29
|
export declare function claudeKeychainSuffix(configDir: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Keychain service name for a config directory. Claude Code drops the suffix
|
|
32
|
+
* when `CLAUDE_CONFIG_DIR` is unset — not when the path merely equals
|
|
33
|
+
* `~/.claude` — so the no-argument form follows the process environment.
|
|
34
|
+
*/
|
|
9
35
|
export declare function claudeKeychainService(configDir?: string): string;
|
|
36
|
+
/** The keychain account name Claude Code uses: the unix username, sanitised. */
|
|
37
|
+
export declare function claudeKeychainAccountName(): string;
|
|
38
|
+
/** Environment for spawning `claude` against a specific home. */
|
|
39
|
+
export declare function buildClaudeChildEnv(home: string, baseEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
40
|
+
/**
|
|
41
|
+
* Where the identity (`oauthAccount`) of a Claude home lives. Recent Claude
|
|
42
|
+
* Code versions keep it in `<home>/.claude.json`; older ones (and the default
|
|
43
|
+
* home on this machine's history) use `~/.claude.json`. Prefer the in-home file
|
|
44
|
+
* when it carries an identity, otherwise fall back to the legacy location for
|
|
45
|
+
* the default home only.
|
|
46
|
+
*/
|
|
47
|
+
export declare function getClaudeConfigPath(home?: string): string;
|
|
48
|
+
/** Identity of the account currently logged in to a Claude home (default: the live home). */
|
|
49
|
+
export declare function readLiveClaudeIdentity(home?: string): ClaudeProfileIdentity | null;
|
|
10
50
|
export declare function readClaudeProfileIdentity(home: string): ClaudeProfileIdentity | null;
|
|
11
51
|
export declare function keychainServiceExists(service: string): boolean;
|
|
12
52
|
export declare function isClaudeProfileAuthenticated(home: string): boolean;
|
package/dist/claudeProfiles.js
CHANGED
|
@@ -33,48 +33,139 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV = void 0;
|
|
37
|
+
exports.getDefaultClaudeHome = getDefaultClaudeHome;
|
|
38
|
+
exports.isDefaultClaudeHome = isDefaultClaudeHome;
|
|
39
|
+
exports.getLiveClaudeHome = getLiveClaudeHome;
|
|
36
40
|
exports.getClaudeProfilesDir = getClaudeProfilesDir;
|
|
41
|
+
exports.getClaudeProfileDir = getClaudeProfileDir;
|
|
37
42
|
exports.getClaudeProfileHome = getClaudeProfileHome;
|
|
38
43
|
exports.ensureClaudeProfileDirs = ensureClaudeProfileDirs;
|
|
39
44
|
exports.claudeKeychainSuffix = claudeKeychainSuffix;
|
|
40
45
|
exports.claudeKeychainService = claudeKeychainService;
|
|
46
|
+
exports.claudeKeychainAccountName = claudeKeychainAccountName;
|
|
47
|
+
exports.buildClaudeChildEnv = buildClaudeChildEnv;
|
|
48
|
+
exports.getClaudeConfigPath = getClaudeConfigPath;
|
|
49
|
+
exports.readLiveClaudeIdentity = readLiveClaudeIdentity;
|
|
41
50
|
exports.readClaudeProfileIdentity = readClaudeProfileIdentity;
|
|
42
51
|
exports.keychainServiceExists = keychainServiceExists;
|
|
43
52
|
exports.isClaudeProfileAuthenticated = isClaudeProfileAuthenticated;
|
|
44
53
|
const child_process_1 = require("child_process");
|
|
45
54
|
const crypto_1 = require("crypto");
|
|
46
55
|
const fs = __importStar(require("fs"));
|
|
56
|
+
const os = __importStar(require("os"));
|
|
47
57
|
const path = __importStar(require("path"));
|
|
48
58
|
const accountRegistry_1 = require("./accountRegistry");
|
|
49
59
|
const DEFAULT_CLAUDE_KEYCHAIN_SERVICE = 'Claude Code-credentials';
|
|
60
|
+
const KEYCHAIN_ACCOUNT_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
61
|
+
const KEYCHAIN_FALLBACK_ACCOUNT = 'claude-code-user';
|
|
62
|
+
/**
|
|
63
|
+
* Claude Code honours this variable ahead of `CLAUDE_CONFIG_DIR` when it
|
|
64
|
+
* derives the keychain item name. Any `claude` child sidekick spawns must not
|
|
65
|
+
* inherit it, or the login would land in an item nobody else reads.
|
|
66
|
+
*/
|
|
67
|
+
exports.CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV = 'CLAUDE_SECURESTORAGE_CONFIG_DIR';
|
|
68
|
+
function getDefaultClaudeHome() {
|
|
69
|
+
return path.join(os.homedir(), '.claude');
|
|
70
|
+
}
|
|
71
|
+
/** True when this process runs against the default `~/.claude` home. */
|
|
72
|
+
function isDefaultClaudeHome() {
|
|
73
|
+
return !process.env.CLAUDE_CONFIG_DIR?.trim();
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The home the `claude` CLI reads and writes for this process: `CLAUDE_CONFIG_DIR`
|
|
77
|
+
* when set, else `~/.claude`. Every "live" credential or identity read goes
|
|
78
|
+
* through this so sidekick follows the same home the CLI does.
|
|
79
|
+
*/
|
|
80
|
+
function getLiveClaudeHome() {
|
|
81
|
+
const explicit = process.env.CLAUDE_CONFIG_DIR?.trim();
|
|
82
|
+
return explicit ? explicit : getDefaultClaudeHome();
|
|
83
|
+
}
|
|
50
84
|
function getClaudeProfilesDir() {
|
|
51
85
|
return path.join((0, accountRegistry_1.getAccountsDir)(), 'claude', 'profiles');
|
|
52
86
|
}
|
|
87
|
+
function getClaudeProfileDir(uuid) {
|
|
88
|
+
return path.join(getClaudeProfilesDir(), uuid);
|
|
89
|
+
}
|
|
53
90
|
function getClaudeProfileHome(uuid) {
|
|
54
|
-
return path.join(
|
|
91
|
+
return path.join(getClaudeProfileDir(uuid), 'home');
|
|
55
92
|
}
|
|
56
93
|
function ensureClaudeProfileDirs(uuid) {
|
|
57
94
|
fs.mkdirSync(getClaudeProfileHome(uuid), { recursive: true, mode: 0o700 });
|
|
58
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Claude Code hashes the NFC-normalised config directory; macOS hands paths
|
|
98
|
+
* back decomposed (NFD), so a home directory with a non-ASCII name would
|
|
99
|
+
* otherwise hash differently from the item the CLI wrote.
|
|
100
|
+
*/
|
|
59
101
|
function claudeKeychainSuffix(configDir) {
|
|
60
|
-
return (0, crypto_1.createHash)('sha256').update(configDir).digest('hex').slice(0, 8);
|
|
102
|
+
return (0, crypto_1.createHash)('sha256').update(configDir.normalize('NFC')).digest('hex').slice(0, 8);
|
|
61
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Keychain service name for a config directory. Claude Code drops the suffix
|
|
106
|
+
* when `CLAUDE_CONFIG_DIR` is unset — not when the path merely equals
|
|
107
|
+
* `~/.claude` — so the no-argument form follows the process environment.
|
|
108
|
+
*/
|
|
62
109
|
function claudeKeychainService(configDir) {
|
|
63
|
-
|
|
64
|
-
|
|
110
|
+
const dir = configDir ?? (isDefaultClaudeHome() ? undefined : getLiveClaudeHome());
|
|
111
|
+
return dir
|
|
112
|
+
? `${DEFAULT_CLAUDE_KEYCHAIN_SERVICE}-${claudeKeychainSuffix(dir)}`
|
|
65
113
|
: DEFAULT_CLAUDE_KEYCHAIN_SERVICE;
|
|
66
114
|
}
|
|
67
|
-
|
|
115
|
+
/** The keychain account name Claude Code uses: the unix username, sanitised. */
|
|
116
|
+
function claudeKeychainAccountName() {
|
|
117
|
+
let name;
|
|
68
118
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
119
|
+
name = process.env.USER || os.userInfo().username;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
name = undefined;
|
|
123
|
+
}
|
|
124
|
+
return name && KEYCHAIN_ACCOUNT_PATTERN.test(name) ? name : KEYCHAIN_FALLBACK_ACCOUNT;
|
|
125
|
+
}
|
|
126
|
+
/** Environment for spawning `claude` against a specific home. */
|
|
127
|
+
function buildClaudeChildEnv(home, baseEnv = process.env) {
|
|
128
|
+
const env = { ...baseEnv, CLAUDE_CONFIG_DIR: home };
|
|
129
|
+
delete env[exports.CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV];
|
|
130
|
+
return env;
|
|
131
|
+
}
|
|
132
|
+
function readOauthAccount(configPath) {
|
|
133
|
+
try {
|
|
134
|
+
const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
135
|
+
return raw?.oauthAccount && typeof raw.oauthAccount === 'object' ? raw.oauthAccount : null;
|
|
73
136
|
}
|
|
74
137
|
catch {
|
|
75
138
|
return null;
|
|
76
139
|
}
|
|
77
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Where the identity (`oauthAccount`) of a Claude home lives. Recent Claude
|
|
143
|
+
* Code versions keep it in `<home>/.claude.json`; older ones (and the default
|
|
144
|
+
* home on this machine's history) use `~/.claude.json`. Prefer the in-home file
|
|
145
|
+
* when it carries an identity, otherwise fall back to the legacy location for
|
|
146
|
+
* the default home only.
|
|
147
|
+
*/
|
|
148
|
+
function getClaudeConfigPath(home = getLiveClaudeHome()) {
|
|
149
|
+
const primary = path.join(home, '.claude.json');
|
|
150
|
+
if (readOauthAccount(primary))
|
|
151
|
+
return primary;
|
|
152
|
+
if (path.resolve(home) === path.resolve(getDefaultClaudeHome())) {
|
|
153
|
+
return path.join(os.homedir(), '.claude.json');
|
|
154
|
+
}
|
|
155
|
+
return primary;
|
|
156
|
+
}
|
|
157
|
+
function identityFromOauthAccount(oauthAccount) {
|
|
158
|
+
const email = oauthAccount?.emailAddress;
|
|
159
|
+
const uuid = oauthAccount?.accountUuid;
|
|
160
|
+
return typeof email === 'string' && typeof uuid === 'string' ? { email, uuid } : null;
|
|
161
|
+
}
|
|
162
|
+
/** Identity of the account currently logged in to a Claude home (default: the live home). */
|
|
163
|
+
function readLiveClaudeIdentity(home = getLiveClaudeHome()) {
|
|
164
|
+
return identityFromOauthAccount(readOauthAccount(getClaudeConfigPath(home)));
|
|
165
|
+
}
|
|
166
|
+
function readClaudeProfileIdentity(home) {
|
|
167
|
+
return identityFromOauthAccount(readOauthAccount(path.join(home, '.claude.json')));
|
|
168
|
+
}
|
|
78
169
|
function keychainServiceExists(service) {
|
|
79
170
|
try {
|
|
80
171
|
(0, child_process_1.execFileSync)('security', ['find-generic-password', '-s', service], {
|
|
@@ -90,7 +181,8 @@ function keychainServiceExists(service) {
|
|
|
90
181
|
}
|
|
91
182
|
function isClaudeProfileAuthenticated(home) {
|
|
92
183
|
const hasCredentials = process.platform === 'darwin'
|
|
93
|
-
? keychainServiceExists(claudeKeychainService(home))
|
|
184
|
+
? keychainServiceExists(claudeKeychainService(home)) ||
|
|
185
|
+
fs.existsSync(path.join(home, '.credentials.json'))
|
|
94
186
|
: fs.existsSync(path.join(home, '.credentials.json'));
|
|
95
187
|
return hasCredentials && readClaudeProfileIdentity(home) !== null;
|
|
96
188
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface CodexAuthJsonFile {
|
|
2
|
+
auth_mode?: string;
|
|
3
|
+
OPENAI_API_KEY?: string;
|
|
4
|
+
last_refresh?: string;
|
|
5
|
+
tokens?: {
|
|
6
|
+
id_token?: string;
|
|
7
|
+
access_token?: string;
|
|
8
|
+
refresh_token?: string;
|
|
9
|
+
account_id?: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export interface CodexAuthIdentity {
|
|
13
|
+
email?: string;
|
|
14
|
+
workspaceId?: string;
|
|
15
|
+
planType?: string;
|
|
16
|
+
authMode: 'chatgpt' | 'api-key';
|
|
17
|
+
}
|
|
18
|
+
export declare const STALE_AUTH_THRESHOLD_MS: number;
|
|
19
|
+
export declare function parseJwtPayload<T>(jwt: string): T | null;
|
|
20
|
+
export declare function parseAuthJson(raw: string | null): CodexAuthJsonFile | null;
|
|
21
|
+
export declare function readAuthIdentityFromRaw(raw: string | null): CodexAuthIdentity | null;
|
|
22
|
+
export declare function readLastRefresh(raw: string | null, fallbackPath?: string): number | null;
|
|
23
|
+
/** Expiry (ms since epoch) of the access token JWT, when it carries `exp`. */
|
|
24
|
+
export declare function readAccessTokenExpiry(raw: string | null): number | null;
|
|
25
|
+
/** Workspace id first (a ChatGPT workspace is one seat), then email. */
|
|
26
|
+
export declare function identitiesMatch(a: {
|
|
27
|
+
email?: string;
|
|
28
|
+
workspaceId?: string;
|
|
29
|
+
} | null | undefined, b: {
|
|
30
|
+
email?: string;
|
|
31
|
+
workspaceId?: string;
|
|
32
|
+
} | null | undefined): boolean;
|
|
@@ -0,0 +1,127 @@
|
|
|
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.STALE_AUTH_THRESHOLD_MS = void 0;
|
|
37
|
+
exports.parseJwtPayload = parseJwtPayload;
|
|
38
|
+
exports.parseAuthJson = parseAuthJson;
|
|
39
|
+
exports.readAuthIdentityFromRaw = readAuthIdentityFromRaw;
|
|
40
|
+
exports.readLastRefresh = readLastRefresh;
|
|
41
|
+
exports.readAccessTokenExpiry = readAccessTokenExpiry;
|
|
42
|
+
exports.identitiesMatch = identitiesMatch;
|
|
43
|
+
/**
|
|
44
|
+
* Pure helpers over the Codex CLI `auth.json` payload. No filesystem access
|
|
45
|
+
* beyond an optional mtime fallback, no child processes.
|
|
46
|
+
*/
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
// Codex refreshes OAuth tokens at most every 8 days; a stored refresh token
|
|
49
|
+
// older than that may already be rejected by the auth server.
|
|
50
|
+
exports.STALE_AUTH_THRESHOLD_MS = 8 * 24 * 60 * 60 * 1000;
|
|
51
|
+
function parseJwtPayload(jwt) {
|
|
52
|
+
const parts = jwt.split('.');
|
|
53
|
+
if (parts.length < 2)
|
|
54
|
+
return null;
|
|
55
|
+
try {
|
|
56
|
+
const payload = Buffer.from(parts[1], 'base64url').toString('utf8');
|
|
57
|
+
return JSON.parse(payload);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function parseAuthJson(raw) {
|
|
64
|
+
if (!raw)
|
|
65
|
+
return null;
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(raw);
|
|
68
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function readAuthIdentityFromRaw(raw) {
|
|
75
|
+
const parsed = parseAuthJson(raw);
|
|
76
|
+
if (!parsed)
|
|
77
|
+
return null;
|
|
78
|
+
const idToken = parsed.tokens?.id_token;
|
|
79
|
+
const claims = idToken ? parseJwtPayload(idToken) : null;
|
|
80
|
+
const profileClaims = claims?.['https://api.openai.com/profile'];
|
|
81
|
+
const authClaims = claims?.['https://api.openai.com/auth'];
|
|
82
|
+
const email = typeof claims?.email === 'string'
|
|
83
|
+
? claims.email
|
|
84
|
+
: typeof profileClaims?.email === 'string'
|
|
85
|
+
? profileClaims.email
|
|
86
|
+
: undefined;
|
|
87
|
+
const workspaceId = typeof authClaims?.chatgpt_account_id === 'string'
|
|
88
|
+
? authClaims.chatgpt_account_id
|
|
89
|
+
: parsed.tokens?.account_id;
|
|
90
|
+
const planType = typeof authClaims?.chatgpt_plan_type === 'string' ? authClaims.chatgpt_plan_type : undefined;
|
|
91
|
+
const authMode = parsed.OPENAI_API_KEY || parsed.auth_mode === 'api_key' ? 'api-key' : 'chatgpt';
|
|
92
|
+
return { email, workspaceId, planType, authMode };
|
|
93
|
+
}
|
|
94
|
+
function readLastRefresh(raw, fallbackPath) {
|
|
95
|
+
const parsed = parseAuthJson(raw);
|
|
96
|
+
if (parsed?.last_refresh) {
|
|
97
|
+
const ts = Date.parse(parsed.last_refresh);
|
|
98
|
+
if (!Number.isNaN(ts))
|
|
99
|
+
return ts;
|
|
100
|
+
}
|
|
101
|
+
if (fallbackPath) {
|
|
102
|
+
try {
|
|
103
|
+
return fs.statSync(fallbackPath).mtimeMs;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
/* fall through */
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
/** Expiry (ms since epoch) of the access token JWT, when it carries `exp`. */
|
|
112
|
+
function readAccessTokenExpiry(raw) {
|
|
113
|
+
const parsed = parseAuthJson(raw);
|
|
114
|
+
const accessToken = parsed?.tokens?.access_token;
|
|
115
|
+
if (!accessToken)
|
|
116
|
+
return null;
|
|
117
|
+
const claims = parseJwtPayload(accessToken);
|
|
118
|
+
return typeof claims?.exp === 'number' && Number.isFinite(claims.exp) ? claims.exp * 1000 : null;
|
|
119
|
+
}
|
|
120
|
+
/** Workspace id first (a ChatGPT workspace is one seat), then email. */
|
|
121
|
+
function identitiesMatch(a, b) {
|
|
122
|
+
if (!a || !b)
|
|
123
|
+
return false;
|
|
124
|
+
if (a.workspaceId && b.workspaceId)
|
|
125
|
+
return a.workspaceId === b.workspaceId;
|
|
126
|
+
return Boolean(a.email && b.email && a.email === b.email);
|
|
127
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare function getDefaultSystemCodexHome(): string;
|
|
2
|
+
export declare function getExplicitCodexHome(): string | null;
|
|
3
|
+
/** The home the `codex` CLI reads for this process: `CODEX_HOME` or `~/.codex`. */
|
|
4
|
+
export declare function getSystemCodexHome(): string;
|
|
5
|
+
export declare function getCodexProfilesDir(): string;
|
|
6
|
+
export declare function getCodexProfileDir(profileId: string): string;
|
|
7
|
+
export declare function getCodexProfileHome(profileId: string): string;
|
|
8
|
+
export declare function getCodexProfileStatePath(profileId: string): string;
|
|
9
|
+
export declare function ensureCodexProfileDirs(profileId: string): void;
|
|
10
|
+
export declare function readFileOrNull(filePath: string): string | null;
|
|
11
|
+
export type CodexCredentialStoreMode = 'file' | 'keyring' | 'auto' | 'ephemeral' | 'unknown';
|
|
12
|
+
/**
|
|
13
|
+
* How the `codex` CLI persists credentials for a home, from its `config.toml`.
|
|
14
|
+
* `unknown` means the key is absent (older CLIs: file; newer CLIs: auto).
|
|
15
|
+
*/
|
|
16
|
+
export declare function getCodexCredentialStoreMode(codexHome?: string): CodexCredentialStoreMode;
|
|
17
|
+
/**
|
|
18
|
+
* Rewrite a `config.toml` so an isolated login writes `auth.json` instead of a
|
|
19
|
+
* keyring entry sidekick could never swap. Adds the key when absent.
|
|
20
|
+
*/
|
|
21
|
+
export declare function forceFileCredentialStore(configToml: string): string;
|
|
@@ -0,0 +1,117 @@
|
|
|
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.getDefaultSystemCodexHome = getDefaultSystemCodexHome;
|
|
37
|
+
exports.getExplicitCodexHome = getExplicitCodexHome;
|
|
38
|
+
exports.getSystemCodexHome = getSystemCodexHome;
|
|
39
|
+
exports.getCodexProfilesDir = getCodexProfilesDir;
|
|
40
|
+
exports.getCodexProfileDir = getCodexProfileDir;
|
|
41
|
+
exports.getCodexProfileHome = getCodexProfileHome;
|
|
42
|
+
exports.getCodexProfileStatePath = getCodexProfileStatePath;
|
|
43
|
+
exports.ensureCodexProfileDirs = ensureCodexProfileDirs;
|
|
44
|
+
exports.readFileOrNull = readFileOrNull;
|
|
45
|
+
exports.getCodexCredentialStoreMode = getCodexCredentialStoreMode;
|
|
46
|
+
exports.forceFileCredentialStore = forceFileCredentialStore;
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
const os = __importStar(require("os"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
const accountRegistry_1 = require("./accountRegistry");
|
|
51
|
+
function getDefaultSystemCodexHome() {
|
|
52
|
+
return path.join(os.homedir(), '.codex');
|
|
53
|
+
}
|
|
54
|
+
function getExplicitCodexHome() {
|
|
55
|
+
const explicitHome = process.env.CODEX_HOME?.trim();
|
|
56
|
+
return explicitHome ? explicitHome : null;
|
|
57
|
+
}
|
|
58
|
+
/** The home the `codex` CLI reads for this process: `CODEX_HOME` or `~/.codex`. */
|
|
59
|
+
function getSystemCodexHome() {
|
|
60
|
+
return getExplicitCodexHome() ?? getDefaultSystemCodexHome();
|
|
61
|
+
}
|
|
62
|
+
function getCodexProfilesDir() {
|
|
63
|
+
return path.join((0, accountRegistry_1.getAccountsDir)(), 'codex', 'profiles');
|
|
64
|
+
}
|
|
65
|
+
function getCodexProfileDir(profileId) {
|
|
66
|
+
return path.join(getCodexProfilesDir(), profileId);
|
|
67
|
+
}
|
|
68
|
+
function getCodexProfileHome(profileId) {
|
|
69
|
+
return path.join(getCodexProfileDir(profileId), 'codex-home');
|
|
70
|
+
}
|
|
71
|
+
function getCodexProfileStatePath(profileId) {
|
|
72
|
+
return path.join(getCodexProfileDir(profileId), 'profile.json');
|
|
73
|
+
}
|
|
74
|
+
function ensureCodexProfileDirs(profileId) {
|
|
75
|
+
fs.mkdirSync(getCodexProfileHome(profileId), { recursive: true, mode: 0o700 });
|
|
76
|
+
}
|
|
77
|
+
function readFileOrNull(filePath) {
|
|
78
|
+
try {
|
|
79
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const STORE_MODE_PATTERN = /^\s*cli_auth_credentials_store\s*=\s*"([a-z]+)"/m;
|
|
86
|
+
/**
|
|
87
|
+
* How the `codex` CLI persists credentials for a home, from its `config.toml`.
|
|
88
|
+
* `unknown` means the key is absent (older CLIs: file; newer CLIs: auto).
|
|
89
|
+
*/
|
|
90
|
+
function getCodexCredentialStoreMode(codexHome = getSystemCodexHome()) {
|
|
91
|
+
const config = readFileOrNull(path.join(codexHome, 'config.toml'));
|
|
92
|
+
const match = config?.match(STORE_MODE_PATTERN);
|
|
93
|
+
const value = match?.[1];
|
|
94
|
+
return value === 'file' || value === 'keyring' || value === 'auto' || value === 'ephemeral'
|
|
95
|
+
? value
|
|
96
|
+
: 'unknown';
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Rewrite a `config.toml` so an isolated login writes `auth.json` instead of a
|
|
100
|
+
* keyring entry sidekick could never swap. Adds the key when absent.
|
|
101
|
+
*/
|
|
102
|
+
function forceFileCredentialStore(configToml) {
|
|
103
|
+
if (STORE_MODE_PATTERN.test(configToml)) {
|
|
104
|
+
return configToml.replace(STORE_MODE_PATTERN, 'cli_auth_credentials_store = "file"');
|
|
105
|
+
}
|
|
106
|
+
const line = 'cli_auth_credentials_store = "file"';
|
|
107
|
+
const trimmed = configToml.replace(/\s*$/, '');
|
|
108
|
+
if (!trimmed)
|
|
109
|
+
return `${line}\n`;
|
|
110
|
+
// A key after a `[table]` header belongs to that table: keep this one at the
|
|
111
|
+
// top level by inserting it before the first table header.
|
|
112
|
+
const firstTable = trimmed.search(/^\s*\[/m);
|
|
113
|
+
if (firstTable === -1)
|
|
114
|
+
return `${trimmed}\n${line}\n`;
|
|
115
|
+
const head = trimmed.slice(0, firstTable).replace(/\s*$/, '');
|
|
116
|
+
return `${head ? `${head}\n` : ''}${line}\n\n${trimmed.slice(firstTable).replace(/^\s*/, '')}\n`;
|
|
117
|
+
}
|