feat: rebuild web consumer as React 19 + Zustand + Tailwind v4 + Vite app#4
feat: rebuild web consumer as React 19 + Zustand + Tailwind v4 + Vite app#4
Conversation
… app Replace the 689-line vanilla HTML/CSS/JS consumer with a modern React app that builds to a single HTML file (299 KB, 94 KB gzip). Frontend (web/): - React 19 with Zustand 5 state management (Record-based, not Map) - Tailwind CSS v4 with Amber Beam design system (@theme tokens) - Vite 6 + vite-plugin-singlefile for single-file output - marked + DOMPurify for markdown rendering (~100KB lighter than react-markdown) - 19 components: 3-panel layout, message feed with 3-level grouping, streaming indicator, slash command typeahead, permission cards with tool-specific previews, session sidebar with adapter badges, context gauge - WebSocket connect-on-switch pattern (avoids browser 6-connection limit) - Accessibility: ARIA live regions, keyboard navigation, prefers-reduced-motion Backend changes: - Extract HTTP routing from beamcode.ts into src/http/ modules - Add REST API: GET/POST/DELETE /api/sessions with body size limit and path traversal validation - Pre-compressed gzip serving with security headers (CSP, X-Frame-Options) - Fix startup race: launch session after WS server is ready - Add SessionManager.setServer() for deferred wiring Shared types: - shared/consumer-types.ts with zero imports from backend internals Build pipeline: - build:lib (tsdown) + build:web (vite) + copy-consumer - dev:web with proxy to beamcode server for HMR development
Summary of ChangesHello @teng-lin, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a significant overhaul of the web consumer, transforming it into a robust and modern React application. The primary goal is to enhance the user experience, improve maintainability, and provide a more scalable foundation for future development. This change impacts the entire frontend architecture, build process, and user interface, while also bolstering security and performance aspects of the application. Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
The web/ directory has its own package.json with React, Vite, and Tailwind dependencies. CI was only running `pnpm install` at the root level, causing `build:web` to fail with missing @vitejs/plugin-react.
There was a problem hiding this comment.
Code Review
This pull request introduces a significant rewrite of the web consumer using a modern React stack, bringing many thoughtful features and architectural improvements. However, a critical security vulnerability has been identified: the application lacks authentication, and its 'localhost' restriction can be bypassed, potentially leading to Remote Code Execution (RCE) due to a flawed path traversal check in session creation. Additionally, the Content-Security-Policy uses 'unsafe-inline' which violates a security rule for this repository, and improvements are needed for type safety in streaming events. Addressing these security and typing issues is crucial for a robust and secure deployment.
| const tunnelActive = typeof isTunnelActive === "function" ? isTunnelActive() : isTunnelActive; | ||
| if (tunnelActive && url.pathname.startsWith("/api/")) { | ||
| const remote = req.socket.remoteAddress; | ||
| const isLocal = remote === "127.0.0.1" || remote === "::1" || remote === "::ffff:127.0.0.1"; | ||
| if (!isLocal) { | ||
| res.writeHead(403, { "Content-Type": "application/json" }); | ||
| res.end(JSON.stringify({ error: "API access restricted to localhost" })); | ||
| return; | ||
| } | ||
| } |
There was a problem hiding this comment.
The application attempts to restrict access to the /api/* endpoints to localhost when a tunnel is active. However, this check is fundamentally flawed because tunneling tools like cloudflared forward requests from the local machine, causing them to appear as if they originate from 127.0.0.1. Consequently, the remoteAddress check passes for any request coming through the tunnel.
Furthermore, when the tunnel is not active, no restriction is applied at all, and the server listens on all interfaces by default, exposing the unauthenticated API to the local network.
Since the API allows listing all sessions (including their secret IDs) and creating new sessions with arbitrary working directories, an attacker can gain full control over the host machine via Remote Code Execution (RCE).
There was a problem hiding this comment.
Addressed in commit 39a29ac: Replaced the flawed IP-based localhost check with API key authentication. A random API key is generated at startup and required via Authorization: Bearer <key> for all /api/* endpoints. This works correctly regardless of whether requests come through cloudflared or directly.
| const cwd = opts.cwd as string | undefined; | ||
| if (cwd) { | ||
| const resolved = resolvePath(cwd); | ||
| if (resolved.includes("..") || !resolved.startsWith("/")) { | ||
| json(res, 400, { error: "Invalid cwd" }); | ||
| return; | ||
| } | ||
| } |
There was a problem hiding this comment.
The POST /api/sessions endpoint is vulnerable to path traversal. The current validation for cwd attempts to prevent this by checking for .. after path.resolve(cwd). This is ineffective because path.resolve() normalizes .. segments, allowing an attacker to bypass the check and start a session in any directory on the host filesystem, potentially leading to Remote Code Execution (RCE). Additionally, the !resolved.startsWith("/") check is not cross-platform. A more robust solution would be to verify that the resolved path is strictly within a designated base directory (e.g., process.cwd()).
There was a problem hiding this comment.
Addressed in commit 39a29ac: Replaced the ineffective .includes('..') check (which never triggers after path.resolve() normalization) with filesystem validation — the resolved path must exist and be a directory (existsSync + statSync.isDirectory).
| "Content-Security-Policy": | ||
| "default-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self' ws: wss:;", |
There was a problem hiding this comment.
The Content-Security-Policy (CSP) header includes 'unsafe-inline' for both script-src and style-src. This significantly increases the risk of Cross-Site Scripting (XSS) attacks, even with DOMPurify in place, if a sanitization bypass is found. This also violates a security rule for this repository. Given the single-file nature of the build, it is strongly recommended to implement a nonce-based CSP or use hashes for legitimate inline blocks instead of allowing all inline scripts. Vite plugins can often help automate this.
References
- For security hardening, avoid using 'unsafe-inline' in Content-Security-Policy. Prefer using nonce-based CSP or moving inline scripts and styles to external files.
There was a problem hiding this comment.
Addressed in commit 39a29ac: Replaced 'unsafe-inline' with SHA-256 hash-based CSP. At startup, loadConsumerHtml() computes SHA-256 hashes of all inline <script> and <style> blocks from the built HTML and constructs the CSP header dynamically. This eliminates the XSS risk from blanket inline allowance.
| | { type: "session_init"; session: ConsumerSessionState & Record<string, unknown> } | ||
| | { type: "session_update"; session: Partial<ConsumerSessionState> & Record<string, unknown> } | ||
| | { type: "assistant"; message: AssistantContent; parent_tool_use_id: string | null } | ||
| | { type: "stream_event"; event: unknown; parent_tool_use_id: string | null } |
There was a problem hiding this comment.
The event property in the stream_event type is defined as unknown. While this is better than any, you can provide more specific types here to improve type safety. Based on its usage in web/src/ws.ts, it appears to be a discriminated union. Defining this union type would make the message handling code more robust and self-documenting. For example:
type StreamEvent =
| { type: 'message_start', ... }
| { type: 'content_block_delta', delta: { type: 'text_delta', text: string } }
| { type: 'message_delta', usage: { output_tokens: number } };Then you could use event: StreamEvent in the ConsumerMessage type.
There was a problem hiding this comment.
Addressed in commit 39a29ac: Added a StreamEvent discriminated union type to shared/consumer-types.ts covering message_start, content_block_delta, content_block_start, content_block_stop, message_delta, and message_stop event types. Updated ws.ts to use the typed event directly instead of casting to Record<string, unknown>.
- Replace broken IP-based localhost check with API key authentication for /api/* endpoints (cloudflared forwards as 127.0.0.1, making IP checks unreliable) - Fix CWD path traversal: validate that resolved path is an existing directory instead of checking for ".." after path.resolve() normalization - Replace 'unsafe-inline' CSP with SHA-256 hash-based policy computed from the actual inline script/style content at startup - Add typed StreamEvent discriminated union replacing `event: unknown` in stream_event messages for better type safety
Fix architecture principle violations #1, #3, #4, #7: - Fix #1: Remove dual lifecycle mutation by deleting applyLifecycleFromBackendMessage() which was redundant with reduceLifecycle() - Fix #3: Make transitionLifecycle() private. Add SESSION_CLOSING system signal and route coordinator's direct calls through process() - Fix #7: Route ConsumerGateway's backend:relaunch_needed emission through the runtime as BACKEND_RELAUNCH_NEEDED system signal instead of emitting directly - Fix #4: Route BackendConnector's inline broadcasts and event emissions through the runtime via routeSystemSignal callback: - Part A: Connect/disconnect lifecycle (cli_connected, backend:connected, etc.) - Part B: Slash passthrough results/errors (slash_command_result, slash_command_error) This enforces the core design principle that all state mutations flow through SessionRuntime.process(), which dispatches to the pure reducer and executes effects. All 2902 tests pass, typecheck clean. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
…ONSUMER effect (Tier 1 #4 + P3) Add SEND_TO_CONSUMER effect type so targeted per-consumer errors go through the reducer-effect pipeline instead of being sent inline. Tier 1 #4: MessageQueueHandler.sendTo() calls replaced with QUEUE_ERROR signal dispatches; reducer produces SEND_TO_CONSUMER effects. P3 fix: handleInboundCommand() post-reducer user_message and set_adapter rejections now append SEND_TO_CONSUMER effects to the effect list instead of calling broadcaster.sendTo() directly.
Summary
Rebuilds the web consumer from a single vanilla HTML/CSS/JS file (~689 lines) into a modern React 19 + Zustand + Tailwind v4 + Vite app with the "Amber Beam" design aesthetic.
vite-plugin-singlefileArchitecture
Record-based session data (not Maps) andpatchSession()helpershared/consumer-types.ts) — zero imports from backend internalsbeamcode.tsintosrc/http/modules (server.ts,api-sessions.ts,consumer-html.ts,health.ts)UI Components
Security & Performance
React.memoon heavy components, stable keys, optimized selectorsBuild Pipeline
Test plan
cd web && npx vite buildproduces singleindex.html< 300 KBpnpm build(root) builds library + consumer successfullydev:webproxies to running beamcode server/inputGenerated with Claude Code