sidekick-shared 0.27.5 → 0.27.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +35 -34
- package/dist/accountHealth.d.ts +14 -0
- package/dist/accountHealth.js +61 -14
- package/dist/accountLaunch.d.ts +6 -0
- package/dist/accountLaunch.js +61 -4
- package/dist/accountLease.d.ts +5 -0
- package/dist/accountLease.js +98 -0
- package/dist/accountManager.d.ts +7 -0
- package/dist/accountManager.js +226 -89
- package/dist/accountSync.d.ts +1 -5
- package/dist/accountSync.js +22 -6
- package/dist/accountSyncTypes.d.ts +9 -0
- package/dist/accounts.d.ts +75 -18
- package/dist/accounts.js +712 -189
- package/dist/claudeCredentialDoctor.d.ts +50 -0
- package/dist/claudeCredentialDoctor.js +277 -0
- package/dist/claudeCredentialLedger.d.ts +15 -0
- package/dist/claudeCredentialLedger.js +130 -0
- package/dist/claudeCredentials.d.ts +20 -3
- package/dist/claudeCredentials.js +50 -3
- package/dist/claudeLiveGuard.d.ts +33 -0
- package/dist/claudeLiveGuard.js +46 -0
- package/dist/claudeProfiles.d.ts +14 -0
- package/dist/claudeProfiles.js +22 -0
- package/dist/claudeRefreshLock.d.ts +15 -0
- package/dist/claudeRefreshLock.js +145 -0
- package/dist/codexAuth.d.ts +4 -0
- package/dist/codexAuth.js +18 -0
- package/dist/codexCredentialDoctor.d.ts +33 -0
- package/dist/codexCredentialDoctor.js +302 -0
- package/dist/codexCredentialStore.d.ts +29 -0
- package/dist/codexCredentialStore.js +213 -0
- package/dist/codexKeepAlive.d.ts +2 -0
- package/dist/codexKeepAlive.js +116 -0
- package/dist/codexPaths.d.ts +8 -2
- package/dist/codexPaths.js +51 -7
- package/dist/codexProfiles.d.ts +37 -31
- package/dist/codexProfiles.js +842 -671
- package/dist/credentialIO.d.ts +67 -7
- package/dist/credentialIO.js +211 -47
- package/dist/ensureDefaultAccounts.js +1 -8
- package/dist/index.d.ts +14 -4
- package/dist/index.js +29 -6
- package/dist/keepAlive.d.ts +2 -1
- package/dist/keepAlive.js +116 -29
- package/dist/liveCredentialGuard.d.ts +28 -0
- package/dist/liveCredentialGuard.js +36 -0
- package/dist/processDetection.d.ts +10 -3
- package/dist/processDetection.js +44 -11
- package/dist/schemas/accountManager.d.ts +22 -0
- package/dist/schemas/accountManager.js +4 -0
- package/dist/schemas/assistantTurn.d.ts +2 -2
- package/dist/terminalSync.d.ts +12 -2
- package/dist/terminalSync.js +27 -2
- package/package.json +1 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ProviderSyncReport } from './accountSyncTypes';
|
|
2
|
+
export type ClaudeCredentialFindingKind = 'orphan' | 'live-duplicate' | 'corrupt' | 'stale-entry';
|
|
3
|
+
export interface ClaudeCredentialFinding {
|
|
4
|
+
kind: ClaudeCredentialFindingKind;
|
|
5
|
+
configDir: string;
|
|
6
|
+
service: string;
|
|
7
|
+
/** How Sidekick knows it owns this store. */
|
|
8
|
+
ownership: 'ledger' | 'profile-home' | 'keychain-tag';
|
|
9
|
+
accountId?: string;
|
|
10
|
+
email?: string;
|
|
11
|
+
detail: string;
|
|
12
|
+
/** What repair does about it. */
|
|
13
|
+
action: string;
|
|
14
|
+
}
|
|
15
|
+
/** Attributes of one Keychain item (never its secret). */
|
|
16
|
+
export interface KeychainItemSummary {
|
|
17
|
+
service: string;
|
|
18
|
+
comment?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface InspectClaudeCredentialStoresOptions {
|
|
21
|
+
now?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Enumerate `Claude Code-credentials-*` Keychain items, attributes only.
|
|
24
|
+
* Default on macOS: `security dump-keychain` (no `-d`, so no secret is read
|
|
25
|
+
* and nothing prompts). Elsewhere there is no Keychain to scan.
|
|
26
|
+
*/
|
|
27
|
+
listKeychainItems?: () => KeychainItemSummary[];
|
|
28
|
+
}
|
|
29
|
+
export interface ClaudeCredentialInspection {
|
|
30
|
+
findings: ClaudeCredentialFinding[];
|
|
31
|
+
homesChecked: number;
|
|
32
|
+
keychainScanned: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface ClaudeCredentialRepairOutcome {
|
|
35
|
+
finding: ClaudeCredentialFinding;
|
|
36
|
+
ok: boolean;
|
|
37
|
+
error?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface ClaudeCredentialRepairResult extends ClaudeCredentialInspection {
|
|
40
|
+
dryRun: boolean;
|
|
41
|
+
repaired: ClaudeCredentialRepairOutcome[];
|
|
42
|
+
sync?: ProviderSyncReport;
|
|
43
|
+
}
|
|
44
|
+
/** Parse `security dump-keychain` output (attributes only) into item summaries. */
|
|
45
|
+
export declare function parseKeychainDump(output: string): KeychainItemSummary[];
|
|
46
|
+
export declare function inspectClaudeCredentialStores(options?: InspectClaudeCredentialStoresOptions): ClaudeCredentialInspection;
|
|
47
|
+
/** Inspect, then (unless `dryRun`) repair every finding. Dry run by default. */
|
|
48
|
+
export declare function repairClaudeCredentialStores(options?: InspectClaudeCredentialStoresOptions & {
|
|
49
|
+
dryRun?: boolean;
|
|
50
|
+
}): ClaudeCredentialRepairResult;
|
|
@@ -0,0 +1,277 @@
|
|
|
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.parseKeychainDump = parseKeychainDump;
|
|
37
|
+
exports.inspectClaudeCredentialStores = inspectClaudeCredentialStores;
|
|
38
|
+
exports.repairClaudeCredentialStores = repairClaudeCredentialStores;
|
|
39
|
+
/**
|
|
40
|
+
* Inspect and repair the Claude credential stores Sidekick owns.
|
|
41
|
+
*
|
|
42
|
+
* Ownership is proven, never guessed: a store counts only when its directory
|
|
43
|
+
* is in the ledger (claudeCredentialLedger.ts), is a profile home under
|
|
44
|
+
* Sidekick's accounts directory, or its Keychain item carries Sidekick's
|
|
45
|
+
* comment tag naming the directory. Other tools legitimately create
|
|
46
|
+
* `Claude Code-credentials-<hash>` items for their own `CLAUDE_CONFIG_DIR`, so
|
|
47
|
+
* nothing is ever matched by name pattern.
|
|
48
|
+
*
|
|
49
|
+
* Findings:
|
|
50
|
+
* - `orphan`: the store's directory is gone, or it belongs to no saved account
|
|
51
|
+
* and no login younger than a day. It holds a copy of some login nothing
|
|
52
|
+
* will use; repair deletes it.
|
|
53
|
+
* - `live-duplicate`: the live account's profile home holds a CLI-usable copy
|
|
54
|
+
* of its login (a second copy of the live chain); repair runs the sync that
|
|
55
|
+
* removes it (repairing live first when the copy is newer).
|
|
56
|
+
* - `corrupt`: the store does not parse (for example a credential truncated by
|
|
57
|
+
* an older build's Keychain write); repair deletes it, and the account needs
|
|
58
|
+
* a new sign-in unless its backup is still good.
|
|
59
|
+
* - `stale-entry`: a ledger entry whose store is already gone; repair forgets it.
|
|
60
|
+
*/
|
|
61
|
+
const child_process_1 = require("child_process");
|
|
62
|
+
const fs = __importStar(require("fs"));
|
|
63
|
+
const path = __importStar(require("path"));
|
|
64
|
+
const accountRegistry_1 = require("./accountRegistry");
|
|
65
|
+
const claudeProfiles_1 = require("./claudeProfiles");
|
|
66
|
+
const claudeCredentialLedger_1 = require("./claudeCredentialLedger");
|
|
67
|
+
const credentialIO_1 = require("./credentialIO");
|
|
68
|
+
const accounts_1 = require("./accounts");
|
|
69
|
+
/** Logins younger than this may still be finalized; the doctor leaves them alone. */
|
|
70
|
+
const PENDING_LOGIN_GRACE_MS = 24 * 60 * 60 * 1000;
|
|
71
|
+
function decodeKeychainValue(raw) {
|
|
72
|
+
const quoted = raw.match(/^"(.*)"$/);
|
|
73
|
+
if (quoted)
|
|
74
|
+
return quoted[1];
|
|
75
|
+
const hex = raw.match(/^0x([0-9A-Fa-f]+)/);
|
|
76
|
+
if (hex)
|
|
77
|
+
return Buffer.from(hex[1], 'hex').toString('utf8').replace(/\0+$/, '');
|
|
78
|
+
return raw;
|
|
79
|
+
}
|
|
80
|
+
/** Parse `security dump-keychain` output (attributes only) into item summaries. */
|
|
81
|
+
function parseKeychainDump(output) {
|
|
82
|
+
const items = [];
|
|
83
|
+
for (const block of output.split(/^keychain: /m)) {
|
|
84
|
+
const service = block.match(/^\s*"svce"<blob>=(.+)$/m)?.[1];
|
|
85
|
+
if (!service)
|
|
86
|
+
continue;
|
|
87
|
+
const comment = block.match(/^\s*"icmt"<blob>=(.+)$/m)?.[1];
|
|
88
|
+
const decodedService = decodeKeychainValue(service.trim());
|
|
89
|
+
const decodedComment = comment && comment.trim() !== '<NULL>' ? decodeKeychainValue(comment.trim()) : undefined;
|
|
90
|
+
items.push({ service: decodedService, comment: decodedComment });
|
|
91
|
+
}
|
|
92
|
+
return items;
|
|
93
|
+
}
|
|
94
|
+
function defaultListKeychainItems() {
|
|
95
|
+
if (process.platform !== 'darwin')
|
|
96
|
+
return [];
|
|
97
|
+
const output = (0, child_process_1.execFileSync)('security', ['dump-keychain'], {
|
|
98
|
+
encoding: 'utf8',
|
|
99
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
100
|
+
timeout: 30_000,
|
|
101
|
+
killSignal: 'SIGKILL',
|
|
102
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
103
|
+
});
|
|
104
|
+
return parseKeychainDump(String(output)).filter((item) => item.service.startsWith('Claude Code-credentials-'));
|
|
105
|
+
}
|
|
106
|
+
function collectOwnedHomes(keychainItems) {
|
|
107
|
+
const homes = new Map();
|
|
108
|
+
const add = (configDir, ownership, inLedger = false) => {
|
|
109
|
+
const resolved = path.resolve(configDir);
|
|
110
|
+
const existing = homes.get(resolved);
|
|
111
|
+
if (existing) {
|
|
112
|
+
existing.inLedger ||= inLedger;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
homes.set(resolved, {
|
|
116
|
+
configDir: resolved,
|
|
117
|
+
service: (0, claudeProfiles_1.claudeKeychainService)(resolved),
|
|
118
|
+
ownership,
|
|
119
|
+
inLedger,
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
for (const entry of (0, claudeCredentialLedger_1.readClaudeCredentialLedger)())
|
|
123
|
+
add(entry.configDir, 'ledger', true);
|
|
124
|
+
try {
|
|
125
|
+
for (const id of fs.readdirSync((0, claudeProfiles_1.getClaudeProfilesDir)())) {
|
|
126
|
+
add((0, claudeProfiles_1.getClaudeProfileHome)(id), 'profile-home');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
/* no profiles yet */
|
|
131
|
+
}
|
|
132
|
+
for (const item of keychainItems) {
|
|
133
|
+
if (!item.comment?.startsWith(credentialIO_1.SIDEKICK_KEYCHAIN_COMMENT_PREFIX))
|
|
134
|
+
continue;
|
|
135
|
+
const configDir = item.comment.slice(credentialIO_1.SIDEKICK_KEYCHAIN_COMMENT_PREFIX.length);
|
|
136
|
+
// The tag must name the directory the service was derived from.
|
|
137
|
+
if (configDir && (0, claudeProfiles_1.claudeKeychainService)(configDir) === item.service) {
|
|
138
|
+
add(configDir, 'keychain-tag');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return [...homes.values()];
|
|
142
|
+
}
|
|
143
|
+
function isRecentPendingLogin(configDir, now) {
|
|
144
|
+
const profilesDir = path.resolve((0, claudeProfiles_1.getClaudeProfilesDir)());
|
|
145
|
+
const loginDir = path.dirname(configDir);
|
|
146
|
+
if (path.dirname(loginDir) !== profilesDir)
|
|
147
|
+
return false;
|
|
148
|
+
try {
|
|
149
|
+
const pending = JSON.parse(fs.readFileSync(path.join((0, claudeProfiles_1.getClaudeProfileDir)(path.basename(loginDir)), 'profile.json'), 'utf8'));
|
|
150
|
+
const addedAt = pending.addedAt ? Date.parse(pending.addedAt) : NaN;
|
|
151
|
+
return !Number.isNaN(addedAt) && now - addedAt < PENDING_LOGIN_GRACE_MS;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function inspectClaudeCredentialStores(options = {}) {
|
|
158
|
+
const now = options.now ?? Date.now();
|
|
159
|
+
let keychainItems = [];
|
|
160
|
+
let keychainScanned = false;
|
|
161
|
+
try {
|
|
162
|
+
keychainItems = (options.listKeychainItems ?? defaultListKeychainItems)();
|
|
163
|
+
keychainScanned = Boolean(options.listKeychainItems) || process.platform === 'darwin';
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
keychainScanned = false;
|
|
167
|
+
}
|
|
168
|
+
const profilesByHome = new Map();
|
|
169
|
+
for (const profile of (0, accountRegistry_1.listSavedAccountProfiles)('claude-code')) {
|
|
170
|
+
profilesByHome.set(path.resolve((0, claudeProfiles_1.getClaudeProfileHome)(profile.providerAccountId ?? profile.id)), profile);
|
|
171
|
+
}
|
|
172
|
+
// Inside a profile-home shell the "live" identity is that profile's own; its
|
|
173
|
+
// store is not a duplicate of anything.
|
|
174
|
+
const liveUuid = (0, claudeProfiles_1.isLiveClaudeHomeSidekickProfile)()
|
|
175
|
+
? null
|
|
176
|
+
: ((0, claudeProfiles_1.readLiveClaudeIdentity)()?.uuid ?? null);
|
|
177
|
+
const findings = [];
|
|
178
|
+
const homes = collectOwnedHomes(keychainItems);
|
|
179
|
+
for (const home of homes) {
|
|
180
|
+
const store = (0, credentialIO_1.readClaudeCredentialStore)(home.configDir);
|
|
181
|
+
if (store.status === 'error')
|
|
182
|
+
continue;
|
|
183
|
+
const profile = profilesByHome.get(home.configDir);
|
|
184
|
+
const base = {
|
|
185
|
+
configDir: home.configDir,
|
|
186
|
+
service: home.service,
|
|
187
|
+
ownership: home.ownership,
|
|
188
|
+
accountId: profile?.id,
|
|
189
|
+
email: profile?.email ?? profile?.metadata?.email,
|
|
190
|
+
};
|
|
191
|
+
if (store.status === 'absent') {
|
|
192
|
+
if (home.inLedger && !profile && !isRecentPendingLogin(home.configDir, now)) {
|
|
193
|
+
findings.push({
|
|
194
|
+
...base,
|
|
195
|
+
kind: 'stale-entry',
|
|
196
|
+
detail: 'Ledger entry for a store that no longer exists.',
|
|
197
|
+
action: 'Forget the entry.',
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (!profile) {
|
|
203
|
+
if (isRecentPendingLogin(home.configDir, now))
|
|
204
|
+
continue;
|
|
205
|
+
findings.push({
|
|
206
|
+
...base,
|
|
207
|
+
kind: 'orphan',
|
|
208
|
+
detail: fs.existsSync(home.configDir)
|
|
209
|
+
? 'Holds a login but belongs to no saved account.'
|
|
210
|
+
: 'Holds a login but its directory no longer exists.',
|
|
211
|
+
action: 'Delete the stored credential.',
|
|
212
|
+
});
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (liveUuid && (profile.providerAccountId ?? profile.id) === liveUuid) {
|
|
216
|
+
findings.push({
|
|
217
|
+
...base,
|
|
218
|
+
kind: 'live-duplicate',
|
|
219
|
+
detail: "The live account's profile home holds a second usable copy of its login; whichever refreshes first invalidates the other.",
|
|
220
|
+
action: 'Remove the copy (installing it into live first if it is newer).',
|
|
221
|
+
});
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (store.status === 'corrupt') {
|
|
225
|
+
findings.push({
|
|
226
|
+
...base,
|
|
227
|
+
kind: 'corrupt',
|
|
228
|
+
detail: 'The stored credential is not valid JSON (possibly truncated); it cannot be used.',
|
|
229
|
+
action: 'Delete the unusable store; sign in to this account again if its backup is expired.',
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return { findings, homesChecked: homes.length, keychainScanned };
|
|
234
|
+
}
|
|
235
|
+
/** Inspect, then (unless `dryRun`) repair every finding. Dry run by default. */
|
|
236
|
+
function repairClaudeCredentialStores(options = {}) {
|
|
237
|
+
const dryRun = options.dryRun ?? true;
|
|
238
|
+
const inspection = inspectClaudeCredentialStores(options);
|
|
239
|
+
const result = { ...inspection, dryRun, repaired: [] };
|
|
240
|
+
if (dryRun)
|
|
241
|
+
return result;
|
|
242
|
+
if (inspection.findings.some((finding) => finding.kind === 'live-duplicate')) {
|
|
243
|
+
result.sync = (0, accounts_1.syncClaudeLiveState)();
|
|
244
|
+
}
|
|
245
|
+
for (const finding of inspection.findings) {
|
|
246
|
+
try {
|
|
247
|
+
if (finding.kind === 'live-duplicate') {
|
|
248
|
+
const still = (0, credentialIO_1.readClaudeCredentialStore)(finding.configDir).status;
|
|
249
|
+
result.repaired.push({
|
|
250
|
+
finding,
|
|
251
|
+
ok: still === 'absent',
|
|
252
|
+
error: still === 'absent' ? undefined : result.sync?.warnings.join(' ') || 'still present',
|
|
253
|
+
});
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (finding.kind === 'stale-entry') {
|
|
257
|
+
(0, claudeCredentialLedger_1.forgetClaudeCredentialHome)(finding.configDir);
|
|
258
|
+
result.repaired.push({ finding, ok: true });
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const ok = (0, credentialIO_1.deleteStoredCredentials)(finding.configDir);
|
|
262
|
+
if (ok && finding.kind === 'orphan') {
|
|
263
|
+
// An abandoned login directory under our profiles dir goes with it.
|
|
264
|
+
const loginDir = path.dirname(finding.configDir);
|
|
265
|
+
if (path.dirname(loginDir) === path.resolve((0, claudeProfiles_1.getClaudeProfilesDir)()) &&
|
|
266
|
+
fs.existsSync(loginDir)) {
|
|
267
|
+
fs.rmSync(loginDir, { recursive: true, force: true });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
result.repaired.push({ finding, ok, error: ok ? undefined : 'could not delete' });
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
result.repaired.push({ finding, ok: false, error: String(err) });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type ClaudeCredentialHomePurpose = 'profile' | 'login';
|
|
2
|
+
export interface ClaudeCredentialLedgerEntry {
|
|
3
|
+
configDir: string;
|
|
4
|
+
/** The Keychain service Claude Code derives for this directory (used on macOS). */
|
|
5
|
+
service: string;
|
|
6
|
+
purpose: ClaudeCredentialHomePurpose;
|
|
7
|
+
createdAt: string;
|
|
8
|
+
lastWrittenAt: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function getClaudeCredentialLedgerPath(): string;
|
|
11
|
+
export declare function readClaudeCredentialLedger(): ClaudeCredentialLedgerEntry[];
|
|
12
|
+
/** Record (or refresh) a home Sidekick is about to hold a Claude credential in. */
|
|
13
|
+
export declare function recordClaudeCredentialHome(configDir: string, purpose: ClaudeCredentialHomePurpose, now?: Date): void;
|
|
14
|
+
/** Drop a home whose credential store has been deleted. */
|
|
15
|
+
export declare function forgetClaudeCredentialHome(configDir: string): void;
|
|
@@ -0,0 +1,130 @@
|
|
|
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.getClaudeCredentialLedgerPath = getClaudeCredentialLedgerPath;
|
|
37
|
+
exports.readClaudeCredentialLedger = readClaudeCredentialLedger;
|
|
38
|
+
exports.recordClaudeCredentialHome = recordClaudeCredentialHome;
|
|
39
|
+
exports.forgetClaudeCredentialHome = forgetClaudeCredentialHome;
|
|
40
|
+
/**
|
|
41
|
+
* Ledger of the Claude credential homes Sidekick created.
|
|
42
|
+
*
|
|
43
|
+
* Claude Code names a config directory's Keychain item after a hash of the
|
|
44
|
+
* directory, so once the directory is gone nothing ties the item back to it.
|
|
45
|
+
* Every home Sidekick writes a credential into (a saved profile) or points a
|
|
46
|
+
* `claude` login at is recorded here first, which is how `accounts doctor`
|
|
47
|
+
* proves an item is Sidekick's before it deletes anything. Items are never
|
|
48
|
+
* matched by name pattern: other tools legitimately use `CLAUDE_CONFIG_DIR`.
|
|
49
|
+
*
|
|
50
|
+
* Best effort by design: a ledger write failure must never fail the credential
|
|
51
|
+
* operation it describes. The lock is a leaf lock (callers may hold the swap
|
|
52
|
+
* or registry lock; nothing is acquired inside it).
|
|
53
|
+
*/
|
|
54
|
+
const fs = __importStar(require("fs"));
|
|
55
|
+
const path = __importStar(require("path"));
|
|
56
|
+
const accountRegistry_1 = require("./accountRegistry");
|
|
57
|
+
const claudeProfiles_1 = require("./claudeProfiles");
|
|
58
|
+
const atomic_1 = require("./writers/atomic");
|
|
59
|
+
function emptyLedger() {
|
|
60
|
+
return { version: 1, entries: [] };
|
|
61
|
+
}
|
|
62
|
+
function normalizeLedger(raw) {
|
|
63
|
+
const entries = raw?.entries;
|
|
64
|
+
if (!Array.isArray(entries))
|
|
65
|
+
return emptyLedger();
|
|
66
|
+
return {
|
|
67
|
+
version: 1,
|
|
68
|
+
entries: entries.filter((entry) => !!entry &&
|
|
69
|
+
typeof entry === 'object' &&
|
|
70
|
+
typeof entry.configDir === 'string' &&
|
|
71
|
+
typeof entry.service === 'string'),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function getClaudeCredentialLedgerPath() {
|
|
75
|
+
return path.join((0, accountRegistry_1.getAccountsDir)(), 'claude', 'credential-homes.json');
|
|
76
|
+
}
|
|
77
|
+
function readClaudeCredentialLedger() {
|
|
78
|
+
try {
|
|
79
|
+
return normalizeLedger(JSON.parse(fs.readFileSync(getClaudeCredentialLedgerPath(), 'utf8')))
|
|
80
|
+
.entries;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Record (or refresh) a home Sidekick is about to hold a Claude credential in. */
|
|
87
|
+
function recordClaudeCredentialHome(configDir, purpose, now = new Date()) {
|
|
88
|
+
const resolved = path.resolve(configDir);
|
|
89
|
+
const stamp = now.toISOString();
|
|
90
|
+
try {
|
|
91
|
+
(0, atomic_1.updateJsonStoreAtomicSync)(getClaudeCredentialLedgerPath(), emptyLedger, (latest) => {
|
|
92
|
+
const ledger = normalizeLedger(latest);
|
|
93
|
+
const existing = ledger.entries.find((entry) => entry.configDir === resolved);
|
|
94
|
+
if (existing) {
|
|
95
|
+
existing.lastWrittenAt = stamp;
|
|
96
|
+
// A login home that became a profile keeps the stronger purpose.
|
|
97
|
+
if (purpose === 'profile')
|
|
98
|
+
existing.purpose = 'profile';
|
|
99
|
+
return ledger;
|
|
100
|
+
}
|
|
101
|
+
ledger.entries.push({
|
|
102
|
+
configDir: resolved,
|
|
103
|
+
service: (0, claudeProfiles_1.claudeKeychainService)(resolved),
|
|
104
|
+
purpose,
|
|
105
|
+
createdAt: stamp,
|
|
106
|
+
lastWrittenAt: stamp,
|
|
107
|
+
});
|
|
108
|
+
return ledger;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
/* best effort: the doctor still scans the profile homes on disk */
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Drop a home whose credential store has been deleted. */
|
|
116
|
+
function forgetClaudeCredentialHome(configDir) {
|
|
117
|
+
const resolved = path.resolve(configDir);
|
|
118
|
+
try {
|
|
119
|
+
if (!fs.existsSync(getClaudeCredentialLedgerPath()))
|
|
120
|
+
return;
|
|
121
|
+
(0, atomic_1.updateJsonStoreAtomicSync)(getClaudeCredentialLedgerPath(), emptyLedger, (latest) => {
|
|
122
|
+
const ledger = normalizeLedger(latest);
|
|
123
|
+
ledger.entries = ledger.entries.filter((entry) => entry.configDir !== resolved);
|
|
124
|
+
return ledger;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
/* best effort */
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure helpers over the Claude Code credential blob (`claudeAiOauth`). No I/O.
|
|
3
|
-
*/
|
|
4
1
|
export interface ClaudeOauthCredentials {
|
|
5
2
|
accessToken: string;
|
|
6
3
|
refreshToken?: string;
|
|
@@ -36,3 +33,23 @@ export declare function resolveClaudeRefreshExpiry(credentials: ClaudeOauthCrede
|
|
|
36
33
|
refreshExpiresAt: number;
|
|
37
34
|
estimated: boolean;
|
|
38
35
|
} | null;
|
|
36
|
+
/**
|
|
37
|
+
* Short, non-reversible fingerprint of the refresh token. Claude Code rotates
|
|
38
|
+
* the refresh token on every refresh, so equal fingerprints mean two stores
|
|
39
|
+
* hold the same link of one chain. Safe to persist and log; tokens never are.
|
|
40
|
+
*/
|
|
41
|
+
export declare function claudeRefreshFingerprint(credentials: ClaudeOauthCredentials | null): string | null;
|
|
42
|
+
/**
|
|
43
|
+
* The part of a blob a profile home keeps: the Claude login only. MCP OAuth
|
|
44
|
+
* tokens (`mcpOAuth`) rotate too and follow the live store, so a profile copy
|
|
45
|
+
* never duplicates one of their chains (and stays far below the Keychain
|
|
46
|
+
* line limit).
|
|
47
|
+
*/
|
|
48
|
+
export declare function claudeLoginOnly(blob: unknown): Record<string, unknown> | null;
|
|
49
|
+
/**
|
|
50
|
+
* The blob to install into the live store when `candidate`'s login replaces
|
|
51
|
+
* the live one: everything live holds (its MCP tokens are the freshest links
|
|
52
|
+
* of their chains) with the candidate's `claudeAiOauth`, plus any MCP server
|
|
53
|
+
* only the candidate knows. Mirrors what Claude Code's own `/login` keeps.
|
|
54
|
+
*/
|
|
55
|
+
export declare function mergeClaudeLoginIntoLive(live: unknown, candidate: unknown): Record<string, unknown>;
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Pure helpers over the Claude Code credential blob (`claudeAiOauth`). No I/O.
|
|
4
|
-
*/
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.CLAUDE_REFRESH_TOKEN_LIFETIME_ESTIMATE_MS = exports.CLAUDE_ACCESS_TOKEN_LIFETIME_MS = void 0;
|
|
7
4
|
exports.parseClaudeCredentialBlob = parseClaudeCredentialBlob;
|
|
8
5
|
exports.claudeCredentialFreshness = claudeCredentialFreshness;
|
|
9
6
|
exports.isNewerClaudeCredential = isNewerClaudeCredential;
|
|
10
7
|
exports.resolveClaudeRefreshExpiry = resolveClaudeRefreshExpiry;
|
|
8
|
+
exports.claudeRefreshFingerprint = claudeRefreshFingerprint;
|
|
9
|
+
exports.claudeLoginOnly = claudeLoginOnly;
|
|
10
|
+
exports.mergeClaudeLoginIntoLive = mergeClaudeLoginIntoLive;
|
|
11
|
+
/**
|
|
12
|
+
* Pure helpers over the Claude Code credential blob (`claudeAiOauth`). No I/O.
|
|
13
|
+
*/
|
|
14
|
+
const crypto_1 = require("crypto");
|
|
11
15
|
/** Lifetime of a Claude Code access token. */
|
|
12
16
|
exports.CLAUDE_ACCESS_TOKEN_LIFETIME_MS = 8 * 60 * 60 * 1000;
|
|
13
17
|
/**
|
|
@@ -72,3 +76,46 @@ function resolveClaudeRefreshExpiry(credentials) {
|
|
|
72
76
|
}
|
|
73
77
|
return null;
|
|
74
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Short, non-reversible fingerprint of the refresh token. Claude Code rotates
|
|
81
|
+
* the refresh token on every refresh, so equal fingerprints mean two stores
|
|
82
|
+
* hold the same link of one chain. Safe to persist and log; tokens never are.
|
|
83
|
+
*/
|
|
84
|
+
function claudeRefreshFingerprint(credentials) {
|
|
85
|
+
const token = credentials?.refreshToken;
|
|
86
|
+
return token ? (0, crypto_1.createHash)('sha256').update(token).digest('hex').slice(0, 16) : null;
|
|
87
|
+
}
|
|
88
|
+
function recordOrUndefined(value) {
|
|
89
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
90
|
+
? value
|
|
91
|
+
: undefined;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The part of a blob a profile home keeps: the Claude login only. MCP OAuth
|
|
95
|
+
* tokens (`mcpOAuth`) rotate too and follow the live store, so a profile copy
|
|
96
|
+
* never duplicates one of their chains (and stays far below the Keychain
|
|
97
|
+
* line limit).
|
|
98
|
+
*/
|
|
99
|
+
function claudeLoginOnly(blob) {
|
|
100
|
+
const oauth = recordOrUndefined(recordOrUndefined(blob)?.claudeAiOauth);
|
|
101
|
+
return oauth ? { claudeAiOauth: oauth } : null;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The blob to install into the live store when `candidate`'s login replaces
|
|
105
|
+
* the live one: everything live holds (its MCP tokens are the freshest links
|
|
106
|
+
* of their chains) with the candidate's `claudeAiOauth`, plus any MCP server
|
|
107
|
+
* only the candidate knows. Mirrors what Claude Code's own `/login` keeps.
|
|
108
|
+
*/
|
|
109
|
+
function mergeClaudeLoginIntoLive(live, candidate) {
|
|
110
|
+
const liveRecord = recordOrUndefined(live) ?? {};
|
|
111
|
+
const candidateRecord = recordOrUndefined(candidate) ?? {};
|
|
112
|
+
const merged = {
|
|
113
|
+
...liveRecord,
|
|
114
|
+
claudeAiOauth: candidateRecord.claudeAiOauth,
|
|
115
|
+
};
|
|
116
|
+
const liveMcp = recordOrUndefined(liveRecord.mcpOAuth);
|
|
117
|
+
const candidateMcp = recordOrUndefined(candidateRecord.mcpOAuth);
|
|
118
|
+
if (liveMcp || candidateMcp)
|
|
119
|
+
merged.mcpOAuth = { ...candidateMcp, ...liveMcp };
|
|
120
|
+
return merged;
|
|
121
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type LiveClaudeWriteIntent = 'switch' | 'relogin' | 'repair' | 'rollback';
|
|
2
|
+
/** The live store as read just before the write. */
|
|
3
|
+
export type LiveClaudeStoreState = {
|
|
4
|
+
status: 'ok';
|
|
5
|
+
blob: unknown;
|
|
6
|
+
} | {
|
|
7
|
+
status: 'absent';
|
|
8
|
+
} | {
|
|
9
|
+
status: 'corrupt';
|
|
10
|
+
} | {
|
|
11
|
+
status: 'error';
|
|
12
|
+
};
|
|
13
|
+
export interface LiveClaudeWriteRequest {
|
|
14
|
+
intent: LiveClaudeWriteIntent;
|
|
15
|
+
live: LiveClaudeStoreState;
|
|
16
|
+
/** Account uuid of the live identity (`oauthAccount`), null when logged out. */
|
|
17
|
+
liveAccountUuid: string | null;
|
|
18
|
+
candidate: unknown;
|
|
19
|
+
candidateAccountUuid: string;
|
|
20
|
+
/** `switch`: the outgoing live credential was saved and verified. */
|
|
21
|
+
capturedOutgoing?: boolean;
|
|
22
|
+
/** `rollback`: the blob the failed switch wrote. */
|
|
23
|
+
written?: unknown;
|
|
24
|
+
}
|
|
25
|
+
export type LiveClaudeWriteRefusal = 'live-unreadable' | 'invalid-candidate' | 'same-account' | 'different-account' | 'same-link' | 'not-captured' | 'not-newer' | 'live-changed';
|
|
26
|
+
export type LiveClaudeWriteDecision = {
|
|
27
|
+
allow: true;
|
|
28
|
+
} | {
|
|
29
|
+
allow: false;
|
|
30
|
+
code: LiveClaudeWriteRefusal;
|
|
31
|
+
reason: string;
|
|
32
|
+
};
|
|
33
|
+
export declare function decideLiveClaudeWrite(request: LiveClaudeWriteRequest): LiveClaudeWriteDecision;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.decideLiveClaudeWrite = decideLiveClaudeWrite;
|
|
4
|
+
/**
|
|
5
|
+
* The decision every write into the live Claude credential store must pass.
|
|
6
|
+
* Pure: callers read the stores, this decides.
|
|
7
|
+
*
|
|
8
|
+
* Claude Code rotates the refresh token on every refresh, so the live store
|
|
9
|
+
* always holds the newest link of its chain. Writing an older link over it
|
|
10
|
+
* destroys a working login, and installing another account without saving the
|
|
11
|
+
* live one first loses that account's chain. The rules:
|
|
12
|
+
*
|
|
13
|
+
* - `switch`: another account replaces live. Allowed only after the outgoing
|
|
14
|
+
* live credential was captured (whenever live holds one), and never with a
|
|
15
|
+
* candidate that is the same refresh-token link as live.
|
|
16
|
+
* - `relogin` / `repair`: the same account's credential replaces live. Allowed
|
|
17
|
+
* only when the candidate is strictly fresher (a different refresh token
|
|
18
|
+
* with a later expiry); otherwise live is kept.
|
|
19
|
+
* - `rollback`: restore what a failed switch displaced. Allowed only while the
|
|
20
|
+
* live store still holds exactly what that switch wrote (or is empty or
|
|
21
|
+
* unparseable); a write Claude Code made in between is never clobbered.
|
|
22
|
+
* - An unreadable live store (`error`: timeout, locked Keychain) refuses all.
|
|
23
|
+
*/
|
|
24
|
+
const liveCredentialGuard_1 = require("./liveCredentialGuard");
|
|
25
|
+
const claudeCredentials_1 = require("./claudeCredentials");
|
|
26
|
+
function decideLiveClaudeWrite(request) {
|
|
27
|
+
const candidate = (0, claudeCredentials_1.parseClaudeCredentialBlob)(request.candidate);
|
|
28
|
+
const live = request.live.status === 'ok' ? (0, claudeCredentials_1.parseClaudeCredentialBlob)(request.live.blob) : null;
|
|
29
|
+
const liveFp = (0, claudeCredentials_1.claudeRefreshFingerprint)(live);
|
|
30
|
+
return (0, liveCredentialGuard_1.decideLiveCredentialWrite)({
|
|
31
|
+
provider: 'Claude',
|
|
32
|
+
intent: request.intent,
|
|
33
|
+
liveStatus: request.live.status,
|
|
34
|
+
candidateValid: candidate !== null,
|
|
35
|
+
liveHasCredential: live !== null,
|
|
36
|
+
liveAccountId: request.liveAccountUuid,
|
|
37
|
+
candidateAccountId: request.candidateAccountUuid,
|
|
38
|
+
sameLink: liveFp !== null && liveFp === (0, claudeCredentials_1.claudeRefreshFingerprint)(candidate),
|
|
39
|
+
strictlyNewer: (0, claudeCredentials_1.claudeCredentialFreshness)(candidate) > (0, claudeCredentials_1.claudeCredentialFreshness)(live),
|
|
40
|
+
capturedOutgoing: request.capturedOutgoing,
|
|
41
|
+
matchesWritten: request.live.status === 'ok' &&
|
|
42
|
+
JSON.stringify(request.live.blob) === JSON.stringify(request.written),
|
|
43
|
+
allowNonOkRollback: true,
|
|
44
|
+
requireDifferentLinkForAdvance: true,
|
|
45
|
+
});
|
|
46
|
+
}
|
package/dist/claudeProfiles.d.ts
CHANGED
|
@@ -18,8 +18,22 @@ export declare function isDefaultClaudeHome(): boolean;
|
|
|
18
18
|
*/
|
|
19
19
|
export declare function getLiveClaudeHome(): string;
|
|
20
20
|
export declare function getClaudeProfilesDir(): string;
|
|
21
|
+
/**
|
|
22
|
+
* True when the live home is one of sidekick's own profile homes: a shell
|
|
23
|
+
* that eval'd `sidekick accounts env` or runs inside `sidekick accounts shell`.
|
|
24
|
+
* That profile's store then reads as the live store, so sync, re-login, and
|
|
25
|
+
* switching must not treat it as live: they would delete the shell's only
|
|
26
|
+
* usable copy and re-point the active account at it.
|
|
27
|
+
*/
|
|
28
|
+
export declare function isLiveClaudeHomeSidekickProfile(): boolean;
|
|
21
29
|
export declare function getClaudeProfileDir(uuid: string): string;
|
|
22
30
|
export declare function getClaudeProfileHome(uuid: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Sidekick's private backup of a profile's credential. Claude Code never reads
|
|
33
|
+
* it, so it is not a CLI-usable copy: this is where the live account's
|
|
34
|
+
* credential is mirrored while its profile home stays empty.
|
|
35
|
+
*/
|
|
36
|
+
export declare function getClaudeCredentialBackupPath(uuid: string): string;
|
|
23
37
|
export declare function ensureClaudeProfileDirs(uuid: string): void;
|
|
24
38
|
/**
|
|
25
39
|
* Claude Code hashes the NFC-normalised config directory; macOS hands paths
|