Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ codex auth doctor --json

## Release Notes

- Current stable: [docs/releases/v1.2.3.md](docs/releases/v1.2.3.md)
- Current stable: [docs/releases/v1.2.4.md](docs/releases/v1.2.4.md)
- Previous stable: [docs/releases/v1.2.2.md](docs/releases/v1.2.2.md)
- Earlier stable: [docs/releases/v1.2.1.md](docs/releases/v1.2.1.md)
- Archived prerelease: [docs/releases/v0.1.0-beta.0.md](docs/releases/v0.1.0-beta.0.md)
Expand Down
4 changes: 2 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Public documentation for `codex-multi-auth`.
| [configuration.md](configuration.md) | Stable defaults, precedence, and environment overrides |
| [architecture.md](architecture.md) | Public system overview of the wrapper, storage, and optional plugin runtime |
| [privacy.md](privacy.md) | Data handling and local storage behavior |
| [releases/v1.2.3.md](releases/v1.2.3.md) | Stable release notes |
| [releases/v1.2.4.md](releases/v1.2.4.md) | Stable release notes |
| [releases/v1.2.2.md](releases/v1.2.2.md) | Previous stable release notes |
Comment thread
ndycode marked this conversation as resolved.
| [releases/v1.2.1.md](releases/v1.2.1.md) | Earlier stable release notes |
| [releases/v1.2.0.md](releases/v1.2.0.md) | Archived stable release notes |
Expand Down Expand Up @@ -52,7 +52,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.3.md](releases/v1.2.3.md) | Current stable release notes |
| [releases/v1.2.4.md](releases/v1.2.4.md) | Current stable release notes |
| [releases/v0.1.0-beta.0.md](releases/v0.1.0-beta.0.md) | Archived prerelease reference |
| [Daily Use release notes](#daily-use) | 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
35 changes: 35 additions & 0 deletions docs/releases/v1.2.4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Release v1.2.4

Release line: `stable`

This patch release follows the already-published `v1.2.3` rebuild and lands the post-merge review fixes that were raised afterward.

## Scope

- Current package version in `package.json` is `1.2.4`.
- Canonical command family remains `codex auth ...`.
- Canonical package name remains `codex-multi-auth`.
- This patch is prepared from `main` after merge commit `c1da059852d698fd53014b9b529eeeeb2db1d39d`.

## What Changed

- preserved standalone `config.json` values when unified settings are malformed, and hardened unified-settings writes so invalid primaries can be rebuilt safely without masking real unreadable-file errors
- retried flagged-account primary, backup, and legacy reads before fallback so transient Windows file locks do not trigger unnecessary recovery paths
- avoided capability-policy failure penalties on fallback stream-failover `429` responses
- retried shadow-home sync-back renames in the Codex wrapper so transient `EBUSY` and `EPERM` locks do not drop auth-state sync
- removed scheduler-fragile midpoint assertions from quota-refresh CLI tests and tightened dashboard/unified-settings regressions around the real legacy/unified read paths

## Validation

- `npm run lint`
- `npm run typecheck`
- `npm run build`
- `npm test`
- Full suite passed: `222/222` files, `3307/3307` tests

## Related

- [v1.2.3.md](v1.2.3.md)
- [../getting-started.md](../getting-started.md)
- [../upgrade.md](../upgrade.md)
- [../reference/commands.md](../reference/commands.md)
8 changes: 4 additions & 4 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2192,11 +2192,11 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
modelFamily,
model,
);
capabilityPolicyStore.recordFailure(
fallbackEntitlementAccountKey,
capabilityModelKey,
);
Comment thread
ndycode marked this conversation as resolved.
}
capabilityPolicyStore.recordFailure(
fallbackEntitlementAccountKey,
capabilityModelKey,
);
continue;
}

Expand Down
5 changes: 1 addition & 4 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -647,10 +647,7 @@ export async function savePluginConfig(
: loadUnifiedPluginConfigSync();
const unifiedConfig = sanitizeStoredPluginConfigRecord(unifiedConfigRecord);
const legacyPath =
unifiedConfigState.status === "missing" ||
(unifiedConfigState.status === "ok" && !unifiedConfig)
? resolvePluginConfigPath()
: null;
unifiedConfig === null ? resolvePluginConfigPath() : null;
const legacyConfigState = legacyPath
? await readConfigRecordForSave(legacyPath)
: null;
Expand Down
2 changes: 1 addition & 1 deletion lib/storage/flagged-storage-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ function isRetryableReadError(error: unknown): boolean {
return typeof code === "string" && RETRYABLE_READ_CODES.has(code);
}

async function readFileWithRetry(
export async function readFileWithRetry(
path: string,
deps: {
readFile: typeof import("node:fs").promises.readFile;
Expand Down
14 changes: 11 additions & 3 deletions lib/storage/flagged-storage-io.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { existsSync, promises as fs } from "node:fs";
import { dirname } from "node:path";
import type { FlaggedAccountStorageV1 } from "../storage.js";
import { readFileWithRetry } from "./flagged-storage-file.js";

const RETRYABLE_UNLINK_CODES = new Set(["EBUSY", "EAGAIN", "EPERM"]);

Expand Down Expand Up @@ -75,7 +76,9 @@ export async function loadFlaggedAccountsState(params: {
continue;
}
try {
const backupContent = await fs.readFile(backupPath, "utf-8");
const backupContent = await readFileWithRetry(backupPath, {
readFile: fs.readFile,
});
Comment thread
ndycode marked this conversation as resolved.
const backupData = JSON.parse(backupContent) as unknown;
const recovered = params.normalizeFlaggedStorage(backupData);
if (!isValidFlaggedStorageCandidate(backupData, recovered)) {
Expand Down Expand Up @@ -103,6 +106,7 @@ export async function loadFlaggedAccountsState(params: {
to: params.path,
error: String(persistError),
});
return recovered;
}
}
params.logInfo("Recovered flagged account storage from backup", {
Expand All @@ -123,7 +127,9 @@ export async function loadFlaggedAccountsState(params: {
};

try {
const content = await fs.readFile(params.path, "utf-8");
const content = await readFileWithRetry(params.path, {
readFile: fs.readFile,
});
const data = JSON.parse(content) as unknown;
const loaded = params.normalizeFlaggedStorage(data);
if (!isValidFlaggedStorageCandidate(data, loaded)) {
Expand Down Expand Up @@ -154,7 +160,9 @@ export async function loadFlaggedAccountsState(params: {
}

try {
const legacyContent = await fs.readFile(params.legacyPath, "utf-8");
const legacyContent = await readFileWithRetry(params.legacyPath, {
readFile: fs.readFile,
});
const legacyData = JSON.parse(legacyContent) as unknown;
const migrated = params.normalizeFlaggedStorage(legacyData);
if (migrated.accounts.length > 0) {
Expand Down
41 changes: 35 additions & 6 deletions lib/unified-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,16 @@ function cloneRecord(value: unknown): JsonRecord | null {
return { ...value };
}

class InvalidSettingsRecordError extends Error {
override readonly name = "InvalidSettingsRecordError";
}

function parseSettingsRecord(content: string): JsonRecord {
const parsed = cloneRecord(JSON.parse(content));
if (!parsed) {
throw new Error("Unified settings must contain a JSON object at the root.");
throw new InvalidSettingsRecordError(
"Unified settings must contain a JSON object at the root.",
);
}
return parsed;
}
Expand Down Expand Up @@ -98,24 +104,34 @@ async function readSettingsRecordAsyncFromPath(
* Best-effort backup reader for sync callers.
*
* Backup corruption is treated as an unavailable backup so callers can keep
* their legacy null-on-unavailable behavior.
* their legacy null-on-unavailable behavior, but unreadable or locked backups
* still surface so writers do not rebuild from `{}` over a transient failure.
*/
function readSettingsBackupSync(): JsonRecord | null {
try {
return readSettingsRecordSyncFromPath(UNIFIED_SETTINGS_BACKUP_PATH);
} catch {
return null;
} catch (error) {
if (isInvalidSettingsRecordError(error)) {
return null;
}
throw error;
}
}

/**
* Best-effort backup reader for async callers.
*
* Like the sync variant, only corrupt backups are collapsed to `null`.
* Unreadable or locked backups are rethrown so callers can fail closed.
*/
async function readSettingsBackupAsync(): Promise<JsonRecord | null> {
try {
return await readSettingsRecordAsyncFromPath(UNIFIED_SETTINGS_BACKUP_PATH);
} catch {
return null;
} catch (error) {
if (isInvalidSettingsRecordError(error)) {
return null;
}
throw error;
}
}

Expand Down Expand Up @@ -143,6 +159,13 @@ function shouldFallbackToSettingsBackup(
return true;
}

function isInvalidSettingsRecordError(error: unknown): boolean {
if (error instanceof SyntaxError) {
return true;
}
return error instanceof InvalidSettingsRecordError;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Snapshot the primary settings file into `settings.json.bak` for sync writes.
*
Expand Down Expand Up @@ -221,6 +244,9 @@ function readSettingsRecordSyncInternal(): SettingsReadResult {
if (backupRecord) {
return { record: backupRecord, usedBackup: true };
}
if (isInvalidSettingsRecordError(error)) {
return { record: null, usedBackup: false };
}
throw error;
}

Expand Down Expand Up @@ -255,6 +281,9 @@ async function readSettingsRecordAsyncInternal(): Promise<SettingsReadResult> {
if (backupRecord) {
return { record: backupRecord, usedBackup: true };
}
if (isInvalidSettingsRecordError(error)) {
return { record: null, usedBackup: false };
}
throw error;
}

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codex-multi-auth",
"version": "1.2.3",
"version": "1.2.4",
"description": "Multi-account OAuth manager and codex auth wrapper for the official @openai/codex CLI, with switching, health checks, and recovery tools",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down
Loading