codex-multi-auth 0.1.5 → 0.1.8
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/LICENSE +21 -1
- package/README.md +19 -6
- package/assets/readme-hero.svg +31 -31
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/codex-cli/state.js +4 -4
- package/dist/lib/codex-cli/state.js.map +1 -1
- package/dist/lib/codex-cli/sync.d.ts +9 -16
- package/dist/lib/codex-cli/sync.d.ts.map +1 -1
- package/dist/lib/codex-cli/sync.js +51 -284
- package/dist/lib/codex-cli/sync.js.map +1 -1
- package/dist/lib/codex-cli/writer.d.ts.map +1 -1
- package/dist/lib/codex-cli/writer.js +62 -5
- package/dist/lib/codex-cli/writer.js.map +1 -1
- package/dist/lib/codex-manager/settings-hub.d.ts +44 -2
- package/dist/lib/codex-manager/settings-hub.d.ts.map +1 -1
- package/dist/lib/codex-manager/settings-hub.js +746 -117
- package/dist/lib/codex-manager/settings-hub.js.map +1 -1
- package/dist/lib/codex-manager.d.ts.map +1 -1
- package/dist/lib/codex-manager.js +3 -8
- package/dist/lib/codex-manager.js.map +1 -1
- package/dist/lib/context-overflow.js +10 -10
- package/dist/lib/named-backup-export.d.ts +69 -0
- package/dist/lib/named-backup-export.d.ts.map +1 -0
- package/dist/lib/named-backup-export.js +207 -0
- package/dist/lib/named-backup-export.js.map +1 -0
- package/dist/lib/oauth-success.html +66 -210
- package/dist/lib/oc-chatgpt-import-adapter.d.ts +64 -0
- package/dist/lib/oc-chatgpt-import-adapter.d.ts.map +1 -0
- package/dist/lib/oc-chatgpt-import-adapter.js +506 -0
- package/dist/lib/oc-chatgpt-import-adapter.js.map +1 -0
- package/dist/lib/oc-chatgpt-orchestrator.d.ts +80 -0
- package/dist/lib/oc-chatgpt-orchestrator.d.ts.map +1 -0
- package/dist/lib/oc-chatgpt-orchestrator.js +130 -0
- package/dist/lib/oc-chatgpt-orchestrator.js.map +1 -0
- package/dist/lib/oc-chatgpt-target-detection.d.ts +59 -0
- package/dist/lib/oc-chatgpt-target-detection.d.ts.map +1 -0
- package/dist/lib/oc-chatgpt-target-detection.js +327 -0
- package/dist/lib/oc-chatgpt-target-detection.js.map +1 -0
- package/dist/lib/prompts/codex.js +2 -2
- package/dist/lib/runtime-paths.d.ts.map +1 -1
- package/dist/lib/runtime-paths.js +44 -2
- package/dist/lib/runtime-paths.js.map +1 -1
- package/dist/lib/storage.d.ts +42 -3
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/storage.js +634 -49
- package/dist/lib/storage.js.map +1 -1
- package/dist/lib/ui/auth-menu.d.ts.map +1 -1
- package/dist/lib/ui/auth-menu.js +13 -1
- package/dist/lib/ui/auth-menu.js.map +1 -1
- package/dist/lib/ui/copy.d.ts +15 -1
- package/dist/lib/ui/copy.d.ts.map +1 -1
- package/dist/lib/ui/copy.js +15 -1
- package/dist/lib/ui/copy.js.map +1 -1
- package/node_modules/@codex-ai/plugin/dist/index.d.ts +1 -1
- package/node_modules/@codex-ai/plugin/dist/tool.d.ts +41 -41
- package/node_modules/@codex-ai/plugin/dist/tool.js +28 -28
- package/package.json +132 -133
- package/scripts/bench-format/hashline-v2.mjs +642 -642
- package/scripts/bench-format/render.mjs +496 -496
- package/scripts/bench-format/stats.mjs +54 -54
- package/scripts/bench-format/tasks.mjs +151 -151
- package/scripts/benchmark-render-dashboard.mjs +49 -49
- package/scripts/codex-multi-auth.js +12 -0
- package/scripts/codex.js +315 -2
- package/scripts/install-codex-auth.js +9 -9
- package/scripts/test-all-models.sh +7 -7
- package/scripts/test-model-matrix.js +247 -148
- package/scripts/validate-model-map.sh +7 -7
- package/vendor/codex-ai-plugin/dist/index.d.ts +1 -1
- package/vendor/codex-ai-plugin/dist/tool.d.ts +41 -41
- package/vendor/codex-ai-plugin/dist/tool.js +28 -28
- package/vendor/codex-ai-sdk/dist/index.d.ts +3 -3
package/dist/lib/storage.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
2
|
import { createHash } from "node:crypto";
|
|
3
|
+
import { existsSync, promises as fs } from "node:fs";
|
|
4
|
+
import { basename, dirname, join } from "node:path";
|
|
4
5
|
import { ACCOUNT_LIMITS } from "./constants.js";
|
|
5
6
|
import { createLogger } from "./logger.js";
|
|
7
|
+
import { exportNamedBackupFile, resolveNamedBackupPath, } from "./named-backup-export.js";
|
|
6
8
|
import { MODEL_FAMILIES } from "./prompts/codex.js";
|
|
7
9
|
import { AnyAccountStorageSchema, getValidationErrors } from "./schemas.js";
|
|
8
|
-
import { getConfigDir, getProjectConfigDir, getProjectGlobalConfigDir, findProjectRoot, resolvePath, resolveProjectStorageIdentityRoot, } from "./storage/paths.js";
|
|
9
10
|
import { migrateV1ToV3, } from "./storage/migrations.js";
|
|
11
|
+
import { findProjectRoot, getConfigDir, getProjectConfigDir, getProjectGlobalConfigDir, resolvePath, resolveProjectStorageIdentityRoot, } from "./storage/paths.js";
|
|
10
12
|
const log = createLogger("storage");
|
|
11
13
|
const ACCOUNTS_FILE_NAME = "openai-codex-accounts.json";
|
|
12
14
|
const FLAGGED_ACCOUNTS_FILE_NAME = "openai-codex-flagged-accounts.json";
|
|
@@ -16,6 +18,7 @@ const ACCOUNTS_WAL_SUFFIX = ".wal";
|
|
|
16
18
|
const ACCOUNTS_BACKUP_HISTORY_DEPTH = 3;
|
|
17
19
|
const BACKUP_COPY_MAX_ATTEMPTS = 5;
|
|
18
20
|
const BACKUP_COPY_BASE_DELAY_MS = 10;
|
|
21
|
+
const RESET_MARKER_SUFFIX = ".reset-intent";
|
|
19
22
|
let storageBackupEnabled = true;
|
|
20
23
|
let lastAccountsSaveTimestamp = 0;
|
|
21
24
|
/**
|
|
@@ -59,6 +62,7 @@ export function formatStorageErrorHint(error, path) {
|
|
|
59
62
|
}
|
|
60
63
|
}
|
|
61
64
|
let storageMutex = Promise.resolve();
|
|
65
|
+
const transactionSnapshotContext = new AsyncLocalStorage();
|
|
62
66
|
function withStorageLock(fn) {
|
|
63
67
|
const previousMutex = storageMutex;
|
|
64
68
|
let releaseLock;
|
|
@@ -67,6 +71,32 @@ function withStorageLock(fn) {
|
|
|
67
71
|
});
|
|
68
72
|
return previousMutex.then(fn).finally(() => releaseLock());
|
|
69
73
|
}
|
|
74
|
+
function looksLikeSyntheticFixtureAccount(account) {
|
|
75
|
+
const email = typeof account.email === "string" ? account.email.trim().toLowerCase() : "";
|
|
76
|
+
const refreshToken = typeof account.refreshToken === "string"
|
|
77
|
+
? account.refreshToken.trim().toLowerCase()
|
|
78
|
+
: "";
|
|
79
|
+
const accountId = typeof account.accountId === "string"
|
|
80
|
+
? account.accountId.trim().toLowerCase()
|
|
81
|
+
: "";
|
|
82
|
+
if (!/^account\d+@example\.com$/.test(email)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
const hasSyntheticRefreshToken = refreshToken.startsWith("fake_refresh") ||
|
|
86
|
+
/^fake_refresh_token_\d+(_for_testing_only)?$/.test(refreshToken);
|
|
87
|
+
if (!hasSyntheticRefreshToken) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
if (accountId.length === 0) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
return /^acc(_|-)?\d+$/.test(accountId);
|
|
94
|
+
}
|
|
95
|
+
function looksLikeSyntheticFixtureStorage(storage) {
|
|
96
|
+
if (!storage || storage.accounts.length === 0)
|
|
97
|
+
return false;
|
|
98
|
+
return storage.accounts.every((account) => looksLikeSyntheticFixtureAccount(account));
|
|
99
|
+
}
|
|
70
100
|
async function ensureGitignore(storagePath) {
|
|
71
101
|
if (!currentStoragePath)
|
|
72
102
|
return;
|
|
@@ -82,7 +112,10 @@ async function ensureGitignore(storagePath) {
|
|
|
82
112
|
if (existsSync(gitignorePath)) {
|
|
83
113
|
content = await fs.readFile(gitignorePath, "utf-8");
|
|
84
114
|
const lines = content.split("\n").map((l) => l.trim());
|
|
85
|
-
if (lines.includes(".codex") ||
|
|
115
|
+
if (lines.includes(".codex") ||
|
|
116
|
+
lines.includes(".codex/") ||
|
|
117
|
+
lines.includes("/.codex") ||
|
|
118
|
+
lines.includes("/.codex/")) {
|
|
86
119
|
return;
|
|
87
120
|
}
|
|
88
121
|
}
|
|
@@ -117,6 +150,47 @@ function getAccountsBackupRecoveryCandidates(path) {
|
|
|
117
150
|
}
|
|
118
151
|
return candidates;
|
|
119
152
|
}
|
|
153
|
+
async function getAccountsBackupRecoveryCandidatesWithDiscovery(path) {
|
|
154
|
+
const knownCandidates = getAccountsBackupRecoveryCandidates(path);
|
|
155
|
+
const discoveredCandidates = new Set();
|
|
156
|
+
const candidatePrefix = `${basename(path)}.`;
|
|
157
|
+
const knownCandidateSet = new Set(knownCandidates);
|
|
158
|
+
const directoryPath = dirname(path);
|
|
159
|
+
try {
|
|
160
|
+
const entries = await fs.readdir(directoryPath, { withFileTypes: true });
|
|
161
|
+
for (const entry of entries) {
|
|
162
|
+
if (!entry.isFile())
|
|
163
|
+
continue;
|
|
164
|
+
if (!entry.name.startsWith(candidatePrefix))
|
|
165
|
+
continue;
|
|
166
|
+
if (isCacheLikeBackupArtifactName(entry.name))
|
|
167
|
+
continue;
|
|
168
|
+
if (entry.name.endsWith(RESET_MARKER_SUFFIX))
|
|
169
|
+
continue;
|
|
170
|
+
if (entry.name.endsWith(".tmp"))
|
|
171
|
+
continue;
|
|
172
|
+
if (entry.name.includes(".rotate."))
|
|
173
|
+
continue;
|
|
174
|
+
if (entry.name.endsWith(ACCOUNTS_WAL_SUFFIX))
|
|
175
|
+
continue;
|
|
176
|
+
const candidatePath = join(directoryPath, entry.name);
|
|
177
|
+
if (knownCandidateSet.has(candidatePath))
|
|
178
|
+
continue;
|
|
179
|
+
discoveredCandidates.add(candidatePath);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
const code = error.code;
|
|
184
|
+
if (code !== "ENOENT") {
|
|
185
|
+
log.warn("Failed to discover account backup candidates", {
|
|
186
|
+
path,
|
|
187
|
+
error: String(error),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const discoveredOrdered = Array.from(discoveredCandidates).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
|
|
192
|
+
return [...knownCandidates, ...discoveredOrdered];
|
|
193
|
+
}
|
|
120
194
|
function getAccountsWalPath(path) {
|
|
121
195
|
return `${path}${ACCOUNTS_WAL_SUFFIX}`;
|
|
122
196
|
}
|
|
@@ -173,7 +247,9 @@ async function createRotatingAccountsBackup(path) {
|
|
|
173
247
|
continue;
|
|
174
248
|
}
|
|
175
249
|
const stagedPath = buildStagedPath(currentPath, `slot-${i}`);
|
|
176
|
-
await copyFileWithRetry(previousPath, stagedPath, {
|
|
250
|
+
await copyFileWithRetry(previousPath, stagedPath, {
|
|
251
|
+
allowMissingSource: true,
|
|
252
|
+
});
|
|
177
253
|
if (existsSync(stagedPath)) {
|
|
178
254
|
stagedWrites.push({ targetPath: currentPath, stagedPath });
|
|
179
255
|
}
|
|
@@ -185,7 +261,10 @@ async function createRotatingAccountsBackup(path) {
|
|
|
185
261
|
const latestStagedPath = buildStagedPath(latestBackupPath, "latest");
|
|
186
262
|
await copyFileWithRetry(path, latestStagedPath);
|
|
187
263
|
if (existsSync(latestStagedPath)) {
|
|
188
|
-
stagedWrites.push({
|
|
264
|
+
stagedWrites.push({
|
|
265
|
+
targetPath: latestBackupPath,
|
|
266
|
+
stagedPath: latestStagedPath,
|
|
267
|
+
});
|
|
189
268
|
}
|
|
190
269
|
for (const stagedWrite of stagedWrites) {
|
|
191
270
|
await renameFileWithRetry(stagedWrite.stagedPath, stagedWrite.targetPath);
|
|
@@ -207,7 +286,8 @@ async function createRotatingAccountsBackup(path) {
|
|
|
207
286
|
}
|
|
208
287
|
function isRotatingBackupTempArtifact(storagePath, candidatePath) {
|
|
209
288
|
const backupPrefix = `${storagePath}${ACCOUNTS_BACKUP_SUFFIX}`;
|
|
210
|
-
if (!candidatePath.startsWith(backupPrefix) ||
|
|
289
|
+
if (!candidatePath.startsWith(backupPrefix) ||
|
|
290
|
+
!candidatePath.endsWith(".tmp")) {
|
|
211
291
|
return false;
|
|
212
292
|
}
|
|
213
293
|
const suffix = candidatePath.slice(backupPrefix.length);
|
|
@@ -224,7 +304,9 @@ function isRotatingBackupTempArtifact(storagePath, candidatePath) {
|
|
|
224
304
|
async function cleanupStaleRotatingBackupArtifacts(path) {
|
|
225
305
|
const directoryPath = dirname(path);
|
|
226
306
|
try {
|
|
227
|
-
const directoryEntries = await fs.readdir(directoryPath, {
|
|
307
|
+
const directoryEntries = await fs.readdir(directoryPath, {
|
|
308
|
+
withFileTypes: true,
|
|
309
|
+
});
|
|
228
310
|
const staleArtifacts = directoryEntries
|
|
229
311
|
.filter((entry) => entry.isFile())
|
|
230
312
|
.map((entry) => join(directoryPath, entry.name))
|
|
@@ -257,6 +339,198 @@ async function cleanupStaleRotatingBackupArtifacts(path) {
|
|
|
257
339
|
function computeSha256(value) {
|
|
258
340
|
return createHash("sha256").update(value).digest("hex");
|
|
259
341
|
}
|
|
342
|
+
function getIntentionalResetMarkerPath(path) {
|
|
343
|
+
return `${path}${RESET_MARKER_SUFFIX}`;
|
|
344
|
+
}
|
|
345
|
+
function createEmptyStorageWithMetadata(restoreEligible, restoreReason) {
|
|
346
|
+
return {
|
|
347
|
+
version: 3,
|
|
348
|
+
accounts: [],
|
|
349
|
+
activeIndex: 0,
|
|
350
|
+
activeIndexByFamily: {},
|
|
351
|
+
restoreEligible,
|
|
352
|
+
restoreReason,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function withRestoreMetadata(storage, restoreEligible, restoreReason) {
|
|
356
|
+
return {
|
|
357
|
+
...storage,
|
|
358
|
+
restoreEligible,
|
|
359
|
+
restoreReason,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function isCacheLikeBackupArtifactName(entryName) {
|
|
363
|
+
return entryName.toLowerCase().includes(".cache");
|
|
364
|
+
}
|
|
365
|
+
async function statSnapshot(path) {
|
|
366
|
+
try {
|
|
367
|
+
const stats = await fs.stat(path);
|
|
368
|
+
return { exists: true, bytes: stats.size, mtimeMs: stats.mtimeMs };
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
const code = error.code;
|
|
372
|
+
if (code !== "ENOENT") {
|
|
373
|
+
log.warn("Failed to stat backup candidate", {
|
|
374
|
+
path,
|
|
375
|
+
error: String(error),
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
return { exists: false };
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
async function describeAccountSnapshot(path, kind, index) {
|
|
382
|
+
const stats = await statSnapshot(path);
|
|
383
|
+
if (!stats.exists) {
|
|
384
|
+
return { kind, path, index, exists: false, valid: false };
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
const { normalized, schemaErrors, storedVersion } = await loadAccountsFromPath(path);
|
|
388
|
+
return {
|
|
389
|
+
kind,
|
|
390
|
+
path,
|
|
391
|
+
index,
|
|
392
|
+
exists: true,
|
|
393
|
+
valid: !!normalized,
|
|
394
|
+
bytes: stats.bytes,
|
|
395
|
+
mtimeMs: stats.mtimeMs,
|
|
396
|
+
version: typeof storedVersion === "number" ? storedVersion : undefined,
|
|
397
|
+
accountCount: normalized?.accounts.length,
|
|
398
|
+
schemaErrors: schemaErrors.length > 0 ? schemaErrors : undefined,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
const code = error.code;
|
|
403
|
+
if (code !== "ENOENT") {
|
|
404
|
+
log.warn("Failed to inspect account snapshot", {
|
|
405
|
+
path,
|
|
406
|
+
error: String(error),
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
kind,
|
|
411
|
+
path,
|
|
412
|
+
index,
|
|
413
|
+
exists: true,
|
|
414
|
+
valid: false,
|
|
415
|
+
bytes: stats.bytes,
|
|
416
|
+
mtimeMs: stats.mtimeMs,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async function describeAccountsWalSnapshot(path) {
|
|
421
|
+
const stats = await statSnapshot(path);
|
|
422
|
+
if (!stats.exists) {
|
|
423
|
+
return { kind: "accounts-wal", path, exists: false, valid: false };
|
|
424
|
+
}
|
|
425
|
+
try {
|
|
426
|
+
const raw = await fs.readFile(path, "utf-8");
|
|
427
|
+
const parsed = JSON.parse(raw);
|
|
428
|
+
if (!isRecord(parsed)) {
|
|
429
|
+
return {
|
|
430
|
+
kind: "accounts-wal",
|
|
431
|
+
path,
|
|
432
|
+
exists: true,
|
|
433
|
+
valid: false,
|
|
434
|
+
bytes: stats.bytes,
|
|
435
|
+
mtimeMs: stats.mtimeMs,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
const entry = parsed;
|
|
439
|
+
if (entry.version !== 1 ||
|
|
440
|
+
typeof entry.content !== "string" ||
|
|
441
|
+
typeof entry.checksum !== "string" ||
|
|
442
|
+
computeSha256(entry.content) !== entry.checksum) {
|
|
443
|
+
return {
|
|
444
|
+
kind: "accounts-wal",
|
|
445
|
+
path,
|
|
446
|
+
exists: true,
|
|
447
|
+
valid: false,
|
|
448
|
+
bytes: stats.bytes,
|
|
449
|
+
mtimeMs: stats.mtimeMs,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
const { normalized, storedVersion, schemaErrors } = parseAndNormalizeStorage(JSON.parse(entry.content));
|
|
453
|
+
return {
|
|
454
|
+
kind: "accounts-wal",
|
|
455
|
+
path,
|
|
456
|
+
exists: true,
|
|
457
|
+
valid: !!normalized,
|
|
458
|
+
bytes: stats.bytes,
|
|
459
|
+
mtimeMs: stats.mtimeMs,
|
|
460
|
+
version: typeof storedVersion === "number" ? storedVersion : undefined,
|
|
461
|
+
accountCount: normalized?.accounts.length,
|
|
462
|
+
schemaErrors: schemaErrors.length > 0 ? schemaErrors : undefined,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
return {
|
|
467
|
+
kind: "accounts-wal",
|
|
468
|
+
path,
|
|
469
|
+
exists: true,
|
|
470
|
+
valid: false,
|
|
471
|
+
bytes: stats.bytes,
|
|
472
|
+
mtimeMs: stats.mtimeMs,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
async function loadFlaggedAccountsFromPath(path) {
|
|
477
|
+
const content = await fs.readFile(path, "utf-8");
|
|
478
|
+
const data = JSON.parse(content);
|
|
479
|
+
return normalizeFlaggedStorage(data);
|
|
480
|
+
}
|
|
481
|
+
async function describeFlaggedSnapshot(path, kind, index) {
|
|
482
|
+
const stats = await statSnapshot(path);
|
|
483
|
+
if (!stats.exists) {
|
|
484
|
+
return { kind, path, index, exists: false, valid: false };
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
const storage = await loadFlaggedAccountsFromPath(path);
|
|
488
|
+
return {
|
|
489
|
+
kind,
|
|
490
|
+
path,
|
|
491
|
+
index,
|
|
492
|
+
exists: true,
|
|
493
|
+
valid: true,
|
|
494
|
+
bytes: stats.bytes,
|
|
495
|
+
mtimeMs: stats.mtimeMs,
|
|
496
|
+
version: storage.version,
|
|
497
|
+
flaggedCount: storage.accounts.length,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
const code = error.code;
|
|
502
|
+
if (code !== "ENOENT") {
|
|
503
|
+
log.warn("Failed to inspect flagged snapshot", {
|
|
504
|
+
path,
|
|
505
|
+
error: String(error),
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
return {
|
|
509
|
+
kind,
|
|
510
|
+
path,
|
|
511
|
+
index,
|
|
512
|
+
exists: true,
|
|
513
|
+
valid: false,
|
|
514
|
+
bytes: stats.bytes,
|
|
515
|
+
mtimeMs: stats.mtimeMs,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
function latestValidSnapshot(snapshots) {
|
|
520
|
+
return snapshots
|
|
521
|
+
.filter((snapshot) => snapshot.valid)
|
|
522
|
+
.sort((left, right) => (right.mtimeMs ?? 0) - (left.mtimeMs ?? 0))[0];
|
|
523
|
+
}
|
|
524
|
+
function buildMetadataSection(storagePath, snapshots) {
|
|
525
|
+
const latestValid = latestValidSnapshot(snapshots);
|
|
526
|
+
return {
|
|
527
|
+
storagePath,
|
|
528
|
+
latestValidPath: latestValid?.path,
|
|
529
|
+
snapshotCount: snapshots.length,
|
|
530
|
+
validSnapshotCount: snapshots.filter((snapshot) => snapshot.valid).length,
|
|
531
|
+
snapshots,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
260
534
|
export function getLastAccountsSaveTimestamp() {
|
|
261
535
|
return lastAccountsSaveTimestamp;
|
|
262
536
|
}
|
|
@@ -276,7 +550,9 @@ export function setStoragePath(projectPath) {
|
|
|
276
550
|
currentLegacyProjectStoragePath = join(getProjectConfigDir(projectRoot), ACCOUNTS_FILE_NAME);
|
|
277
551
|
const previousWorktreeScopedPath = join(getProjectGlobalConfigDir(projectRoot), ACCOUNTS_FILE_NAME);
|
|
278
552
|
currentLegacyWorktreeStoragePath =
|
|
279
|
-
previousWorktreeScopedPath !== currentStoragePath
|
|
553
|
+
previousWorktreeScopedPath !== currentStoragePath
|
|
554
|
+
? previousWorktreeScopedPath
|
|
555
|
+
: null;
|
|
280
556
|
}
|
|
281
557
|
else {
|
|
282
558
|
currentStoragePath = null;
|
|
@@ -301,6 +577,15 @@ export function getStoragePath() {
|
|
|
301
577
|
}
|
|
302
578
|
return join(getConfigDir(), ACCOUNTS_FILE_NAME);
|
|
303
579
|
}
|
|
580
|
+
export function buildNamedBackupPath(name) {
|
|
581
|
+
return resolveNamedBackupPath(name, getStoragePath());
|
|
582
|
+
}
|
|
583
|
+
export async function exportNamedBackup(name, options) {
|
|
584
|
+
return exportNamedBackupFile(name, {
|
|
585
|
+
getStoragePath,
|
|
586
|
+
exportAccounts,
|
|
587
|
+
}, options);
|
|
588
|
+
}
|
|
304
589
|
export function getFlaggedAccountsPath() {
|
|
305
590
|
return join(dirname(getStoragePath()), FLAGGED_ACCOUNTS_FILE_NAME);
|
|
306
591
|
}
|
|
@@ -311,8 +596,13 @@ async function migrateLegacyProjectStorageIfNeeded(persist = saveAccounts) {
|
|
|
311
596
|
if (!currentStoragePath) {
|
|
312
597
|
return null;
|
|
313
598
|
}
|
|
314
|
-
const candidatePaths = [
|
|
315
|
-
|
|
599
|
+
const candidatePaths = [
|
|
600
|
+
currentLegacyWorktreeStoragePath,
|
|
601
|
+
currentLegacyProjectStoragePath,
|
|
602
|
+
]
|
|
603
|
+
.filter((path) => typeof path === "string" &&
|
|
604
|
+
path.length > 0 &&
|
|
605
|
+
path !== currentStoragePath)
|
|
316
606
|
.filter((path, index, all) => all.indexOf(path) === index);
|
|
317
607
|
if (candidatePaths.length === 0) {
|
|
318
608
|
return null;
|
|
@@ -506,7 +796,8 @@ export function deduplicateAccountsByEmail(accounts) {
|
|
|
506
796
|
const existingAddedAt = existing.addedAt || 0;
|
|
507
797
|
const candidateAddedAt = account.addedAt || 0;
|
|
508
798
|
const isNewer = candidateLastUsed > existingLastUsed ||
|
|
509
|
-
(candidateLastUsed === existingLastUsed &&
|
|
799
|
+
(candidateLastUsed === existingLastUsed &&
|
|
800
|
+
candidateAddedAt > existingAddedAt);
|
|
510
801
|
if (isNewer) {
|
|
511
802
|
emailToNewestIndex.set(email, i);
|
|
512
803
|
}
|
|
@@ -578,7 +869,9 @@ export function normalizeAccountStorage(data) {
|
|
|
578
869
|
const baseStorage = fromVersion === 1
|
|
579
870
|
? migrateV1ToV3(data)
|
|
580
871
|
: data;
|
|
581
|
-
const validAccounts = rawAccounts.filter((account) => isRecord(account) &&
|
|
872
|
+
const validAccounts = rawAccounts.filter((account) => isRecord(account) &&
|
|
873
|
+
typeof account.refreshToken === "string" &&
|
|
874
|
+
!!account.refreshToken.trim());
|
|
582
875
|
const deduplicatedAccounts = deduplicateAccountsByEmail(deduplicateAccountsByKey(validAccounts));
|
|
583
876
|
const activeIndex = (() => {
|
|
584
877
|
if (deduplicatedAccounts.length === 0)
|
|
@@ -625,10 +918,88 @@ export function normalizeAccountStorage(data) {
|
|
|
625
918
|
export async function loadAccounts() {
|
|
626
919
|
return loadAccountsInternal(saveAccounts);
|
|
627
920
|
}
|
|
921
|
+
export async function getBackupMetadata() {
|
|
922
|
+
const storagePath = getStoragePath();
|
|
923
|
+
const walPath = getAccountsWalPath(storagePath);
|
|
924
|
+
const accountCandidates = await getAccountsBackupRecoveryCandidatesWithDiscovery(storagePath);
|
|
925
|
+
const accountSnapshots = [
|
|
926
|
+
await describeAccountSnapshot(storagePath, "accounts-primary"),
|
|
927
|
+
await describeAccountsWalSnapshot(walPath),
|
|
928
|
+
];
|
|
929
|
+
for (const [index, candidate] of accountCandidates.entries()) {
|
|
930
|
+
const kind = candidate === `${storagePath}.bak`
|
|
931
|
+
? "accounts-backup"
|
|
932
|
+
: candidate.startsWith(`${storagePath}.bak.`)
|
|
933
|
+
? "accounts-backup-history"
|
|
934
|
+
: "accounts-discovered-backup";
|
|
935
|
+
accountSnapshots.push(await describeAccountSnapshot(candidate, kind, index));
|
|
936
|
+
}
|
|
937
|
+
const flaggedPath = getFlaggedAccountsPath();
|
|
938
|
+
const flaggedCandidates = await getAccountsBackupRecoveryCandidatesWithDiscovery(flaggedPath);
|
|
939
|
+
const flaggedSnapshots = [
|
|
940
|
+
await describeFlaggedSnapshot(flaggedPath, "flagged-primary"),
|
|
941
|
+
];
|
|
942
|
+
for (const [index, candidate] of flaggedCandidates.entries()) {
|
|
943
|
+
const kind = candidate === `${flaggedPath}.bak`
|
|
944
|
+
? "flagged-backup"
|
|
945
|
+
: candidate.startsWith(`${flaggedPath}.bak.`)
|
|
946
|
+
? "flagged-backup-history"
|
|
947
|
+
: "flagged-discovered-backup";
|
|
948
|
+
flaggedSnapshots.push(await describeFlaggedSnapshot(candidate, kind, index));
|
|
949
|
+
}
|
|
950
|
+
return {
|
|
951
|
+
accounts: buildMetadataSection(storagePath, accountSnapshots),
|
|
952
|
+
flaggedAccounts: buildMetadataSection(flaggedPath, flaggedSnapshots),
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
export async function getRestoreAssessment() {
|
|
956
|
+
const storagePath = getStoragePath();
|
|
957
|
+
const resetMarkerPath = getIntentionalResetMarkerPath(storagePath);
|
|
958
|
+
const backupMetadata = await getBackupMetadata();
|
|
959
|
+
if (existsSync(resetMarkerPath)) {
|
|
960
|
+
return {
|
|
961
|
+
storagePath,
|
|
962
|
+
restoreEligible: false,
|
|
963
|
+
restoreReason: "intentional-reset",
|
|
964
|
+
backupMetadata,
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
const primarySnapshot = backupMetadata.accounts.snapshots.find((snapshot) => snapshot.kind === "accounts-primary");
|
|
968
|
+
if (!primarySnapshot?.exists) {
|
|
969
|
+
return {
|
|
970
|
+
storagePath,
|
|
971
|
+
restoreEligible: true,
|
|
972
|
+
restoreReason: "missing-storage",
|
|
973
|
+
latestSnapshot: backupMetadata.accounts.latestValidPath
|
|
974
|
+
? backupMetadata.accounts.snapshots.find((snapshot) => snapshot.path === backupMetadata.accounts.latestValidPath)
|
|
975
|
+
: undefined,
|
|
976
|
+
backupMetadata,
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
if (primarySnapshot.valid && primarySnapshot.accountCount === 0) {
|
|
980
|
+
return {
|
|
981
|
+
storagePath,
|
|
982
|
+
restoreEligible: true,
|
|
983
|
+
restoreReason: "empty-storage",
|
|
984
|
+
latestSnapshot: primarySnapshot,
|
|
985
|
+
backupMetadata,
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
return {
|
|
989
|
+
storagePath,
|
|
990
|
+
restoreEligible: false,
|
|
991
|
+
latestSnapshot: backupMetadata.accounts.latestValidPath
|
|
992
|
+
? backupMetadata.accounts.snapshots.find((snapshot) => snapshot.path === backupMetadata.accounts.latestValidPath)
|
|
993
|
+
: undefined,
|
|
994
|
+
backupMetadata,
|
|
995
|
+
};
|
|
996
|
+
}
|
|
628
997
|
function parseAndNormalizeStorage(data) {
|
|
629
998
|
const schemaErrors = getValidationErrors(AnyAccountStorageSchema, data);
|
|
630
999
|
const normalized = normalizeAccountStorage(data);
|
|
631
|
-
const storedVersion = isRecord(data)
|
|
1000
|
+
const storedVersion = isRecord(data)
|
|
1001
|
+
? data.version
|
|
1002
|
+
: undefined;
|
|
632
1003
|
return { normalized, storedVersion, schemaErrors };
|
|
633
1004
|
}
|
|
634
1005
|
async function loadAccountsFromPath(path) {
|
|
@@ -638,8 +1009,15 @@ async function loadAccountsFromPath(path) {
|
|
|
638
1009
|
}
|
|
639
1010
|
async function loadAccountsFromJournal(path) {
|
|
640
1011
|
const walPath = getAccountsWalPath(path);
|
|
1012
|
+
const resetMarkerPath = getIntentionalResetMarkerPath(path);
|
|
1013
|
+
if (existsSync(resetMarkerPath)) {
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
641
1016
|
try {
|
|
642
1017
|
const raw = await fs.readFile(walPath, "utf-8");
|
|
1018
|
+
if (existsSync(resetMarkerPath)) {
|
|
1019
|
+
return null;
|
|
1020
|
+
}
|
|
643
1021
|
const parsed = JSON.parse(raw);
|
|
644
1022
|
if (!isRecord(parsed))
|
|
645
1023
|
return null;
|
|
@@ -663,13 +1041,17 @@ async function loadAccountsFromJournal(path) {
|
|
|
663
1041
|
catch (error) {
|
|
664
1042
|
const code = error.code;
|
|
665
1043
|
if (code !== "ENOENT") {
|
|
666
|
-
log.warn("Failed to load account WAL journal", {
|
|
1044
|
+
log.warn("Failed to load account WAL journal", {
|
|
1045
|
+
path: walPath,
|
|
1046
|
+
error: String(error),
|
|
1047
|
+
});
|
|
667
1048
|
}
|
|
668
1049
|
return null;
|
|
669
1050
|
}
|
|
670
1051
|
}
|
|
671
1052
|
async function loadAccountsInternal(persistMigration) {
|
|
672
1053
|
const path = getStoragePath();
|
|
1054
|
+
const resetMarkerPath = getIntentionalResetMarkerPath(path);
|
|
673
1055
|
await cleanupStaleRotatingBackupArtifacts(path);
|
|
674
1056
|
const migratedLegacyStorage = persistMigration
|
|
675
1057
|
? await migrateLegacyProjectStorageIfNeeded(persistMigration)
|
|
@@ -677,16 +1059,73 @@ async function loadAccountsInternal(persistMigration) {
|
|
|
677
1059
|
try {
|
|
678
1060
|
const { normalized, storedVersion, schemaErrors } = await loadAccountsFromPath(path);
|
|
679
1061
|
if (schemaErrors.length > 0) {
|
|
680
|
-
log.warn("Account storage schema validation warnings", {
|
|
1062
|
+
log.warn("Account storage schema validation warnings", {
|
|
1063
|
+
errors: schemaErrors.slice(0, 5),
|
|
1064
|
+
});
|
|
681
1065
|
}
|
|
682
1066
|
if (normalized && storedVersion !== normalized.version) {
|
|
683
|
-
log.info("Migrating account storage to v3", {
|
|
1067
|
+
log.info("Migrating account storage to v3", {
|
|
1068
|
+
from: storedVersion,
|
|
1069
|
+
to: normalized.version,
|
|
1070
|
+
});
|
|
684
1071
|
if (persistMigration) {
|
|
685
1072
|
try {
|
|
686
1073
|
await persistMigration(normalized);
|
|
687
1074
|
}
|
|
688
1075
|
catch (saveError) {
|
|
689
|
-
log.warn("Failed to persist migrated storage", {
|
|
1076
|
+
log.warn("Failed to persist migrated storage", {
|
|
1077
|
+
error: String(saveError),
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
if (existsSync(resetMarkerPath)) {
|
|
1083
|
+
return createEmptyStorageWithMetadata(false, "intentional-reset");
|
|
1084
|
+
}
|
|
1085
|
+
if (normalized && normalized.accounts.length === 0) {
|
|
1086
|
+
return withRestoreMetadata(normalized, true, "empty-storage");
|
|
1087
|
+
}
|
|
1088
|
+
const primaryLooksSynthetic = looksLikeSyntheticFixtureStorage(normalized);
|
|
1089
|
+
if (storageBackupEnabled && normalized && primaryLooksSynthetic) {
|
|
1090
|
+
const backupCandidates = await getAccountsBackupRecoveryCandidatesWithDiscovery(path);
|
|
1091
|
+
for (const backupPath of backupCandidates) {
|
|
1092
|
+
if (backupPath === path)
|
|
1093
|
+
continue;
|
|
1094
|
+
try {
|
|
1095
|
+
const backup = await loadAccountsFromPath(backupPath);
|
|
1096
|
+
if (!backup.normalized)
|
|
1097
|
+
continue;
|
|
1098
|
+
if (looksLikeSyntheticFixtureStorage(backup.normalized))
|
|
1099
|
+
continue;
|
|
1100
|
+
if (backup.normalized.accounts.length <= 0)
|
|
1101
|
+
continue;
|
|
1102
|
+
log.warn("Detected synthetic primary account storage; promoting backup", {
|
|
1103
|
+
path,
|
|
1104
|
+
backupPath,
|
|
1105
|
+
primaryAccounts: normalized.accounts.length,
|
|
1106
|
+
backupAccounts: backup.normalized.accounts.length,
|
|
1107
|
+
});
|
|
1108
|
+
if (persistMigration) {
|
|
1109
|
+
try {
|
|
1110
|
+
await persistMigration(backup.normalized);
|
|
1111
|
+
}
|
|
1112
|
+
catch (persistError) {
|
|
1113
|
+
log.warn("Failed to persist promoted backup storage", {
|
|
1114
|
+
path,
|
|
1115
|
+
error: String(persistError),
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return backup.normalized;
|
|
1120
|
+
}
|
|
1121
|
+
catch (backupError) {
|
|
1122
|
+
const backupCode = backupError.code;
|
|
1123
|
+
if (backupCode !== "ENOENT") {
|
|
1124
|
+
log.warn("Failed to load candidate backup for synthetic-primary promotion", {
|
|
1125
|
+
path: backupPath,
|
|
1126
|
+
error: String(backupError),
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
690
1129
|
}
|
|
691
1130
|
}
|
|
692
1131
|
}
|
|
@@ -694,6 +1133,9 @@ async function loadAccountsInternal(persistMigration) {
|
|
|
694
1133
|
}
|
|
695
1134
|
catch (error) {
|
|
696
1135
|
const code = error.code;
|
|
1136
|
+
if (existsSync(resetMarkerPath)) {
|
|
1137
|
+
return createEmptyStorageWithMetadata(false, "intentional-reset");
|
|
1138
|
+
}
|
|
697
1139
|
if (code === "ENOENT" && migratedLegacyStorage) {
|
|
698
1140
|
return migratedLegacyStorage;
|
|
699
1141
|
}
|
|
@@ -712,8 +1154,11 @@ async function loadAccountsInternal(persistMigration) {
|
|
|
712
1154
|
}
|
|
713
1155
|
return recoveredFromWal;
|
|
714
1156
|
}
|
|
1157
|
+
if (existsSync(resetMarkerPath)) {
|
|
1158
|
+
return createEmptyStorageWithMetadata(false, "intentional-reset");
|
|
1159
|
+
}
|
|
715
1160
|
if (storageBackupEnabled) {
|
|
716
|
-
const backupCandidates =
|
|
1161
|
+
const backupCandidates = await getAccountsBackupRecoveryCandidatesWithDiscovery(path);
|
|
717
1162
|
for (const backupPath of backupCandidates) {
|
|
718
1163
|
try {
|
|
719
1164
|
const backup = await loadAccountsFromPath(backupPath);
|
|
@@ -724,7 +1169,10 @@ async function loadAccountsInternal(persistMigration) {
|
|
|
724
1169
|
});
|
|
725
1170
|
}
|
|
726
1171
|
if (backup.normalized) {
|
|
727
|
-
log.warn("Recovered account storage from backup file", {
|
|
1172
|
+
log.warn("Recovered account storage from backup file", {
|
|
1173
|
+
path,
|
|
1174
|
+
backupPath,
|
|
1175
|
+
});
|
|
728
1176
|
if (persistMigration) {
|
|
729
1177
|
try {
|
|
730
1178
|
await persistMigration(backup.normalized);
|
|
@@ -753,17 +1201,37 @@ async function loadAccountsInternal(persistMigration) {
|
|
|
753
1201
|
if (code !== "ENOENT") {
|
|
754
1202
|
log.error("Failed to load account storage", { error: String(error) });
|
|
755
1203
|
}
|
|
1204
|
+
if (code === "ENOENT") {
|
|
1205
|
+
return createEmptyStorageWithMetadata(true, "missing-storage");
|
|
1206
|
+
}
|
|
756
1207
|
return null;
|
|
757
1208
|
}
|
|
758
1209
|
}
|
|
759
1210
|
async function saveAccountsUnlocked(storage) {
|
|
760
1211
|
const path = getStoragePath();
|
|
1212
|
+
const resetMarkerPath = getIntentionalResetMarkerPath(path);
|
|
761
1213
|
const uniqueSuffix = `${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
|
|
762
1214
|
const tempPath = `${path}.${uniqueSuffix}.tmp`;
|
|
763
1215
|
const walPath = getAccountsWalPath(path);
|
|
764
1216
|
try {
|
|
765
1217
|
await fs.mkdir(dirname(path), { recursive: true });
|
|
766
1218
|
await ensureGitignore(path);
|
|
1219
|
+
if (looksLikeSyntheticFixtureStorage(storage)) {
|
|
1220
|
+
try {
|
|
1221
|
+
const existing = await loadNormalizedStorageFromPath(path, "existing account storage");
|
|
1222
|
+
if (existing &&
|
|
1223
|
+
existing.accounts.length > 0 &&
|
|
1224
|
+
!looksLikeSyntheticFixtureStorage(existing)) {
|
|
1225
|
+
throw new StorageError("Refusing to overwrite non-synthetic account storage with synthetic fixture payload", "EINVALID", path, "Detected synthetic fixture-like account payload. Use explicit account import/login commands instead.");
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
catch (error) {
|
|
1229
|
+
if (error instanceof StorageError) {
|
|
1230
|
+
throw error;
|
|
1231
|
+
}
|
|
1232
|
+
// Ignore existing-file probe failures and continue with normal save flow.
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
767
1235
|
if (storageBackupEnabled && existsSync(path)) {
|
|
768
1236
|
try {
|
|
769
1237
|
await createRotatingAccountsBackup(path);
|
|
@@ -799,6 +1267,12 @@ async function saveAccountsUnlocked(storage) {
|
|
|
799
1267
|
for (let attempt = 0; attempt < 5; attempt++) {
|
|
800
1268
|
try {
|
|
801
1269
|
await fs.rename(tempPath, path);
|
|
1270
|
+
try {
|
|
1271
|
+
await fs.unlink(resetMarkerPath);
|
|
1272
|
+
}
|
|
1273
|
+
catch {
|
|
1274
|
+
// Best effort cleanup.
|
|
1275
|
+
}
|
|
802
1276
|
lastAccountsSaveTimestamp = Date.now();
|
|
803
1277
|
try {
|
|
804
1278
|
await fs.unlink(walPath);
|
|
@@ -812,7 +1286,7 @@ async function saveAccountsUnlocked(storage) {
|
|
|
812
1286
|
const code = renameError.code;
|
|
813
1287
|
if (code === "EPERM" || code === "EBUSY") {
|
|
814
1288
|
lastError = renameError;
|
|
815
|
-
await new Promise(r => setTimeout(r, 10 *
|
|
1289
|
+
await new Promise((r) => setTimeout(r, 10 * 2 ** attempt));
|
|
816
1290
|
continue;
|
|
817
1291
|
}
|
|
818
1292
|
throw renameError;
|
|
@@ -842,8 +1316,16 @@ async function saveAccountsUnlocked(storage) {
|
|
|
842
1316
|
}
|
|
843
1317
|
export async function withAccountStorageTransaction(handler) {
|
|
844
1318
|
return withStorageLock(async () => {
|
|
845
|
-
const
|
|
846
|
-
|
|
1319
|
+
const state = {
|
|
1320
|
+
snapshot: await loadAccountsInternal(saveAccountsUnlocked),
|
|
1321
|
+
active: true,
|
|
1322
|
+
};
|
|
1323
|
+
const current = state.snapshot;
|
|
1324
|
+
const persist = async (storage) => {
|
|
1325
|
+
await saveAccountsUnlocked(storage);
|
|
1326
|
+
state.snapshot = storage;
|
|
1327
|
+
};
|
|
1328
|
+
return transactionSnapshotContext.run(state, () => handler(current, persist));
|
|
847
1329
|
});
|
|
848
1330
|
}
|
|
849
1331
|
/**
|
|
@@ -865,8 +1347,10 @@ export async function saveAccounts(storage) {
|
|
|
865
1347
|
export async function clearAccounts() {
|
|
866
1348
|
return withStorageLock(async () => {
|
|
867
1349
|
const path = getStoragePath();
|
|
1350
|
+
const resetMarkerPath = getIntentionalResetMarkerPath(path);
|
|
868
1351
|
const walPath = getAccountsWalPath(path);
|
|
869
|
-
const backupPaths =
|
|
1352
|
+
const backupPaths = await getAccountsBackupRecoveryCandidatesWithDiscovery(path);
|
|
1353
|
+
await fs.writeFile(resetMarkerPath, JSON.stringify({ version: 1, createdAt: Date.now() }), { encoding: "utf-8", mode: 0o600 });
|
|
870
1354
|
const clearPath = async (targetPath) => {
|
|
871
1355
|
try {
|
|
872
1356
|
await fs.unlink(targetPath);
|
|
@@ -882,7 +1366,11 @@ export async function clearAccounts() {
|
|
|
882
1366
|
}
|
|
883
1367
|
};
|
|
884
1368
|
try {
|
|
885
|
-
await Promise.all([
|
|
1369
|
+
await Promise.all([
|
|
1370
|
+
clearPath(path),
|
|
1371
|
+
clearPath(walPath),
|
|
1372
|
+
...backupPaths.map(clearPath),
|
|
1373
|
+
]);
|
|
886
1374
|
}
|
|
887
1375
|
catch {
|
|
888
1376
|
// Individual path cleanup is already best-effort with per-artifact logging.
|
|
@@ -897,13 +1385,22 @@ function normalizeFlaggedStorage(data) {
|
|
|
897
1385
|
for (const rawAccount of data.accounts) {
|
|
898
1386
|
if (!isRecord(rawAccount))
|
|
899
1387
|
continue;
|
|
900
|
-
const refreshToken = typeof rawAccount.refreshToken === "string"
|
|
1388
|
+
const refreshToken = typeof rawAccount.refreshToken === "string"
|
|
1389
|
+
? rawAccount.refreshToken.trim()
|
|
1390
|
+
: "";
|
|
901
1391
|
if (!refreshToken)
|
|
902
1392
|
continue;
|
|
903
|
-
const flaggedAt = typeof rawAccount.flaggedAt === "number"
|
|
904
|
-
|
|
1393
|
+
const flaggedAt = typeof rawAccount.flaggedAt === "number"
|
|
1394
|
+
? rawAccount.flaggedAt
|
|
1395
|
+
: Date.now();
|
|
1396
|
+
const isAccountIdSource = (value) => value === "token" ||
|
|
1397
|
+
value === "id_token" ||
|
|
1398
|
+
value === "org" ||
|
|
1399
|
+
value === "manual";
|
|
905
1400
|
const isSwitchReason = (value) => value === "rate-limit" || value === "initial" || value === "rotation";
|
|
906
|
-
const isCooldownReason = (value) => value === "auth-failure" ||
|
|
1401
|
+
const isCooldownReason = (value) => value === "auth-failure" ||
|
|
1402
|
+
value === "network-error" ||
|
|
1403
|
+
value === "rate-limit";
|
|
907
1404
|
let rateLimitResetTimes;
|
|
908
1405
|
if (isRecord(rawAccount.rateLimitResetTimes)) {
|
|
909
1406
|
const normalizedRateLimits = {};
|
|
@@ -928,19 +1425,33 @@ function normalizeFlaggedStorage(data) {
|
|
|
928
1425
|
const normalized = {
|
|
929
1426
|
refreshToken,
|
|
930
1427
|
addedAt: typeof rawAccount.addedAt === "number" ? rawAccount.addedAt : flaggedAt,
|
|
931
|
-
lastUsed: typeof rawAccount.lastUsed === "number"
|
|
932
|
-
|
|
1428
|
+
lastUsed: typeof rawAccount.lastUsed === "number"
|
|
1429
|
+
? rawAccount.lastUsed
|
|
1430
|
+
: flaggedAt,
|
|
1431
|
+
accountId: typeof rawAccount.accountId === "string"
|
|
1432
|
+
? rawAccount.accountId
|
|
1433
|
+
: undefined,
|
|
933
1434
|
accountIdSource,
|
|
934
|
-
accountLabel: typeof rawAccount.accountLabel === "string"
|
|
1435
|
+
accountLabel: typeof rawAccount.accountLabel === "string"
|
|
1436
|
+
? rawAccount.accountLabel
|
|
1437
|
+
: undefined,
|
|
935
1438
|
email: typeof rawAccount.email === "string" ? rawAccount.email : undefined,
|
|
936
|
-
enabled: typeof rawAccount.enabled === "boolean"
|
|
1439
|
+
enabled: typeof rawAccount.enabled === "boolean"
|
|
1440
|
+
? rawAccount.enabled
|
|
1441
|
+
: undefined,
|
|
937
1442
|
lastSwitchReason,
|
|
938
1443
|
rateLimitResetTimes,
|
|
939
|
-
coolingDownUntil: typeof rawAccount.coolingDownUntil === "number"
|
|
1444
|
+
coolingDownUntil: typeof rawAccount.coolingDownUntil === "number"
|
|
1445
|
+
? rawAccount.coolingDownUntil
|
|
1446
|
+
: undefined,
|
|
940
1447
|
cooldownReason,
|
|
941
1448
|
flaggedAt,
|
|
942
|
-
flaggedReason: typeof rawAccount.flaggedReason === "string"
|
|
943
|
-
|
|
1449
|
+
flaggedReason: typeof rawAccount.flaggedReason === "string"
|
|
1450
|
+
? rawAccount.flaggedReason
|
|
1451
|
+
: undefined,
|
|
1452
|
+
lastError: typeof rawAccount.lastError === "string"
|
|
1453
|
+
? rawAccount.lastError
|
|
1454
|
+
: undefined,
|
|
944
1455
|
};
|
|
945
1456
|
byRefreshToken.set(refreshToken, normalized);
|
|
946
1457
|
}
|
|
@@ -951,16 +1462,24 @@ function normalizeFlaggedStorage(data) {
|
|
|
951
1462
|
}
|
|
952
1463
|
export async function loadFlaggedAccounts() {
|
|
953
1464
|
const path = getFlaggedAccountsPath();
|
|
1465
|
+
const resetMarkerPath = getIntentionalResetMarkerPath(path);
|
|
954
1466
|
const empty = { version: 1, accounts: [] };
|
|
955
1467
|
try {
|
|
956
1468
|
const content = await fs.readFile(path, "utf-8");
|
|
957
1469
|
const data = JSON.parse(content);
|
|
958
|
-
|
|
1470
|
+
const loaded = normalizeFlaggedStorage(data);
|
|
1471
|
+
if (existsSync(resetMarkerPath)) {
|
|
1472
|
+
return empty;
|
|
1473
|
+
}
|
|
1474
|
+
return loaded;
|
|
959
1475
|
}
|
|
960
1476
|
catch (error) {
|
|
961
1477
|
const code = error.code;
|
|
962
1478
|
if (code !== "ENOENT") {
|
|
963
|
-
log.error("Failed to load flagged account storage", {
|
|
1479
|
+
log.error("Failed to load flagged account storage", {
|
|
1480
|
+
path,
|
|
1481
|
+
error: String(error),
|
|
1482
|
+
});
|
|
964
1483
|
return empty;
|
|
965
1484
|
}
|
|
966
1485
|
}
|
|
@@ -1000,13 +1519,31 @@ export async function loadFlaggedAccounts() {
|
|
|
1000
1519
|
export async function saveFlaggedAccounts(storage) {
|
|
1001
1520
|
return withStorageLock(async () => {
|
|
1002
1521
|
const path = getFlaggedAccountsPath();
|
|
1522
|
+
const markerPath = getIntentionalResetMarkerPath(path);
|
|
1003
1523
|
const uniqueSuffix = `${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
|
|
1004
1524
|
const tempPath = `${path}.${uniqueSuffix}.tmp`;
|
|
1005
1525
|
try {
|
|
1006
1526
|
await fs.mkdir(dirname(path), { recursive: true });
|
|
1527
|
+
if (existsSync(path)) {
|
|
1528
|
+
try {
|
|
1529
|
+
await fs.copyFile(path, `${path}.bak`);
|
|
1530
|
+
}
|
|
1531
|
+
catch (backupError) {
|
|
1532
|
+
log.warn("Failed to create flagged backup snapshot", {
|
|
1533
|
+
path,
|
|
1534
|
+
error: String(backupError),
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1007
1538
|
const content = JSON.stringify(normalizeFlaggedStorage(storage), null, 2);
|
|
1008
1539
|
await fs.writeFile(tempPath, content, { encoding: "utf-8", mode: 0o600 });
|
|
1009
1540
|
await fs.rename(tempPath, path);
|
|
1541
|
+
try {
|
|
1542
|
+
await fs.unlink(markerPath);
|
|
1543
|
+
}
|
|
1544
|
+
catch {
|
|
1545
|
+
// Best effort cleanup.
|
|
1546
|
+
}
|
|
1010
1547
|
}
|
|
1011
1548
|
catch (error) {
|
|
1012
1549
|
try {
|
|
@@ -1015,20 +1552,48 @@ export async function saveFlaggedAccounts(storage) {
|
|
|
1015
1552
|
catch {
|
|
1016
1553
|
// Ignore cleanup failures.
|
|
1017
1554
|
}
|
|
1018
|
-
log.error("Failed to save flagged account storage", {
|
|
1555
|
+
log.error("Failed to save flagged account storage", {
|
|
1556
|
+
path,
|
|
1557
|
+
error: String(error),
|
|
1558
|
+
});
|
|
1019
1559
|
throw error;
|
|
1020
1560
|
}
|
|
1021
1561
|
});
|
|
1022
1562
|
}
|
|
1023
1563
|
export async function clearFlaggedAccounts() {
|
|
1024
1564
|
return withStorageLock(async () => {
|
|
1565
|
+
const path = getFlaggedAccountsPath();
|
|
1566
|
+
const markerPath = getIntentionalResetMarkerPath(path);
|
|
1025
1567
|
try {
|
|
1026
|
-
await fs.
|
|
1568
|
+
await fs.writeFile(markerPath, "reset", {
|
|
1569
|
+
encoding: "utf-8",
|
|
1570
|
+
mode: 0o600,
|
|
1571
|
+
});
|
|
1027
1572
|
}
|
|
1028
1573
|
catch (error) {
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1574
|
+
log.error("Failed to write flagged reset marker", {
|
|
1575
|
+
path,
|
|
1576
|
+
markerPath,
|
|
1577
|
+
error: String(error),
|
|
1578
|
+
});
|
|
1579
|
+
throw error;
|
|
1580
|
+
}
|
|
1581
|
+
const backupPaths = await getAccountsBackupRecoveryCandidatesWithDiscovery(path);
|
|
1582
|
+
for (const candidate of [path, ...backupPaths, markerPath]) {
|
|
1583
|
+
try {
|
|
1584
|
+
await fs.unlink(candidate);
|
|
1585
|
+
}
|
|
1586
|
+
catch (error) {
|
|
1587
|
+
const code = error.code;
|
|
1588
|
+
if (code !== "ENOENT") {
|
|
1589
|
+
log.error("Failed to clear flagged account storage", {
|
|
1590
|
+
path: candidate,
|
|
1591
|
+
error: String(error),
|
|
1592
|
+
});
|
|
1593
|
+
if (candidate === path) {
|
|
1594
|
+
throw error;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1032
1597
|
}
|
|
1033
1598
|
}
|
|
1034
1599
|
});
|
|
@@ -1039,19 +1604,34 @@ export async function clearFlaggedAccounts() {
|
|
|
1039
1604
|
* @param force - If true, overwrite existing file (default: true)
|
|
1040
1605
|
* @throws Error if file exists and force is false, or if no accounts to export
|
|
1041
1606
|
*/
|
|
1042
|
-
export async function exportAccounts(filePath, force = true) {
|
|
1607
|
+
export async function exportAccounts(filePath, force = true, beforeCommit) {
|
|
1043
1608
|
const resolvedPath = resolvePath(filePath);
|
|
1044
1609
|
if (!force && existsSync(resolvedPath)) {
|
|
1045
1610
|
throw new Error(`File already exists: ${resolvedPath}`);
|
|
1046
1611
|
}
|
|
1047
|
-
const
|
|
1612
|
+
const transactionState = transactionSnapshotContext.getStore();
|
|
1613
|
+
const storage = transactionState?.active
|
|
1614
|
+
? transactionState.snapshot
|
|
1615
|
+
: await withAccountStorageTransaction((current) => Promise.resolve(current));
|
|
1048
1616
|
if (!storage || storage.accounts.length === 0) {
|
|
1049
1617
|
throw new Error("No accounts to export");
|
|
1050
1618
|
}
|
|
1051
1619
|
await fs.mkdir(dirname(resolvedPath), { recursive: true });
|
|
1052
|
-
|
|
1620
|
+
await beforeCommit?.(resolvedPath);
|
|
1621
|
+
if (!force && existsSync(resolvedPath)) {
|
|
1622
|
+
throw new Error(`File already exists: ${resolvedPath}`);
|
|
1623
|
+
}
|
|
1624
|
+
const content = JSON.stringify({
|
|
1625
|
+
version: storage.version,
|
|
1626
|
+
accounts: storage.accounts,
|
|
1627
|
+
activeIndex: storage.activeIndex,
|
|
1628
|
+
activeIndexByFamily: storage.activeIndexByFamily,
|
|
1629
|
+
}, null, 2);
|
|
1053
1630
|
await fs.writeFile(resolvedPath, content, { encoding: "utf-8", mode: 0o600 });
|
|
1054
|
-
log.info("Exported accounts", {
|
|
1631
|
+
log.info("Exported accounts", {
|
|
1632
|
+
path: resolvedPath,
|
|
1633
|
+
count: storage.accounts.length,
|
|
1634
|
+
});
|
|
1055
1635
|
}
|
|
1056
1636
|
/**
|
|
1057
1637
|
* Imports accounts from a JSON file, merging with existing accounts.
|
|
@@ -1077,7 +1657,7 @@ export async function importAccounts(filePath) {
|
|
|
1077
1657
|
if (!normalized) {
|
|
1078
1658
|
throw new Error("Invalid account storage format");
|
|
1079
1659
|
}
|
|
1080
|
-
const { imported: importedCount, total, skipped: skippedCount } = await withAccountStorageTransaction(async (existing, persist) => {
|
|
1660
|
+
const { imported: importedCount, total, skipped: skippedCount, } = await withAccountStorageTransaction(async (existing, persist) => {
|
|
1081
1661
|
const existingAccounts = existing?.accounts ?? [];
|
|
1082
1662
|
const existingActiveIndex = existing?.activeIndex ?? 0;
|
|
1083
1663
|
const merged = [...existingAccounts, ...normalized.accounts];
|
|
@@ -1099,7 +1679,12 @@ export async function importAccounts(filePath) {
|
|
|
1099
1679
|
const skipped = normalized.accounts.length - imported;
|
|
1100
1680
|
return { imported, total: deduplicatedAccounts.length, skipped };
|
|
1101
1681
|
});
|
|
1102
|
-
log.info("Imported accounts", {
|
|
1682
|
+
log.info("Imported accounts", {
|
|
1683
|
+
path: resolvedPath,
|
|
1684
|
+
imported: importedCount,
|
|
1685
|
+
skipped: skippedCount,
|
|
1686
|
+
total,
|
|
1687
|
+
});
|
|
1103
1688
|
return { imported: importedCount, total, skipped: skippedCount };
|
|
1104
1689
|
}
|
|
1105
1690
|
//# sourceMappingURL=storage.js.map
|