sidekick-shared 0.27.4 → 0.27.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/README.md +92 -45
- package/dist/accountHealth.d.ts +14 -0
- package/dist/accountHealth.js +61 -14
- package/dist/accountLaunch.d.ts +6 -0
- package/dist/accountLaunch.js +61 -4
- package/dist/accountLease.d.ts +5 -0
- package/dist/accountLease.js +98 -0
- package/dist/accountManager.d.ts +7 -0
- package/dist/accountManager.js +226 -89
- package/dist/accountSync.d.ts +1 -5
- package/dist/accountSync.js +22 -6
- package/dist/accountSyncTypes.d.ts +9 -0
- package/dist/accounts.d.ts +75 -18
- package/dist/accounts.js +712 -189
- package/dist/claudeCredentialDoctor.d.ts +50 -0
- package/dist/claudeCredentialDoctor.js +277 -0
- package/dist/claudeCredentialLedger.d.ts +15 -0
- package/dist/claudeCredentialLedger.js +130 -0
- package/dist/claudeCredentials.d.ts +20 -3
- package/dist/claudeCredentials.js +50 -3
- package/dist/claudeLiveGuard.d.ts +33 -0
- package/dist/claudeLiveGuard.js +46 -0
- package/dist/claudeProfiles.d.ts +14 -0
- package/dist/claudeProfiles.js +22 -0
- package/dist/claudeRefreshLock.d.ts +15 -0
- package/dist/claudeRefreshLock.js +145 -0
- package/dist/codexAuth.d.ts +4 -0
- package/dist/codexAuth.js +18 -0
- package/dist/codexCredentialDoctor.d.ts +33 -0
- package/dist/codexCredentialDoctor.js +302 -0
- package/dist/codexCredentialStore.d.ts +29 -0
- package/dist/codexCredentialStore.js +213 -0
- package/dist/codexKeepAlive.d.ts +2 -0
- package/dist/codexKeepAlive.js +116 -0
- package/dist/codexPaths.d.ts +8 -2
- package/dist/codexPaths.js +51 -7
- package/dist/codexProfiles.d.ts +37 -31
- package/dist/codexProfiles.js +842 -671
- package/dist/credentialIO.d.ts +67 -7
- package/dist/credentialIO.js +211 -47
- package/dist/ensureDefaultAccounts.js +1 -8
- package/dist/index.d.ts +17 -4
- package/dist/index.js +36 -11
- package/dist/keepAlive.d.ts +2 -1
- package/dist/keepAlive.js +116 -29
- package/dist/liveCredentialGuard.d.ts +28 -0
- package/dist/liveCredentialGuard.js +36 -0
- package/dist/node.d.ts +3 -0
- package/dist/node.js +4 -2
- package/dist/processDetection.d.ts +10 -3
- package/dist/processDetection.js +44 -11
- package/dist/promptHistory.d.ts +66 -0
- package/dist/promptHistory.js +37 -18
- package/dist/schemas/accountManager.d.ts +22 -0
- package/dist/schemas/accountManager.js +4 -0
- package/dist/schemas/assistantTurn.d.ts +2 -2
- package/dist/sessionPromptHistory.d.ts +120 -0
- package/dist/sessionPromptHistory.js +278 -0
- package/dist/sessionPromptSignals.d.ts +56 -0
- package/dist/sessionPromptSignals.js +355 -0
- package/dist/terminalSync.d.ts +12 -2
- package/dist/terminalSync.js +27 -2
- package/package.json +1 -1
package/dist/accountLaunch.d.ts
CHANGED
|
@@ -26,5 +26,11 @@ export interface AccountLaunchOptions {
|
|
|
26
26
|
materialize?: boolean;
|
|
27
27
|
/** Launch even when the stored credential is reported expired. */
|
|
28
28
|
force?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Claude: when the profile home has no credential but its backup does,
|
|
31
|
+
* restore it there first (default true). Keep-alive passes false: it never
|
|
32
|
+
* restores backups.
|
|
33
|
+
*/
|
|
34
|
+
restoreBackup?: boolean;
|
|
29
35
|
}
|
|
30
36
|
export declare function getAccountLaunchEnv(provider: AccountProviderId, accountId: string, options?: AccountLaunchOptions): AccountLaunchEnv;
|
package/dist/accountLaunch.js
CHANGED
|
@@ -44,6 +44,9 @@ const path = __importStar(require("path"));
|
|
|
44
44
|
const accountRegistry_1 = require("./accountRegistry");
|
|
45
45
|
const accountHealth_1 = require("./accountHealth");
|
|
46
46
|
const claudeProfiles_1 = require("./claudeProfiles");
|
|
47
|
+
const accounts_1 = require("./accounts");
|
|
48
|
+
const codexCredentialStore_1 = require("./codexCredentialStore");
|
|
49
|
+
const codexProfiles_1 = require("./codexProfiles");
|
|
47
50
|
const codexPaths_1 = require("./codexPaths");
|
|
48
51
|
const atomic_1 = require("./writers/atomic");
|
|
49
52
|
function materializeClaudeHome(home) {
|
|
@@ -70,10 +73,11 @@ function materializeClaudeHome(home) {
|
|
|
70
73
|
function materializeCodexHome(home) {
|
|
71
74
|
fs.mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
72
75
|
const configTarget = path.join(home, 'config.toml');
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
const current = (0, codexPaths_1.readFileOrNull)(configTarget);
|
|
77
|
+
const source = current ?? (0, codexPaths_1.readFileOrNull)(path.join((0, codexPaths_1.getSystemCodexHome)(), 'config.toml')) ?? '';
|
|
78
|
+
const fileMode = (0, codexPaths_1.forceFileCredentialStore)(source);
|
|
79
|
+
if (fileMode !== current)
|
|
80
|
+
(0, atomic_1.atomicWriteFileSync)(configTarget, fileMode);
|
|
77
81
|
}
|
|
78
82
|
function getAccountLaunchEnv(provider, accountId, options = {}) {
|
|
79
83
|
const profile = (0, accountRegistry_1.listSavedAccountProfiles)(provider).find((p) => p.id === accountId);
|
|
@@ -98,6 +102,40 @@ function getAccountLaunchEnv(provider, accountId, options = {}) {
|
|
|
98
102
|
if (!profile) {
|
|
99
103
|
return { ...base, error: `${provider} account ${accountId} not found.` };
|
|
100
104
|
}
|
|
105
|
+
if (provider === 'codex' && (0, codexCredentialStore_1.isSidekickCodexHome)((0, codexPaths_1.getSystemCodexHome)())) {
|
|
106
|
+
return {
|
|
107
|
+
...base,
|
|
108
|
+
error: 'Unset CODEX_HOME before managing accounts; it names a Sidekick-owned home.',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (provider === 'codex' && !['file', 'unknown'].includes((0, codexPaths_1.getCodexCredentialStoreMode)())) {
|
|
112
|
+
return { ...base, error: 'Codex account launches require readable file-mode credentials.' };
|
|
113
|
+
}
|
|
114
|
+
if (provider === 'codex' && (0, codexProfiles_1.isLiveCodexProfile)(profile)) {
|
|
115
|
+
const liveHome = (0, codexPaths_1.getSystemCodexHome)();
|
|
116
|
+
return {
|
|
117
|
+
...base,
|
|
118
|
+
home: liveHome,
|
|
119
|
+
env: { CODEX_HOME: liveHome },
|
|
120
|
+
warnings: ['This account is the live login, so the session uses the normal Codex home.'],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (provider === 'claude-code' && health.isLive) {
|
|
124
|
+
// The live account's only CLI-usable copy is the live store; a session in
|
|
125
|
+
// its profile home would be a second copy racing it over the refresh
|
|
126
|
+
// token. Point the session at the live home instead.
|
|
127
|
+
const liveHome = (0, claudeProfiles_1.getLiveClaudeHome)();
|
|
128
|
+
const customHome = !(0, claudeProfiles_1.isDefaultClaudeHome)();
|
|
129
|
+
return {
|
|
130
|
+
...base,
|
|
131
|
+
home: liveHome,
|
|
132
|
+
env: customHome ? { CLAUDE_CONFIG_DIR: liveHome } : {},
|
|
133
|
+
envUnset: customHome
|
|
134
|
+
? [claudeProfiles_1.CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV]
|
|
135
|
+
: ['CLAUDE_CONFIG_DIR', claudeProfiles_1.CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV],
|
|
136
|
+
warnings: ['This account is the live login, so the session uses the normal Claude home.'],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
101
139
|
if ((health.state === 'expired' || health.state === 'missing') && !options.force) {
|
|
102
140
|
return {
|
|
103
141
|
...base,
|
|
@@ -109,6 +147,25 @@ function getAccountLaunchEnv(provider, accountId, options = {}) {
|
|
|
109
147
|
}
|
|
110
148
|
if (health.state === 'expiring' && health.reason)
|
|
111
149
|
warnings.push(health.reason);
|
|
150
|
+
if (provider === 'claude-code' && options.restoreBackup !== false) {
|
|
151
|
+
if ((0, accounts_1.restoreClaudeProfileFromBackup)(accountId) === 'restored') {
|
|
152
|
+
warnings.push("Restored this account's saved login into its profile home. If a session elsewhere still uses this login, close it first: two copies of one login invalidate each other.");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (provider === 'codex') {
|
|
156
|
+
const restored = (0, codexProfiles_1.restoreCodexProfileFromBackup)(accountId, {
|
|
157
|
+
restore: options.restoreBackup !== false,
|
|
158
|
+
});
|
|
159
|
+
if (restored === 'none')
|
|
160
|
+
return { ...base, error: 'No safe inactive Codex credential is available; sign in again.' };
|
|
161
|
+
if (restored === 'live')
|
|
162
|
+
return { ...base, home: (0, codexPaths_1.getSystemCodexHome)(), env: { CODEX_HOME: (0, codexPaths_1.getSystemCodexHome)() } };
|
|
163
|
+
}
|
|
164
|
+
if (provider === 'codex' &&
|
|
165
|
+
options.materialize === false &&
|
|
166
|
+
!['file', 'unknown'].includes((0, codexPaths_1.getCodexCredentialStoreMode)(home))) {
|
|
167
|
+
return { ...base, error: 'The isolated Codex home must use file credential storage.' };
|
|
168
|
+
}
|
|
112
169
|
if (options.materialize !== false) {
|
|
113
170
|
try {
|
|
114
171
|
if (provider === 'codex')
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AccountProviderId } from './accountRegistry';
|
|
2
|
+
export declare function isAccountLeaseHeld(provider: AccountProviderId, accountId: string, now?: number): boolean;
|
|
3
|
+
/** Take the lease; the caller holds the provider's swap lock. False when held elsewhere. */
|
|
4
|
+
export declare function acquireAccountLease(provider: AccountProviderId, accountId: string, durationMs: number, now?: number): boolean;
|
|
5
|
+
export declare function releaseAccountLease(provider: AccountProviderId, accountId: string): void;
|
|
@@ -0,0 +1,98 @@
|
|
|
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.isAccountLeaseHeld = isAccountLeaseHeld;
|
|
37
|
+
exports.acquireAccountLease = acquireAccountLease;
|
|
38
|
+
exports.releaseAccountLease = releaseAccountLease;
|
|
39
|
+
/**
|
|
40
|
+
* Keep-alive leases: while keep-alive runs a provider CLI inside a profile
|
|
41
|
+
* home, that home's credential may be mid-refresh. A switch to that account
|
|
42
|
+
* would copy the pre-refresh token into the live store just as the CLI kills
|
|
43
|
+
* it, so switches refuse a leased target. Leases are acquired under the
|
|
44
|
+
* provider's swap lock (the same lock a switch holds), expire on their own,
|
|
45
|
+
* and are ignored once their process is gone.
|
|
46
|
+
*/
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
const claudeProfiles_1 = require("./claudeProfiles");
|
|
50
|
+
const codexPaths_1 = require("./codexPaths");
|
|
51
|
+
const atomic_1 = require("./writers/atomic");
|
|
52
|
+
function leasePath(provider, accountId) {
|
|
53
|
+
const dir = provider === 'codex' ? (0, codexPaths_1.getCodexProfileDir)(accountId) : (0, claudeProfiles_1.getClaudeProfileDir)(accountId);
|
|
54
|
+
return path.join(dir, 'keepalive.lease');
|
|
55
|
+
}
|
|
56
|
+
function processAlive(pid) {
|
|
57
|
+
if (pid === process.pid)
|
|
58
|
+
return true;
|
|
59
|
+
try {
|
|
60
|
+
process.kill(pid, 0);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return error.code === 'EPERM';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function isAccountLeaseHeld(provider, accountId, now = Date.now()) {
|
|
68
|
+
try {
|
|
69
|
+
const lease = JSON.parse(fs.readFileSync(leasePath(provider, accountId), 'utf8'));
|
|
70
|
+
return (typeof lease.pid === 'number' &&
|
|
71
|
+
typeof lease.expiresAt === 'number' &&
|
|
72
|
+
lease.expiresAt > now &&
|
|
73
|
+
processAlive(lease.pid));
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Take the lease; the caller holds the provider's swap lock. False when held elsewhere. */
|
|
80
|
+
function acquireAccountLease(provider, accountId, durationMs, now = Date.now()) {
|
|
81
|
+
if (isAccountLeaseHeld(provider, accountId, now))
|
|
82
|
+
return false;
|
|
83
|
+
const target = leasePath(provider, accountId);
|
|
84
|
+
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
85
|
+
(0, atomic_1.atomicWriteJsonSync)(target, {
|
|
86
|
+
pid: process.pid,
|
|
87
|
+
expiresAt: now + durationMs,
|
|
88
|
+
});
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
function releaseAccountLease(provider, accountId) {
|
|
92
|
+
try {
|
|
93
|
+
fs.rmSync(leasePath(provider, accountId), { force: true });
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
/* expires on its own */
|
|
97
|
+
}
|
|
98
|
+
}
|
package/dist/accountManager.d.ts
CHANGED
|
@@ -73,6 +73,13 @@ export declare function getAccountLoginStatusAsync(provider: AccountProviderId,
|
|
|
73
73
|
export declare function finalizeAccountLogin(provider: AccountProviderId, loginId: string, opts?: FinalizeAccountLoginOptions): AccountManagerResult;
|
|
74
74
|
/** {@link finalizeAccountLogin} without blocking the event loop on CLI probes. */
|
|
75
75
|
export declare function finalizeAccountLoginAsync(provider: AccountProviderId, loginId: string, opts?: FinalizeAccountLoginOptions): Promise<AccountManagerResult>;
|
|
76
|
+
/**
|
|
77
|
+
* Abandon a login started with {@link beginAccountLogin} (cancelled, timed
|
|
78
|
+
* out, or failed): removes the temporary home and, for Claude, its Keychain
|
|
79
|
+
* item. Refuses ids that belong to a saved profile. Returns true when
|
|
80
|
+
* something was discarded.
|
|
81
|
+
*/
|
|
82
|
+
export declare function discardAccountLogin(provider: AccountProviderId, loginId: string): boolean;
|
|
76
83
|
export declare function spawnAccountLogin(provider: AccountProviderId, label: string, opts?: SpawnAccountLoginOptions): Promise<AccountManagerResult>;
|
|
77
84
|
export declare function switchAccount(provider: AccountProviderId, id: string, options?: SwitchAccountOptions): SwitchAccountResult;
|
|
78
85
|
/**
|
package/dist/accountManager.js
CHANGED
|
@@ -39,6 +39,7 @@ exports.getAccountLoginStatus = getAccountLoginStatus;
|
|
|
39
39
|
exports.getAccountLoginStatusAsync = getAccountLoginStatusAsync;
|
|
40
40
|
exports.finalizeAccountLogin = finalizeAccountLogin;
|
|
41
41
|
exports.finalizeAccountLoginAsync = finalizeAccountLoginAsync;
|
|
42
|
+
exports.discardAccountLogin = discardAccountLogin;
|
|
42
43
|
exports.spawnAccountLogin = spawnAccountLogin;
|
|
43
44
|
exports.switchAccount = switchAccount;
|
|
44
45
|
exports.switchAccountAsync = switchAccountAsync;
|
|
@@ -54,7 +55,9 @@ const atomic_1 = require("./writers/atomic");
|
|
|
54
55
|
const accounts_1 = require("./accounts");
|
|
55
56
|
const claudeProfiles_1 = require("./claudeProfiles");
|
|
56
57
|
const credentialIO_1 = require("./credentialIO");
|
|
58
|
+
const claudeCredentialLedger_1 = require("./claudeCredentialLedger");
|
|
57
59
|
const codexProfiles_1 = require("./codexProfiles");
|
|
60
|
+
const codexPaths_1 = require("./codexPaths");
|
|
58
61
|
const accountSwitch_1 = require("./accountSwitch");
|
|
59
62
|
function getPendingClaudeProfilePath(loginId) {
|
|
60
63
|
return path.join((0, claudeProfiles_1.getClaudeProfileDir)(loginId), 'profile.json');
|
|
@@ -109,7 +112,16 @@ function copyClaudeProfileToCanonicalHome(sourceHome, accountUuid, credentials,
|
|
|
109
112
|
fs.mkdirSync(canonicalHome, { recursive: true, mode: 0o700 });
|
|
110
113
|
if (path.resolve(sourceHome) !== path.resolve(canonicalHome)) {
|
|
111
114
|
try {
|
|
112
|
-
|
|
115
|
+
// Credentials go only through the chokepoint below: when this account is
|
|
116
|
+
// the live one its profile home must not receive a CLI-usable copy.
|
|
117
|
+
fs.cpSync(sourceHome, canonicalHome, {
|
|
118
|
+
recursive: true,
|
|
119
|
+
force: true,
|
|
120
|
+
filter: (src) => {
|
|
121
|
+
const base = path.basename(src);
|
|
122
|
+
return base !== '.credentials.json' && !base.startsWith('.oauth_refresh.lock');
|
|
123
|
+
},
|
|
124
|
+
});
|
|
113
125
|
}
|
|
114
126
|
catch {
|
|
115
127
|
// The credential and identity files are re-written below when possible.
|
|
@@ -148,7 +160,7 @@ function beginAccountLogin(provider, label, opts = {}) {
|
|
|
148
160
|
const effectiveLabel = existing
|
|
149
161
|
? `${existing.label ?? existing.email ?? 'codex'} (re-login ${(0, crypto_1.randomUUID)().slice(0, 8)})`
|
|
150
162
|
: label.trim() || `Codex ${(0, codexProfiles_1.listCodexAccounts)().length + 1}`;
|
|
151
|
-
const prepared = (0, codexProfiles_1.prepareCodexAccount)(effectiveLabel);
|
|
163
|
+
const prepared = (0, codexProfiles_1.prepareCodexAccount)(effectiveLabel, { importCurrent: false });
|
|
152
164
|
if (!prepared.success || !prepared.profileId || !prepared.codexHome) {
|
|
153
165
|
return { success: false, error: prepared.error ?? 'Could not prepare Codex account login.' };
|
|
154
166
|
}
|
|
@@ -186,6 +198,9 @@ function beginAccountLogin(provider, label, opts = {}) {
|
|
|
186
198
|
existingAccountId: existing?.id,
|
|
187
199
|
});
|
|
188
200
|
seedClaudeLoginHome(home);
|
|
201
|
+
// Claude Code creates this home's Keychain item itself; record the home
|
|
202
|
+
// first so the item is provably Sidekick's even if everything else fails.
|
|
203
|
+
(0, claudeCredentialLedger_1.recordClaudeCredentialHome)(home, 'login');
|
|
189
204
|
const loginCommand = resolveClaudeLoginCommand(opts);
|
|
190
205
|
return {
|
|
191
206
|
success: true,
|
|
@@ -207,7 +222,7 @@ function getClaudeAccountLoginStatus(loginId) {
|
|
|
207
222
|
}
|
|
208
223
|
function getAccountLoginStatus(provider, loginId) {
|
|
209
224
|
if (provider === 'codex') {
|
|
210
|
-
const codexHome = (0,
|
|
225
|
+
const codexHome = (0, codexPaths_1.getCodexLoginHome)(loginId);
|
|
211
226
|
if (!(0, codexProfiles_1.isCodexProfileAuthenticated)(codexHome)) {
|
|
212
227
|
return { state: 'pending' };
|
|
213
228
|
}
|
|
@@ -226,7 +241,7 @@ function getAccountLoginStatus(provider, loginId) {
|
|
|
226
241
|
*/
|
|
227
242
|
async function getAccountLoginStatusAsync(provider, loginId) {
|
|
228
243
|
if (provider === 'codex') {
|
|
229
|
-
const codexHome = (0,
|
|
244
|
+
const codexHome = (0, codexPaths_1.getCodexLoginHome)(loginId);
|
|
230
245
|
if (!(await (0, codexProfiles_1.isCodexProfileAuthenticatedAsync)(codexHome))) {
|
|
231
246
|
return { state: 'pending' };
|
|
232
247
|
}
|
|
@@ -257,44 +272,55 @@ function finalizeClaudeAccountLogin(loginId, opts = {}) {
|
|
|
257
272
|
if (!identity) {
|
|
258
273
|
return { success: false, error: 'Claude profile is not authenticated yet.' };
|
|
259
274
|
}
|
|
260
|
-
const
|
|
261
|
-
if (
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const oauthAccount = readClaudeOauthAccount(home);
|
|
265
|
-
if (!oauthAccount) {
|
|
266
|
-
return { success: false, error: 'Could not read Claude profile config.' };
|
|
275
|
+
const read = (0, credentialIO_1.readClaudeCredentialStore)(home);
|
|
276
|
+
if (read.status === 'error') {
|
|
277
|
+
// Transient (locked Keychain, timeout): keep the login home for a retry.
|
|
278
|
+
return { success: false, error: 'Could not read the new Claude login yet; try again.' };
|
|
267
279
|
}
|
|
268
|
-
const pending = readPendingClaudeProfile(loginId);
|
|
269
|
-
const existing = (0, accountRegistry_1.listSavedAccountProfiles)('claude-code').find((account) => (account.providerAccountId ?? account.id) === identity.uuid);
|
|
270
|
-
copyClaudeProfileToCanonicalHome(home, identity.uuid, credentials, oauthAccount);
|
|
271
280
|
const warnings = [];
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
providerAccountId: identity.uuid,
|
|
279
|
-
email: identity.email,
|
|
280
|
-
label: existing?.label ?? (pending?.label || identity.email),
|
|
281
|
-
addedAt: existing?.addedAt ?? pending?.addedAt ?? new Date().toISOString(),
|
|
282
|
-
metadata: {
|
|
283
|
-
...existing?.metadata,
|
|
284
|
-
email: identity.email,
|
|
285
|
-
origin: 'login',
|
|
286
|
-
},
|
|
287
|
-
});
|
|
288
|
-
removePendingClaudeProfile(loginId);
|
|
289
|
-
if (loginId !== identity.uuid) {
|
|
290
|
-
removePendingClaudeProfile(identity.uuid);
|
|
291
|
-
// The temporary home's keychain item holds live tokens; nothing references it now.
|
|
292
|
-
(0, credentialIO_1.deleteStoredCredentials)((0, claudeProfiles_1.getClaudeProfileHome)(loginId));
|
|
293
|
-
try {
|
|
294
|
-
fs.rmSync((0, claudeProfiles_1.getClaudeProfileDir)(loginId), { recursive: true, force: true });
|
|
281
|
+
try {
|
|
282
|
+
if (read.status !== 'ok') {
|
|
283
|
+
return {
|
|
284
|
+
success: false,
|
|
285
|
+
error: 'The sign-in finished without a readable credential; sign in again.',
|
|
286
|
+
};
|
|
295
287
|
}
|
|
296
|
-
|
|
297
|
-
|
|
288
|
+
const oauthAccount = readClaudeOauthAccount(home);
|
|
289
|
+
if (!oauthAccount) {
|
|
290
|
+
return { success: false, error: 'Could not read Claude profile config.' };
|
|
291
|
+
}
|
|
292
|
+
const pending = readPendingClaudeProfile(loginId);
|
|
293
|
+
const existing = (0, accountRegistry_1.listSavedAccountProfiles)('claude-code').find((account) => (account.providerAccountId ?? account.id) === identity.uuid);
|
|
294
|
+
// The chokepoint writes the private backup first, so a failure below
|
|
295
|
+
// still leaves the new login saved.
|
|
296
|
+
copyClaudeProfileToCanonicalHome(home, identity.uuid, read.blob, oauthAccount);
|
|
297
|
+
if (pending?.existingAccountId && pending.existingAccountId !== identity.uuid) {
|
|
298
|
+
warnings.push(`You signed in as ${identity.email}, which is a different account from the one you chose to re-authenticate; it was saved separately.`);
|
|
299
|
+
}
|
|
300
|
+
(0, accountRegistry_1.upsertSavedAccountProfile)({
|
|
301
|
+
id: existing?.id ?? identity.uuid,
|
|
302
|
+
providerId: 'claude-code',
|
|
303
|
+
providerAccountId: identity.uuid,
|
|
304
|
+
email: identity.email,
|
|
305
|
+
label: existing?.label ?? (pending?.label || identity.email),
|
|
306
|
+
addedAt: existing?.addedAt ?? pending?.addedAt ?? new Date().toISOString(),
|
|
307
|
+
metadata: {
|
|
308
|
+
...existing?.metadata,
|
|
309
|
+
email: identity.email,
|
|
310
|
+
origin: 'login',
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
return { success: false, error: `Could not save the new Claude login: ${err}` };
|
|
316
|
+
}
|
|
317
|
+
finally {
|
|
318
|
+
// Every exit after authentication: the temporary home's Keychain item
|
|
319
|
+
// holds a live refresh token that nothing will ever reference again.
|
|
320
|
+
removePendingClaudeProfile(loginId);
|
|
321
|
+
if (loginId !== identity.uuid) {
|
|
322
|
+
removePendingClaudeProfile(identity.uuid);
|
|
323
|
+
discardClaudeLoginHome(loginId);
|
|
298
324
|
}
|
|
299
325
|
}
|
|
300
326
|
if (opts.activate === false) {
|
|
@@ -306,6 +332,8 @@ function finalizeClaudeAccountLogin(loginId, opts = {}) {
|
|
|
306
332
|
const applied = (0, accounts_1.applyClaudeProfileToLiveHome)(identity.uuid);
|
|
307
333
|
if (!applied.success)
|
|
308
334
|
return applied;
|
|
335
|
+
if (applied.warning)
|
|
336
|
+
warnings.push(applied.warning);
|
|
309
337
|
}
|
|
310
338
|
const switched = (0, accounts_1.switchToAccount)(identity.uuid);
|
|
311
339
|
return warnings.length
|
|
@@ -356,22 +384,82 @@ function latestHomeActivity(home) {
|
|
|
356
384
|
}
|
|
357
385
|
return latest;
|
|
358
386
|
}
|
|
387
|
+
/** Delete a temporary Claude login home: its Keychain item first, then the directory. */
|
|
388
|
+
function discardClaudeLoginHome(loginId) {
|
|
389
|
+
const dir = (0, claudeProfiles_1.getClaudeProfileDir)(loginId);
|
|
390
|
+
if (!path.resolve(dir).startsWith(path.resolve((0, accountRegistry_1.getAccountsDir)()) + path.sep))
|
|
391
|
+
return;
|
|
392
|
+
(0, credentialIO_1.deleteStoredCredentials)((0, claudeProfiles_1.getClaudeProfileHome)(loginId));
|
|
393
|
+
try {
|
|
394
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
/* the temporary login home is disposable */
|
|
398
|
+
}
|
|
399
|
+
}
|
|
359
400
|
function discardPendingLogin(provider, loginId) {
|
|
360
401
|
try {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
: (0, claudeProfiles_1.getClaudeProfileDir)(loginId);
|
|
364
|
-
if (!dir.startsWith((0, accountRegistry_1.getAccountsDir)()))
|
|
402
|
+
if (provider === 'claude-code') {
|
|
403
|
+
discardClaudeLoginHome(loginId);
|
|
365
404
|
return;
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
405
|
+
}
|
|
406
|
+
(0, codexProfiles_1.discardCodexLogin)(loginId);
|
|
369
407
|
}
|
|
370
408
|
catch {
|
|
371
409
|
/* best effort */
|
|
372
410
|
}
|
|
373
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Abandon a login started with {@link beginAccountLogin} (cancelled, timed
|
|
414
|
+
* out, or failed): removes the temporary home and, for Claude, its Keychain
|
|
415
|
+
* item. Refuses ids that belong to a saved profile. Returns true when
|
|
416
|
+
* something was discarded.
|
|
417
|
+
*/
|
|
418
|
+
function discardAccountLogin(provider, loginId) {
|
|
419
|
+
const registered = provider === 'codex'
|
|
420
|
+
? (0, codexProfiles_1.listCodexAccounts)().some((account) => account.id === loginId)
|
|
421
|
+
: (0, accountRegistry_1.listSavedAccountProfiles)('claude-code').some((account) => account.id === loginId || account.providerAccountId === loginId);
|
|
422
|
+
if (registered)
|
|
423
|
+
return false;
|
|
424
|
+
if (provider === 'claude-code' && !readPendingClaudeProfile(loginId)) {
|
|
425
|
+
// Not a pending login; still drop a stray item for a home we created.
|
|
426
|
+
(0, credentialIO_1.deleteStoredCredentials)((0, claudeProfiles_1.getClaudeProfileHome)(loginId));
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
discardPendingLogin(provider, loginId);
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
class AccountLoginWriterActiveError extends Error {
|
|
433
|
+
}
|
|
374
434
|
async function spawnAccountLogin(provider, label, opts = {}) {
|
|
435
|
+
let loginId;
|
|
436
|
+
let result;
|
|
437
|
+
let writerActive = false;
|
|
438
|
+
try {
|
|
439
|
+
result = await runAccountLogin(provider, label, opts, (id) => {
|
|
440
|
+
loginId = id;
|
|
441
|
+
});
|
|
442
|
+
return result;
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
if (error instanceof AccountLoginWriterActiveError) {
|
|
446
|
+
writerActive = true;
|
|
447
|
+
return {
|
|
448
|
+
success: false,
|
|
449
|
+
error: 'The Codex login process could not be stopped. Its temporary home was preserved; close that process before retrying.',
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
throw error;
|
|
453
|
+
}
|
|
454
|
+
finally {
|
|
455
|
+
// Every exit that did not end in a saved login (spawn error, early exit,
|
|
456
|
+
// abort, timeout, a throw) removes the temporary home and its Keychain item.
|
|
457
|
+
if (!writerActive && loginId && (provider === 'codex' || !result?.success)) {
|
|
458
|
+
discardPendingLogin(provider, loginId);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
async function runAccountLogin(provider, label, opts, onBegin) {
|
|
375
463
|
if (opts.signal?.aborted) {
|
|
376
464
|
return { success: false, error: 'Account login aborted.' };
|
|
377
465
|
}
|
|
@@ -381,6 +469,7 @@ async function spawnAccountLogin(provider, label, opts = {}) {
|
|
|
381
469
|
});
|
|
382
470
|
if (!begin.success)
|
|
383
471
|
return { success: false, error: begin.error };
|
|
472
|
+
onBegin(begin.loginId);
|
|
384
473
|
if (begin.alreadyComplete) {
|
|
385
474
|
return finalizeAccountLoginAsync(provider, begin.loginId, { activate: opts.activate ?? true });
|
|
386
475
|
}
|
|
@@ -419,57 +508,105 @@ async function spawnAccountLogin(provider, label, opts = {}) {
|
|
|
419
508
|
childExited = true;
|
|
420
509
|
childState.spawnError = error;
|
|
421
510
|
});
|
|
422
|
-
const
|
|
423
|
-
|
|
511
|
+
const stopCodexChild = async () => {
|
|
512
|
+
if (provider !== 'codex' || childExited)
|
|
513
|
+
return;
|
|
514
|
+
const waitForExit = (ms) => new Promise((resolve) => {
|
|
515
|
+
if (childExited) {
|
|
516
|
+
resolve();
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const done = () => {
|
|
520
|
+
clearTimeout(timer);
|
|
521
|
+
child.off('exit', done);
|
|
522
|
+
child.off('error', done);
|
|
523
|
+
resolve();
|
|
524
|
+
};
|
|
525
|
+
const timer = setTimeout(done, ms);
|
|
526
|
+
child.once('exit', done);
|
|
527
|
+
child.once('error', done);
|
|
528
|
+
});
|
|
529
|
+
await waitForExit(100);
|
|
530
|
+
try {
|
|
531
|
+
if (!childExited) {
|
|
532
|
+
child.kill();
|
|
533
|
+
await waitForExit(1000);
|
|
534
|
+
}
|
|
535
|
+
if (!childExited) {
|
|
536
|
+
child.kill('SIGKILL');
|
|
537
|
+
await waitForExit(1000);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
throw new AccountLoginWriterActiveError();
|
|
542
|
+
}
|
|
543
|
+
if (!childExited)
|
|
544
|
+
throw new AccountLoginWriterActiveError();
|
|
545
|
+
};
|
|
546
|
+
const abort = async () => {
|
|
547
|
+
if (provider === 'codex')
|
|
548
|
+
await stopCodexChild();
|
|
549
|
+
else
|
|
550
|
+
child.kill();
|
|
424
551
|
discardPendingLogin(provider, begin.loginId);
|
|
425
552
|
emitStatus(opts, { state: 'failed', error: 'Account login aborted.' });
|
|
426
553
|
return { success: false, error: 'Account login aborted.' };
|
|
427
554
|
};
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
}
|
|
437
|
-
if (childExited) {
|
|
438
|
-
if (childState.spawnError) {
|
|
439
|
-
const error = `Could not spawn account login: ${childState.spawnError.message}`;
|
|
440
|
-
discardPendingLogin(provider, begin.loginId);
|
|
441
|
-
emitStatus(opts, { state: 'failed', error });
|
|
442
|
-
return { success: false, error };
|
|
443
|
-
}
|
|
444
|
-
const finalStatus = emitStatus(opts, await getAccountLoginStatusAsync(provider, begin.loginId));
|
|
445
|
-
if (finalStatus.state === 'authenticated') {
|
|
446
|
-
return finalizeAccountLoginAsync(provider, begin.loginId, {
|
|
555
|
+
try {
|
|
556
|
+
while (true) {
|
|
557
|
+
if (opts.signal?.aborted)
|
|
558
|
+
return await abort();
|
|
559
|
+
const status = emitStatus(opts, await getAccountLoginStatusAsync(provider, begin.loginId));
|
|
560
|
+
if (status.state === 'authenticated') {
|
|
561
|
+
await stopCodexChild();
|
|
562
|
+
return await finalizeAccountLoginAsync(provider, begin.loginId, {
|
|
447
563
|
activate: opts.activate ?? true,
|
|
448
564
|
});
|
|
449
565
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
566
|
+
if (childExited) {
|
|
567
|
+
if (childState.spawnError) {
|
|
568
|
+
const error = `Could not spawn account login: ${childState.spawnError.message}`;
|
|
569
|
+
discardPendingLogin(provider, begin.loginId);
|
|
570
|
+
emitStatus(opts, { state: 'failed', error });
|
|
571
|
+
return { success: false, error };
|
|
572
|
+
}
|
|
573
|
+
const finalStatus = emitStatus(opts, await getAccountLoginStatusAsync(provider, begin.loginId));
|
|
574
|
+
if (finalStatus.state === 'authenticated') {
|
|
575
|
+
await stopCodexChild();
|
|
576
|
+
return await finalizeAccountLoginAsync(provider, begin.loginId, {
|
|
577
|
+
activate: opts.activate ?? true,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
discardPendingLogin(provider, begin.loginId);
|
|
581
|
+
emitStatus(opts, {
|
|
582
|
+
state: 'failed',
|
|
583
|
+
error: `Account login exited before authentication completed${childExitCode === null ? '.' : ` (exit ${childExitCode}).`}`,
|
|
584
|
+
});
|
|
585
|
+
return { success: false, error: 'Account login did not complete.' };
|
|
586
|
+
}
|
|
587
|
+
const now = Date.now();
|
|
588
|
+
const mtime = latestHomeActivity(begin.configDir);
|
|
589
|
+
if (mtime > lastSeenMtime) {
|
|
590
|
+
lastSeenMtime = mtime;
|
|
591
|
+
lastActivity = now;
|
|
592
|
+
}
|
|
593
|
+
const remainingMs = Math.min(inactivityMs - (now - lastActivity), maxMs - (now - startedAt));
|
|
594
|
+
if (remainingMs <= 0) {
|
|
595
|
+
if (provider === 'codex')
|
|
596
|
+
await stopCodexChild();
|
|
597
|
+
else
|
|
598
|
+
child.kill();
|
|
599
|
+
discardPendingLogin(provider, begin.loginId);
|
|
600
|
+
emitStatus(opts, { state: 'failed', error: 'Account login timed out.' });
|
|
601
|
+
return { success: false, error: 'Account login timed out.' };
|
|
602
|
+
}
|
|
603
|
+
const shouldContinue = await waitForNextPoll(Math.min(2_000, remainingMs), opts.signal);
|
|
604
|
+
if (!shouldContinue)
|
|
605
|
+
return await abort();
|
|
469
606
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
607
|
+
}
|
|
608
|
+
finally {
|
|
609
|
+
await stopCodexChild();
|
|
473
610
|
}
|
|
474
611
|
}
|
|
475
612
|
function switchAccount(provider, id, options = {}) {
|
package/dist/accountSync.d.ts
CHANGED
|
@@ -12,11 +12,6 @@ export interface SyncLiveAccountStateOptions {
|
|
|
12
12
|
keychainFoldIntervalMs?: number;
|
|
13
13
|
now?: number;
|
|
14
14
|
}
|
|
15
|
-
/**
|
|
16
|
-
* Reconcile the live logins with the saved profiles for both providers.
|
|
17
|
-
* Safe to call from watchers: re-entrant calls return an empty report, and
|
|
18
|
-
* `onlyIfChanged` short-circuits when the live files are unchanged.
|
|
19
|
-
*/
|
|
20
15
|
export declare function syncLiveAccountStateSync(options?: SyncLiveAccountStateOptions): SyncReport;
|
|
21
16
|
/** Promise-returning twin of {@link syncLiveAccountStateSync} for async hosts. */
|
|
22
17
|
export declare function syncLiveAccountState(options?: SyncLiveAccountStateOptions): Promise<SyncReport>;
|
|
@@ -28,3 +23,4 @@ export interface CleanupAbandonedLoginsResult {
|
|
|
28
23
|
}
|
|
29
24
|
/** Remove isolated login homes that never authenticated (both providers). */
|
|
30
25
|
export declare function cleanupAbandonedLogins(olderThanMs?: number, now?: number): CleanupAbandonedLoginsResult;
|
|
26
|
+
export declare function cleanupAbandonedLoginsAsync(olderThanMs?: number, now?: number): Promise<CleanupAbandonedLoginsResult>;
|