The proxy logs captured the INBOUND client IP (x-forwarded-for) but never the
OUTBOUND/egress IP — so there was no way to confirm by which IP each account
leaves. For rotating providers that is the whole game: probing the servers
showed every codex account had a distinct proxy assigned, but ALL proxies were
dead (egress timeout) while still marked status=active, so accounts fell back
to the shared host IPv6 /64 (same /64 across .16/.17/.18) → OpenAI anomaly
revocation ("authentication token has been invalidated").
- src/lib/proxyEgress.ts: resolveEgressIp() (echo-IP via the resolved proxy,
cached), analyzeEgressSharing() (flags >=2 same-rotation-group accounts on
one egress IP), diagnoseAllEgressIps(), and validateProxyPool() (probes each
proxy and persists status=active/error so the dead-proxy filter takes them out
of rotation automatically).
- proxyLogger: new egressIp field + a structured [ProxyEgress] line
(in=clientIp out=egressIp) so the proxy logs show entry AND exit IP.
- chatHelpers: populate egressIp from a non-blocking cache, warm in background.
- GET/POST /api/settings/proxies/egress: diagnose + validate from the dashboard.
TDD: 5 tests (egress resolve/cache, sharing analysis, diagnose wiring, pool
validation). 37/37 proxy+oauth tests green, typecheck:core=0, lint clean.
Resolution queries JOINed proxy_registry without any status filter, so a proxy
marked inactive/error/disabled (by an operator or a health check) kept being
handed out — every request paid the dead-proxy timeout or leaked out the host
IP. Add a conservative PROXY_ALIVE_PREDICATE (excludes only known-dead states;
active/null/unknown stay usable) to both resolveProxyForConnectionFromRegistry
(account/provider/global) and resolveProxyForScopeFromRegistry.
Part of the codex anomaly-revocation hardening (the dead proxy pool was a
factor in accounts being routed out a shared host IP). TDD: 4 tests.
Root cause of the production token-invalidation storm (claude/aa5dd5cf
refreshed 1352x, kimi-coding 270x): when a refresh endpoint returned
invalid_grant in a non-canonical body shape (a JSON string, a double-encoded
string, a nested {error:{code}}, or raw text wrapped by a proxy/MITM),
refreshClaudeOAuthToken's errorBody.error === "invalid_grant" check failed and
returned null instead of the unrecoverable sentinel. The HealthCheck then kept
the connection active and re-tried every 60s forever (the loop + log flood +
upstream hammering that contributed to the 429s).
- extractOAuthErrorCode(): shape-agnostic OAuth error extractor (object, nested,
bare string, double-encoded JSON string, raw text) restricted to a known
unrecoverable set so transient errors (server_error, 502 HTML) never become
false positives.
- refreshClaudeOAuthToken + refreshAccessToken/Cline/Qoder/GitHub now classify
invalid_grant/invalid_request via the helper -> always emit the unrecoverable
sentinel so the HealthCheck deactivates cleanly instead of looping.
- HealthCheck refresh circuit breaker: track consecutive refresh failures in
providerSpecificData.refreshCircuit and back off exponentially (5->10->20...
->240min cap) instead of retrying every 60s; cleared on a successful refresh.
Stops the loop for null/network failures (e.g. dead proxy / Kimi fetch failed).
TDD: 26 new tests (19 resilience + 7 circuit breaker); 113 oauth/refresh/
healthcheck tests green. typecheck:core=0, lint clean.
- Set release date in CHANGELOG [3.8.22] to 2026-06-11
- Add HERMES_HOME to .env.example (from #3628/#3663)
- Add HERMES_HOME + OMNIROUTE_PREFER_CLAUDE_CODE_FOR_UNPREFIXED_CLAUDE_MODELS to ENVIRONMENT.md (#3628/#3540)
#3501 Phase 1f: god-component 4,948->4,062 LOC. Connection state+handlers, settings, and model metadata moved into hooks/.
Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Integrated into release/v3.8.22 — page.tsx conflict (god-component split #3501) resolved by re-applying the bulk-action deltas to ProviderDetailPageClient.tsx
Part 2 of #3485. WebDAV server (PROPFIND/GET/PUT/DELETE/MKCOL/MOVE/OPTIONS) handled in the custom server layer (standalone-server-ws.mjs) since the App Router cannot export WebDAV methods. Basic-Auth (constant-time), path-traversal hardened, password decrypt ported from encryption.ts (parity-tested), DATA_DIR resolution parity-tested against dataPaths.ts. End-to-end Obsidian-over-Tailscale validation is a live VPS step (Rule #18).
Part 1 of #3485. Adds /api/settings/obsidian/webdav (GET/POST/DELETE) wiring the ready obsidianSync lib, encrypts webdav password + obsidian token at rest, removes the duplicate UI block, drops the KNOWN_MISSING entry. WebDAV file server is part 2.
The mimocode RegistryEntry omitted the required authHeader field, which broke
typecheck:core (TS2741). Match the no-auth convention (authType:"none" + authHeader:"none")
used by veoaifree-web and other free providers. Follow-up to #3659 (@pizzav-xyz).
Integrated into release/v3.8.22 — page.tsx conflict resolved (re-applied toggle to ProviderDetailPageClient) + disable-test updated for catalog drift in review.
npm pack --dry-run --json on large packages exceeds the default 1 MB
buffer. Set maxBuffer to 64 MB so check:pack-artifact does not fail
with ENOBUFS on the CI runner.
Replaces request.json() + TestRequestSchema.parse() pattern with the
canonical validateBody()/isValidationFailure() pattern from
@/shared/validation/helpers, satisfying check:route-validation:t06.