The README merged in #2216 claimed browsers cannot reach netmux and
that both transports require Hydra. Wrong on the central point:
netmux serves WebSocket natively on its ordinary game ports
(mux/src/websocket.cpp, RFC 6455) with first-byte protocol detection
sharing each port between telnet and WebSocket (#1074/#2193,
proto_detect_window), and the handshake accepts both /wsclient and /
— which is exactly the path js/connection.js dials. The minimal
browser deployment is netmux plus static files, no extra process;
Hydra is the OPTIONAL layer for session resume, multi-game links,
stored credentials, and gRPC-Web.
The error came from concluding absence out of a truncated grep: the
file listing was piped through head and mux/proxy's matches filled the
window before mux/src/websocket.cpp appeared. A truncated listing is
not a complete listing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
client/web was a complete HTML5 client with nothing self-describing in
it — and players have started asking publicly whether TinyMUX has
browser support, with no page to point at. Covers what it is, why
both transports go through Hydra (browsers cannot open raw TCP), which
transport is proxy-agnostic, deployment (static hosting + hydra.conf,
the mixed-content and cors_origin rules), the test_web.js harness and
its keep-modules-dependency-free convention, and the 2.13-vs-2.14
status.
Facts checked against the sources: transport split per
connection.js/hydra_connection.js (including the /h-command list and
gRPC-Web fallback), listener types and CORS default from
hydra.conf.example, hydra's standalone make from mux/proxy/Makefile,
localStorage persistence from settings.js, and `node test_web.js`
run green before writing (all PASS).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
McpParser stored every unterminated #$#* tag without bound. Cap pending
messages (32), per-message bytes (256 KiB), and total pending (1 MiB);
evict oldest or drop the overflowing tag with a diagnostic. Regression
covers flood of unique tags, fat continuations, and happy-path reassembly.
Same unbounded line/SB/Hydra reassembly as console/iOS. Cap text at 64 KiB
and telnet SB at 4 KiB; cap web grpc-web frame reassembly. Stamp J1–J6 on
the audit map.
The 22 per-tracker ISSUES.md files carried both their open items and a
full FIXED / FALSE ALARM / NOT A BUG audit history. The 57 still-open
items have been migrated to GitHub issues #706-#762 with a 2.14-aligned
label taxonomy (area:* / type:* / priority:* / topic:*), so open work
now lives in the issue tracker instead of in-tree Markdown.
The closed/audit history of every tracker is preserved here in git
history (this commit's parent); nothing is lost.
Open items migrated by tracker:
mux/src/ (1) -> #706
mux/lib/ (10) -> #707-#716
mux/modules/engine/ (18) -> #717-#734
mux/ganl/ (10) -> #735-#744
mux/modules/sqlslave/ (9) -> #745-#753
testcases/ (4) -> #754-#757
client/tf/ + client/ (5) -> #758-#762
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Console (Unix): worlds.txt saved with mode 0600, warns on load if
permissions are loose. Password zeroed from memory via secure_zero()
after Hydra authentication succeeds.
Win32 GUI: Hydra passwords moved from plaintext worlds.json to
Windows Credential Manager (CredWriteW/CredReadW). Transparent
migration from existing JSON on first load; passwords stripped from
JSON on next save. Username and non-secret fields remain in JSON.
iOS (Keychain) and Android (EncryptedSharedPreferences) were already
using platform credential stores — no changes needed.
Web client localStorage exposure deferred as lower priority (different
threat model, not the primary deployment target).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
renderAnsiLine() in client/web/js/terminal.js parsed `\e[38;5;Nm`
(256-color) and `\e[38;2;R;G;Bm` (truecolor) by directly reading
codes[j+2..j+4] with no check that the codes array was that long.
Malformed short sequences from the server leaked `undefined` into
`XTERM_COLORS[undefined]` and into `rgb(r,g,b)` CSS strings, producing
wrong fallback colors or literal `NaN` in styles.
Add an `isByte(v)` guard that requires a finite integer in [0, 255]
for every palette index and RGB component on both the fg (38) and bg
(48) paths. Short or out-of-range sequences now fall through to plain
text instead of emitting broken CSS.
Verified with `node --check` and a 15-case harness (well-formed,
short, out-of-range, bg variants, bold mix, plain, empty).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Step 6/7 of WebSocket GameSession transport.
Primary transport is now a WebSocket with hydra-gamesession subprotocol
carrying binary protobuf ClientMessage/ServerMessage frames. Falls back
to grpc-web unary SendInput when the WebSocket is not connected.
Changes:
- Proto.encodeMessage() for nested message encoding in ClientMessage
- ClientMessage/ServerMessage field maps for bidi protobuf codec
- _startGameSession(): opens WS, sends first-message auth via
SetPreferences.session_id, decodes ServerMessage (game_output,
gmcp, pong, system_notice)
- _sendInput() prefers WS when open, falls back to grpc-web
- sendNaws() sends preferences update over WS
- Keepalive sends PingMessage over WS instead of unary RPC
- Reconnect uses _startGameSession() instead of _startSubscribe()
- _startSubscribe() preserved as legacy fallback (no longer called)
- All unary control RPCs (/hconnect, /hlinks, etc.) unchanged on
grpc-web Fetch path
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three client ISSUES fixes:
1. TF TLS: HydraConnection now accepts use_tls parameter and creates
SslCredentials channel when the world has SSL flag set. Worlds
with 'x' flag get TLS gRPC; without it, plaintext (for local dev).
2. TF send_naws: tracks currentColorFormat_ from initial SetPreferences
and uses it on resize, instead of always hardcoding ANSI_TRUECOLOR.
Prevents preference resets when window resizes.
3. HTML5 Subscribe: explicitly sends color_format=1 (ANSI_TRUECOLOR)
matching the new proto enum where 0=COLOR_UNSPECIFIED.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SessionRequest now carries terminal_width and terminal_height fields
(proto fields 3-4). When a Subscribe RPC provides non-zero values,
the server forwards NAWS to the active game back-door link so the
game renders at the correct width.
Implemented in both native gRPC (grpc_server.cpp) and grpc-web
(session_manager.cpp handleGrpcWebRequest) Subscribe handlers.
HTML5 client sends approximate terminal dimensions based on window
size in _startSubscribe().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
localStorage persists across browser sessions and is accessible from
any tab on the same origin, making long-lived Hydra session tokens
vulnerable to XSS exfiltration and shared-browser leakage.
sessionStorage is scoped to the current tab and cleared on tab close.
Session resume still works across page reloads within the same tab,
but tokens no longer persist after the tab is closed or leak to
other tabs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both clients now expose 16 Hydra commands covering 23 of 24 proto
RPCs (only SubscribeGmcp is uncovered — it's a dedicated streaming
RPC for GMCP-only consumers, not suited to a terminal command).
New: /hdetach marks the Hydra session as detached (back-door links
stay alive server-side for later resume).
Full command list: /hconnect /hswitch /hdisconnect /hlinks /hgames
/hscroll /haddcred /hdelcred /hcreds /hstart /hstop /hrestart
/hstatus /hdetach /hhelp (plus automatic auth, keepalive, reconnect).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
/hstart, /hstop, /hrestart, /hstatus commands dispatch to StartGame,
StopGame, RestartGame, GetGameStatus gRPC RPCs. Users can manage
local game processes from any Hydra-connected client. Both TF and
HTML5 clients now expose the full set of 15 Hydra commands via /h*.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both clients now have:
1. Keepalive pings: TF sends ClientMessage.ping every 60s via the
bidi stream (checked in read_lines poll cycle). HTML5 sends
unary Ping RPC every 60s via setInterval. Prevents silent
connection drops through NAT.
2. Credential management: /haddcred, /hdelcred, /hcreds commands
dispatch to AddCredential, DeleteCredential, ListCredentials
RPCs. Users can now manage stored game login credentials for
Hydra auto-login without using the telnet interface.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two fixes:
1. grpc-web Subscribe now keeps the HTTP connection open as a live
chunked stream instead of flushing queued output and closing.
The subscriber fd is registered in the session's frontDoors list
so onBackDoorData() sends game output as chunked grpc-web data
frames in real-time. sendToClient() also handles grpc-web
subscribers for system messages. This makes the HTML5 Hydra
transport fully functional for interactive play.
2. HTML5 session persistence: sessionId is stored in localStorage
keyed by world name. On connect, the client first tries to
resume the saved session via GetSession RPC. If valid, it skips
authentication and immediately starts the Subscribe stream.
Intentional disconnect clears the stored session.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three critical fixes across both clients:
1. Command interceptor: /hconnect, /hswitch, /hdisconnect, /hlinks,
/hgames, /hscroll, /hhelp commands are now intercepted in
send_line() and dispatched to the corresponding gRPC RPCs instead
of being sent as game input.
2. Auto-reconnect: when the bidi stream (TF) or Subscribe stream
(HTML5) breaks, the client retries up to 5 times with 3s delay
using the same session_id. Hydra sessions persist server-side
so reconnect resumes seamlessly.
3. HTML5 protobuf encoder fix: no longer skips fields with value 0
or false. Only undefined/null are skipped. This fixes encoding
of enums (e.g. ColorFormat=0 is ANSI_TRUECOLOR) and explicit
zero values.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
HydraConnection class implements the same interface as Connection
(WebSocket) using grpc-web protocol via fetch() API. No build tools
or npm dependencies — includes a minimal hand-coded protobuf
encoder/decoder for the subset of Hydra messages the browser needs.
World edit dialog adds Transport dropdown (WebSocket vs Hydra),
password field, and game name field. Hydra worlds authenticate via
gRPC-Web, auto-connect to the configured game, and receive output
via server-streaming Subscribe with chunked response parsing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Trigger system: regex matching on incoming lines with gag, priority,
shot count. Trigger Manager dialog (Add/Edit/Delete) accessible from
toolbar button. Triggers persist to localStorage. /def, /undef, /list
commands work from the input line.
Incoming lines run through TriggerDB.check() before display — gagged
lines are suppressed, trigger actions execute as commands.
Input auto-complete: Tab key completes /commands from a built-in list.
Multiple matches shown in output, single match completes with space.
Reconnect button on disconnected tabs — italic styling with a green
recycle arrow that reconnects with the same host/port/ssl.
Slash commands: /connect, /disconnect, /worlds, /triggers, /find,
/clear, /def, /undef, /list, /help. Unknown /commands pass through
to the server (some MUDs use them).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Browser eats Ctrl+N and Ctrl+F. Added a visible toolbar with
Connect, Worlds, Disconnect, Find, and Clear buttons. Keyboard
shortcuts changed to Ctrl+Shift+N/F to avoid browser conflicts.
Welcome message updated to reference the toolbar buttons.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Zero frameworks, zero build step. Pure HTML + CSS + vanilla JS.
Opens index.html in any modern browser.
Architecture:
- WebSocket connects directly to TinyMUX's existing port (server
detects WebSocket upgrade in the initial negotiation phase)
- Telnet parser in JS (~200 lines): NAWS, TTYPE, CHARSET, GMCP, MSSP
- ANSI SGR renderer: 16-color, 256-color, truecolor (24-bit RGB)
parsed from escape sequences into inline CSS spans
- CSS flexbox layout: tab bar, output pane, input line, status bar
Features:
- Tabbed multi-world with activity indicators
- Click or middle-click to switch/close tabs, Ctrl+Tab to cycle
- Command history (Up/Down), PgUp/PgDn scrollback
- Connect dialog (Ctrl+N), World Manager with add/edit/delete
- Find in scrollback (Ctrl+F) with highlight
- Settings in localStorage (worlds, font, scrollback)
- Dark theme, monospace font, responsive layout
Files: index.html, css/titan.css, js/main.js, js/connection.js,
js/telnet.js, js/terminal.js, js/settings.js
No xterm.js dependency — custom ANSI renderer using DOM spans.
No npm, no webpack, no node_modules. Just open the HTML file.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>