Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Embeddable JavaScript chat widget for GoClaw AI agent gateway.
```
src/
├── index.ts Entry point, init(), window auto-attach
├── types.ts All TypeScript interfaces
├── types.ts All TypeScript interfaces (GoClawConfig — proxy-only, no direct mode)
├── websocket-client.ts WS connection, auth, RPC, events, reconnect
├── chat-widget.ts Shadow DOM UI, message rendering
├── markdown-renderer.ts Lightweight markdown→HTML
Expand All @@ -22,14 +22,30 @@ src/
└── wrappers/
├── react-wrapper.tsx React component
└── vue-wrapper.ts Vue 3 plugin
server/ Backend proxy (keeps auth token server-side)
├── src/
│ ├── index.ts Entry point
│ ├── proxy-config.ts Config from environment variables
│ ├── proxy-server.ts HTTP + WebSocket server
│ ├── websocket-proxy-session.ts Single client↔upstream proxy session
│ └── connection-tracker.ts Per-IP connection rate limiting
├── .env.example Example configuration
├── package.json Server dependencies (ws)
└── tsconfig.json Server TypeScript config
```

## Commands
```bash
npm install # Install deps
npm run dev # Dev server
npm install # Install widget deps
npm run dev # Widget dev server
npm run build # tsc --noEmit && vite build
npm run lint # Type-check only

# Proxy server
cd server && npm install
npm run dev # Start proxy (tsx watch)
npm run build # Compile to dist/
npm start # Run compiled proxy
```

## Key Patterns
Expand All @@ -38,3 +54,4 @@ npm run lint # Type-check only
- GoClaw WebSocket Protocol v3 (req/res/event frames)
- Exponential backoff reconnection
- Async snippet loader pattern (like Intercom)
- Proxy-only: backend injects auth token into WS connect frame, client never sees it (no direct mode)
49 changes: 32 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,19 @@

Embeddable chat widget for [GoClaw](https://goclaw.sh) AI agent gateway. Drop a `<script>` tag on any website and let visitors chat with your AI agents — like Intercom, but for GoClaw.

## Architecture

The widget connects through a **proxy server** that keeps the gateway auth token server-side. The token is never exposed to the browser.

```
Browser Widget ←→ Proxy Server (:3100) ←→ GoClaw Gateway (:9090)
(no token) (holds token) (validates token)
```

## Features

- **Zero dependencies** — Vanilla TypeScript, ~8KB gzipped
- **Secure by default** — Auth token never leaves the server
- **Shadow DOM isolation** — Styles never leak into or from host page
- **Framework-agnostic** — Works with React, Vue, Angular, or plain HTML
- **Real-time streaming** — Token-by-token LLM responses via WebSocket
Expand All @@ -15,6 +25,16 @@ Embeddable chat widget for [GoClaw](https://goclaw.sh) AI agent gateway. Drop a
- **Mobile responsive** — Full-screen on small viewports
- **Accessible** — Keyboard navigation, ARIA labels

## Proxy Server Setup

```bash
cd server/
cp .env.example .env
# Edit .env: set GOCLAW_URL, GOCLAW_TOKEN, and optionally PROXY_API_KEY
npm install
npm run dev
```

## Quick Start

### Script Tag (simplest)
Expand All @@ -23,8 +43,7 @@ Embeddable chat widget for [GoClaw](https://goclaw.sh) AI agent gateway. Drop a
<script src="https://cdn.example.com/goclaw-webchat.umd.js"></script>
<script>
GoClaw.init({
url: 'wss://your-goclaw-server.com/ws',
token: 'your-gateway-token',
url: 'wss://proxy.example.com/ws',
title: 'Chat with us',
theme: 'auto',
});
Expand All @@ -43,8 +62,7 @@ Embeddable chat widget for [GoClaw](https://goclaw.sh) AI agent gateway. Drop a
})(window,document,'script','https://cdn.example.com/goclaw-webchat.umd.js');

GoClaw.init({
url: 'wss://your-server.com/ws',
token: 'your-token',
url: 'wss://proxy.example.com/ws',
title: 'Support',
theme: 'auto',
});
Expand All @@ -61,8 +79,7 @@ npm install @goclaw/webchat
import { init } from '@goclaw/webchat';

const widget = init({
url: 'wss://goclaw.example.com/ws',
token: 'your-token',
url: 'wss://proxy.example.com/ws',
title: 'AI Assistant',
theme: 'dark',
});
Expand All @@ -82,8 +99,7 @@ import { GoClawChat } from '@goclaw/webchat/react';
function App() {
return (
<GoClawChat
url="wss://goclaw.example.com/ws"
token="your-token"
url="wss://proxy.example.com/ws"
title="Support"
theme="auto"
/>
Expand All @@ -97,8 +113,7 @@ function App() {
import { GoClawPlugin } from '@goclaw/webchat/vue';

app.use(GoClawPlugin, {
url: 'wss://goclaw.example.com/ws',
token: 'your-token',
url: 'wss://proxy.example.com/ws',
title: 'AI Assistant',
});
```
Expand All @@ -107,8 +122,8 @@ app.use(GoClawPlugin, {

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `url` | `string` | *required* | GoClaw WebSocket URL (`wss://...`) |
| `token` | `string` | — | Gateway authentication token |
| `url` | `string` | *required* | Proxy server WebSocket URL (`wss://...`) |
| `apiKey` | `string` | — | API key for proxy authentication |
| `userId` | `string` | auto-generated | User identifier |
| `agentId` | `string` | — | Specific agent to chat with |
| `sessionId` | `string` | — | Resume a previous session |
Expand All @@ -129,7 +144,7 @@ app.use(GoClawPlugin, {

```js
GoClaw.init({
url: 'wss://...',
url: 'wss://proxy.example.com/ws',
theme: {
base: 'dark',
primaryColor: '#8b5cf6',
Expand Down Expand Up @@ -162,7 +177,7 @@ widget.destroy(); // Remove widget & disconnect

```js
GoClaw.init({
url: 'wss://...',
url: 'wss://proxy.example.com/ws',
onOpen: () => console.log('Chat opened'),
onClose: () => console.log('Chat closed'),
onMessage: (msg) => console.log('Message:', msg),
Expand All @@ -175,17 +190,17 @@ GoClaw.init({
## GoClaw Server Setup

1. Ensure your GoClaw server has WebSocket enabled (default on `/ws`)
2. Add your widget's domain to `allowed_origins` in GoClaw config:
2. Add the **proxy server's** origin to `allowed_origins` in GoClaw config:

```json
{
"gateway": {
"allowed_origins": ["https://your-website.com"]
"allowed_origins": ["https://your-proxy-server.com"]
}
}
```

3. Use a gateway token for authentication, or set up browser pairing
3. Configure the gateway token in the proxy server's `.env` file

## Development

Expand Down
39 changes: 37 additions & 2 deletions docs/code-standards.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Code Standards

## Language & Tooling
## Widget (src/)

### Language & Tooling
- TypeScript strict mode
- ESM modules (Vite library mode)
- No runtime dependencies
Expand All @@ -21,8 +23,41 @@
- Event-driven WebSocket communication
- Builder pattern for configuration (single config object)

## Security
### Security
- HTML escaping for all user content
- No `eval()` or `innerHTML` with raw user input
- Markdown renderer escapes HTML before processing
- XSS-safe link rendering (only http/https)

## Proxy Server (server/)

### Language & Tooling
- TypeScript strict mode
- Node.js ESM (type: module in package.json)
- Runtime dependency: `ws` (WebSocket library v8.18+)
- Dev dependencies: TypeScript, tsx (development server), @types/node, @types/ws

### File Naming
- kebab-case for all source files
- Descriptive names (e.g., `websocket-proxy-session.ts`, `connection-tracker.ts`)

### Code Style
- 2-space indentation
- Single quotes for strings
- Explicit types on public API, inferred internally
- Error handling with try-catch blocks

### Architecture Patterns
- Config-driven initialization (environment variables via `proxy-config.ts`)
- Per-IP connection tracking with exponential backoff
- WebSocket frame buffering until upstream connection ready
- Graceful shutdown with drain timeout
- Non-JSON frames dropped silently

### Security
- Auth token stored server-side only, never sent to client
- Origin validation via `ALLOWED_ORIGINS` environment variable (empty = allow all)
- Per-IP connection limits via `MAX_CONNECTIONS_PER_IP` (default: 10)
- TRUST_PROXY flag for reverse proxy (nginx, Cloudflare) deployments
- Max frame size: 512KB
- Upstream gateway URL not included in health endpoint responses
22 changes: 20 additions & 2 deletions docs/project-overview-pdr.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,43 @@ Embeddable JavaScript chat widget allowing website owners to add AI chat powered
- **Protocol**: GoClaw WebSocket Protocol v3 (req/res/event frames)

## Architecture

### Widget (src/)
- `src/index.ts` — Entry point, `init()` function, window auto-attach
- `src/websocket-client.ts` — WebSocket connection, auth, RPC, event handling, reconnection
- `src/websocket-client.ts` — WebSocket connection, auth, RPC, event handling, reconnection (supports proxy mode)
- `src/chat-widget.ts` — Shadow DOM UI, message rendering, input handling
- `src/types.ts` — All TypeScript interfaces
- `src/types.ts` — All TypeScript interfaces (includes `proxyUrl` config option)
- `src/markdown-renderer.ts` — Lightweight markdown-to-HTML
- `src/svg-icons.ts` — Inline SVG icons
- `src/styles/theme-variables.ts` — Theme system (light/dark/auto/custom)
- `src/styles/widget-styles.ts` — Shadow DOM CSS
- `src/wrappers/react-wrapper.tsx` — React component
- `src/wrappers/vue-wrapper.ts` — Vue 3 plugin

### Proxy Server (server/)
- `server/src/index.ts` — Server entry point, HTTP + WebSocket listener
- `server/src/proxy-config.ts` — Configuration from environment variables
- `server/src/proxy-server.ts` — HTTP + WebSocket server with origin validation, per-IP limits, graceful shutdown
- `server/src/websocket-proxy-session.ts` — Single proxy session: intercepts WS `connect` frame to inject gateway token, buffers upstream messages
- `server/src/connection-tracker.ts` — Per-IP connection rate limiting

## Distribution

### Widget
- Script tag (UMD): `<script src="goclaw-webchat.umd.js">`
- NPM package: `import { init } from '@goclaw/webchat'`
- Async snippet: Non-blocking loader with queue (like Intercom)
- Framework wrappers: React component, Vue plugin

### Proxy Server
- Docker-friendly Node.js server (TypeScript)
- Can be self-hosted or deployed to any Node.js-compatible platform
- Dependency: `ws` (WebSocket library, single production dependency)

## Key Decisions
- Shadow DOM over iframe: Better performance, same-origin access, no CORS issues
- Vanilla TS over framework: Zero deps, universal compatibility
- CSS custom properties: Runtime theming without rebuild
- Exponential backoff reconnect: Resilient WS connections
- Backend proxy for production: Keeps auth tokens server-side, prevents token exposure in client code
- Dual-mode architecture: Direct mode for dev/testing, proxy mode for production security
60 changes: 57 additions & 3 deletions docs/system-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@
└─────────────────────────────────────────┘
│ WebSocket
┌─────────────────────────────────────────┐ (optional)
│ WebChat Proxy Server │
│ ┌──────────┐ ┌───────────────────┐ │
│ │ WS Server │ │ Token Injection │ │
│ │ /ws :3100 │──│ (connect frame) │ │
│ └──────────┘ └───────────────────┘ │
│ Origin validation │ Per-IP limits │
└─────────────────────────────────────────┘
│ WebSocket (with token)
┌─────────────────────────────────────────┐
│ GoClaw Gateway │
│ ┌──────────┐ ┌───────────────────┐ │
Expand All @@ -36,24 +46,68 @@
└─────────────────────────────────────────┘
```

### Connection Modes

1. **Direct mode** (`url` + `token`): Widget connects directly to GoClaw Gateway. Token exposed in client-side JS. Suitable for development or trusted environments.
2. **Proxy mode** (`proxyUrl`): Widget connects to proxy server, which holds the token server-side and injects it into the WS `connect` frame. Recommended for production.

## Data Flow

1. **Init**: `GoClaw.init(config)` → creates ChatWidget → attaches Shadow DOM → connects WebSocket
2. **Auth**: WS open → sends `connect` frame with token → server validates → connected state
1. **Init**: `GoClaw.init(config)` → creates ChatWidget → attaches Shadow DOM → connects WebSocket (to `proxyUrl` if set, else `url`)
2. **Auth**: WS open → sends `connect` frame (token injected by proxy in proxy mode, or sent directly) → server validates → connected state
3. **Send**: User types → `chat.send` RPC → server runs agent → streams events back
4. **Stream**: `run.started` → `chunk` (tokens) → `tool.call/result` → `run.completed`
5. **Reconnect**: WS close (unclean) → exponential backoff → re-authenticate → resume

## Module Dependencies

### Widget
```
index.ts
└── chat-widget.ts
├── websocket-client.ts
├── websocket-client.ts (uses proxyUrl or direct url+token)
│ └── types.ts
├── markdown-renderer.ts
├── svg-icons.ts
└── styles/
├── theme-variables.ts
└── widget-styles.ts
```

### Proxy Server
```
index.ts
├── proxy-config.ts (reads env vars)
├── proxy-server.ts (HTTP + WebSocket server)
│ ├── websocket-proxy-session.ts (per-connection handler)
│ └── connection-tracker.ts (per-IP rate limiting)
└── ws (external: WebSocket library)
```

## Security Model

### Direct Mode (Development)
```
Browser (holds token) → GoClaw Gateway
- Widget config: { url, token }
- Risk: Token visible in client-side JS
- Use case: Local dev, trusted environments only
```

### Proxy Mode (Production - Recommended)
```
Browser (no token) → Proxy Server (holds token) → GoClaw Gateway
- Widget config: { proxyUrl }
- Proxy config: GOCLAW_TOKEN env var
- Benefits: Token never exposed to client, origin validation, rate limiting
- Deployment: Self-host or use managed proxy service
```

## Proxy Security Features
- **Token injection**: Proxy intercepts WS `connect` frame and injects gateway token server-side
- **Origin validation**: Rejects connections from unauthorized origins (ALLOWED_ORIGINS env)
- **Rate limiting**: Max connections per IP (MAX_CONNECTIONS_PER_IP env, default 10)
- **Reverse proxy support**: TRUST_PROXY flag for X-Forwarded-For header when behind nginx/Cloudflare
- **Frame size limit**: 512KB max WebSocket frame
- **Silent filtering**: Non-JSON frames dropped without error
- **Secure defaults**: Health endpoint excludes upstream URL
Loading
Loading