sidekick-shared 0.26.4 → 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.
Files changed (69) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +59 -51
  3. package/dist/accountChanges.js +21 -2
  4. package/dist/accountHealth.d.ts +72 -0
  5. package/dist/accountHealth.js +286 -0
  6. package/dist/accountLaunch.d.ts +30 -0
  7. package/dist/accountLaunch.js +124 -0
  8. package/dist/accountManager.d.ts +33 -8
  9. package/dist/accountManager.js +183 -50
  10. package/dist/accountRegistry.d.ts +10 -0
  11. package/dist/accountRegistry.js +42 -22
  12. package/dist/accountSwitch.d.ts +43 -0
  13. package/dist/accountSwitch.js +115 -0
  14. package/dist/accountSync.d.ts +30 -0
  15. package/dist/accountSync.js +180 -0
  16. package/dist/accountSyncTypes.d.ts +25 -0
  17. package/dist/accountSyncTypes.js +7 -0
  18. package/dist/accounts.d.ts +58 -6
  19. package/dist/accounts.js +408 -189
  20. package/dist/claudeCredentials.d.ts +38 -0
  21. package/dist/claudeCredentials.js +74 -0
  22. package/dist/claudeProfiles.d.ts +40 -0
  23. package/dist/claudeProfiles.js +102 -10
  24. package/dist/codexAuth.d.ts +32 -0
  25. package/dist/codexAuth.js +127 -0
  26. package/dist/codexPaths.d.ts +21 -0
  27. package/dist/codexPaths.js +117 -0
  28. package/dist/codexProfiles.d.ts +58 -5
  29. package/dist/codexProfiles.js +574 -289
  30. package/dist/credentialIO.d.ts +12 -5
  31. package/dist/credentialIO.js +63 -29
  32. package/dist/credentials.d.ts +8 -0
  33. package/dist/credentials.js +28 -32
  34. package/dist/ensureDefaultAccounts.d.ts +8 -0
  35. package/dist/ensureDefaultAccounts.js +42 -82
  36. package/dist/index.d.ts +31 -12
  37. package/dist/index.js +68 -13
  38. package/dist/keepAlive.d.ts +44 -0
  39. package/dist/keepAlive.js +106 -0
  40. package/dist/node.d.ts +4 -1
  41. package/dist/node.js +4 -1
  42. package/dist/observedSessionCollector.d.ts +117 -5
  43. package/dist/observedSessionCollector.js +483 -76
  44. package/dist/processDetection.d.ts +32 -0
  45. package/dist/processDetection.js +210 -0
  46. package/dist/providers/claudeCode.d.ts +23 -2
  47. package/dist/providers/claudeCode.js +143 -51
  48. package/dist/providers/codex.d.ts +10 -2
  49. package/dist/providers/codex.js +95 -12
  50. package/dist/providers/directoryListingCache.d.ts +85 -0
  51. package/dist/providers/directoryListingCache.js +244 -0
  52. package/dist/providers/rolloutWalker.d.ts +28 -5
  53. package/dist/providers/rolloutWalker.js +100 -16
  54. package/dist/providers/types.d.ts +48 -1
  55. package/dist/schemas/accountManager.d.ts +283 -1
  56. package/dist/schemas/accountManager.js +122 -1
  57. package/dist/schemas/assistantTurn.d.ts +2 -2
  58. package/dist/schemas/index.d.ts +6 -1
  59. package/dist/schemas/index.js +14 -1
  60. package/dist/schemas/observedSessionV1.d.ts +21 -21
  61. package/dist/schemas/quota.d.ts +18 -18
  62. package/dist/schemas/quotaHistory.d.ts +1 -1
  63. package/dist/schemas/stateFile.d.ts +5 -5
  64. package/dist/sessionMonitor.js +24 -2
  65. package/dist/terminalSync.d.ts +5 -5
  66. package/dist/terminalSync.js +7 -112
  67. package/dist/writers/atomic.d.ts +2 -0
  68. package/dist/writers/atomic.js +49 -2
  69. 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
+ }
@@ -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;
@@ -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(getClaudeProfilesDir(), uuid, 'home');
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
- return configDir
64
- ? `${DEFAULT_CLAUDE_KEYCHAIN_SERVICE}-${claudeKeychainSuffix(configDir)}`
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
- function readClaudeProfileIdentity(home) {
115
+ /** The keychain account name Claude Code uses: the unix username, sanitised. */
116
+ function claudeKeychainAccountName() {
117
+ let name;
68
118
  try {
69
- const raw = JSON.parse(fs.readFileSync(path.join(home, '.claude.json'), 'utf8'));
70
- const email = raw?.oauthAccount?.emailAddress;
71
- const uuid = raw?.oauthAccount?.accountUuid;
72
- return typeof email === 'string' && typeof uuid === 'string' ? { email, uuid } : null;
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
+ }