diff --git a/package-lock.json b/package-lock.json index 4f1eeb6..1bc8da1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "authmux", - "version": "0.1.24", + "version": "0.1.25", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "authmux", - "version": "0.1.24", + "version": "0.1.25", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 3f44835..c30268d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "authmux", - "version": "0.1.24", + "version": "0.1.25", "description": "Multi-account auth multiplexer for AI CLI agents — Claude Code, Codex, Kiro CLI.", "license": "MIT", "bin": { diff --git a/releases/v0.1.25.md b/releases/v0.1.25.md index 2723fa7..ab8ba67 100644 --- a/releases/v0.1.25.md +++ b/releases/v0.1.25.md @@ -1,44 +1,132 @@ # authmux v0.1.25 +Now-horizon release: themes **N1** (durability), **N2** (account-service +split), **N3** (error taxonomy + `--json`), **N4** (lazy path resolvers), +plus the **P0 wave** from the 18k-line improvement protocol. + ## Added -- `src/infra/fs/atomic-write.ts` — single durable file-write primitive with - fsync-before-rename and POSIX dir-fsync after rename, plus mode applied - before rename so 0600 files are never visible at a looser mode. -- `src/infra/fs/registry-lock.ts` — advisory lock around `registry.json` - with PID-liveness probing plus a 30 s wall-clock stale-lock heuristic. -- `persistRegistryAtomic` in `src/lib/accounts/registry.ts` is now the single - registry write path; it reloads under the lock and merges accounts so two - concurrent writers (daemon + interactive) no longer race-lose each other's - mutations. -- `src/tests/registry-durability.test.ts` covers the SIGKILL-between- - writeFile-and-rename window and the stale-lock-reaping path. + +- **N1 — Atomic writes & registry lock.** + - `src/infra/fs/atomic-write.ts` — single durable file-write primitive + with `fsync`-before-rename, POSIX dir-fsync after rename, and mode + applied before rename so 0600 files are never visible at a looser mode. + - `src/infra/fs/registry-lock.ts` — `O_EXCL` advisory lock around + `registry.json` with PID-liveness probe plus 30 s wall-clock stale-lock + heuristic. + - `persistRegistryAtomic` in `src/lib/accounts/registry.ts` — single + registry write path; reloads under the lock and merges account entries + so two concurrent writers no longer race-lose each other's mutations. + - `src/tests/registry-durability.test.ts` — covers SIGKILL between + `writeFile` and `rename` plus the stale-lock-reaping path. +- **N2 — `account-service.ts` decomposition.** 1675 LOC → 164 LOC + orchestrator backed by 12 focused modules under + `src/lib/accounts/{sync,read,write,config,auto-switch,usage,safety,session,identity}/` + plus 4 `_internal/` helpers. 31 new unit tests across the clusters. +- **N3 — Error taxonomy.** `AuthmuxError` base class in + `src/lib/accounts/errors.ts` with stable `code`, `severity`, `hint`, + `details`, and `toJSON()`. Every existing error + (`AuthFileMissingError`, `AccountNotFoundError`, + `NoAccountsSavedError`, `InvalidAccountNameError`, + `AccountNameInferenceError`, `SnapshotEmailMismatchError`, + `PromptCancelledError`, `InvalidRemoveSelectionError`, + `AmbiguousAccountQueryError`, `AutoSwitchConfigError`) now carries an + `E_*` code. `src/tests/error-taxonomy.test.ts` enforces the §6.2 code + allowlist and §6.3 exit-code table. +- **N3 — `--json` flag** on `list`, `current`, `status`, `use`, `save`. + Emits a single JSON envelope on stdout (`{ ok, data }` or + `{ ok, error: { code, severity, message, hint, details } }`). + Interactive prompts are skipped under `--json`. +- **N4 — Lazy path resolver tests.** `src/tests/paths.test.ts` proves + env-var changes apply after module load. +- **P0 — CI test matrix.** New GitHub Actions workflow runs `npm test` + across Ubuntu/macOS/Windows × Node 18/20/22 (previously only an LLM + review bot existed; `npm test` was never run in CI). +- **Docs.** `docs/future/*` — 19 source-grounded improvement documents + (~18 016 lines) covering architecture, accounts, paths, config, daemon, + usage, hooks, multi-CLI, security, testing, release, observability, + cross-platform, perf, docs, roadmap, glossary. ## Changed -- `secureWriteFile` now delegates to `atomicWriteFile`; behavior is the same - (atomic temp+rename, 0600 perms) but every snapshot, `current`, and - `sessions.json` write now also fsyncs the file and the containing directory - before returning. -- `AccountService.useAccount` no longer calls `saveRegistry` directly; all - registry writes go through the locked `persistRegistry` path. + +- **N1.** `secureWriteFile` now delegates to `atomicWriteFile`; every + snapshot, `current`, and `sessions.json` write fsyncs the file and the + containing directory before returning. +- **N1.** `AccountService.useAccount` no longer calls `saveRegistry` + directly; all registry writes route through the locked + `persistRegistry` path. +- **N2.** `AccountService` is now a thin orchestrator (164 LOC). All 21 + public method signatures preserved; the singleton in + `src/lib/accounts/index.ts` is byte-compatible. +- **N3.** `BaseCommand` central error handler routes `AuthmuxError` + instances to the JSON envelope under `--json` and exits with the §6.3 + exit code (`3` `E_AUTH_MISSING`, `4` `E_ACCOUNT_NOT_FOUND`, + `5` `E_SNAPSHOT_EMAIL_MISMATCH`, `6` `E_REGISTRY_LOCKED`, + `7` `E_REGISTRY_CORRUPT`, `8` `E_PROVIDER_NOT_INSTALLED`, + `64` `E_PROMPT_CANCELLED`, `1` generic). +- **P0.** Init-hook update prompt flipped from `[Y/n]` (default-yes) to + `[y/N]` (default-no) in `src/hooks/init/update-notifier.ts`. Bare + `authmux` invocations no longer auto-install updates by default. ## Fixed -- Half-written `registry.json` after SIGKILL or laptop sleep no longer - shadows the on-disk file: the rename is the commit point. -- Concurrent `authmux daemon --watch` + `authmux use foo` mutations are - serialized through the registry lock. + +- **N1.** Half-written `registry.json` after SIGKILL or laptop sleep no + longer shadows the on-disk file; the rename is the commit point. +- **N1.** Concurrent `authmux daemon --watch` + `authmux use ` + mutations are serialized through the registry lock. +- **P0.** `registry.ts` sanitization no longer drops `"proxy"` as a + usage source; saved snapshots with `source: "proxy"` round-trip + correctly instead of being coerced to `"cached"`. +- **P0.** `src/commands/kiro.ts` no longer throws `ENOENT` when the + target file does not exist (the `fs.existsSync(...) || fs.lstatSync(...).isSymbolicLink()` + short-circuit was inverted). ## Deprecated -- None. + +- `codexDir`, `accountsDir`, `authPath`, `currentNamePath`, + `registryPath`, `sessionMapPath` in `src/lib/config/paths.ts` — the bare + eager constants. Use the `resolveX()` getters instead so env-var + overrides set after import are honored. Scheduled for removal in + **v0.2.0**. ## Removed + - None. ## Security -- File mode 0600 is applied to the temp file BEFORE the rename, closing the - brief window where the previous chmod-after-rename path could expose - freshly-renamed files at the umask default. -## Durability +- **0600 / 0700 perms.** `secureWriteFile` (now backed by + `atomicWriteFile`) applies file mode 0600 to the temp file *before* + the rename, closing the brief window where chmod-after-rename could + expose freshly-renamed snapshot, `current`, or `sessions.json` files at + the umask default. Accounts dir is created 0700. +- **Atomic mode-before-rename** means there is no observable moment at + which a refresh-token file exists on disk at a looser mode than 0600. +- **Default-no update prompt.** Flipping the init-hook update prompt to + `[y/N]` reduces the chance of accidental unattended updates triggered + by a bare `authmux` invocation in CI or scripts. + +## Migration + +No migration is required for end users. The on-disk layout of +`registry.json`, `current`, `sessions.json`, and per-account snapshot +files is unchanged. + +External library consumers of `src/lib/config/paths.ts` should switch +from the bare constants (`codexDir`, `accountsDir`, `authPath`, +`currentNamePath`, `registryPath`, `sessionMapPath`) to the resolver +functions (`resolveCodexDir()`, `resolveAccountsDir()`, `resolveAuthPath()`, +`resolveCurrentNamePath()`, `resolveRegistryPath()`, +`resolveSessionMapPath()`) before **v0.2.0**. + +CLI consumers that grep stdout still work — human-readable `message` +strings are unchanged. New scripts should prefer `--json` for stable +parsing. + +--- + +## Theme deep-dives + +### Durability (N1) This release closes the two data-loss windows tracked under Theme N1 of `docs/future/17-ROADMAP.md`: @@ -48,19 +136,61 @@ This release closes the two data-loss windows tracked under Theme N1 of and (on POSIX) `fsync`s the containing directory. A power loss between any of these steps leaves the previous content of the target intact. 2. **Concurrent-writer lost mutations.** `withRegistryLock` serializes - writers; `persistRegistryAtomic` reloads the registry under the lock and - merges accounts before writing so neither writer silently discards the - other. + writers; `persistRegistryAtomic` reloads the registry under the lock + and merges accounts before writing so neither writer silently discards + the other. -Crash-safety guarantees promised after this release match the table in +Crash-safety guarantees match the table in `docs/future/01-ARCHITECTURE.md` §5.3 rows 1, 2, and 4. `fsync` adds latency on spinning disks; on SSDs it is dominated by the -network/parse work the CLI was already doing and is not user-visible. The -dir-fsync is gated on `process.platform !== "win32"` because Windows does -not allow opening a directory as a file. +network/parse work the CLI was already doing and is not user-visible. +The dir-fsync is gated on `process.platform !== "win32"` because Windows +does not allow opening a directory as a file. -## Migration +### Account-service split (N2) + +| Module | LOC | Cluster | +| --- | --- | --- | +| `sync/external-sync.ts` | 216 | external-auth sync orchestrator | +| `read/listing.ts` | 211 | listings + `getCurrentAccountName` + find | +| `write/save.ts` | 159 | `saveAccount` + safety guard + name inference | +| `write/use.ts` | 128 | `useAccount` + `activateSnapshot` | +| `write/remove.ts` | 105 | remove one / by-query / all | +| `config/auto-switch-config.ts` | 82 | status + threshold setters | +| `auto-switch/policy.ts` | 141 | `runAutoSwitchOnce` + `runDaemon` | +| `usage/adapter.ts` | 192 | refresh + proxy shim | +| `safety/snapshot-vault.ts` | 125 | backup vault + clobber recovery | +| `session/pin.ts` | 243 | sessions.json I/O + Linux PPID heuristic | +| `identity/equality.ts` | 86 | snapshot identity comparisons (pure) | +| `naming.ts` | 40 | `normalizeAccountName` + `accountFilePath` | + +Orchestrator `account-service.ts`: **164 LOC** (was 1675; ceiling was +400 per N2 exit criteria). + +### Error taxonomy & --json (N3) + +JSON envelope shape: + +```jsonc +// success +{ "ok": true, "data": { /* command-specific payload */ } } + +// error +{ "ok": false, + "error": { "code": "E_ACCOUNT_NOT_FOUND", "severity": "fatal", + "message": "...", "hint": "...", + "details": { "name": "alice" } } } +``` + +`CodexAuthError` is preserved as a back-compat subclass of +`AuthmuxError` so existing `instanceof CodexAuthError` catches continue +to work. + +### Lazy path resolvers (N4) -No migration is required. The on-disk layout of `registry.json`, `current`, -`sessions.json`, and the per-account snapshot files is unchanged. +The bare exports in `src/lib/config/paths.ts` were evaluated at module +import time, so env-var overrides set after the first `import` had no +effect. The `resolveX()` getters re-read the environment on every call; +all internal call sites already use them. The bare constants are kept +through one release for external consumers.