From 59e5f800d52f16e6c5d72d68f2c5934be27207c4 Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Fri, 27 Mar 2026 16:43:31 +0700 Subject: [PATCH] feat: add working proxy examples, fix protocol params, redesign example pages - Fix GoClaw Protocol v3 params: message (not content), agentId (camelCase), nested event:agent wrapper, stream flag, sessionKey - Fix proxy interceptFrame: inject agentId into chat.send (not just connect) - Add dotenv support to proxy server for .env file loading - Fix typing indicator position: move inside messages container - Remove client-side apiKey (security: API keys are backend-only) - Add new examples: proxy-with-api-key, docker-compose, express-embedded, Dockerfile.proxy, nginx.conf - Redesign all example pages with shared CSS (examples.css): proper code blocks, architecture diagrams, step indicators, feature tags - Update README with step-by-step Getting Started guide and proxy config table - Update all docs (system-architecture, code-standards, project-overview) to reflect proxy-only architecture and correct protocol details --- README.md | 145 ++++++++++++++-- docs/code-standards.md | 25 ++- docs/project-overview-pdr.md | 29 +++- docs/system-architecture.md | 107 ++++++++---- examples/Dockerfile.proxy | 8 + examples/async-snippet.html | 54 ++++-- examples/docker-compose.yml | 33 ++++ examples/examples.css | 238 ++++++++++++++++++++++++++ examples/express-embedded-proxy.ts | 130 ++++++++++++++ examples/nginx.conf | 29 ++++ examples/proxy-mode.html | 80 ++++++--- examples/proxy-with-api-key.html | 102 +++++++++++ examples/react-example.tsx | 13 +- examples/vanilla-basic.html | 41 +++-- examples/vanilla-customized.html | 39 +++-- examples/vue-example.ts | 13 +- server/package-lock.json | 13 ++ server/package.json | 1 + server/src/index.ts | 1 + server/src/websocket-proxy-session.ts | 26 +-- src/chat-widget.ts | 11 +- src/types.ts | 2 - src/websocket-client.ts | 53 +++--- 23 files changed, 996 insertions(+), 197 deletions(-) create mode 100644 examples/Dockerfile.proxy create mode 100644 examples/docker-compose.yml create mode 100644 examples/examples.css create mode 100644 examples/express-embedded-proxy.ts create mode 100644 examples/nginx.conf create mode 100644 examples/proxy-with-api-key.html diff --git a/README.md b/README.md index b3399c3..27e5e8c 100644 --- a/README.md +++ b/README.md @@ -7,35 +7,122 @@ Embeddable chat widget for [GoClaw](https://goclaw.sh) AI agent gateway. Drop a 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) +Browser Widget ←→ Proxy Server (:3100) ←→ GoClaw Gateway + (no token) (injects token) (validates token) ``` ## Features -- **Zero dependencies** — Vanilla TypeScript, ~8KB gzipped +- **Zero dependencies** — Vanilla TypeScript, ~18KB 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 - **Theming** — Light, dark, auto, or fully custom via CSS variables -- **Configurable** — Position, size, colors, fonts, avatars, everything - **Auto-reconnect** — Exponential backoff with configurable retries - **Async loading** — Non-blocking snippet pattern (like Intercom) -- **Mobile responsive** — Full-screen on small viewports -- **Accessible** — Keyboard navigation, ARIA labels -## Proxy Server Setup +## Getting Started + +### Step 1: Clone & install + +```bash +git clone https://github.com/nextlevelbuilder/goclaw-plugin-webchat.git +cd goclaw-plugin-webchat +npm install +``` + +### Step 2: Configure the proxy server + +The proxy keeps your GoClaw gateway token server-side. Create a `.env` file: ```bash -cd server/ +cd server cp .env.example .env -# Edit .env: set GOCLAW_URL, GOCLAW_TOKEN, and optionally PROXY_API_KEY +``` + +Edit `server/.env` with your GoClaw credentials: + +```env +# Required: your GoClaw gateway WebSocket URL +GOCLAW_URL=wss://your-workspace.goclaw.sh/ws + +# Required: gateway auth token (never exposed to browser) +GOCLAW_TOKEN=your-gateway-token-here + +# Optional: default agent ID (UUID or slug) +DEFAULT_AGENT_ID=your-agent-id + +# Optional: restrict which origins can connect +# ALLOWED_ORIGINS=https://example.com,https://app.example.com + +# Optional: require API key for backend-to-proxy auth (server-to-server only, never in browser!) +# PROXY_API_KEY=your-secret-key +``` + +Install proxy dependencies: + +```bash npm install +``` + +### Step 3: Start the proxy server + +```bash npm run dev ``` -## Quick Start +You should see: + +``` +[proxy] listening on :3100 +[proxy] upstream: wss://your-workspace.goclaw.sh/ws +[proxy] auth token: configured +``` + +### Step 4: Build the widget + +In a new terminal, from the project root: + +```bash +cd .. +npm run build +``` + +This outputs `dist/goclaw-webchat.umd.js` and `dist/goclaw-webchat.es.js`. + +### Step 5: Add to your website + +```html + + +``` + +Open the page in a browser — the chat widget appears in the bottom-right corner. No token in client code. + +### Step 6: Try the examples + +Open any example file in your browser while the proxy is running: + +```bash +# Serve the project directory +npx serve . + +# Then visit: +# http://localhost:3000/examples/proxy-mode.html — basic proxy setup +# http://localhost:3000/examples/vanilla-basic.html — simplest integration +# http://localhost:3000/examples/vanilla-customized.html — custom theme + API +# http://localhost:3000/examples/async-snippet.html — non-blocking loader +# http://localhost:3000/examples/proxy-with-api-key.html — API key auth +``` + +## Integration Patterns ### Script Tag (simplest) @@ -84,10 +171,8 @@ const widget = init({ theme: 'dark', }); -// Programmatic control widget.open(); widget.send('Hello!'); -widget.close(); widget.destroy(); ``` @@ -118,12 +203,19 @@ app.use(GoClawPlugin, { }); ``` +### Embed Proxy in Express + +If you already run Express/Node.js, embed the proxy directly instead of running a separate process. See `examples/express-embedded-proxy.ts`. + +### Docker Compose + +For production deployment with nginx reverse proxy, see `examples/docker-compose.yml`. + ## Configuration | Option | Type | Default | Description | |--------|------|---------|-------------| | `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 | @@ -140,6 +232,20 @@ app.use(GoClawPlugin, { | `maxReconnectAttempts` | `number` | `10` | Max reconnection attempts | | `zIndex` | `number` | `999999` | CSS z-index | +## Proxy Server Configuration + +| Env Variable | Required | Default | Description | +|-------------|----------|---------|-------------| +| `GOCLAW_URL` | Yes | — | GoClaw gateway WebSocket URL | +| `GOCLAW_TOKEN` | Yes | — | Gateway auth token (kept server-side) | +| `PORT` | No | `3100` | Proxy server port | +| `DEFAULT_AGENT_ID` | No | — | Default agent for `chat.send` | +| `ALLOWED_ORIGINS` | No | `*` | Comma-separated allowed origins | +| `MAX_CONNECTIONS_PER_IP` | No | `10` | Per-IP connection limit | +| `TRUST_PROXY` | No | `false` | Trust X-Forwarded-For headers | +| `PROXY_API_KEY` | No | — | Backend-to-proxy auth (server-to-server only) | +| `LOG_LEVEL` | No | `info` | `debug` / `info` / `warn` / `error` | + ## Custom Themes ```js @@ -205,10 +311,17 @@ GoClaw.init({ ## Development ```bash +npm install # Widget deps +npm run dev # Widget dev server with HMR +npm run build # Production build +npm run lint # Type-check + +# Proxy server +cd server npm install -npm run dev # Dev server with HMR -npm run build # Production build -npm run lint # Type-check +npm run dev # Start proxy (tsx watch) +npm run build # Compile to dist/ +npm start # Run compiled proxy ``` ## License diff --git a/docs/code-standards.md b/docs/code-standards.md index a2e454d..ea3726a 100644 --- a/docs/code-standards.md +++ b/docs/code-standards.md @@ -22,20 +22,28 @@ - CSS custom properties for theming - Event-driven WebSocket communication - Builder pattern for configuration (single config object) +- Proxy-only: widget never handles auth tokens, proxy injects them server-side + +### GoClaw Protocol v3 Conventions +- Parameter names use **camelCase**: `agentId`, `sessionKey`, `runId` +- Chat method: `chat.send` with `{ message, agentId, stream }` (not `content`) +- Events wrapped: `{ event: "agent", payload: { type: "chunk", payload: { content } } }` +- Connect method: `connect` with `{ protocol: 3, user_id }` — proxy adds `token` ### 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) +- No client-side tokens or API keys — all auth is server-side via proxy ## 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 +- Runtime dependencies: `ws` (WebSocket v8.18+), `dotenv` (env loading) +- Dev dependencies: TypeScript, tsx (dev server), @types/node, @types/ws ### File Naming - kebab-case for all source files @@ -49,15 +57,22 @@ ### Architecture Patterns - Config-driven initialization (environment variables via `proxy-config.ts`) -- Per-IP connection tracking with exponential backoff +- `dotenv/config` loaded at entry point for `.env` file support +- Per-IP connection tracking +- Per-session message rate limiting (60 msg/min) +- WebSocket frame interception: injects `GOCLAW_TOKEN` into `connect`, `DEFAULT_AGENT_ID` into `chat.send` - WebSocket frame buffering until upstream connection ready +- Upstream response sanitization (strips token fields) - Graceful shutdown with drain timeout - Non-JSON frames dropped silently ### Security -- Auth token stored server-side only, never sent to client +- Auth token (`GOCLAW_TOKEN`) stored server-side only, never sent to client +- `PROXY_API_KEY` is backend-to-backend only (Express, nginx → proxy), never from browser - 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 +- Per-session rate limiting: 60 messages per minute +- `TRUST_PROXY` flag for reverse proxy (nginx, Cloudflare) deployments - Max frame size: 512KB - Upstream gateway URL not included in health endpoint responses +- Connect timeout: 10 seconds for upstream connection diff --git a/docs/project-overview-pdr.md b/docs/project-overview-pdr.md index 126032e..34a0e23 100644 --- a/docs/project-overview-pdr.md +++ b/docs/project-overview-pdr.md @@ -6,7 +6,7 @@ Embeddable JavaScript chat widget allowing website owners to add AI chat powered ## Tech Stack - **Language**: TypeScript (strict mode) - **Build**: Vite 6 (library mode), ESBuild minification -- **Output**: UMD + ESM bundles (~8KB gzipped) +- **Output**: UMD + ESM bundles (~18KB gzipped) - **UI**: Vanilla DOM + Shadow DOM for style isolation - **Styling**: CSS custom properties for theming - **Protocol**: GoClaw WebSocket Protocol v3 (req/res/event frames) @@ -15,9 +15,9 @@ Embeddable JavaScript chat widget allowing website owners to add AI chat powered ### Widget (src/) - `src/index.ts` — Entry point, `init()` function, window auto-attach -- `src/websocket-client.ts` — WebSocket connection, auth, RPC, event handling, reconnection (supports proxy mode) +- `src/websocket-client.ts` — WebSocket connection, auth, RPC, event handling, reconnection (proxy-only) - `src/chat-widget.ts` — Shadow DOM UI, message rendering, input handling -- `src/types.ts` — All TypeScript interfaces (includes `proxyUrl` config option) +- `src/types.ts` — All TypeScript interfaces (proxy-only config, no client-side tokens) - `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) @@ -26,12 +26,22 @@ Embeddable JavaScript chat widget allowing website owners to add AI chat powered - `src/wrappers/vue-wrapper.ts` — Vue 3 plugin ### Proxy Server (server/) -- `server/src/index.ts` — Server entry point, HTTP + WebSocket listener +- `server/src/index.ts` — Server entry point, loads dotenv, starts HTTP + WebSocket - `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/proxy-server.ts` — HTTP + WebSocket server with origin validation, per-IP limits, API key auth, graceful shutdown +- `server/src/websocket-proxy-session.ts` — Single proxy session: intercepts `connect` frame to inject gateway token, injects `DEFAULT_AGENT_ID` into `chat.send`, buffers upstream messages, rate limits per session - `server/src/connection-tracker.ts` — Per-IP connection rate limiting +### Examples (examples/) +- `examples/proxy-mode.html` — Basic proxy setup with architecture walkthrough +- `examples/vanilla-basic.html` — Simplest integration +- `examples/vanilla-customized.html` — Custom theme, bottom-left, programmatic API +- `examples/async-snippet.html` — Non-blocking Intercom-style loader +- `examples/proxy-with-api-key.html` — Backend-to-proxy API key auth +- `examples/express-embedded-proxy.ts` — Embed proxy in Express app +- `examples/docker-compose.yml` — Production nginx + proxy deployment +- `examples/examples.css` — Shared CSS for example pages + ## Distribution ### Widget @@ -43,12 +53,13 @@ Embeddable JavaScript chat widget allowing website owners to add AI chat powered ### 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) +- Dependencies: `ws` (WebSocket library), `dotenv` (env loading) ## 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 +- Proxy-only architecture: All auth tokens stay server-side, no client-side token exposure +- No client-side API key: `PROXY_API_KEY` is backend-to-backend only, never sent from browser +- GoClaw Protocol params: `agentId` (camelCase), `message` (not `content`), nested `event:agent` wrapper diff --git a/docs/system-architecture.md b/docs/system-architecture.md index 961d464..32e2d65 100644 --- a/docs/system-architecture.md +++ b/docs/system-architecture.md @@ -20,17 +20,18 @@ │ │ └───────────────────────────┘ │ │ │ └───────────────────────────────────┘ │ └─────────────────────────────────────────┘ - │ WebSocket + │ WebSocket (no token) ▼ -┌─────────────────────────────────────────┐ (optional) +┌─────────────────────────────────────────┐ │ WebChat Proxy Server │ │ ┌──────────┐ ┌───────────────────┐ │ │ │ WS Server │ │ Token Injection │ │ │ │ /ws :3100 │──│ (connect frame) │ │ │ └──────────┘ └───────────────────┘ │ │ Origin validation │ Per-IP limits │ +│ API key auth (opt) │ agentId inject │ └─────────────────────────────────────────┘ - │ WebSocket (with token) + │ WebSocket (with GOCLAW_TOKEN) ▼ ┌─────────────────────────────────────────┐ │ GoClaw Gateway │ @@ -41,31 +42,55 @@ │ │ │ │ │ ┌────┴────┐ ┌─────┴──────┐ │ │ │ Auth │ │ LLM Provider│ │ -│ │ Pairing │ │ (13+ LLMs) │ │ +│ │ API Keys│ │ (13+ LLMs) │ │ │ └─────────┘ └────────────┘ │ └─────────────────────────────────────────┘ ``` -### 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 (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` +1. **Init**: `GoClaw.init({ url })` → creates ChatWidget → attaches Shadow DOM → connects WebSocket to proxy +2. **Auth**: WS open → sends `connect` frame (no token) → proxy intercepts and injects `GOCLAW_TOKEN` → forwards to gateway → connected +3. **Send**: User types → `chat.send` RPC with `{ message, agentId, stream }` → proxy injects `DEFAULT_AGENT_ID` if not set → forwards to gateway +4. **Stream**: `event:agent { type: "run.started" }` → `{ type: "chunk", payload: { content } }` → `{ type: "run.completed" }` 5. **Reconnect**: WS close (unclean) → exponential backoff → re-authenticate → resume +## GoClaw WebSocket Protocol v3 + +### Request Frame +```json +{ "type": "req", "id": "req_1", "method": "chat.send", "params": { "message": "hello", "agentId": "uuid-or-slug", "stream": true } } +``` + +### Response Frame +```json +{ "type": "res", "id": "req_1", "ok": true, "payload": { "runId": "...", "content": "..." } } +``` + +### Event Frame (agent events are nested) +```json +{ "type": "event", "event": "agent", "payload": { "type": "chunk", "agentId": "kha-dao", "runId": "...", "payload": { "content": "token text" } } } +``` + +### Agent Event Types +| Event | Description | +|-------|-------------| +| `run.started` | Agent started processing | +| `activity` | Agent phase change (thinking, acting) | +| `thinking` | Thinking tokens (internal reasoning) | +| `chunk` | Response content tokens | +| `tool.call` | Tool invocation started | +| `tool.result` | Tool execution completed | +| `run.completed` | Agent finished, includes final content + usage | +| `run.failed` | Agent failed with error | + ## Module Dependencies ### Widget ``` index.ts └── chat-widget.ts - ├── websocket-client.ts (uses proxyUrl or direct url+token) + ├── websocket-client.ts (connects to proxy url) │ └── types.ts ├── markdown-renderer.ts ├── svg-icons.ts @@ -76,7 +101,7 @@ index.ts ### Proxy Server ``` -index.ts +index.ts (loads dotenv) ├── proxy-config.ts (reads env vars) ├── proxy-server.ts (HTTP + WebSocket server) │ ├── websocket-proxy-session.ts (per-connection handler) @@ -86,28 +111,40 @@ index.ts ## 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) +### Proxy-Only Architecture (Production) ``` 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 +- Widget config: { url: 'wss://proxy/ws' } +- Proxy env: GOCLAW_TOKEN, DEFAULT_AGENT_ID +- Token never exposed to client ``` -## 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 +### Dual Auth Layers +| Layer | Mechanism | Purpose | +|-------|-----------|---------| +| **Client → Proxy** | `PROXY_API_KEY` (optional) | Control who can use the proxy | +| **Proxy → Gateway** | `GOCLAW_TOKEN` (required) | Authenticate proxy to GoClaw | + +### Proxy Security Features +- **Token injection**: Intercepts WS `connect` frame, injects gateway token server-side +- **agentId injection**: Injects `DEFAULT_AGENT_ID` into `chat.send` if client omits it +- **Token stripping**: Removes token fields from upstream responses (defense in depth) +- **API key auth**: Optional `PROXY_API_KEY` — clients authenticate via `?apiKey=xxx` or `X-API-Key` header +- **Origin validation**: Rejects connections from unauthorized origins (`ALLOWED_ORIGINS`) +- **Rate limiting**: Max connections per IP (`MAX_CONNECTIONS_PER_IP`, default 10) +- **Rate limiting (per-session)**: 60 messages/minute per WebSocket session +- **Reverse proxy support**: `TRUST_PROXY` for X-Forwarded-For 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 +- **Non-JSON filtering**: Non-JSON frames silently dropped + +## Examples + +| Example | File | Description | +|---------|------|-------------| +| Proxy mode | `examples/proxy-mode.html` | Basic proxy setup, architecture walkthrough | +| Basic | `examples/vanilla-basic.html` | Simplest integration | +| Customized | `examples/vanilla-customized.html` | Custom theme, bottom-left, programmatic API | +| Async snippet | `examples/async-snippet.html` | Non-blocking Intercom-style loader | +| API key auth | `examples/proxy-with-api-key.html` | Proxy with `PROXY_API_KEY` | +| Express embedded | `examples/express-embedded-proxy.ts` | Embed proxy in Express app | +| Docker compose | `examples/docker-compose.yml` | Production nginx + proxy setup | diff --git a/examples/Dockerfile.proxy b/examples/Dockerfile.proxy new file mode 100644 index 0000000..e209d0c --- /dev/null +++ b/examples/Dockerfile.proxy @@ -0,0 +1,8 @@ +FROM node:20-alpine +WORKDIR /app +COPY server/package*.json ./ +RUN npm ci --production=false +COPY server/src ./src +COPY server/tsconfig.json ./ +RUN npx tsc +CMD ["node", "dist/index.js"] diff --git a/examples/async-snippet.html b/examples/async-snippet.html index 5cec186..52c040b 100644 --- a/examples/async-snippet.html +++ b/examples/async-snippet.html @@ -3,36 +3,53 @@ - GoClaw WebChat - Async Snippet (like Intercom) - + GoClaw WebChat - Async Snippet + -

Async Snippet Example

-

This loading pattern is non-blocking — the script loads asynchronously and processes queued commands. Identical to how Intercom, Drift, and similar widgets load.

+ + +

Features

+
+ Async loading + Queue-based init + Non-blocking + Copy-paste ready +

Copy-Paste Snippet

<script>
-  // GoClaw WebChat async loader
   (function(w,d,s,u){
     w.__goclaw_queue=w.__goclaw_queue||[];
-    w.GoClaw=w.GoClaw||{init:function(){w.__goclaw_queue.push(['init',arguments[0]])}};
-    var f=d.getElementsByTagName(s)[0],j=d.createElement(s);
-    j.async=true;j.src=u;f.parentNode.insertBefore(j,f);
-  })(window,document,'script','https://cdn.example.com/goclaw-webchat.umd.js');
+    w.GoClaw=w.GoClaw||{
+      init:function(){
+        w.__goclaw_queue.push(['init',arguments[0]])
+      }
+    };
+    var f=d.getElementsByTagName(s)[0],
+        j=d.createElement(s);
+    j.async=true;
+    j.src=u;
+    f.parentNode.insertBefore(j,f);
+  })(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: 'Chat with us',
     theme: 'auto',
   });
 </script>
- +
+ + Commands called before the script loads are queued and replayed automatically. + The page never blocks on the widget script. +
+ diff --git a/examples/docker-compose.yml b/examples/docker-compose.yml new file mode 100644 index 0000000..87760d4 --- /dev/null +++ b/examples/docker-compose.yml @@ -0,0 +1,33 @@ +# Full-stack GoClaw WebChat: proxy + static file server for the widget +# +# Usage: +# 1. Copy server/.env.example to server/.env and configure it +# 2. docker compose -f examples/docker-compose.yml up +# 3. Open http://localhost:8080 in your browser +# +# The proxy server runs on :3100 (internal), and nginx serves +# the widget files on :8080 with /ws proxied to the backend. + +services: + # WebSocket proxy — keeps auth token server-side + proxy: + build: + context: ../server + dockerfile: ../examples/Dockerfile.proxy + env_file: ../server/.env + expose: + - "3100" + restart: unless-stopped + + # Static file server + reverse proxy + web: + image: nginx:alpine + ports: + - "8080:80" + volumes: + - ../dist:/usr/share/nginx/html/dist:ro + - ./proxy-mode.html:/usr/share/nginx/html/index.html:ro + - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro + depends_on: + - proxy + restart: unless-stopped diff --git a/examples/examples.css b/examples/examples.css new file mode 100644 index 0000000..d42a4fb --- /dev/null +++ b/examples/examples.css @@ -0,0 +1,238 @@ +/* Shared styles for GoClaw WebChat example pages */ + +:root { + --brand: #6366f1; + --brand-light: #eef2ff; + --emerald: #059669; + --amber: #f59e0b; + --success: #22c55e; + --surface: #ffffff; + --surface-alt: #f8fafc; + --text: #1e293b; + --text-secondary: #64748b; + --border: #e2e8f0; + --code-bg: #0f172a; + --code-text: #e2e8f0; + --code-comment: #64748b; + --code-string: #a5f3fc; + --code-keyword: #c4b5fd; + --radius: 10px; +} + +*, *::before, *::after { box-sizing: border-box; } + +body { + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + max-width: 720px; + margin: 0 auto; + padding: 48px 24px 80px; + color: var(--text); + line-height: 1.6; + background: var(--surface); + -webkit-font-smoothing: antialiased; +} + +h1 { + font-size: 1.75rem; + font-weight: 700; + margin: 0 0 8px; + letter-spacing: -0.02em; +} + +h2 { + font-size: 1.125rem; + font-weight: 600; + margin: 32px 0 12px; + color: var(--text); + letter-spacing: -0.01em; +} + +p { margin: 0 0 16px; color: var(--text-secondary); } +ul { margin: 0 0 16px; padding-left: 20px; color: var(--text-secondary); } +li { margin: 4px 0; } + +a { color: var(--brand); text-decoration: none; } +a:hover { text-decoration: underline; } + +/* Inline code */ +code { + font-family: "JetBrains Mono", "Fira Code", "SF Mono", Consolas, monospace; + font-size: 0.85em; + background: var(--brand-light); + color: var(--brand); + padding: 2px 6px; + border-radius: 4px; + font-weight: 500; +} + +/* Code blocks */ +pre { + background: var(--code-bg); + border-radius: var(--radius); + padding: 20px 24px; + margin: 12px 0 20px; + overflow-x: auto; + border: 1px solid #1e293b; +} + +pre code { + background: none; + color: var(--code-text); + padding: 0; + font-size: 0.8125rem; + font-weight: 400; + line-height: 1.7; + display: block; + white-space: pre; + word-break: normal; + overflow-wrap: normal; +} + +/* Badge */ +.badge { + display: inline-block; + font-size: 0.6875rem; + font-weight: 600; + padding: 2px 10px; + border-radius: 999px; + vertical-align: middle; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.badge-green { background: var(--success); color: white; } +.badge-amber { background: var(--amber); color: white; } +.badge-brand { background: var(--brand); color: white; } + +/* Step list */ +.steps { margin: 12px 0 20px; } + +.step { + display: flex; + align-items: baseline; + gap: 12px; + padding: 10px 16px; + margin: 0; + background: var(--surface-alt); + border-left: 3px solid var(--brand); +} + +.step:first-child { border-radius: var(--radius) var(--radius) 0 0; } +.step:last-child { border-radius: 0 0 var(--radius) var(--radius); } +.step + .step { border-top: 1px solid var(--border); } + +.step-num { + font-weight: 700; + color: var(--brand); + font-size: 0.8125rem; + flex-shrink: 0; + width: 20px; + text-align: center; +} + +.step-text { color: var(--text-secondary); font-size: 0.9375rem; } + +/* Note/callout */ +.note { + display: flex; + gap: 12px; + padding: 14px 18px; + margin: 16px 0; + border-radius: var(--radius); + border: 1px solid var(--border); + background: var(--surface-alt); + font-size: 0.875rem; + color: var(--text-secondary); + line-height: 1.5; +} + +.note-icon { flex-shrink: 0; font-size: 1rem; } +.note strong { color: var(--text); } + +/* Demo buttons */ +.demo-buttons { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin: 16px 0; +} + +.demo-buttons button { + padding: 8px 16px; + border-radius: 8px; + border: 1px solid var(--border); + background: var(--surface); + cursor: pointer; + font-size: 0.8125rem; + font-weight: 500; + color: var(--text); + transition: background 0.15s, border-color 0.15s; +} + +.demo-buttons button:hover { + background: var(--surface-alt); + border-color: #cbd5e1; +} + +/* Header area */ +.page-header { margin-bottom: 24px; } +.page-header p { margin: 0; font-size: 0.9375rem; } + +/* Architecture diagram */ +.arch-diagram { + display: flex; + align-items: center; + justify-content: center; + gap: 0; + padding: 24px 16px; + margin: 12px 0 20px; + background: var(--surface-alt); + border-radius: var(--radius); + border: 1px solid var(--border); + overflow-x: auto; +} + +.arch-box { + text-align: center; + padding: 12px 16px; + border-radius: 8px; + border: 1px solid var(--border); + background: var(--surface); + min-width: 120px; +} + +.arch-box-label { + font-size: 0.8125rem; + font-weight: 600; + color: var(--text); + margin-bottom: 4px; +} + +.arch-box-sub { + font-size: 0.6875rem; + color: var(--text-secondary); +} + +.arch-arrow { + font-size: 1.25rem; + color: var(--text-secondary); + padding: 0 8px; + flex-shrink: 0; +} + +/* Feature tags */ +.features { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin: 12px 0 20px; +} + +.feature-tag { + font-size: 0.75rem; + font-weight: 500; + padding: 4px 12px; + border-radius: 999px; + background: var(--brand-light); + color: var(--brand); +} diff --git a/examples/express-embedded-proxy.ts b/examples/express-embedded-proxy.ts new file mode 100644 index 0000000..10a096f --- /dev/null +++ b/examples/express-embedded-proxy.ts @@ -0,0 +1,130 @@ +/** + * Express Embedded Proxy Example + * + * Shows how to embed the GoClaw WebSocket proxy into an existing Express/Node.js app. + * This is the most common integration pattern — your backend already runs Express, + * and you want to add GoClaw chat without running a separate proxy process. + * + * Usage: + * npx tsx examples/express-embedded-proxy.ts + * + * Prerequisites: + * npm install express ws + * npm install -D @types/express @types/ws tsx + */ + +import express from 'express'; +import { createServer } from 'node:http'; +import { WebSocketServer, WebSocket } from 'ws'; +import path from 'node:path'; + +// ── Config ── +const GOCLAW_URL = process.env.GOCLAW_URL ?? 'wss://digitop.goclaw.sh/ws'; +const GOCLAW_TOKEN = process.env.GOCLAW_TOKEN ?? ''; +const PORT = parseInt(process.env.PORT ?? '3000', 10); + +if (!GOCLAW_TOKEN) { + console.error('Set GOCLAW_TOKEN environment variable'); + process.exit(1); +} + +// ── Express app with your existing routes ── +const app = express(); +app.use(express.json()); + +// Serve widget dist files +app.use('/dist', express.static(path.resolve(import.meta.dirname, '..', 'dist'))); + +// Your existing API routes +app.get('/api/health', (_req, res) => { + res.json({ status: 'ok', chat: 'proxy embedded' }); +}); + +// Serve the chat page +app.get('/', (_req, res) => { + res.send(` + + My App with Embedded GoClaw Proxy + + +

My Express App

+

The GoClaw chat widget connects to /ws on this same server. + No separate proxy process needed.

+ + + +`); +}); + +// ── HTTP server ── +const server = createServer(app); + +// ── WebSocket proxy on /ws ── +const wss = new WebSocketServer({ server, path: '/ws' }); + +wss.on('connection', (clientWs) => { + console.log('[proxy] client connected'); + + // Connect to upstream GoClaw gateway + const upstream = new WebSocket(GOCLAW_URL); + + upstream.on('open', () => { + console.log('[proxy] upstream connected'); + }); + + // Relay upstream → client (strip token fields) + upstream.on('message', (data) => { + if (clientWs.readyState === WebSocket.OPEN) { + clientWs.send(data.toString()); + } + }); + + // Relay client → upstream (inject token into connect frames) + clientWs.on('message', (data) => { + const raw = data.toString(); + try { + const frame = JSON.parse(raw); + + // Inject auth token into connect request + if (frame.type === 'req' && frame.method === 'connect') { + frame.params = frame.params ?? {}; + frame.params.token = GOCLAW_TOKEN; + upstream.send(JSON.stringify(frame)); + return; + } + } catch { /* non-JSON — pass through */ } + + if (upstream.readyState === WebSocket.OPEN) { + upstream.send(raw); + } + }); + + // Clean up + clientWs.on('close', () => { + console.log('[proxy] client disconnected'); + upstream.close(); + }); + + upstream.on('close', () => { + clientWs.close(); + }); + + upstream.on('error', (err) => { + console.error('[proxy] upstream error:', err.message); + clientWs.close(1011, 'upstream error'); + }); +}); + +// ── Start ── +server.listen(PORT, () => { + console.log(`Server running at http://localhost:${PORT}`); + console.log(`WebSocket proxy at ws://localhost:${PORT}/ws`); + console.log(`Upstream: ${GOCLAW_URL}`); +}); diff --git a/examples/nginx.conf b/examples/nginx.conf new file mode 100644 index 0000000..dc76ab5 --- /dev/null +++ b/examples/nginx.conf @@ -0,0 +1,29 @@ +# Nginx config for GoClaw WebChat docker-compose example +# Serves static widget files and proxies /ws to the backend proxy server + +server { + listen 80; + + # Serve widget dist files + location /dist/ { + alias /usr/share/nginx/html/dist/; + } + + # Serve example HTML + location / { + root /usr/share/nginx/html; + index index.html; + } + + # Proxy WebSocket connections to the backend + location /ws { + proxy_pass http://proxy:3100/ws; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_read_timeout 86400; + } +} diff --git a/examples/proxy-mode.html b/examples/proxy-mode.html index bdf0ed8..221dc25 100644 --- a/examples/proxy-mode.html +++ b/examples/proxy-mode.html @@ -4,22 +4,56 @@ GoClaw WebChat - Proxy Mode (Secure) - + -

GoClaw WebChat - Proxy Mode Secure

-

This example uses a proxy server to keep the auth token server-side. - No token appears in client-side code.

+

Architecture

-
Browser Widget ←→ Proxy Server (:3100) ←→ GoClaw Gateway (:9090)
-  (no token)      (holds token)           (validates token)
+
+
+
Browser Widget
+
no token
+
+
←→
+
+
Proxy Server
+
:3100 · injects token
+
+
←→
+
+
GoClaw Gateway
+
validates token
+
+
+ +

How It Works

+
+
+ 1 + Widget connects to proxy via WebSocket at ws://localhost:3100/ws +
+
+ 2 + Widget sends a connect frame (no token) +
+
+ 3 + Proxy intercepts the frame, injects GOCLAW_TOKEN from env, forwards to gateway +
+
+ 4 + All subsequent frames are relayed transparently +
+
+ 5 + Proxy strips any token fields from upstream responses (defense in depth) +
+

Setup

# 1. Configure the proxy server
@@ -27,30 +61,30 @@ 

Setup

cp .env.example .env # Edit .env: set GOCLAW_URL and GOCLAW_TOKEN -# 2. Start the proxy +# 2. Install deps & start the proxy +npm install npm run dev -# 3. Open this page in a browser
+# 3. Build the widget (from project root) +cd .. +npm run build + +# 4. Open this page in a browser -

Client Code (no token needed!)

+

Client Code

GoClaw.init({
-  url: 'wss://your-goclaw-server.com/ws',  // fallback for direct mode
-  proxyUrl: 'ws://localhost:3100/ws',       // proxy handles auth
+  url: 'ws://localhost:3100/ws',   // proxy server — no token needed!
   title: 'Chat with us',
   theme: 'auto',
 });
- + + + diff --git a/examples/react-example.tsx b/examples/react-example.tsx index fc1d10c..b537a88 100644 --- a/examples/react-example.tsx +++ b/examples/react-example.tsx @@ -1,10 +1,12 @@ /** - * React Integration Example + * React Integration Example (Proxy Mode) * - * Install: - * npm install @goclaw/webchat + * Prerequisites: + * 1. Start the proxy server: cd server && npm run dev + * 2. npm install @goclaw/webchat * - * Usage in your React app: + * The proxy server keeps the auth token server-side. + * The widget only needs the proxy URL — no token required. */ import { useRef } from 'react'; @@ -24,8 +26,7 @@ export default function App() { GoClaw WebChat - Basic Example - + -

GoClaw WebChat - Basic Example

-

This page demonstrates the simplest integration. A chat widget appears in the bottom-right corner.

+ + +

Features

+
+ Auto theme + Proxy-only auth + Bottom-right position + Welcome message +

Integration Code

<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',
   });
 </script>
-

Features Demonstrated

- +
+ + The url points to your proxy server. The gateway auth token is kept + server-side and never exposed to the browser. +
-