Wednesday, 16 July 2026
Brainstormed and scaffolded rokojori-tunnel; Phase 1 relay built; Electron agent app, production deployment, streaming protocol, Roject integration, and client-side chunk animation added in a second session.
New standalone service at C:\rokojori\projects\web-projects\tunnel,
same stack as the rest of the ecosystem (Node.js, Express, ts-node, JSON file
storage, shared JWT verification).
source/server/db.ts — JSON file storage for tunnel configs
(build/data/db/tunnels.json)source/server/middleware/requireAuth.ts — JWT verification,
same pattern as rokojori-authsource/server/relay/TunnelRegistry.ts — in-memory
Map<tunnelId, WebSocket> of active agent connectionssource/server/relay/pending.ts — pending request callbacks
keyed by reqId for matching responses to waiting HTTP connectionssource/server/routes/tunnels.ts — full CRUD plus
GET /api/tunnels/available?purpose= discovery endpointsource/server/routes/agent.ts — WebSocket upgrade handler;
verifies JWT, confirms ownership, registers socket in registrysource/server/routes/proxy.ts — ALL /t/:tunnelId/*
relay; soft auth check for access mode, raw body forwarding, 30s timeoutsource/server/index.ts — Express + HTTP server with manual
WebSocket upgrade routing; JSON middleware applied only to /api
routes so proxy receives raw body streamsscripts/test-agent.ts — standalone Node.js agent for
testing before the Electron app exists; connects via WebSocket and forwards
inbound relay requests to a local port
Registered a tunnel via POST /api/tunnels, started the test agent
forwarding to port 8900 (gemma4-coding-Q4_K_M.gguf running locally),
and sent an OpenAI-compatible /v1/chat/completions request through
the relay. Full round-trip succeeded — request forwarded, response relayed back,
streaming token count confirmed in the response.
New plan document at workspace/outline/tunneling.html covering:
the relay mechanic (three-leg model, minimal inspection, raw byte forwarding),
access modes (private / public / password-protected), tunnel metadata shape and
purpose tags, all three components (relay server, Electron agent app, rokojori-auth
permission), API endpoint reference, Roject LLM provider integration example,
file structure, and phased implementation plan.
Full Electron desktop app at tunnel/electron-agent/ — system tray icon,
login window (email + password → account.rokojori.com), persistent token storage in
userData/tokens.json, and a main window with a tunnel list. Each tunnel
row shows its status (green dot when agent connected) and Start / Stop / Delete
buttons. A modal handles creating new tunnels. The agent process runs in Electron's
main process via the existing TunnelAgent class (WebSocket, auto-reconnect,
exponential back-off). Build: npm run electron:dev.
Deployed to the same server as roject.rokojori.com. Key config:
deploy/nginx-tunnel.conf — HTTP proxy with WebSocket upgrade for
/api/agent/ (proxy_read_timeout 3600s), TLS via Let's Encryptdeploy/tunnel-rokojori.service — systemd unit; EnvironmentFile
points to /opt/tunnel-rokojori/.env; PORT=3003
(3002 was already taken by styles.rokojori.com)ts-node moved from devDependencies → dependencies
so npm install --omit=dev still installs it on the serverExecStart uses the local node_modules/.bin/ts-node
to avoid version mismatches with any globally installed npx wrapper
Replaced the single-shot RelayResponse (collect all chunks, send one JSON blob)
with a three-message streaming protocol over the agent WebSocket:
res_start (status + headers), res_data (base64 chunk),
res_end. The proxy route calls res.flushHeaders() on the first
res_start, streams each chunk with res.write(), and ends with
res.end(). The 30 s timeout was extended to 120 s and cancelled the moment
res_start arrives. Legacy single-shot RelayResponse messages
still work via a backward-compat path in pending.ts.
Two integration points added in Roject:
rojo-settings-panel — calls
GET /api/rojos/tunnels/browse (Roject server proxies to
tunnel.rokojori.com/api/tunnels/available), renders a picker list;
clicking an entry fills the Tunnel ID field automatically.baseURL to append
/v1 (/t/:tunnelId/v1); fixed auth token forwarding
(cookie OR Authorization Bearer header — dual-source pattern) so the Electron app
can use tunnels without a cookie.TUNNEL_SERVER_URL env var used throughout; defaults to
https://tunnel.rokojori.com in production.
TCP batching through the relay delivers larger chunks than direct local streaming,
making the chat feel like it buffers instead of streams. Fixed client-side in
rojo-chat-panel.ts: a typeText() function splits any
chunk longer than 6 characters into 3-character pieces and awaits 18 ms between
each piece (~166 chars/sec). Chunks of ≤ 6 chars are displayed instantly, so
true single-token responses from a local direct LLM are unaffected.
Separate service, not part of rokojori-auth. Auth stays focused on identity. tunnel.rokojori.com is its own Express process that verifies the shared JWT but owns all relay logic independently.
HTTP-only for Phase 1. Stable Diffusion (AUTOMATIC1111) and OpenAI-compatible LLMs (Ollama, LM Studio) all speak HTTP with SSE streaming — no WebSocket from the app side needed. Language servers use raw TCP and are out of scope for now.
JSON envelope protocol for Phase 1. Requests and responses are
wrapped as { reqId, method, path, headers, body (base64) } JSON
messages over the agent WebSocket. No raw byte framing needed at this stage;
the JSON envelope is simple enough and sidesteps binary WebSocket complexity.
No express.json() on proxy routes. The JSON middleware is applied
only to /api/tunnels so the proxy handler always receives a raw
readable body stream, regardless of content type.
ts-node in production dependencies. Moving ts-node (and typescript)
from devDependencies to dependencies is the simplest way to ensure they survive
npm install --omit=dev on the server without a separate build step.
Dual-source token extraction. The Roject server extracts the JWT
from the accessToken cookie first, then falls back to the
Authorization: Bearer header. This lets both browser-based (cookie)
and Electron-based (Bearer) clients use the tunnel chat without separate code paths.
Client-side fake streaming instead of server changes. Splitting large relay chunks in the browser is zero-risk and zero-latency overhead — no server changes, no protocol changes. The animation runs entirely in the UI and is transparent to the LLM backend.