Architecture How every component fits together

ProxyServer is a ~2300-line Node.js application with only 2 npm dependencies (ws and node-forge). Everything else is built from scratch using Node's standard library. This page explains every module, its role, and how they communicate.

System Architecture

┌─────────────────────────────────────────────────────────────────────────┐ │ server.js (entry point / orchestrator) │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌────────────────────────────┐ │ │ │ TrafficStore │ │ RuleStore │ │ CertManager │ │ │ │ (ring buffer) │ │ (rules.json) │ │ (CA + per-host certs) │ │ │ │ EventEmitter │ └──────┬───────┘ └───────────┬───────────────┘ │ │ └──────┬───────┘ │ │ │ │ │ ┌────┴─────┐ │ │ │ │ │RuleEngine│ │ │ │ │ └────┬─────┘ │ │ │ │ │ │ │ │ ┌──────┴───────────────────┴────────────────────────┴───────────────┐ │ │ │ ProxyServer (:9080) │ │ │ │ │ │ │ │ HTTP handler ──► RequestInterceptor ──► upstream (:80) │ │ │ │ │ (hold/forward/drop) │ │ │ │ │ ResponseInterceptor ◄── upstream │ │ │ │ │ (hold/forward/drop) │ │ │ │ │ │ │ │ │ CONNECT handler ──► TLSHandler ──► decrypt ──► upstream (:443) │ │ │ │ • TLS termination with generated certs │ │ │ │ • HTTP/2 ALPN to upstream (h2 / h1.1) │ │ │ │ • HTTPStreamParser for keep-alive │ │ │ └────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ EventEmitter (add / update / clear) │ │ ▼ │ │ ┌────────────────────────────────────────────────────────────────────┐ │ │ │ DashboardServer (:9081) │ │ │ │ │ │ │ │ Static files ← index.html, app.js, app.css, chat.js, chat.css │ │ │ │ REST API ← /api/traffic, /api/rules, /api/sessions, ... │ │ │ │ WSBridge ← WebSocket push (traffic + chat messages) │ │ │ │ ChatHandler ← Routes chat:* messages to ClaudeSession │ │ │ └────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ WebSocket │ │ ┌─────────────────┐ │ │ │ Browser UI │ │ │ │ app.js + chat.js│ │ │ └─────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘

Module-by-Module Breakdown

server.js — Entry Point

The orchestrator. It creates every component, wires them together, starts both servers, and handles graceful shutdown on SIGINT/SIGTERM.

Proxy Layer

proxy-server.js — HTTP Forward Proxy

The core proxy. Handles two types of requests:

Handles gzip/deflate/brotli decompression of response bodies for display, while forwarding the original compressed bytes to the client.

tls-handler.js — TLS MITM + HTTP/2

The most complex module. Handles HTTPS interception:

  1. Receives CONNECT tunnel from proxy-server.js
  2. Creates a tls.TLSSocket wrapping the client socket, using a generated certificate for the target hostname
  3. Parses HTTP/1.1 requests from the decrypted stream using HTTPStreamParser
  4. Sends upstream requests via HTTP/2 (ALPN negotiation) or falls back to HTTP/1.1
  5. Translates HTTP/2 responses back to HTTP/1.1 for the client tunnel
  6. Supports keep-alive — multiple requests per CONNECT tunnel

HTTPStreamParser is an inner class that provides robust HTTP/1.1 message parsing from any Node.js stream, supporting:

cert-manager.js — Certificate Generation

Manages the CA certificate and generates per-host leaf certificates on demand. Uses node-forge for RSA 2048-bit key generation and X.509 certificate creation. Two-tier cache: memory Map + disk (certs/hosts/).

See TLS & Certificates for a deep dive.

request-interceptor.js — Request Hold/Forward/Drop

When intercept is enabled and a rule matches, this module holds the request via a Promise. The promise resolves when the user clicks Forward (with optional modifications) or rejects when they click Drop. A 5-minute timeout auto-forwards to prevent connection leaks.

Request arrives → RuleEngine.matchRequest(entry) → match? │ no ────► return null (pass through) yes ────► new Promise() │ ┌──────────────┴──────────────┐ │ │ TrafficStore emits setTimeout(5 min) 'forward' or 'drop' auto-resolves │ ┌───────────┴───────────┐ │ │ resolve(mods) reject("Dropped")

response-interceptor.js — Response Hold/Forward/Drop

Mirror of RequestInterceptor but for the response phase. Listens for forward-response and drop-response events from TrafficStore. Same 5-minute timeout.

Data Layer

traffic-entry.js — Request/Response Data Model

Each captured request/response pair is a TrafficEntry with:

TrafficEntry ├── id UUID (crypto.randomUUID) ├── seq Monotonic counter (1, 2, 3, ...) ├── state pendinginterceptedforwardedcompleted │ → error │ → aborted ├── request │ ├── method, url, httpVersion, headers │ ├── body (Buffer, max 2MB captured) │ └── contentType ├── response │ ├── statusCode, statusMessage, httpVersion │ ├── headers, body (Buffer, max 2MB, decompressed) │ └── contentType ├── target { host, port, protocol } ├── timing { start, ttfb, end, duration (ms) } ├── intercept { wasIntercepted, wasModified, matchedRuleId, phase } └── clientIp, clientPort

Two serialization methods: toJSON() (full detail with base64-encoded bodies) and toSummary() (lightweight, no bodies — used for the traffic list).

traffic-store.js — Ring Buffer + Event Emitter

In-memory storage with a configurable size limit (default 5000). Uses a Map for O(1) lookups and an Array for ordered iteration. When full, the oldest entry is evicted (FIFO).

Extends EventEmitter and fires:

har-export.js — HAR 1.2 Export

Converts TrafficEntry arrays to the HAR 1.2 format. Maps request/response data, calculates timings, and extracts query strings.

Rules Layer

rule-store.js — Persistent Rule Storage

CRUD operations for intercept rules. Persists to rules.json on every modification. Each rule has a UUID, pattern fields, and a direction.

rule-engine.js — Pattern Matching

Evaluates rules against traffic entries. Converts glob patterns to RegExp (e.g., *api*/^.*api.*$/i). Matches by URL, method, content-type, and header presence/value. Filters by direction for request vs. response phase.

Dashboard Layer

dashboard-server.js — HTTP Server + REST API

A plain Node.js http.createServer that serves static files and the REST API. No Express, no frameworks — just URL parsing and pattern matching. Serves all files from the static/ directory with proper MIME types.

ws-bridge.js — WebSocket Bridge

Creates a WebSocket.Server attached to the dashboard HTTP server. Listens for TrafficStore events and broadcasts them to all connected clients. Also routes incoming chat:* messages to ChatHandler.

Chat Layer

chat-handler.js — Message Router

Routes chat:send, chat:reset, chat:compact, and chat:context-toggle messages. Manages per-client context toggles and a shared ClaudeSession instance.

claude-session.js — CLI Subprocess Manager

Spawns claude -p <prompt> --output-format stream-json per turn. Maintains conversation history server-side. Parses streaming JSON events and emits chunk and action events for real-time UI updates.

context-builder.js — System Prompt Assembly

Assembles the system prompt from conditional context blocks (traffic summaries, selected entry detail, rules, source code, browser context). Caches source files with fs.watch invalidation. Estimates token counts for the budget indicator.

Frontend Layer

app.js — Main Dashboard UI (Vanilla JS IIFE)

~700 lines of vanilla JavaScript in an IIFE closure. State management via a single state object. WebSocket for real-time updates. Renders the traffic list, detail panel, modals, and handles keyboard shortcuts.

chat.js — Chat Panel UI (Vanilla JS IIFE)

~500 lines in a separate IIFE. Creates its DOM programmatically and mounts to #chat-container. Uses the same WebSocket connection (messages are discriminated by chat: prefix). Handles streaming display, context toggles, and slash commands.

Project Structure

ProxyServer/ ├── server.js Entry point — wires everything ├── package.json 2 deps: ws, node-forge │ ├── src/ │ ├── proxy/ │ │ ├── proxy-server.js HTTP forward proxy (absolute-URI + CONNECT) │ │ ├── cert-manager.js CA + per-host cert generation + 2-tier cache │ │ ├── tls-handler.js TLS MITM + H2/H1 upstream + stream parser │ │ ├── request-interceptor.js Promise-based hold/forward/drop (requests) │ │ └── response-interceptor.js Same for responses │ │ │ ├── traffic/ │ │ ├── traffic-entry.js Data model (UUID, state machine, 2MB cap) │ │ ├── traffic-store.js Ring buffer (5000) + EventEmitter │ │ └── har-export.js Export to HAR 1.2 format │ │ │ ├── dashboard/ │ │ ├── dashboard-server.js Static files + REST API + chat routes │ │ └── ws-bridge.js WebSocket: traffic events + chat routing │ │ │ ├── rules/ │ │ ├── rule-engine.js Glob/method/header pattern matching │ │ └── rule-store.js CRUD + persistence to rules.json │ │ │ └── chat/ │ ├── chat-handler.js WebSocket chat message router │ ├── claude-session.js Claude CLI subprocess manager │ └── context-builder.js System prompt assembly + token estimation │ ├── static/ │ ├── index.html Dashboard shell │ ├── app.js Main UI logic (vanilla JS IIFE) │ ├── app.css Dark theme (CSS variables) │ ├── chat.js Chat panel UI (vanilla JS IIFE) │ └── chat.css Chat panel styles │ ├── extension/ Chrome MV3 extension (optional) │ ├── manifest.json │ ├── popup.html / popup.js │ ├── docs/ This documentation ├── certs/ Generated CA + host certs (gitignored) └── sessions/ Saved traffic captures (gitignored)

Communication Patterns

EventEmitter (Server-side)

TrafficStore is the central event bus. Components communicate by emitting and listening to events on the store:

TrafficStore events: ProxyServer ── add(entry) ──► TrafficStore ──► WSBridge (broadcast to clients) TLSHandler ── update(id) ──► ──► WSBridge (broadcast to clients) Dashboard ── emit('forward', id, mods) ──► TrafficStore ──► RequestInterceptor (resolve promise) Dashboard ── emit('drop', id) ──► ──► RequestInterceptor (reject promise)

WebSocket (Client-Server)

A single WebSocket connection per browser tab carries both traffic updates and chat messages:

DirectionMessage Types
Server → Clientinit, add, update, clear (traffic); chat:status, chat:chunk, chat:done, chat:error, chat:action (chat)
Client → Serverchat:send, chat:reset, chat:compact, chat:context-toggle

Design Decisions

No Frameworks

ProxyServer uses no web frameworks (no Express, no React). This is intentional — it keeps the codebase small, dependency-free, and educational. Every HTTP handler, every DOM element, every WebSocket message is explicit.

Two Ports

Proxy (:9080) and Dashboard (:9081) are separate to avoid feedback loops where dashboard requests create traffic entries.

Ring Buffer, Not Database

Traffic is stored in memory with FIFO eviction. This keeps the proxy fast and simple — no disk I/O on the hot path. Sessions can be saved to disk explicitly.

Promise-Based Interception

Interceptors use Promises to hold requests. This integrates naturally with Node's async/await and lets the proxy pipeline wait cleanly without callbacks.