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
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": "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": {
Expand Down
202 changes: 166 additions & 36 deletions releases/v0.1.25.md
Original file line number Diff line number Diff line change
@@ -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 <name>`
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`:
Expand All @@ -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.
Loading