codex-multi-auth 0.1.7 → 0.1.9
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 +16 -3
- 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 +38 -65
- package/dist/index.js.map +1 -1
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +29 -10
- package/dist/lib/accounts.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-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 +240 -216
- package/dist/lib/codex-manager.js.map +1 -1
- package/dist/lib/context-overflow.js +10 -10
- package/dist/lib/entitlement-cache.d.ts +8 -5
- package/dist/lib/entitlement-cache.d.ts.map +1 -1
- package/dist/lib/entitlement-cache.js +24 -9
- package/dist/lib/entitlement-cache.js.map +1 -1
- 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/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 +522 -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 +21 -1
- package/dist/lib/runtime-paths.js.map +1 -1
- package/dist/lib/storage.d.ts +67 -16
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/storage.js +797 -169
- package/dist/lib/storage.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/package.json +130 -131
- 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/install-codex-auth.js +9 -9
- package/scripts/test-all-models.sh +7 -7
- package/scripts/test-model-matrix.js +138 -46
- package/scripts/validate-model-map.sh +7 -7
package/dist/lib/storage.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { basename, 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;
|
|
@@ -69,8 +73,12 @@ function withStorageLock(fn) {
|
|
|
69
73
|
}
|
|
70
74
|
function looksLikeSyntheticFixtureAccount(account) {
|
|
71
75
|
const email = typeof account.email === "string" ? account.email.trim().toLowerCase() : "";
|
|
72
|
-
const refreshToken = typeof account.refreshToken === "string"
|
|
73
|
-
|
|
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
|
+
: "";
|
|
74
82
|
if (!/^account\d+@example\.com$/.test(email)) {
|
|
75
83
|
return false;
|
|
76
84
|
}
|
|
@@ -104,7 +112,10 @@ async function ensureGitignore(storagePath) {
|
|
|
104
112
|
if (existsSync(gitignorePath)) {
|
|
105
113
|
content = await fs.readFile(gitignorePath, "utf-8");
|
|
106
114
|
const lines = content.split("\n").map((l) => l.trim());
|
|
107
|
-
if (lines.includes(".codex") ||
|
|
115
|
+
if (lines.includes(".codex") ||
|
|
116
|
+
lines.includes(".codex/") ||
|
|
117
|
+
lines.includes("/.codex") ||
|
|
118
|
+
lines.includes("/.codex/")) {
|
|
108
119
|
return;
|
|
109
120
|
}
|
|
110
121
|
}
|
|
@@ -152,6 +163,10 @@ async function getAccountsBackupRecoveryCandidatesWithDiscovery(path) {
|
|
|
152
163
|
continue;
|
|
153
164
|
if (!entry.name.startsWith(candidatePrefix))
|
|
154
165
|
continue;
|
|
166
|
+
if (isCacheLikeBackupArtifactName(entry.name))
|
|
167
|
+
continue;
|
|
168
|
+
if (entry.name.endsWith(RESET_MARKER_SUFFIX))
|
|
169
|
+
continue;
|
|
155
170
|
if (entry.name.endsWith(".tmp"))
|
|
156
171
|
continue;
|
|
157
172
|
if (entry.name.includes(".rotate."))
|
|
@@ -232,7 +247,9 @@ async function createRotatingAccountsBackup(path) {
|
|
|
232
247
|
continue;
|
|
233
248
|
}
|
|
234
249
|
const stagedPath = buildStagedPath(currentPath, `slot-${i}`);
|
|
235
|
-
await copyFileWithRetry(previousPath, stagedPath, {
|
|
250
|
+
await copyFileWithRetry(previousPath, stagedPath, {
|
|
251
|
+
allowMissingSource: true,
|
|
252
|
+
});
|
|
236
253
|
if (existsSync(stagedPath)) {
|
|
237
254
|
stagedWrites.push({ targetPath: currentPath, stagedPath });
|
|
238
255
|
}
|
|
@@ -244,7 +261,10 @@ async function createRotatingAccountsBackup(path) {
|
|
|
244
261
|
const latestStagedPath = buildStagedPath(latestBackupPath, "latest");
|
|
245
262
|
await copyFileWithRetry(path, latestStagedPath);
|
|
246
263
|
if (existsSync(latestStagedPath)) {
|
|
247
|
-
stagedWrites.push({
|
|
264
|
+
stagedWrites.push({
|
|
265
|
+
targetPath: latestBackupPath,
|
|
266
|
+
stagedPath: latestStagedPath,
|
|
267
|
+
});
|
|
248
268
|
}
|
|
249
269
|
for (const stagedWrite of stagedWrites) {
|
|
250
270
|
await renameFileWithRetry(stagedWrite.stagedPath, stagedWrite.targetPath);
|
|
@@ -266,7 +286,8 @@ async function createRotatingAccountsBackup(path) {
|
|
|
266
286
|
}
|
|
267
287
|
function isRotatingBackupTempArtifact(storagePath, candidatePath) {
|
|
268
288
|
const backupPrefix = `${storagePath}${ACCOUNTS_BACKUP_SUFFIX}`;
|
|
269
|
-
if (!candidatePath.startsWith(backupPrefix) ||
|
|
289
|
+
if (!candidatePath.startsWith(backupPrefix) ||
|
|
290
|
+
!candidatePath.endsWith(".tmp")) {
|
|
270
291
|
return false;
|
|
271
292
|
}
|
|
272
293
|
const suffix = candidatePath.slice(backupPrefix.length);
|
|
@@ -283,7 +304,9 @@ function isRotatingBackupTempArtifact(storagePath, candidatePath) {
|
|
|
283
304
|
async function cleanupStaleRotatingBackupArtifacts(path) {
|
|
284
305
|
const directoryPath = dirname(path);
|
|
285
306
|
try {
|
|
286
|
-
const directoryEntries = await fs.readdir(directoryPath, {
|
|
307
|
+
const directoryEntries = await fs.readdir(directoryPath, {
|
|
308
|
+
withFileTypes: true,
|
|
309
|
+
});
|
|
287
310
|
const staleArtifacts = directoryEntries
|
|
288
311
|
.filter((entry) => entry.isFile())
|
|
289
312
|
.map((entry) => join(directoryPath, entry.name))
|
|
@@ -316,6 +339,198 @@ async function cleanupStaleRotatingBackupArtifacts(path) {
|
|
|
316
339
|
function computeSha256(value) {
|
|
317
340
|
return createHash("sha256").update(value).digest("hex");
|
|
318
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
|
+
}
|
|
319
534
|
export function getLastAccountsSaveTimestamp() {
|
|
320
535
|
return lastAccountsSaveTimestamp;
|
|
321
536
|
}
|
|
@@ -335,7 +550,9 @@ export function setStoragePath(projectPath) {
|
|
|
335
550
|
currentLegacyProjectStoragePath = join(getProjectConfigDir(projectRoot), ACCOUNTS_FILE_NAME);
|
|
336
551
|
const previousWorktreeScopedPath = join(getProjectGlobalConfigDir(projectRoot), ACCOUNTS_FILE_NAME);
|
|
337
552
|
currentLegacyWorktreeStoragePath =
|
|
338
|
-
previousWorktreeScopedPath !== currentStoragePath
|
|
553
|
+
previousWorktreeScopedPath !== currentStoragePath
|
|
554
|
+
? previousWorktreeScopedPath
|
|
555
|
+
: null;
|
|
339
556
|
}
|
|
340
557
|
else {
|
|
341
558
|
currentStoragePath = null;
|
|
@@ -360,6 +577,15 @@ export function getStoragePath() {
|
|
|
360
577
|
}
|
|
361
578
|
return join(getConfigDir(), ACCOUNTS_FILE_NAME);
|
|
362
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
|
+
}
|
|
363
589
|
export function getFlaggedAccountsPath() {
|
|
364
590
|
return join(dirname(getStoragePath()), FLAGGED_ACCOUNTS_FILE_NAME);
|
|
365
591
|
}
|
|
@@ -370,8 +596,13 @@ async function migrateLegacyProjectStorageIfNeeded(persist = saveAccounts) {
|
|
|
370
596
|
if (!currentStoragePath) {
|
|
371
597
|
return null;
|
|
372
598
|
}
|
|
373
|
-
const candidatePaths = [
|
|
374
|
-
|
|
599
|
+
const candidatePaths = [
|
|
600
|
+
currentLegacyWorktreeStoragePath,
|
|
601
|
+
currentLegacyProjectStoragePath,
|
|
602
|
+
]
|
|
603
|
+
.filter((path) => typeof path === "string" &&
|
|
604
|
+
path.length > 0 &&
|
|
605
|
+
path !== currentStoragePath)
|
|
375
606
|
.filter((path, index, all) => all.indexOf(path) === index);
|
|
376
607
|
if (candidatePaths.length === 0) {
|
|
377
608
|
return null;
|
|
@@ -482,106 +713,224 @@ function selectNewestAccount(current, candidate) {
|
|
|
482
713
|
const candidateAddedAt = candidate.addedAt || 0;
|
|
483
714
|
return candidateAddedAt >= currentAddedAt ? candidate : current;
|
|
484
715
|
}
|
|
485
|
-
function
|
|
486
|
-
|
|
487
|
-
|
|
716
|
+
function normalizeAccountIdKey(accountId) {
|
|
717
|
+
if (!accountId)
|
|
718
|
+
return undefined;
|
|
719
|
+
const trimmed = accountId.trim();
|
|
720
|
+
return trimmed || undefined;
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Normalize email keys for case-insensitive account identity matching.
|
|
724
|
+
*/
|
|
725
|
+
export function normalizeEmailKey(email) {
|
|
726
|
+
if (!email)
|
|
727
|
+
return undefined;
|
|
728
|
+
const trimmed = email.trim();
|
|
729
|
+
if (!trimmed)
|
|
730
|
+
return undefined;
|
|
731
|
+
return trimmed.toLowerCase();
|
|
732
|
+
}
|
|
733
|
+
function normalizeRefreshTokenKey(refreshToken) {
|
|
734
|
+
if (!refreshToken)
|
|
735
|
+
return undefined;
|
|
736
|
+
const trimmed = refreshToken.trim();
|
|
737
|
+
return trimmed || undefined;
|
|
738
|
+
}
|
|
739
|
+
function toAccountIdentityRef(account) {
|
|
740
|
+
return {
|
|
741
|
+
accountId: normalizeAccountIdKey(account?.accountId),
|
|
742
|
+
emailKey: normalizeEmailKey(account?.email),
|
|
743
|
+
refreshToken: normalizeRefreshTokenKey(account?.refreshToken),
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
function collectDistinctIdentityValues(values) {
|
|
747
|
+
const distinct = new Set();
|
|
748
|
+
for (const value of values) {
|
|
749
|
+
if (value)
|
|
750
|
+
distinct.add(value);
|
|
751
|
+
}
|
|
752
|
+
return distinct;
|
|
753
|
+
}
|
|
754
|
+
export function getAccountIdentityKey(account) {
|
|
755
|
+
const ref = toAccountIdentityRef(account);
|
|
756
|
+
if (ref.accountId && ref.emailKey) {
|
|
757
|
+
return `account:${ref.accountId}::email:${ref.emailKey}`;
|
|
758
|
+
}
|
|
759
|
+
if (ref.accountId)
|
|
760
|
+
return `account:${ref.accountId}`;
|
|
761
|
+
if (ref.emailKey)
|
|
762
|
+
return `email:${ref.emailKey}`;
|
|
763
|
+
if (ref.refreshToken)
|
|
764
|
+
return `refresh:${ref.refreshToken}`;
|
|
765
|
+
return undefined;
|
|
766
|
+
}
|
|
767
|
+
function findNewestMatchingIndex(accounts, predicate) {
|
|
768
|
+
let matchIndex;
|
|
769
|
+
let match;
|
|
488
770
|
for (let i = 0; i < accounts.length; i += 1) {
|
|
489
771
|
const account = accounts[i];
|
|
490
772
|
if (!account)
|
|
491
773
|
continue;
|
|
492
|
-
const
|
|
493
|
-
if (!
|
|
774
|
+
const ref = toAccountIdentityRef(account);
|
|
775
|
+
if (!predicate(ref))
|
|
494
776
|
continue;
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
777
|
+
if (matchIndex === undefined) {
|
|
778
|
+
matchIndex = i;
|
|
779
|
+
match = account;
|
|
498
780
|
continue;
|
|
499
781
|
}
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
for (const idx of keyToIndex.values()) {
|
|
505
|
-
indicesToKeep.add(idx);
|
|
506
|
-
}
|
|
507
|
-
const result = [];
|
|
508
|
-
for (let i = 0; i < accounts.length; i += 1) {
|
|
509
|
-
if (indicesToKeep.has(i)) {
|
|
510
|
-
const account = accounts[i];
|
|
511
|
-
if (account)
|
|
512
|
-
result.push(account);
|
|
782
|
+
const newest = selectNewestAccount(match, account);
|
|
783
|
+
if (newest === account) {
|
|
784
|
+
matchIndex = i;
|
|
785
|
+
match = account;
|
|
513
786
|
}
|
|
514
787
|
}
|
|
515
|
-
return
|
|
788
|
+
return matchIndex;
|
|
516
789
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
*/
|
|
523
|
-
export function deduplicateAccounts(accounts) {
|
|
524
|
-
return deduplicateAccountsByKey(accounts);
|
|
790
|
+
function findCompositeAccountMatchIndex(accounts, candidateRef) {
|
|
791
|
+
if (!candidateRef.accountId || !candidateRef.emailKey)
|
|
792
|
+
return undefined;
|
|
793
|
+
return findNewestMatchingIndex(accounts, (ref) => ref.accountId === candidateRef.accountId &&
|
|
794
|
+
ref.emailKey === candidateRef.emailKey);
|
|
525
795
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
* Accounts without email are always preserved.
|
|
529
|
-
* @param accounts - Array of accounts to deduplicate
|
|
530
|
-
* @returns New array with email duplicates removed
|
|
531
|
-
*/
|
|
532
|
-
export function normalizeEmailKey(email) {
|
|
533
|
-
if (!email)
|
|
796
|
+
function findSafeEmailMatchIndex(accounts, candidateRef) {
|
|
797
|
+
if (!candidateRef.emailKey)
|
|
534
798
|
return undefined;
|
|
535
|
-
const
|
|
536
|
-
|
|
799
|
+
const emailAccountIds = [candidateRef.accountId];
|
|
800
|
+
let foundAny = false;
|
|
801
|
+
for (let i = 0; i < accounts.length; i += 1) {
|
|
802
|
+
const account = accounts[i];
|
|
803
|
+
if (!account)
|
|
804
|
+
continue;
|
|
805
|
+
const ref = toAccountIdentityRef(account);
|
|
806
|
+
if (ref.emailKey !== candidateRef.emailKey)
|
|
807
|
+
continue;
|
|
808
|
+
foundAny = true;
|
|
809
|
+
emailAccountIds.push(ref.accountId);
|
|
810
|
+
}
|
|
811
|
+
if (!foundAny)
|
|
537
812
|
return undefined;
|
|
538
|
-
|
|
813
|
+
if (collectDistinctIdentityValues(emailAccountIds).size > 1) {
|
|
814
|
+
return undefined;
|
|
815
|
+
}
|
|
816
|
+
return findNewestMatchingIndex(accounts, (ref) => ref.emailKey === candidateRef.emailKey);
|
|
539
817
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
818
|
+
function findCompatibleRefreshTokenMatchIndex(accounts, candidateRef) {
|
|
819
|
+
if (!candidateRef.refreshToken)
|
|
820
|
+
return undefined;
|
|
821
|
+
let matchingIndex;
|
|
822
|
+
let matchingAccount = null;
|
|
543
823
|
for (let i = 0; i < accounts.length; i += 1) {
|
|
544
824
|
const account = accounts[i];
|
|
545
825
|
if (!account)
|
|
546
826
|
continue;
|
|
547
|
-
const
|
|
548
|
-
if (
|
|
549
|
-
indicesToKeep.add(i);
|
|
827
|
+
const ref = toAccountIdentityRef(account);
|
|
828
|
+
if (ref.refreshToken !== candidateRef.refreshToken)
|
|
550
829
|
continue;
|
|
830
|
+
if ((candidateRef.accountId &&
|
|
831
|
+
ref.accountId &&
|
|
832
|
+
ref.accountId !== candidateRef.accountId) ||
|
|
833
|
+
(candidateRef.emailKey &&
|
|
834
|
+
ref.emailKey &&
|
|
835
|
+
ref.emailKey !== candidateRef.emailKey)) {
|
|
836
|
+
return undefined;
|
|
551
837
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
838
|
+
if (matchingIndex !== undefined &&
|
|
839
|
+
!candidateRef.accountId &&
|
|
840
|
+
!candidateRef.emailKey) {
|
|
841
|
+
return undefined;
|
|
556
842
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
emailToNewestIndex.set(email, i);
|
|
843
|
+
if (matchingIndex === undefined || matchingAccount === null) {
|
|
844
|
+
matchingIndex = i;
|
|
845
|
+
matchingAccount = account;
|
|
561
846
|
continue;
|
|
562
847
|
}
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
const isNewer = candidateLastUsed > existingLastUsed ||
|
|
568
|
-
(candidateLastUsed === existingLastUsed && candidateAddedAt > existingAddedAt);
|
|
569
|
-
if (isNewer) {
|
|
570
|
-
emailToNewestIndex.set(email, i);
|
|
848
|
+
const newest = selectNewestAccount(matchingAccount, account);
|
|
849
|
+
if (newest === account) {
|
|
850
|
+
matchingIndex = i;
|
|
851
|
+
matchingAccount = account;
|
|
571
852
|
}
|
|
572
853
|
}
|
|
573
|
-
|
|
574
|
-
|
|
854
|
+
return matchingIndex;
|
|
855
|
+
}
|
|
856
|
+
function findUniqueAccountIdMatchIndex(accounts, candidateRef, options) {
|
|
857
|
+
if (!candidateRef.accountId)
|
|
858
|
+
return undefined;
|
|
859
|
+
if (!candidateRef.emailKey &&
|
|
860
|
+
!options.allowUniqueAccountIdFallbackWithoutEmail) {
|
|
861
|
+
return undefined;
|
|
575
862
|
}
|
|
576
|
-
|
|
863
|
+
let matchingIndex;
|
|
864
|
+
let matchingEmailKey;
|
|
577
865
|
for (let i = 0; i < accounts.length; i += 1) {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
866
|
+
const account = accounts[i];
|
|
867
|
+
if (!account)
|
|
868
|
+
continue;
|
|
869
|
+
const ref = toAccountIdentityRef(account);
|
|
870
|
+
if (ref.accountId !== candidateRef.accountId)
|
|
871
|
+
continue;
|
|
872
|
+
if (matchingIndex !== undefined) {
|
|
873
|
+
return undefined;
|
|
874
|
+
}
|
|
875
|
+
matchingIndex = i;
|
|
876
|
+
matchingEmailKey = ref.emailKey;
|
|
877
|
+
}
|
|
878
|
+
if (matchingIndex !== undefined &&
|
|
879
|
+
matchingEmailKey &&
|
|
880
|
+
candidateRef.emailKey &&
|
|
881
|
+
matchingEmailKey !== candidateRef.emailKey) {
|
|
882
|
+
return undefined;
|
|
883
|
+
}
|
|
884
|
+
return matchingIndex;
|
|
885
|
+
}
|
|
886
|
+
export function findMatchingAccountIndex(accounts, candidate, options = {}) {
|
|
887
|
+
const candidateRef = toAccountIdentityRef(candidate);
|
|
888
|
+
const byComposite = findCompositeAccountMatchIndex(accounts, candidateRef);
|
|
889
|
+
if (byComposite !== undefined)
|
|
890
|
+
return byComposite;
|
|
891
|
+
const byEmail = findSafeEmailMatchIndex(accounts, candidateRef);
|
|
892
|
+
if (byEmail !== undefined)
|
|
893
|
+
return byEmail;
|
|
894
|
+
if (candidateRef.refreshToken) {
|
|
895
|
+
const byRefresh = findCompatibleRefreshTokenMatchIndex(accounts, candidateRef);
|
|
896
|
+
if (byRefresh !== undefined)
|
|
897
|
+
return byRefresh;
|
|
898
|
+
}
|
|
899
|
+
return findUniqueAccountIdMatchIndex(accounts, candidateRef, options);
|
|
900
|
+
}
|
|
901
|
+
export function resolveAccountSelectionIndex(accounts, candidate, fallbackIndex = 0) {
|
|
902
|
+
if (accounts.length === 0)
|
|
903
|
+
return 0;
|
|
904
|
+
const matchedIndex = findMatchingAccountIndex(accounts, candidate, {
|
|
905
|
+
allowUniqueAccountIdFallbackWithoutEmail: true,
|
|
906
|
+
});
|
|
907
|
+
if (matchedIndex !== undefined)
|
|
908
|
+
return matchedIndex;
|
|
909
|
+
return clampIndex(fallbackIndex, accounts.length);
|
|
910
|
+
}
|
|
911
|
+
function deduplicateAccountsByIdentity(accounts) {
|
|
912
|
+
const deduplicated = [];
|
|
913
|
+
for (const account of accounts) {
|
|
914
|
+
if (!account)
|
|
915
|
+
continue;
|
|
916
|
+
const existingIndex = findMatchingAccountIndex(deduplicated, account);
|
|
917
|
+
if (existingIndex === undefined) {
|
|
918
|
+
deduplicated.push(account);
|
|
919
|
+
continue;
|
|
582
920
|
}
|
|
921
|
+
deduplicated[existingIndex] = selectNewestAccount(deduplicated[existingIndex], account);
|
|
583
922
|
}
|
|
584
|
-
return
|
|
923
|
+
return deduplicated;
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Removes duplicate accounts, keeping the most recently used entry for each
|
|
927
|
+
* safely matched identity.
|
|
928
|
+
*/
|
|
929
|
+
export function deduplicateAccounts(accounts) {
|
|
930
|
+
return deduplicateAccountsByIdentity(accounts);
|
|
931
|
+
}
|
|
932
|
+
export function deduplicateAccountsByEmail(accounts) {
|
|
933
|
+
return deduplicateAccountsByIdentity(accounts);
|
|
585
934
|
}
|
|
586
935
|
function isRecord(value) {
|
|
587
936
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -591,20 +940,17 @@ function clampIndex(index, length) {
|
|
|
591
940
|
return 0;
|
|
592
941
|
return Math.max(0, Math.min(index, length - 1));
|
|
593
942
|
}
|
|
594
|
-
function
|
|
595
|
-
return account.accountId || account.refreshToken;
|
|
596
|
-
}
|
|
597
|
-
function extractActiveKey(accounts, activeIndex) {
|
|
943
|
+
function extractActiveAccountRef(accounts, activeIndex) {
|
|
598
944
|
const candidate = accounts[activeIndex];
|
|
599
945
|
if (!isRecord(candidate))
|
|
600
|
-
return
|
|
601
|
-
|
|
602
|
-
? candidate.accountId
|
|
603
|
-
: undefined
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
946
|
+
return {};
|
|
947
|
+
return toAccountIdentityRef({
|
|
948
|
+
accountId: typeof candidate.accountId === "string" ? candidate.accountId : undefined,
|
|
949
|
+
email: typeof candidate.email === "string" ? candidate.email : undefined,
|
|
950
|
+
refreshToken: typeof candidate.refreshToken === "string"
|
|
951
|
+
? candidate.refreshToken
|
|
952
|
+
: undefined,
|
|
953
|
+
});
|
|
608
954
|
}
|
|
609
955
|
/**
|
|
610
956
|
* Normalizes and validates account storage data, migrating from v1 to v3 if needed.
|
|
@@ -632,22 +978,23 @@ export function normalizeAccountStorage(data) {
|
|
|
632
978
|
? data.activeIndex
|
|
633
979
|
: 0;
|
|
634
980
|
const rawActiveIndex = clampIndex(activeIndexValue, rawAccounts.length);
|
|
635
|
-
const
|
|
981
|
+
const activeRef = extractActiveAccountRef(rawAccounts, rawActiveIndex);
|
|
636
982
|
const fromVersion = data.version;
|
|
637
983
|
const baseStorage = fromVersion === 1
|
|
638
984
|
? migrateV1ToV3(data)
|
|
639
985
|
: data;
|
|
640
|
-
const validAccounts = rawAccounts.filter((account) => isRecord(account) &&
|
|
641
|
-
|
|
986
|
+
const validAccounts = rawAccounts.filter((account) => isRecord(account) &&
|
|
987
|
+
typeof account.refreshToken === "string" &&
|
|
988
|
+
!!account.refreshToken.trim());
|
|
989
|
+
const deduplicatedAccounts = deduplicateAccounts(validAccounts);
|
|
642
990
|
const activeIndex = (() => {
|
|
643
991
|
if (deduplicatedAccounts.length === 0)
|
|
644
992
|
return 0;
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
}
|
|
650
|
-
return clampIndex(rawActiveIndex, deduplicatedAccounts.length);
|
|
993
|
+
return resolveAccountSelectionIndex(deduplicatedAccounts, {
|
|
994
|
+
accountId: activeRef.accountId,
|
|
995
|
+
email: activeRef.emailKey,
|
|
996
|
+
refreshToken: activeRef.refreshToken,
|
|
997
|
+
}, rawActiveIndex);
|
|
651
998
|
})();
|
|
652
999
|
const activeIndexByFamily = {};
|
|
653
1000
|
const rawFamilyIndices = isRecord(baseStorage.activeIndexByFamily)
|
|
@@ -659,15 +1006,12 @@ export function normalizeAccountStorage(data) {
|
|
|
659
1006
|
? rawIndexValue
|
|
660
1007
|
: rawActiveIndex;
|
|
661
1008
|
const clampedRawIndex = clampIndex(rawIndex, rawAccounts.length);
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
activeIndexByFamily[family] = mappedIndex;
|
|
1009
|
+
const familyRef = extractActiveAccountRef(rawAccounts, clampedRawIndex);
|
|
1010
|
+
activeIndexByFamily[family] = resolveAccountSelectionIndex(deduplicatedAccounts, {
|
|
1011
|
+
accountId: familyRef.accountId,
|
|
1012
|
+
email: familyRef.emailKey,
|
|
1013
|
+
refreshToken: familyRef.refreshToken,
|
|
1014
|
+
}, rawIndex);
|
|
671
1015
|
}
|
|
672
1016
|
return {
|
|
673
1017
|
version: 3,
|
|
@@ -684,10 +1028,88 @@ export function normalizeAccountStorage(data) {
|
|
|
684
1028
|
export async function loadAccounts() {
|
|
685
1029
|
return loadAccountsInternal(saveAccounts);
|
|
686
1030
|
}
|
|
1031
|
+
export async function getBackupMetadata() {
|
|
1032
|
+
const storagePath = getStoragePath();
|
|
1033
|
+
const walPath = getAccountsWalPath(storagePath);
|
|
1034
|
+
const accountCandidates = await getAccountsBackupRecoveryCandidatesWithDiscovery(storagePath);
|
|
1035
|
+
const accountSnapshots = [
|
|
1036
|
+
await describeAccountSnapshot(storagePath, "accounts-primary"),
|
|
1037
|
+
await describeAccountsWalSnapshot(walPath),
|
|
1038
|
+
];
|
|
1039
|
+
for (const [index, candidate] of accountCandidates.entries()) {
|
|
1040
|
+
const kind = candidate === `${storagePath}.bak`
|
|
1041
|
+
? "accounts-backup"
|
|
1042
|
+
: candidate.startsWith(`${storagePath}.bak.`)
|
|
1043
|
+
? "accounts-backup-history"
|
|
1044
|
+
: "accounts-discovered-backup";
|
|
1045
|
+
accountSnapshots.push(await describeAccountSnapshot(candidate, kind, index));
|
|
1046
|
+
}
|
|
1047
|
+
const flaggedPath = getFlaggedAccountsPath();
|
|
1048
|
+
const flaggedCandidates = await getAccountsBackupRecoveryCandidatesWithDiscovery(flaggedPath);
|
|
1049
|
+
const flaggedSnapshots = [
|
|
1050
|
+
await describeFlaggedSnapshot(flaggedPath, "flagged-primary"),
|
|
1051
|
+
];
|
|
1052
|
+
for (const [index, candidate] of flaggedCandidates.entries()) {
|
|
1053
|
+
const kind = candidate === `${flaggedPath}.bak`
|
|
1054
|
+
? "flagged-backup"
|
|
1055
|
+
: candidate.startsWith(`${flaggedPath}.bak.`)
|
|
1056
|
+
? "flagged-backup-history"
|
|
1057
|
+
: "flagged-discovered-backup";
|
|
1058
|
+
flaggedSnapshots.push(await describeFlaggedSnapshot(candidate, kind, index));
|
|
1059
|
+
}
|
|
1060
|
+
return {
|
|
1061
|
+
accounts: buildMetadataSection(storagePath, accountSnapshots),
|
|
1062
|
+
flaggedAccounts: buildMetadataSection(flaggedPath, flaggedSnapshots),
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
export async function getRestoreAssessment() {
|
|
1066
|
+
const storagePath = getStoragePath();
|
|
1067
|
+
const resetMarkerPath = getIntentionalResetMarkerPath(storagePath);
|
|
1068
|
+
const backupMetadata = await getBackupMetadata();
|
|
1069
|
+
if (existsSync(resetMarkerPath)) {
|
|
1070
|
+
return {
|
|
1071
|
+
storagePath,
|
|
1072
|
+
restoreEligible: false,
|
|
1073
|
+
restoreReason: "intentional-reset",
|
|
1074
|
+
backupMetadata,
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
const primarySnapshot = backupMetadata.accounts.snapshots.find((snapshot) => snapshot.kind === "accounts-primary");
|
|
1078
|
+
if (!primarySnapshot?.exists) {
|
|
1079
|
+
return {
|
|
1080
|
+
storagePath,
|
|
1081
|
+
restoreEligible: true,
|
|
1082
|
+
restoreReason: "missing-storage",
|
|
1083
|
+
latestSnapshot: backupMetadata.accounts.latestValidPath
|
|
1084
|
+
? backupMetadata.accounts.snapshots.find((snapshot) => snapshot.path === backupMetadata.accounts.latestValidPath)
|
|
1085
|
+
: undefined,
|
|
1086
|
+
backupMetadata,
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
if (primarySnapshot.valid && primarySnapshot.accountCount === 0) {
|
|
1090
|
+
return {
|
|
1091
|
+
storagePath,
|
|
1092
|
+
restoreEligible: true,
|
|
1093
|
+
restoreReason: "empty-storage",
|
|
1094
|
+
latestSnapshot: primarySnapshot,
|
|
1095
|
+
backupMetadata,
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
return {
|
|
1099
|
+
storagePath,
|
|
1100
|
+
restoreEligible: false,
|
|
1101
|
+
latestSnapshot: backupMetadata.accounts.latestValidPath
|
|
1102
|
+
? backupMetadata.accounts.snapshots.find((snapshot) => snapshot.path === backupMetadata.accounts.latestValidPath)
|
|
1103
|
+
: undefined,
|
|
1104
|
+
backupMetadata,
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
687
1107
|
function parseAndNormalizeStorage(data) {
|
|
688
1108
|
const schemaErrors = getValidationErrors(AnyAccountStorageSchema, data);
|
|
689
1109
|
const normalized = normalizeAccountStorage(data);
|
|
690
|
-
const storedVersion = isRecord(data)
|
|
1110
|
+
const storedVersion = isRecord(data)
|
|
1111
|
+
? data.version
|
|
1112
|
+
: undefined;
|
|
691
1113
|
return { normalized, storedVersion, schemaErrors };
|
|
692
1114
|
}
|
|
693
1115
|
async function loadAccountsFromPath(path) {
|
|
@@ -697,8 +1119,15 @@ async function loadAccountsFromPath(path) {
|
|
|
697
1119
|
}
|
|
698
1120
|
async function loadAccountsFromJournal(path) {
|
|
699
1121
|
const walPath = getAccountsWalPath(path);
|
|
1122
|
+
const resetMarkerPath = getIntentionalResetMarkerPath(path);
|
|
1123
|
+
if (existsSync(resetMarkerPath)) {
|
|
1124
|
+
return null;
|
|
1125
|
+
}
|
|
700
1126
|
try {
|
|
701
1127
|
const raw = await fs.readFile(walPath, "utf-8");
|
|
1128
|
+
if (existsSync(resetMarkerPath)) {
|
|
1129
|
+
return null;
|
|
1130
|
+
}
|
|
702
1131
|
const parsed = JSON.parse(raw);
|
|
703
1132
|
if (!isRecord(parsed))
|
|
704
1133
|
return null;
|
|
@@ -722,13 +1151,17 @@ async function loadAccountsFromJournal(path) {
|
|
|
722
1151
|
catch (error) {
|
|
723
1152
|
const code = error.code;
|
|
724
1153
|
if (code !== "ENOENT") {
|
|
725
|
-
log.warn("Failed to load account WAL journal", {
|
|
1154
|
+
log.warn("Failed to load account WAL journal", {
|
|
1155
|
+
path: walPath,
|
|
1156
|
+
error: String(error),
|
|
1157
|
+
});
|
|
726
1158
|
}
|
|
727
1159
|
return null;
|
|
728
1160
|
}
|
|
729
1161
|
}
|
|
730
1162
|
async function loadAccountsInternal(persistMigration) {
|
|
731
1163
|
const path = getStoragePath();
|
|
1164
|
+
const resetMarkerPath = getIntentionalResetMarkerPath(path);
|
|
732
1165
|
await cleanupStaleRotatingBackupArtifacts(path);
|
|
733
1166
|
const migratedLegacyStorage = persistMigration
|
|
734
1167
|
? await migrateLegacyProjectStorageIfNeeded(persistMigration)
|
|
@@ -736,19 +1169,32 @@ async function loadAccountsInternal(persistMigration) {
|
|
|
736
1169
|
try {
|
|
737
1170
|
const { normalized, storedVersion, schemaErrors } = await loadAccountsFromPath(path);
|
|
738
1171
|
if (schemaErrors.length > 0) {
|
|
739
|
-
log.warn("Account storage schema validation warnings", {
|
|
1172
|
+
log.warn("Account storage schema validation warnings", {
|
|
1173
|
+
errors: schemaErrors.slice(0, 5),
|
|
1174
|
+
});
|
|
740
1175
|
}
|
|
741
1176
|
if (normalized && storedVersion !== normalized.version) {
|
|
742
|
-
log.info("Migrating account storage to v3", {
|
|
1177
|
+
log.info("Migrating account storage to v3", {
|
|
1178
|
+
from: storedVersion,
|
|
1179
|
+
to: normalized.version,
|
|
1180
|
+
});
|
|
743
1181
|
if (persistMigration) {
|
|
744
1182
|
try {
|
|
745
1183
|
await persistMigration(normalized);
|
|
746
1184
|
}
|
|
747
1185
|
catch (saveError) {
|
|
748
|
-
log.warn("Failed to persist migrated storage", {
|
|
1186
|
+
log.warn("Failed to persist migrated storage", {
|
|
1187
|
+
error: String(saveError),
|
|
1188
|
+
});
|
|
749
1189
|
}
|
|
750
1190
|
}
|
|
751
1191
|
}
|
|
1192
|
+
if (existsSync(resetMarkerPath)) {
|
|
1193
|
+
return createEmptyStorageWithMetadata(false, "intentional-reset");
|
|
1194
|
+
}
|
|
1195
|
+
if (normalized && normalized.accounts.length === 0) {
|
|
1196
|
+
return withRestoreMetadata(normalized, true, "empty-storage");
|
|
1197
|
+
}
|
|
752
1198
|
const primaryLooksSynthetic = looksLikeSyntheticFixtureStorage(normalized);
|
|
753
1199
|
if (storageBackupEnabled && normalized && primaryLooksSynthetic) {
|
|
754
1200
|
const backupCandidates = await getAccountsBackupRecoveryCandidatesWithDiscovery(path);
|
|
@@ -797,6 +1243,9 @@ async function loadAccountsInternal(persistMigration) {
|
|
|
797
1243
|
}
|
|
798
1244
|
catch (error) {
|
|
799
1245
|
const code = error.code;
|
|
1246
|
+
if (existsSync(resetMarkerPath)) {
|
|
1247
|
+
return createEmptyStorageWithMetadata(false, "intentional-reset");
|
|
1248
|
+
}
|
|
800
1249
|
if (code === "ENOENT" && migratedLegacyStorage) {
|
|
801
1250
|
return migratedLegacyStorage;
|
|
802
1251
|
}
|
|
@@ -815,6 +1264,9 @@ async function loadAccountsInternal(persistMigration) {
|
|
|
815
1264
|
}
|
|
816
1265
|
return recoveredFromWal;
|
|
817
1266
|
}
|
|
1267
|
+
if (existsSync(resetMarkerPath)) {
|
|
1268
|
+
return createEmptyStorageWithMetadata(false, "intentional-reset");
|
|
1269
|
+
}
|
|
818
1270
|
if (storageBackupEnabled) {
|
|
819
1271
|
const backupCandidates = await getAccountsBackupRecoveryCandidatesWithDiscovery(path);
|
|
820
1272
|
for (const backupPath of backupCandidates) {
|
|
@@ -827,7 +1279,10 @@ async function loadAccountsInternal(persistMigration) {
|
|
|
827
1279
|
});
|
|
828
1280
|
}
|
|
829
1281
|
if (backup.normalized) {
|
|
830
|
-
log.warn("Recovered account storage from backup file", {
|
|
1282
|
+
log.warn("Recovered account storage from backup file", {
|
|
1283
|
+
path,
|
|
1284
|
+
backupPath,
|
|
1285
|
+
});
|
|
831
1286
|
if (persistMigration) {
|
|
832
1287
|
try {
|
|
833
1288
|
await persistMigration(backup.normalized);
|
|
@@ -856,11 +1311,15 @@ async function loadAccountsInternal(persistMigration) {
|
|
|
856
1311
|
if (code !== "ENOENT") {
|
|
857
1312
|
log.error("Failed to load account storage", { error: String(error) });
|
|
858
1313
|
}
|
|
1314
|
+
if (code === "ENOENT") {
|
|
1315
|
+
return createEmptyStorageWithMetadata(true, "missing-storage");
|
|
1316
|
+
}
|
|
859
1317
|
return null;
|
|
860
1318
|
}
|
|
861
1319
|
}
|
|
862
1320
|
async function saveAccountsUnlocked(storage) {
|
|
863
1321
|
const path = getStoragePath();
|
|
1322
|
+
const resetMarkerPath = getIntentionalResetMarkerPath(path);
|
|
864
1323
|
const uniqueSuffix = `${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
|
|
865
1324
|
const tempPath = `${path}.${uniqueSuffix}.tmp`;
|
|
866
1325
|
const walPath = getAccountsWalPath(path);
|
|
@@ -870,7 +1329,9 @@ async function saveAccountsUnlocked(storage) {
|
|
|
870
1329
|
if (looksLikeSyntheticFixtureStorage(storage)) {
|
|
871
1330
|
try {
|
|
872
1331
|
const existing = await loadNormalizedStorageFromPath(path, "existing account storage");
|
|
873
|
-
if (existing &&
|
|
1332
|
+
if (existing &&
|
|
1333
|
+
existing.accounts.length > 0 &&
|
|
1334
|
+
!looksLikeSyntheticFixtureStorage(existing)) {
|
|
874
1335
|
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.");
|
|
875
1336
|
}
|
|
876
1337
|
}
|
|
@@ -916,6 +1377,12 @@ async function saveAccountsUnlocked(storage) {
|
|
|
916
1377
|
for (let attempt = 0; attempt < 5; attempt++) {
|
|
917
1378
|
try {
|
|
918
1379
|
await fs.rename(tempPath, path);
|
|
1380
|
+
try {
|
|
1381
|
+
await fs.unlink(resetMarkerPath);
|
|
1382
|
+
}
|
|
1383
|
+
catch {
|
|
1384
|
+
// Best effort cleanup.
|
|
1385
|
+
}
|
|
919
1386
|
lastAccountsSaveTimestamp = Date.now();
|
|
920
1387
|
try {
|
|
921
1388
|
await fs.unlink(walPath);
|
|
@@ -929,7 +1396,7 @@ async function saveAccountsUnlocked(storage) {
|
|
|
929
1396
|
const code = renameError.code;
|
|
930
1397
|
if (code === "EPERM" || code === "EBUSY") {
|
|
931
1398
|
lastError = renameError;
|
|
932
|
-
await new Promise(r => setTimeout(r, 10 *
|
|
1399
|
+
await new Promise((r) => setTimeout(r, 10 * 2 ** attempt));
|
|
933
1400
|
continue;
|
|
934
1401
|
}
|
|
935
1402
|
throw renameError;
|
|
@@ -957,10 +1424,63 @@ async function saveAccountsUnlocked(storage) {
|
|
|
957
1424
|
throw new StorageError(`Failed to save accounts: ${err?.message || "Unknown error"}`, code, path, hint, err instanceof Error ? err : undefined);
|
|
958
1425
|
}
|
|
959
1426
|
}
|
|
1427
|
+
function cloneAccountStorageForPersistence(storage) {
|
|
1428
|
+
return {
|
|
1429
|
+
version: 3,
|
|
1430
|
+
accounts: structuredClone(storage?.accounts ?? []),
|
|
1431
|
+
activeIndex: typeof storage?.activeIndex === "number" &&
|
|
1432
|
+
Number.isFinite(storage.activeIndex)
|
|
1433
|
+
? storage.activeIndex
|
|
1434
|
+
: 0,
|
|
1435
|
+
activeIndexByFamily: structuredClone(storage?.activeIndexByFamily ?? {}),
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
960
1438
|
export async function withAccountStorageTransaction(handler) {
|
|
961
1439
|
return withStorageLock(async () => {
|
|
962
|
-
const
|
|
963
|
-
|
|
1440
|
+
const state = {
|
|
1441
|
+
snapshot: await loadAccountsInternal(saveAccountsUnlocked),
|
|
1442
|
+
active: true,
|
|
1443
|
+
};
|
|
1444
|
+
const current = state.snapshot;
|
|
1445
|
+
const persist = async (storage) => {
|
|
1446
|
+
await saveAccountsUnlocked(storage);
|
|
1447
|
+
state.snapshot = storage;
|
|
1448
|
+
};
|
|
1449
|
+
return transactionSnapshotContext.run(state, () => handler(current, persist));
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
export async function withAccountAndFlaggedStorageTransaction(handler) {
|
|
1453
|
+
return withStorageLock(async () => {
|
|
1454
|
+
const state = {
|
|
1455
|
+
snapshot: await loadAccountsInternal(saveAccountsUnlocked),
|
|
1456
|
+
active: true,
|
|
1457
|
+
};
|
|
1458
|
+
const current = state.snapshot;
|
|
1459
|
+
const persist = async (accountStorage, flaggedStorage) => {
|
|
1460
|
+
const previousAccounts = cloneAccountStorageForPersistence(state.snapshot);
|
|
1461
|
+
const nextAccounts = cloneAccountStorageForPersistence(accountStorage);
|
|
1462
|
+
await saveAccountsUnlocked(nextAccounts);
|
|
1463
|
+
try {
|
|
1464
|
+
await saveFlaggedAccountsUnlocked(flaggedStorage);
|
|
1465
|
+
state.snapshot = nextAccounts;
|
|
1466
|
+
}
|
|
1467
|
+
catch (error) {
|
|
1468
|
+
try {
|
|
1469
|
+
await saveAccountsUnlocked(previousAccounts);
|
|
1470
|
+
state.snapshot = previousAccounts;
|
|
1471
|
+
}
|
|
1472
|
+
catch (rollbackError) {
|
|
1473
|
+
const combinedError = new AggregateError([error, rollbackError], "Flagged save failed and account storage rollback also failed");
|
|
1474
|
+
log.error("Failed to rollback account storage after flagged save failure", {
|
|
1475
|
+
error: String(error),
|
|
1476
|
+
rollbackError: String(rollbackError),
|
|
1477
|
+
});
|
|
1478
|
+
throw combinedError;
|
|
1479
|
+
}
|
|
1480
|
+
throw error;
|
|
1481
|
+
}
|
|
1482
|
+
};
|
|
1483
|
+
return transactionSnapshotContext.run(state, () => handler(current, persist));
|
|
964
1484
|
});
|
|
965
1485
|
}
|
|
966
1486
|
/**
|
|
@@ -982,8 +1502,10 @@ export async function saveAccounts(storage) {
|
|
|
982
1502
|
export async function clearAccounts() {
|
|
983
1503
|
return withStorageLock(async () => {
|
|
984
1504
|
const path = getStoragePath();
|
|
1505
|
+
const resetMarkerPath = getIntentionalResetMarkerPath(path);
|
|
985
1506
|
const walPath = getAccountsWalPath(path);
|
|
986
|
-
const backupPaths =
|
|
1507
|
+
const backupPaths = await getAccountsBackupRecoveryCandidatesWithDiscovery(path);
|
|
1508
|
+
await fs.writeFile(resetMarkerPath, JSON.stringify({ version: 1, createdAt: Date.now() }), { encoding: "utf-8", mode: 0o600 });
|
|
987
1509
|
const clearPath = async (targetPath) => {
|
|
988
1510
|
try {
|
|
989
1511
|
await fs.unlink(targetPath);
|
|
@@ -999,7 +1521,11 @@ export async function clearAccounts() {
|
|
|
999
1521
|
}
|
|
1000
1522
|
};
|
|
1001
1523
|
try {
|
|
1002
|
-
await Promise.all([
|
|
1524
|
+
await Promise.all([
|
|
1525
|
+
clearPath(path),
|
|
1526
|
+
clearPath(walPath),
|
|
1527
|
+
...backupPaths.map(clearPath),
|
|
1528
|
+
]);
|
|
1003
1529
|
}
|
|
1004
1530
|
catch {
|
|
1005
1531
|
// Individual path cleanup is already best-effort with per-artifact logging.
|
|
@@ -1014,13 +1540,22 @@ function normalizeFlaggedStorage(data) {
|
|
|
1014
1540
|
for (const rawAccount of data.accounts) {
|
|
1015
1541
|
if (!isRecord(rawAccount))
|
|
1016
1542
|
continue;
|
|
1017
|
-
const refreshToken = typeof rawAccount.refreshToken === "string"
|
|
1543
|
+
const refreshToken = typeof rawAccount.refreshToken === "string"
|
|
1544
|
+
? rawAccount.refreshToken.trim()
|
|
1545
|
+
: "";
|
|
1018
1546
|
if (!refreshToken)
|
|
1019
1547
|
continue;
|
|
1020
|
-
const flaggedAt = typeof rawAccount.flaggedAt === "number"
|
|
1021
|
-
|
|
1548
|
+
const flaggedAt = typeof rawAccount.flaggedAt === "number"
|
|
1549
|
+
? rawAccount.flaggedAt
|
|
1550
|
+
: Date.now();
|
|
1551
|
+
const isAccountIdSource = (value) => value === "token" ||
|
|
1552
|
+
value === "id_token" ||
|
|
1553
|
+
value === "org" ||
|
|
1554
|
+
value === "manual";
|
|
1022
1555
|
const isSwitchReason = (value) => value === "rate-limit" || value === "initial" || value === "rotation";
|
|
1023
|
-
const isCooldownReason = (value) => value === "auth-failure" ||
|
|
1556
|
+
const isCooldownReason = (value) => value === "auth-failure" ||
|
|
1557
|
+
value === "network-error" ||
|
|
1558
|
+
value === "rate-limit";
|
|
1024
1559
|
let rateLimitResetTimes;
|
|
1025
1560
|
if (isRecord(rawAccount.rateLimitResetTimes)) {
|
|
1026
1561
|
const normalizedRateLimits = {};
|
|
@@ -1045,19 +1580,33 @@ function normalizeFlaggedStorage(data) {
|
|
|
1045
1580
|
const normalized = {
|
|
1046
1581
|
refreshToken,
|
|
1047
1582
|
addedAt: typeof rawAccount.addedAt === "number" ? rawAccount.addedAt : flaggedAt,
|
|
1048
|
-
lastUsed: typeof rawAccount.lastUsed === "number"
|
|
1049
|
-
|
|
1583
|
+
lastUsed: typeof rawAccount.lastUsed === "number"
|
|
1584
|
+
? rawAccount.lastUsed
|
|
1585
|
+
: flaggedAt,
|
|
1586
|
+
accountId: typeof rawAccount.accountId === "string"
|
|
1587
|
+
? rawAccount.accountId
|
|
1588
|
+
: undefined,
|
|
1050
1589
|
accountIdSource,
|
|
1051
|
-
accountLabel: typeof rawAccount.accountLabel === "string"
|
|
1590
|
+
accountLabel: typeof rawAccount.accountLabel === "string"
|
|
1591
|
+
? rawAccount.accountLabel
|
|
1592
|
+
: undefined,
|
|
1052
1593
|
email: typeof rawAccount.email === "string" ? rawAccount.email : undefined,
|
|
1053
|
-
enabled: typeof rawAccount.enabled === "boolean"
|
|
1594
|
+
enabled: typeof rawAccount.enabled === "boolean"
|
|
1595
|
+
? rawAccount.enabled
|
|
1596
|
+
: undefined,
|
|
1054
1597
|
lastSwitchReason,
|
|
1055
1598
|
rateLimitResetTimes,
|
|
1056
|
-
coolingDownUntil: typeof rawAccount.coolingDownUntil === "number"
|
|
1599
|
+
coolingDownUntil: typeof rawAccount.coolingDownUntil === "number"
|
|
1600
|
+
? rawAccount.coolingDownUntil
|
|
1601
|
+
: undefined,
|
|
1057
1602
|
cooldownReason,
|
|
1058
1603
|
flaggedAt,
|
|
1059
|
-
flaggedReason: typeof rawAccount.flaggedReason === "string"
|
|
1060
|
-
|
|
1604
|
+
flaggedReason: typeof rawAccount.flaggedReason === "string"
|
|
1605
|
+
? rawAccount.flaggedReason
|
|
1606
|
+
: undefined,
|
|
1607
|
+
lastError: typeof rawAccount.lastError === "string"
|
|
1608
|
+
? rawAccount.lastError
|
|
1609
|
+
: undefined,
|
|
1061
1610
|
};
|
|
1062
1611
|
byRefreshToken.set(refreshToken, normalized);
|
|
1063
1612
|
}
|
|
@@ -1068,16 +1617,24 @@ function normalizeFlaggedStorage(data) {
|
|
|
1068
1617
|
}
|
|
1069
1618
|
export async function loadFlaggedAccounts() {
|
|
1070
1619
|
const path = getFlaggedAccountsPath();
|
|
1620
|
+
const resetMarkerPath = getIntentionalResetMarkerPath(path);
|
|
1071
1621
|
const empty = { version: 1, accounts: [] };
|
|
1072
1622
|
try {
|
|
1073
1623
|
const content = await fs.readFile(path, "utf-8");
|
|
1074
1624
|
const data = JSON.parse(content);
|
|
1075
|
-
|
|
1625
|
+
const loaded = normalizeFlaggedStorage(data);
|
|
1626
|
+
if (existsSync(resetMarkerPath)) {
|
|
1627
|
+
return empty;
|
|
1628
|
+
}
|
|
1629
|
+
return loaded;
|
|
1076
1630
|
}
|
|
1077
1631
|
catch (error) {
|
|
1078
1632
|
const code = error.code;
|
|
1079
1633
|
if (code !== "ENOENT") {
|
|
1080
|
-
log.error("Failed to load flagged account storage", {
|
|
1634
|
+
log.error("Failed to load flagged account storage", {
|
|
1635
|
+
path,
|
|
1636
|
+
error: String(error),
|
|
1637
|
+
});
|
|
1081
1638
|
return empty;
|
|
1082
1639
|
}
|
|
1083
1640
|
}
|
|
@@ -1114,38 +1671,89 @@ export async function loadFlaggedAccounts() {
|
|
|
1114
1671
|
return empty;
|
|
1115
1672
|
}
|
|
1116
1673
|
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
await fs.writeFile(tempPath, content, { encoding: "utf-8", mode: 0o600 });
|
|
1126
|
-
await fs.rename(tempPath, path);
|
|
1127
|
-
}
|
|
1128
|
-
catch (error) {
|
|
1674
|
+
async function saveFlaggedAccountsUnlocked(storage) {
|
|
1675
|
+
const path = getFlaggedAccountsPath();
|
|
1676
|
+
const markerPath = getIntentionalResetMarkerPath(path);
|
|
1677
|
+
const uniqueSuffix = `${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
|
|
1678
|
+
const tempPath = `${path}.${uniqueSuffix}.tmp`;
|
|
1679
|
+
try {
|
|
1680
|
+
await fs.mkdir(dirname(path), { recursive: true });
|
|
1681
|
+
if (existsSync(path)) {
|
|
1129
1682
|
try {
|
|
1130
|
-
await
|
|
1683
|
+
await copyFileWithRetry(path, `${path}.bak`, {
|
|
1684
|
+
allowMissingSource: true,
|
|
1685
|
+
});
|
|
1131
1686
|
}
|
|
1132
|
-
catch {
|
|
1133
|
-
|
|
1687
|
+
catch (backupError) {
|
|
1688
|
+
log.warn("Failed to create flagged backup snapshot", {
|
|
1689
|
+
path,
|
|
1690
|
+
error: String(backupError),
|
|
1691
|
+
});
|
|
1134
1692
|
}
|
|
1135
|
-
log.error("Failed to save flagged account storage", { path, error: String(error) });
|
|
1136
|
-
throw error;
|
|
1137
1693
|
}
|
|
1694
|
+
const content = JSON.stringify(normalizeFlaggedStorage(storage), null, 2);
|
|
1695
|
+
await fs.writeFile(tempPath, content, { encoding: "utf-8", mode: 0o600 });
|
|
1696
|
+
await renameFileWithRetry(tempPath, path);
|
|
1697
|
+
try {
|
|
1698
|
+
await fs.unlink(markerPath);
|
|
1699
|
+
}
|
|
1700
|
+
catch {
|
|
1701
|
+
// Best effort cleanup.
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
catch (error) {
|
|
1705
|
+
try {
|
|
1706
|
+
await fs.unlink(tempPath);
|
|
1707
|
+
}
|
|
1708
|
+
catch {
|
|
1709
|
+
// Ignore cleanup failures.
|
|
1710
|
+
}
|
|
1711
|
+
log.error("Failed to save flagged account storage", {
|
|
1712
|
+
path,
|
|
1713
|
+
error: String(error),
|
|
1714
|
+
});
|
|
1715
|
+
throw error;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
export async function saveFlaggedAccounts(storage) {
|
|
1719
|
+
return withStorageLock(async () => {
|
|
1720
|
+
await saveFlaggedAccountsUnlocked(storage);
|
|
1138
1721
|
});
|
|
1139
1722
|
}
|
|
1140
1723
|
export async function clearFlaggedAccounts() {
|
|
1141
1724
|
return withStorageLock(async () => {
|
|
1725
|
+
const path = getFlaggedAccountsPath();
|
|
1726
|
+
const markerPath = getIntentionalResetMarkerPath(path);
|
|
1142
1727
|
try {
|
|
1143
|
-
await fs.
|
|
1728
|
+
await fs.writeFile(markerPath, "reset", {
|
|
1729
|
+
encoding: "utf-8",
|
|
1730
|
+
mode: 0o600,
|
|
1731
|
+
});
|
|
1144
1732
|
}
|
|
1145
1733
|
catch (error) {
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1734
|
+
log.error("Failed to write flagged reset marker", {
|
|
1735
|
+
path,
|
|
1736
|
+
markerPath,
|
|
1737
|
+
error: String(error),
|
|
1738
|
+
});
|
|
1739
|
+
throw error;
|
|
1740
|
+
}
|
|
1741
|
+
const backupPaths = await getAccountsBackupRecoveryCandidatesWithDiscovery(path);
|
|
1742
|
+
for (const candidate of [path, ...backupPaths, markerPath]) {
|
|
1743
|
+
try {
|
|
1744
|
+
await fs.unlink(candidate);
|
|
1745
|
+
}
|
|
1746
|
+
catch (error) {
|
|
1747
|
+
const code = error.code;
|
|
1748
|
+
if (code !== "ENOENT") {
|
|
1749
|
+
log.error("Failed to clear flagged account storage", {
|
|
1750
|
+
path: candidate,
|
|
1751
|
+
error: String(error),
|
|
1752
|
+
});
|
|
1753
|
+
if (candidate === path) {
|
|
1754
|
+
throw error;
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1149
1757
|
}
|
|
1150
1758
|
}
|
|
1151
1759
|
});
|
|
@@ -1156,23 +1764,38 @@ export async function clearFlaggedAccounts() {
|
|
|
1156
1764
|
* @param force - If true, overwrite existing file (default: true)
|
|
1157
1765
|
* @throws Error if file exists and force is false, or if no accounts to export
|
|
1158
1766
|
*/
|
|
1159
|
-
export async function exportAccounts(filePath, force = true) {
|
|
1767
|
+
export async function exportAccounts(filePath, force = true, beforeCommit) {
|
|
1160
1768
|
const resolvedPath = resolvePath(filePath);
|
|
1161
1769
|
if (!force && existsSync(resolvedPath)) {
|
|
1162
1770
|
throw new Error(`File already exists: ${resolvedPath}`);
|
|
1163
1771
|
}
|
|
1164
|
-
const
|
|
1772
|
+
const transactionState = transactionSnapshotContext.getStore();
|
|
1773
|
+
const storage = transactionState?.active
|
|
1774
|
+
? transactionState.snapshot
|
|
1775
|
+
: await withAccountStorageTransaction((current) => Promise.resolve(current));
|
|
1165
1776
|
if (!storage || storage.accounts.length === 0) {
|
|
1166
1777
|
throw new Error("No accounts to export");
|
|
1167
1778
|
}
|
|
1168
1779
|
await fs.mkdir(dirname(resolvedPath), { recursive: true });
|
|
1169
|
-
|
|
1780
|
+
await beforeCommit?.(resolvedPath);
|
|
1781
|
+
if (!force && existsSync(resolvedPath)) {
|
|
1782
|
+
throw new Error(`File already exists: ${resolvedPath}`);
|
|
1783
|
+
}
|
|
1784
|
+
const content = JSON.stringify({
|
|
1785
|
+
version: storage.version,
|
|
1786
|
+
accounts: storage.accounts,
|
|
1787
|
+
activeIndex: storage.activeIndex,
|
|
1788
|
+
activeIndexByFamily: storage.activeIndexByFamily,
|
|
1789
|
+
}, null, 2);
|
|
1170
1790
|
await fs.writeFile(resolvedPath, content, { encoding: "utf-8", mode: 0o600 });
|
|
1171
|
-
log.info("Exported accounts", {
|
|
1791
|
+
log.info("Exported accounts", {
|
|
1792
|
+
path: resolvedPath,
|
|
1793
|
+
count: storage.accounts.length,
|
|
1794
|
+
});
|
|
1172
1795
|
}
|
|
1173
1796
|
/**
|
|
1174
1797
|
* Imports accounts from a JSON file, merging with existing accounts.
|
|
1175
|
-
* Deduplicates by
|
|
1798
|
+
* Deduplicates by safe account identity, preserving most recently used entries.
|
|
1176
1799
|
* @param filePath - Source file path
|
|
1177
1800
|
* @throws Error if file is invalid or would exceed MAX_ACCOUNTS
|
|
1178
1801
|
*/
|
|
@@ -1194,17 +1817,17 @@ export async function importAccounts(filePath) {
|
|
|
1194
1817
|
if (!normalized) {
|
|
1195
1818
|
throw new Error("Invalid account storage format");
|
|
1196
1819
|
}
|
|
1197
|
-
const { imported: importedCount, total, skipped: skippedCount } = await withAccountStorageTransaction(async (existing, persist) => {
|
|
1820
|
+
const { imported: importedCount, total, skipped: skippedCount, } = await withAccountStorageTransaction(async (existing, persist) => {
|
|
1198
1821
|
const existingAccounts = existing?.accounts ?? [];
|
|
1199
1822
|
const existingActiveIndex = existing?.activeIndex ?? 0;
|
|
1200
1823
|
const merged = [...existingAccounts, ...normalized.accounts];
|
|
1201
1824
|
if (merged.length > ACCOUNT_LIMITS.MAX_ACCOUNTS) {
|
|
1202
|
-
const deduped =
|
|
1825
|
+
const deduped = deduplicateAccounts(merged);
|
|
1203
1826
|
if (deduped.length > ACCOUNT_LIMITS.MAX_ACCOUNTS) {
|
|
1204
1827
|
throw new Error(`Import would exceed maximum of ${ACCOUNT_LIMITS.MAX_ACCOUNTS} accounts (would have ${deduped.length})`);
|
|
1205
1828
|
}
|
|
1206
1829
|
}
|
|
1207
|
-
const deduplicatedAccounts =
|
|
1830
|
+
const deduplicatedAccounts = deduplicateAccounts(merged);
|
|
1208
1831
|
const newStorage = {
|
|
1209
1832
|
version: 3,
|
|
1210
1833
|
accounts: deduplicatedAccounts,
|
|
@@ -1216,7 +1839,12 @@ export async function importAccounts(filePath) {
|
|
|
1216
1839
|
const skipped = normalized.accounts.length - imported;
|
|
1217
1840
|
return { imported, total: deduplicatedAccounts.length, skipped };
|
|
1218
1841
|
});
|
|
1219
|
-
log.info("Imported accounts", {
|
|
1842
|
+
log.info("Imported accounts", {
|
|
1843
|
+
path: resolvedPath,
|
|
1844
|
+
imported: importedCount,
|
|
1845
|
+
skipped: skippedCount,
|
|
1846
|
+
total,
|
|
1847
|
+
});
|
|
1220
1848
|
return { imported: importedCount, total, skipped: skippedCount };
|
|
1221
1849
|
}
|
|
1222
1850
|
//# sourceMappingURL=storage.js.map
|