WIP: Codex WebSocket transport + ChatGPT routing + AUTH_TOKEN strip + self-loop guard#33
Conversation
- Arm idle timer in wss.handleUpgrade callback so a stalled upstream (accepts TCP, never sends 101) is bounded by IDLE_TIMEOUT_MS instead of hanging forever. - Cap client→upstream send buffer at CCXRAY_WS_MAX_QUEUE_BYTES (default 4 MiB) and close 1009 on overflow; the previous queue was unbounded. - Destroy the upstream HTTP request/response on unexpected-response so the underlying socket doesn't leak. ws library hands ownership to the user once a listener is attached. - Drop the unreachable clientQueue path: clientWs is OPEN inside the handleUpgrade callback, so only client→upstream ever needs buffering. - Clamp WS close reasons to 120 bytes (spec cap is 123); the ws library throws RangeError on overflow. - Cover the new behavior with tests: pre-handshake stall timeout, auth token gating, accepted bearer, non-OpenAI 404, subprotocol forwarding. - Document ws-proxy.js / openai-session.js modules and the CCXRAY_WS_IDLE_TIMEOUT_MS / CCXRAY_WS_MAX_QUEUE_BYTES tunables. - Comment detectOpenAISession's intentional behavior: header session_id is honored even when parsedBody is null (covers WS upgrades and body-less HTTP retries). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ccxray accepts `?token=<AUTH_TOKEN>` as an alternative to the
`Authorization: Bearer` header (server/auth.js). The token was previously
preserved on the URL when ccxray forwarded the request upstream and when
it wrote `url` into log entries on disk and SSE broadcasts. That meant a
client authenticating with `?token=...` would send ccxray's secret to
OpenAI/Anthropic/ChatGPT on every request, and the same secret would be
persisted to `~/.ccxray/logs/{id}_req.json` indefinitely.
This was preexisting in the HTTP forward path. PR #29 added a WebSocket
upgrade path that inherited the same bug. Fix both with a single helper.
- Add server/url-sanitize.js: `stripAuthParams(url)` deletes ccxray's
own auth query params (currently just `token`). Upstream API keys
travel in Authorization headers, not query params, so this never
affects upstream auth.
- Apply in server/forward.js at the upstream path build, console log,
and three entry-record sites.
- Apply in server/ws-proxy.js at the upstream WS URL build and both
entry/reqLog record sites.
- Test: 10 cases covering single param, mixed params, empty value,
repeated params, substring-name protection, encoding round-trip, and
non-string inputs.
PR #6 promoted `chatgpt_base_url` to a first-class launcher config (injected on every codex spawn). The existing startup self-loop guard in startServer() only checked the agent's primary upstream (`UPSTREAMS[upstreamFamily]`), so a misconfigured `CHATGPT_BASE_URL` pointing at ccxray would only emit a warn from `resolveChatGPTUpstream` and then silently loop the proxy into itself — burning CPU until the process was killed. Extend the candidate list so it also includes `UPSTREAMS.openaiChatGPT` when the source is user-configured (`CHATGPT_BASE_URL` or `CODEX_CHATGPT_BASE_URL`), and skips it when the source is the built-in default (`chatgpt.com:443`) which can never loop. The override path (`--allow-upstream-loop` / `CCXRAY_ALLOW_UPSTREAM_LOOP`) still applies; the guard refuses startup by default. Tests: - New: exits with helpful error when CHATGPT_BASE_URL self-loops. - New: built-in ChatGPT default never triggers the guard.
…ader forwarding Three integration tests that lock in behaviors verified during PR #33 sign-off: - test/auth-token-strip.e2e.test.js — spawns ccxray with AUTH_TOKEN set against a fake Anthropic upstream, sends `?token=...&trace=keepme`, and asserts the secret never reaches the upstream URL, SSE broadcasts, disk entry logs, or console output, while non-auth params are preserved (covers a5d28f0). - test/socket-error-survival.e2e.test.js — exercises both the client-abort mid-SSE path and the upstream `socket.destroy()` path against a slow fake upstream, asserting the proxy stays alive (follow-up probe returns 200) and stderr contains no uncaughtException trace (covers efd4a70). - test/websocket-headers-forward.e2e.test.js — opens a real WebSocket through ccxray to a fake WS upstream with `chatgpt-account-id` set, and asserts the custom and openai-beta headers reach upstream intact, host is rewritten, and ChatGPT routing transforms `/v1/realtime` to `/backend-api/codex/realtime` (covers PR #29 + 0ff5507). npm test: 480 → 483 pass, 0 fail.
Discovered during PR #33 verification: real codex CLI (ChatGPT-auth) sends its main session traffic as a WebSocket upgrade on POST /v1/responses with `openai-beta: responses_websockets=*`, not on /v1/realtime. Both paths are already routed to the openai upstream by config.js, so this is a no-op for runtime behavior, but the assumption that /v1/realtime is the primary codex path is wrong and easy to encode into future routing changes. Also note the `chatgpt-account-id` header routes to CHATGPT_BASE_URL via getUpstreamForRequestAndHeaders, which is how ChatGPT-auth codex sessions end up at chatgpt.com/backend-api/codex instead of api.openai.com.
PR #33 驗證證據繁中摘要PR #33(
副作用發現(影響未來開發):真實 codex 主流量走 本次同時新增 3 個整合測試到
Full evidence (English)Branch Test suite
D — AUTH_TOKEN
|
| Entry | isSubagent | model | usage | maxContext | status |
|---|---|---|---|---|---|
| 13-49-28-130 | true | claude-opus-4-7 | in:617 out:22 | 200,000 | 200 |
| 13-49-28-175 | false | claude-opus-4-7 | in:6 out:14, cache_create:109,949 | 1,000,000 | 200 |
- SSE: 8 events captured for the subagent turn, terminating in
message_stop. - Cost computed: $0.6876 main + $0.003635 subagent.
maxContext: 1000000on the main turn confirms commit14f20f8(1M-tier
inference from[1m]marker in system prompt) still works.- Cache creation: 109,949 ephemeral_1h tokens written (system prompt + tools).
- Shared content-addressed storage created 4 dedup files (
sys_*,tools_*). - Session ID detected from request headers (
sessionInferred: false).
Documentation update
CLAUDE.md — ### Agent Launching section, one new bullet:
Codex's main session traffic upgrades to a WebSocket on
POST /v1/responses
(withopenai-beta: responses_websockets=*), not/v1/realtime.
/v1/realtimeexists for the older Realtime API but is not what current
codex uses for normal/goal/ chat turns. When ChatGPT auth is active,
codex also sendschatgpt-account-id, whichgetUpstreamForRequestAndHeaders
(seeserver/config.js) uses to route toCHATGPT_BASE_URLinstead of
OPENAI_BASE_URL.
Files in this verification batch
test/auth-token-strip.e2e.test.js (new, +194 lines)
test/socket-error-survival.e2e.test.js (new, +252 lines)
test/websocket-headers-forward.e2e.test.js (new, +138 lines)
CLAUDE.md (modified, +1 line)
The "restoreFromLogs — maxContext re-inference for legacy entries" suite (added in 14f20f8) seeds index.ndjson entries with hardcoded ids like "2026-05-14T13-27-40-199". restoreFromLogs filters by RESTORE_DAYS (default 3 days, Asia/Taipei date string comparison), so the entries silently get dropped from store.entries once we are more than ~3 days past the test's authoring date — turning the suite green on the day it was written and red the following week. Override config.RESTORE_DAYS = 0 in the suite's before() hook and restore it in after(). Avoids brittleness without making the production cutoff configurable from outside, and keeps the rest of the suite intact. Surfaced while validating PR #33 (the same merge result re-runs these tests and fails identically).
Picks up: 14f20f8 fix(context): infer 1M tier from usage when [1m] marker is missing efd4a70 fix(forward): handle late socket errors so the proxy survives EPIPE/ECONNRESET 99fda2b docs: add 1.9.3 changelog entry db636eb fix(test): bypass RESTORE_DAYS filter in maxContext re-inference tests The last commit unblocks CI on this PR: the maxContext re-inference test suite from 14f20f8 uses hardcoded 2026-05-14 ids that fall outside the default 3-day RESTORE_DAYS window once enough wall-clock time passes, which causes the synthetic entries to be filtered out of store.entries during restoreFromLogs and the assertions to fail. db636eb sets RESTORE_DAYS=0 in the suite's before() hook. npm test on the merge result: 498 pass / 0 fail.
Draft. Combines #29 + shhtheonlyperson#6 + 2 new commits:
Excludes b6c7cac (PR #6 evidence files in docs/pr-6-screenshots/).
npm test: 480 pass / 0 fail.
Pending: manual verification of ChatGPT-auth Codex (Y6-W2), API-key Codex abnormal close (normalizeCloseCode), Claude regression, and AUTH_TOKEN strip end-to-end. Do NOT merge until those are done.