codex-multi-auth 0.1.7 → 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 +13 -0
- 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/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 +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/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 +21 -1
- 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 +519 -51
- 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 +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/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;
|
|
@@ -565,7 +796,8 @@ export function deduplicateAccountsByEmail(accounts) {
|
|
|
565
796
|
const existingAddedAt = existing.addedAt || 0;
|
|
566
797
|
const candidateAddedAt = account.addedAt || 0;
|
|
567
798
|
const isNewer = candidateLastUsed > existingLastUsed ||
|
|
568
|
-
(candidateLastUsed === existingLastUsed &&
|
|
799
|
+
(candidateLastUsed === existingLastUsed &&
|
|
800
|
+
candidateAddedAt > existingAddedAt);
|
|
569
801
|
if (isNewer) {
|
|
570
802
|
emailToNewestIndex.set(email, i);
|
|
571
803
|
}
|
|
@@ -637,7 +869,9 @@ export function normalizeAccountStorage(data) {
|
|
|
637
869
|
const baseStorage = fromVersion === 1
|
|
638
870
|
? migrateV1ToV3(data)
|
|
639
871
|
: data;
|
|
640
|
-
const validAccounts = rawAccounts.filter((account) => isRecord(account) &&
|
|
872
|
+
const validAccounts = rawAccounts.filter((account) => isRecord(account) &&
|
|
873
|
+
typeof account.refreshToken === "string" &&
|
|
874
|
+
!!account.refreshToken.trim());
|
|
641
875
|
const deduplicatedAccounts = deduplicateAccountsByEmail(deduplicateAccountsByKey(validAccounts));
|
|
642
876
|
const activeIndex = (() => {
|
|
643
877
|
if (deduplicatedAccounts.length === 0)
|
|
@@ -684,10 +918,88 @@ export function normalizeAccountStorage(data) {
|
|
|
684
918
|
export async function loadAccounts() {
|
|
685
919
|
return loadAccountsInternal(saveAccounts);
|
|
686
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
|
+
}
|
|
687
997
|
function parseAndNormalizeStorage(data) {
|
|
688
998
|
const schemaErrors = getValidationErrors(AnyAccountStorageSchema, data);
|
|
689
999
|
const normalized = normalizeAccountStorage(data);
|
|
690
|
-
const storedVersion = isRecord(data)
|
|
1000
|
+
const storedVersion = isRecord(data)
|
|
1001
|
+
? data.version
|
|
1002
|
+
: undefined;
|
|
691
1003
|
return { normalized, storedVersion, schemaErrors };
|
|
692
1004
|
}
|
|
693
1005
|
async function loadAccountsFromPath(path) {
|
|
@@ -697,8 +1009,15 @@ async function loadAccountsFromPath(path) {
|
|
|
697
1009
|
}
|
|
698
1010
|
async function loadAccountsFromJournal(path) {
|
|
699
1011
|
const walPath = getAccountsWalPath(path);
|
|
1012
|
+
const resetMarkerPath = getIntentionalResetMarkerPath(path);
|
|
1013
|
+
if (existsSync(resetMarkerPath)) {
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
700
1016
|
try {
|
|
701
1017
|
const raw = await fs.readFile(walPath, "utf-8");
|
|
1018
|
+
if (existsSync(resetMarkerPath)) {
|
|
1019
|
+
return null;
|
|
1020
|
+
}
|
|
702
1021
|
const parsed = JSON.parse(raw);
|
|
703
1022
|
if (!isRecord(parsed))
|
|
704
1023
|
return null;
|
|
@@ -722,13 +1041,17 @@ async function loadAccountsFromJournal(path) {
|
|
|
722
1041
|
catch (error) {
|
|
723
1042
|
const code = error.code;
|
|
724
1043
|
if (code !== "ENOENT") {
|
|
725
|
-
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
|
+
});
|
|
726
1048
|
}
|
|
727
1049
|
return null;
|
|
728
1050
|
}
|
|
729
1051
|
}
|
|
730
1052
|
async function loadAccountsInternal(persistMigration) {
|
|
731
1053
|
const path = getStoragePath();
|
|
1054
|
+
const resetMarkerPath = getIntentionalResetMarkerPath(path);
|
|
732
1055
|
await cleanupStaleRotatingBackupArtifacts(path);
|
|
733
1056
|
const migratedLegacyStorage = persistMigration
|
|
734
1057
|
? await migrateLegacyProjectStorageIfNeeded(persistMigration)
|
|
@@ -736,19 +1059,32 @@ async function loadAccountsInternal(persistMigration) {
|
|
|
736
1059
|
try {
|
|
737
1060
|
const { normalized, storedVersion, schemaErrors } = await loadAccountsFromPath(path);
|
|
738
1061
|
if (schemaErrors.length > 0) {
|
|
739
|
-
log.warn("Account storage schema validation warnings", {
|
|
1062
|
+
log.warn("Account storage schema validation warnings", {
|
|
1063
|
+
errors: schemaErrors.slice(0, 5),
|
|
1064
|
+
});
|
|
740
1065
|
}
|
|
741
1066
|
if (normalized && storedVersion !== normalized.version) {
|
|
742
|
-
log.info("Migrating account storage to v3", {
|
|
1067
|
+
log.info("Migrating account storage to v3", {
|
|
1068
|
+
from: storedVersion,
|
|
1069
|
+
to: normalized.version,
|
|
1070
|
+
});
|
|
743
1071
|
if (persistMigration) {
|
|
744
1072
|
try {
|
|
745
1073
|
await persistMigration(normalized);
|
|
746
1074
|
}
|
|
747
1075
|
catch (saveError) {
|
|
748
|
-
log.warn("Failed to persist migrated storage", {
|
|
1076
|
+
log.warn("Failed to persist migrated storage", {
|
|
1077
|
+
error: String(saveError),
|
|
1078
|
+
});
|
|
749
1079
|
}
|
|
750
1080
|
}
|
|
751
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
|
+
}
|
|
752
1088
|
const primaryLooksSynthetic = looksLikeSyntheticFixtureStorage(normalized);
|
|
753
1089
|
if (storageBackupEnabled && normalized && primaryLooksSynthetic) {
|
|
754
1090
|
const backupCandidates = await getAccountsBackupRecoveryCandidatesWithDiscovery(path);
|
|
@@ -797,6 +1133,9 @@ async function loadAccountsInternal(persistMigration) {
|
|
|
797
1133
|
}
|
|
798
1134
|
catch (error) {
|
|
799
1135
|
const code = error.code;
|
|
1136
|
+
if (existsSync(resetMarkerPath)) {
|
|
1137
|
+
return createEmptyStorageWithMetadata(false, "intentional-reset");
|
|
1138
|
+
}
|
|
800
1139
|
if (code === "ENOENT" && migratedLegacyStorage) {
|
|
801
1140
|
return migratedLegacyStorage;
|
|
802
1141
|
}
|
|
@@ -815,6 +1154,9 @@ async function loadAccountsInternal(persistMigration) {
|
|
|
815
1154
|
}
|
|
816
1155
|
return recoveredFromWal;
|
|
817
1156
|
}
|
|
1157
|
+
if (existsSync(resetMarkerPath)) {
|
|
1158
|
+
return createEmptyStorageWithMetadata(false, "intentional-reset");
|
|
1159
|
+
}
|
|
818
1160
|
if (storageBackupEnabled) {
|
|
819
1161
|
const backupCandidates = await getAccountsBackupRecoveryCandidatesWithDiscovery(path);
|
|
820
1162
|
for (const backupPath of backupCandidates) {
|
|
@@ -827,7 +1169,10 @@ async function loadAccountsInternal(persistMigration) {
|
|
|
827
1169
|
});
|
|
828
1170
|
}
|
|
829
1171
|
if (backup.normalized) {
|
|
830
|
-
log.warn("Recovered account storage from backup file", {
|
|
1172
|
+
log.warn("Recovered account storage from backup file", {
|
|
1173
|
+
path,
|
|
1174
|
+
backupPath,
|
|
1175
|
+
});
|
|
831
1176
|
if (persistMigration) {
|
|
832
1177
|
try {
|
|
833
1178
|
await persistMigration(backup.normalized);
|
|
@@ -856,11 +1201,15 @@ async function loadAccountsInternal(persistMigration) {
|
|
|
856
1201
|
if (code !== "ENOENT") {
|
|
857
1202
|
log.error("Failed to load account storage", { error: String(error) });
|
|
858
1203
|
}
|
|
1204
|
+
if (code === "ENOENT") {
|
|
1205
|
+
return createEmptyStorageWithMetadata(true, "missing-storage");
|
|
1206
|
+
}
|
|
859
1207
|
return null;
|
|
860
1208
|
}
|
|
861
1209
|
}
|
|
862
1210
|
async function saveAccountsUnlocked(storage) {
|
|
863
1211
|
const path = getStoragePath();
|
|
1212
|
+
const resetMarkerPath = getIntentionalResetMarkerPath(path);
|
|
864
1213
|
const uniqueSuffix = `${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
|
|
865
1214
|
const tempPath = `${path}.${uniqueSuffix}.tmp`;
|
|
866
1215
|
const walPath = getAccountsWalPath(path);
|
|
@@ -870,7 +1219,9 @@ async function saveAccountsUnlocked(storage) {
|
|
|
870
1219
|
if (looksLikeSyntheticFixtureStorage(storage)) {
|
|
871
1220
|
try {
|
|
872
1221
|
const existing = await loadNormalizedStorageFromPath(path, "existing account storage");
|
|
873
|
-
if (existing &&
|
|
1222
|
+
if (existing &&
|
|
1223
|
+
existing.accounts.length > 0 &&
|
|
1224
|
+
!looksLikeSyntheticFixtureStorage(existing)) {
|
|
874
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.");
|
|
875
1226
|
}
|
|
876
1227
|
}
|
|
@@ -916,6 +1267,12 @@ async function saveAccountsUnlocked(storage) {
|
|
|
916
1267
|
for (let attempt = 0; attempt < 5; attempt++) {
|
|
917
1268
|
try {
|
|
918
1269
|
await fs.rename(tempPath, path);
|
|
1270
|
+
try {
|
|
1271
|
+
await fs.unlink(resetMarkerPath);
|
|
1272
|
+
}
|
|
1273
|
+
catch {
|
|
1274
|
+
// Best effort cleanup.
|
|
1275
|
+
}
|
|
919
1276
|
lastAccountsSaveTimestamp = Date.now();
|
|
920
1277
|
try {
|
|
921
1278
|
await fs.unlink(walPath);
|
|
@@ -929,7 +1286,7 @@ async function saveAccountsUnlocked(storage) {
|
|
|
929
1286
|
const code = renameError.code;
|
|
930
1287
|
if (code === "EPERM" || code === "EBUSY") {
|
|
931
1288
|
lastError = renameError;
|
|
932
|
-
await new Promise(r => setTimeout(r, 10 *
|
|
1289
|
+
await new Promise((r) => setTimeout(r, 10 * 2 ** attempt));
|
|
933
1290
|
continue;
|
|
934
1291
|
}
|
|
935
1292
|
throw renameError;
|
|
@@ -959,8 +1316,16 @@ async function saveAccountsUnlocked(storage) {
|
|
|
959
1316
|
}
|
|
960
1317
|
export async function withAccountStorageTransaction(handler) {
|
|
961
1318
|
return withStorageLock(async () => {
|
|
962
|
-
const
|
|
963
|
-
|
|
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));
|
|
964
1329
|
});
|
|
965
1330
|
}
|
|
966
1331
|
/**
|
|
@@ -982,8 +1347,10 @@ export async function saveAccounts(storage) {
|
|
|
982
1347
|
export async function clearAccounts() {
|
|
983
1348
|
return withStorageLock(async () => {
|
|
984
1349
|
const path = getStoragePath();
|
|
1350
|
+
const resetMarkerPath = getIntentionalResetMarkerPath(path);
|
|
985
1351
|
const walPath = getAccountsWalPath(path);
|
|
986
|
-
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 });
|
|
987
1354
|
const clearPath = async (targetPath) => {
|
|
988
1355
|
try {
|
|
989
1356
|
await fs.unlink(targetPath);
|
|
@@ -999,7 +1366,11 @@ export async function clearAccounts() {
|
|
|
999
1366
|
}
|
|
1000
1367
|
};
|
|
1001
1368
|
try {
|
|
1002
|
-
await Promise.all([
|
|
1369
|
+
await Promise.all([
|
|
1370
|
+
clearPath(path),
|
|
1371
|
+
clearPath(walPath),
|
|
1372
|
+
...backupPaths.map(clearPath),
|
|
1373
|
+
]);
|
|
1003
1374
|
}
|
|
1004
1375
|
catch {
|
|
1005
1376
|
// Individual path cleanup is already best-effort with per-artifact logging.
|
|
@@ -1014,13 +1385,22 @@ function normalizeFlaggedStorage(data) {
|
|
|
1014
1385
|
for (const rawAccount of data.accounts) {
|
|
1015
1386
|
if (!isRecord(rawAccount))
|
|
1016
1387
|
continue;
|
|
1017
|
-
const refreshToken = typeof rawAccount.refreshToken === "string"
|
|
1388
|
+
const refreshToken = typeof rawAccount.refreshToken === "string"
|
|
1389
|
+
? rawAccount.refreshToken.trim()
|
|
1390
|
+
: "";
|
|
1018
1391
|
if (!refreshToken)
|
|
1019
1392
|
continue;
|
|
1020
|
-
const flaggedAt = typeof rawAccount.flaggedAt === "number"
|
|
1021
|
-
|
|
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";
|
|
1022
1400
|
const isSwitchReason = (value) => value === "rate-limit" || value === "initial" || value === "rotation";
|
|
1023
|
-
const isCooldownReason = (value) => value === "auth-failure" ||
|
|
1401
|
+
const isCooldownReason = (value) => value === "auth-failure" ||
|
|
1402
|
+
value === "network-error" ||
|
|
1403
|
+
value === "rate-limit";
|
|
1024
1404
|
let rateLimitResetTimes;
|
|
1025
1405
|
if (isRecord(rawAccount.rateLimitResetTimes)) {
|
|
1026
1406
|
const normalizedRateLimits = {};
|
|
@@ -1045,19 +1425,33 @@ function normalizeFlaggedStorage(data) {
|
|
|
1045
1425
|
const normalized = {
|
|
1046
1426
|
refreshToken,
|
|
1047
1427
|
addedAt: typeof rawAccount.addedAt === "number" ? rawAccount.addedAt : flaggedAt,
|
|
1048
|
-
lastUsed: typeof rawAccount.lastUsed === "number"
|
|
1049
|
-
|
|
1428
|
+
lastUsed: typeof rawAccount.lastUsed === "number"
|
|
1429
|
+
? rawAccount.lastUsed
|
|
1430
|
+
: flaggedAt,
|
|
1431
|
+
accountId: typeof rawAccount.accountId === "string"
|
|
1432
|
+
? rawAccount.accountId
|
|
1433
|
+
: undefined,
|
|
1050
1434
|
accountIdSource,
|
|
1051
|
-
accountLabel: typeof rawAccount.accountLabel === "string"
|
|
1435
|
+
accountLabel: typeof rawAccount.accountLabel === "string"
|
|
1436
|
+
? rawAccount.accountLabel
|
|
1437
|
+
: undefined,
|
|
1052
1438
|
email: typeof rawAccount.email === "string" ? rawAccount.email : undefined,
|
|
1053
|
-
enabled: typeof rawAccount.enabled === "boolean"
|
|
1439
|
+
enabled: typeof rawAccount.enabled === "boolean"
|
|
1440
|
+
? rawAccount.enabled
|
|
1441
|
+
: undefined,
|
|
1054
1442
|
lastSwitchReason,
|
|
1055
1443
|
rateLimitResetTimes,
|
|
1056
|
-
coolingDownUntil: typeof rawAccount.coolingDownUntil === "number"
|
|
1444
|
+
coolingDownUntil: typeof rawAccount.coolingDownUntil === "number"
|
|
1445
|
+
? rawAccount.coolingDownUntil
|
|
1446
|
+
: undefined,
|
|
1057
1447
|
cooldownReason,
|
|
1058
1448
|
flaggedAt,
|
|
1059
|
-
flaggedReason: typeof rawAccount.flaggedReason === "string"
|
|
1060
|
-
|
|
1449
|
+
flaggedReason: typeof rawAccount.flaggedReason === "string"
|
|
1450
|
+
? rawAccount.flaggedReason
|
|
1451
|
+
: undefined,
|
|
1452
|
+
lastError: typeof rawAccount.lastError === "string"
|
|
1453
|
+
? rawAccount.lastError
|
|
1454
|
+
: undefined,
|
|
1061
1455
|
};
|
|
1062
1456
|
byRefreshToken.set(refreshToken, normalized);
|
|
1063
1457
|
}
|
|
@@ -1068,16 +1462,24 @@ function normalizeFlaggedStorage(data) {
|
|
|
1068
1462
|
}
|
|
1069
1463
|
export async function loadFlaggedAccounts() {
|
|
1070
1464
|
const path = getFlaggedAccountsPath();
|
|
1465
|
+
const resetMarkerPath = getIntentionalResetMarkerPath(path);
|
|
1071
1466
|
const empty = { version: 1, accounts: [] };
|
|
1072
1467
|
try {
|
|
1073
1468
|
const content = await fs.readFile(path, "utf-8");
|
|
1074
1469
|
const data = JSON.parse(content);
|
|
1075
|
-
|
|
1470
|
+
const loaded = normalizeFlaggedStorage(data);
|
|
1471
|
+
if (existsSync(resetMarkerPath)) {
|
|
1472
|
+
return empty;
|
|
1473
|
+
}
|
|
1474
|
+
return loaded;
|
|
1076
1475
|
}
|
|
1077
1476
|
catch (error) {
|
|
1078
1477
|
const code = error.code;
|
|
1079
1478
|
if (code !== "ENOENT") {
|
|
1080
|
-
log.error("Failed to load flagged account storage", {
|
|
1479
|
+
log.error("Failed to load flagged account storage", {
|
|
1480
|
+
path,
|
|
1481
|
+
error: String(error),
|
|
1482
|
+
});
|
|
1081
1483
|
return empty;
|
|
1082
1484
|
}
|
|
1083
1485
|
}
|
|
@@ -1117,13 +1519,31 @@ export async function loadFlaggedAccounts() {
|
|
|
1117
1519
|
export async function saveFlaggedAccounts(storage) {
|
|
1118
1520
|
return withStorageLock(async () => {
|
|
1119
1521
|
const path = getFlaggedAccountsPath();
|
|
1522
|
+
const markerPath = getIntentionalResetMarkerPath(path);
|
|
1120
1523
|
const uniqueSuffix = `${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
|
|
1121
1524
|
const tempPath = `${path}.${uniqueSuffix}.tmp`;
|
|
1122
1525
|
try {
|
|
1123
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
|
+
}
|
|
1124
1538
|
const content = JSON.stringify(normalizeFlaggedStorage(storage), null, 2);
|
|
1125
1539
|
await fs.writeFile(tempPath, content, { encoding: "utf-8", mode: 0o600 });
|
|
1126
1540
|
await fs.rename(tempPath, path);
|
|
1541
|
+
try {
|
|
1542
|
+
await fs.unlink(markerPath);
|
|
1543
|
+
}
|
|
1544
|
+
catch {
|
|
1545
|
+
// Best effort cleanup.
|
|
1546
|
+
}
|
|
1127
1547
|
}
|
|
1128
1548
|
catch (error) {
|
|
1129
1549
|
try {
|
|
@@ -1132,20 +1552,48 @@ export async function saveFlaggedAccounts(storage) {
|
|
|
1132
1552
|
catch {
|
|
1133
1553
|
// Ignore cleanup failures.
|
|
1134
1554
|
}
|
|
1135
|
-
log.error("Failed to save flagged account storage", {
|
|
1555
|
+
log.error("Failed to save flagged account storage", {
|
|
1556
|
+
path,
|
|
1557
|
+
error: String(error),
|
|
1558
|
+
});
|
|
1136
1559
|
throw error;
|
|
1137
1560
|
}
|
|
1138
1561
|
});
|
|
1139
1562
|
}
|
|
1140
1563
|
export async function clearFlaggedAccounts() {
|
|
1141
1564
|
return withStorageLock(async () => {
|
|
1565
|
+
const path = getFlaggedAccountsPath();
|
|
1566
|
+
const markerPath = getIntentionalResetMarkerPath(path);
|
|
1142
1567
|
try {
|
|
1143
|
-
await fs.
|
|
1568
|
+
await fs.writeFile(markerPath, "reset", {
|
|
1569
|
+
encoding: "utf-8",
|
|
1570
|
+
mode: 0o600,
|
|
1571
|
+
});
|
|
1144
1572
|
}
|
|
1145
1573
|
catch (error) {
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
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
|
+
}
|
|
1149
1597
|
}
|
|
1150
1598
|
}
|
|
1151
1599
|
});
|
|
@@ -1156,19 +1604,34 @@ export async function clearFlaggedAccounts() {
|
|
|
1156
1604
|
* @param force - If true, overwrite existing file (default: true)
|
|
1157
1605
|
* @throws Error if file exists and force is false, or if no accounts to export
|
|
1158
1606
|
*/
|
|
1159
|
-
export async function exportAccounts(filePath, force = true) {
|
|
1607
|
+
export async function exportAccounts(filePath, force = true, beforeCommit) {
|
|
1160
1608
|
const resolvedPath = resolvePath(filePath);
|
|
1161
1609
|
if (!force && existsSync(resolvedPath)) {
|
|
1162
1610
|
throw new Error(`File already exists: ${resolvedPath}`);
|
|
1163
1611
|
}
|
|
1164
|
-
const
|
|
1612
|
+
const transactionState = transactionSnapshotContext.getStore();
|
|
1613
|
+
const storage = transactionState?.active
|
|
1614
|
+
? transactionState.snapshot
|
|
1615
|
+
: await withAccountStorageTransaction((current) => Promise.resolve(current));
|
|
1165
1616
|
if (!storage || storage.accounts.length === 0) {
|
|
1166
1617
|
throw new Error("No accounts to export");
|
|
1167
1618
|
}
|
|
1168
1619
|
await fs.mkdir(dirname(resolvedPath), { recursive: true });
|
|
1169
|
-
|
|
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);
|
|
1170
1630
|
await fs.writeFile(resolvedPath, content, { encoding: "utf-8", mode: 0o600 });
|
|
1171
|
-
log.info("Exported accounts", {
|
|
1631
|
+
log.info("Exported accounts", {
|
|
1632
|
+
path: resolvedPath,
|
|
1633
|
+
count: storage.accounts.length,
|
|
1634
|
+
});
|
|
1172
1635
|
}
|
|
1173
1636
|
/**
|
|
1174
1637
|
* Imports accounts from a JSON file, merging with existing accounts.
|
|
@@ -1194,7 +1657,7 @@ export async function importAccounts(filePath) {
|
|
|
1194
1657
|
if (!normalized) {
|
|
1195
1658
|
throw new Error("Invalid account storage format");
|
|
1196
1659
|
}
|
|
1197
|
-
const { imported: importedCount, total, skipped: skippedCount } = await withAccountStorageTransaction(async (existing, persist) => {
|
|
1660
|
+
const { imported: importedCount, total, skipped: skippedCount, } = await withAccountStorageTransaction(async (existing, persist) => {
|
|
1198
1661
|
const existingAccounts = existing?.accounts ?? [];
|
|
1199
1662
|
const existingActiveIndex = existing?.activeIndex ?? 0;
|
|
1200
1663
|
const merged = [...existingAccounts, ...normalized.accounts];
|
|
@@ -1216,7 +1679,12 @@ export async function importAccounts(filePath) {
|
|
|
1216
1679
|
const skipped = normalized.accounts.length - imported;
|
|
1217
1680
|
return { imported, total: deduplicatedAccounts.length, skipped };
|
|
1218
1681
|
});
|
|
1219
|
-
log.info("Imported accounts", {
|
|
1682
|
+
log.info("Imported accounts", {
|
|
1683
|
+
path: resolvedPath,
|
|
1684
|
+
imported: importedCount,
|
|
1685
|
+
skipped: skippedCount,
|
|
1686
|
+
total,
|
|
1687
|
+
});
|
|
1220
1688
|
return { imported: importedCount, total, skipped: skippedCount };
|
|
1221
1689
|
}
|
|
1222
1690
|
//# sourceMappingURL=storage.js.map
|