Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,9 +291,9 @@ codex auth doctor --json

## Release Notes

- Current stable: [docs/releases/v1.2.0.md](docs/releases/v1.2.0.md)
- Previous stable: [docs/releases/v1.1.10.md](docs/releases/v1.1.10.md)
- Earlier stable: [docs/releases/v0.1.9.md](docs/releases/v0.1.9.md)
- Current stable: [docs/releases/v1.1.10.md](docs/releases/v1.1.10.md)
- Previous stable: [docs/releases/v0.1.9.md](docs/releases/v0.1.9.md)
- Earlier stable: [docs/releases/v0.1.8.md](docs/releases/v0.1.8.md)
- Archived prerelease: [docs/releases/v0.1.0-beta.0.md](docs/releases/v0.1.0-beta.0.md)

## License
Expand Down
8 changes: 4 additions & 4 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ Public documentation for `codex-multi-auth`.
| [troubleshooting.md](troubleshooting.md) | Recovery playbooks for install, login, switching, and stale state |
| [privacy.md](privacy.md) | Data handling and local storage behavior |
| [upgrade.md](upgrade.md) | Migration from legacy package and path history |
| [releases/v1.2.0.md](releases/v1.2.0.md) | Stable release notes |
| [releases/v1.1.10.md](releases/v1.1.10.md) | Previous stable release notes |
| [releases/v0.1.9.md](releases/v0.1.9.md) | Earlier stable release notes |
| [releases/v1.1.10.md](releases/v1.1.10.md) | Stable release notes |
| [releases/v0.1.9.md](releases/v0.1.9.md) | Previous stable release notes |
| [releases/v0.1.8.md](releases/v0.1.8.md) | Earlier stable release notes |
| [releases/v0.1.7.md](releases/v0.1.7.md) | Archived stable release notes |
| [releases/v0.1.6.md](releases/v0.1.6.md) | Archived stable release notes |
| [releases/v0.1.5.md](releases/v0.1.5.md) | Archived stable release notes |
Expand All @@ -45,7 +45,7 @@ Public documentation for `codex-multi-auth`.
| [reference/storage-paths.md](reference/storage-paths.md) | Canonical and compatibility storage paths |
| [reference/public-api.md](reference/public-api.md) | Public API stability and semver contract |
| [reference/error-contracts.md](reference/error-contracts.md) | CLI, JSON, and helper error semantics |
| [releases/v1.2.0.md](releases/v1.2.0.md) | Current stable release notes |
| [releases/v1.1.10.md](releases/v1.1.10.md) | Current stable release notes |
| [releases/v0.1.0-beta.0.md](releases/v0.1.0-beta.0.md) | Archived prerelease reference |
| [User Guides release notes](#user-guides) | Stable, previous, and archived release notes |
| [releases/legacy-pre-0.1-history.md](releases/legacy-pre-0.1-history.md) | Archived pre-0.1 changelog history |
Expand Down
57 changes: 53 additions & 4 deletions lib/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1926,6 +1926,57 @@ async function loadAccountsInternal(
}
}

async function loadAccountsForExport(): Promise<AccountStorageV3 | null> {
const path = getStoragePath();
const resetMarkerPath = getIntentionalResetMarkerPath(path);
await cleanupStaleRotatingBackupArtifacts(path);
const migratedLegacyStorage =
await migrateLegacyProjectStorageIfNeeded(saveAccountsUnlocked);

if (existsSync(resetMarkerPath)) {
return createEmptyStorageWithMetadata(false, "intentional-reset");
}
if (!existsSync(path)) {
return createEmptyStorageWithMetadata(true, "missing-storage");
}

try {
const { normalized, storedVersion, schemaErrors } =
await loadAccountsFromPath(path);
if (schemaErrors.length > 0) {
log.warn("Account storage schema validation warnings", {
errors: schemaErrors.slice(0, 5),
});
}
if (normalized && storedVersion !== normalized.version) {
log.info("Migrating account storage to v3", {
from: storedVersion,
to: normalized.version,
});
try {
await saveAccountsUnlocked(normalized);
} catch (saveError) {
log.warn("Failed to persist migrated storage", {
error: String(saveError),
});
}
}
if (existsSync(resetMarkerPath)) {
return createEmptyStorageWithMetadata(false, "intentional-reset");
}
return normalized;
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (existsSync(resetMarkerPath)) {
return createEmptyStorageWithMetadata(false, "intentional-reset");
}
if (code === "ENOENT") {
return migratedLegacyStorage;
}
throw error;
}
}
Comment thread
ndycode marked this conversation as resolved.

async function saveAccountsUnlocked(storage: AccountStorageV3): Promise<void> {
const path = getStoragePath();
const resetMarkerPath = getIntentionalResetMarkerPath(path);
Expand Down Expand Up @@ -2489,10 +2540,8 @@ export async function exportAccounts(
transactionState.storagePath === currentStoragePath
? transactionState.snapshot
: transactionState?.active
? await loadAccountsInternal(saveAccountsUnlocked)
: await withAccountStorageTransaction((current) =>
Promise.resolve(current),
);
? await loadAccountsForExport()
: await withStorageLock(loadAccountsForExport);
if (!storage || storage.accounts.length === 0) {
throw new Error("No accounts to export");
}
Expand Down
27 changes: 27 additions & 0 deletions test/storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,33 @@ describe("storage", () => {
);
});

it("should fail export when only backup storage exists", async () => {
const { exportAccounts } = await import("../lib/storage.js");
const backupPath = `${testStoragePath}.bak`;
await fs.writeFile(
backupPath,
JSON.stringify({
version: 3,
activeIndex: 0,
accounts: [
{
accountId: "backup-only",
refreshToken: "backup-refresh",
addedAt: 1,
lastUsed: 1,
},
],
}),
"utf-8",
);

setStoragePathDirect(testStoragePath);
await expect(exportAccounts(exportPath)).rejects.toThrow(
/No accounts to export/,
);
expect(existsSync(exportPath)).toBe(false);
});
Comment thread
ndycode marked this conversation as resolved.

it("should fail import when file does not exist", async () => {
const { importAccounts } = await import("../lib/storage.js");
const nonexistentPath = join(testWorkDir, "nonexistent-file.json");
Expand Down