camofox-browser 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +328 -0
  4. package/bin/camofox-browser.js +6 -0
  5. package/dist/src/middleware/auth.d.ts +6 -0
  6. package/dist/src/middleware/auth.d.ts.map +1 -0
  7. package/dist/src/middleware/auth.js +41 -0
  8. package/dist/src/middleware/auth.js.map +1 -0
  9. package/dist/src/middleware/errors.d.ts +5 -0
  10. package/dist/src/middleware/errors.d.ts.map +1 -0
  11. package/dist/src/middleware/errors.js +33 -0
  12. package/dist/src/middleware/errors.js.map +1 -0
  13. package/dist/src/middleware/logging.d.ts +14 -0
  14. package/dist/src/middleware/logging.d.ts.map +1 -0
  15. package/dist/src/middleware/logging.js +57 -0
  16. package/dist/src/middleware/logging.js.map +1 -0
  17. package/dist/src/routes/core.d.ts +3 -0
  18. package/dist/src/routes/core.d.ts.map +1 -0
  19. package/dist/src/routes/core.js +579 -0
  20. package/dist/src/routes/core.js.map +1 -0
  21. package/dist/src/routes/openclaw.d.ts +3 -0
  22. package/dist/src/routes/openclaw.d.ts.map +1 -0
  23. package/dist/src/routes/openclaw.js +335 -0
  24. package/dist/src/routes/openclaw.js.map +1 -0
  25. package/dist/src/server.d.ts +2 -0
  26. package/dist/src/server.d.ts.map +1 -0
  27. package/dist/src/server.js +74 -0
  28. package/dist/src/server.js.map +1 -0
  29. package/dist/src/services/browser.d.ts +5 -0
  30. package/dist/src/services/browser.d.ts.map +1 -0
  31. package/dist/src/services/browser.js +64 -0
  32. package/dist/src/services/browser.js.map +1 -0
  33. package/dist/src/services/session.d.ts +30 -0
  34. package/dist/src/services/session.d.ts.map +1 -0
  35. package/dist/src/services/session.js +204 -0
  36. package/dist/src/services/session.js.map +1 -0
  37. package/dist/src/services/tab.d.ts +85 -0
  38. package/dist/src/services/tab.d.ts.map +1 -0
  39. package/dist/src/services/tab.js +463 -0
  40. package/dist/src/services/tab.js.map +1 -0
  41. package/dist/src/types.d.ts +66 -0
  42. package/dist/src/types.d.ts.map +1 -0
  43. package/dist/src/types.js +3 -0
  44. package/dist/src/types.js.map +1 -0
  45. package/dist/src/utils/config.d.ts +49 -0
  46. package/dist/src/utils/config.d.ts.map +1 -0
  47. package/dist/src/utils/config.js +59 -0
  48. package/dist/src/utils/config.js.map +1 -0
  49. package/dist/src/utils/cookies.d.ts +30 -0
  50. package/dist/src/utils/cookies.d.ts.map +1 -0
  51. package/dist/src/utils/cookies.js +71 -0
  52. package/dist/src/utils/cookies.js.map +1 -0
  53. package/dist/src/utils/launcher.d.ts +19 -0
  54. package/dist/src/utils/launcher.d.ts.map +1 -0
  55. package/dist/src/utils/launcher.js +50 -0
  56. package/dist/src/utils/launcher.js.map +1 -0
  57. package/dist/src/utils/macros.d.ts +21 -0
  58. package/dist/src/utils/macros.d.ts.map +1 -0
  59. package/dist/src/utils/macros.js +29 -0
  60. package/dist/src/utils/macros.js.map +1 -0
  61. package/dist/src/utils/presets.d.ts +36 -0
  62. package/dist/src/utils/presets.d.ts.map +1 -0
  63. package/dist/src/utils/presets.js +276 -0
  64. package/dist/src/utils/presets.js.map +1 -0
  65. package/dist/tsconfig.tsbuildinfo +1 -0
  66. package/package.json +78 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ ## [1.0.0] - 2026-02-15
4
+
5
+ ### Changed
6
+ - Complete TypeScript rewrite with strict mode
7
+ - Modular architecture (routes/services/middleware/utils)
8
+ - Independent repo (no longer a fork)
9
+
10
+ ### Added
11
+ - Geo preset system with 8 built-in presets (us-east, us-west, japan, uk, germany, vietnam, singapore, australia)
12
+ - Custom preset file support via CAMOFOX_PRESETS_FILE
13
+ - Composite session keys for multi-context support
14
+ - Tab session index for cross-session tab lookup
15
+
16
+ ### Removed
17
+ - Unused dependencies (playwright, playwright-extra, puppeteer-extra-plugin-stealth)
18
+ - ~200MB dependency weight reduction
19
+
20
+ ### Fixed
21
+ - OpenClaw /snapshot ref annotation bug (now uses shared helper)
22
+ - Session cleanup with prefix-based matching
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Jo, Inc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,328 @@
1
+ # CamoFox Browser Server
2
+
3
+ > Anti-detection browser server for AI agents — TypeScript REST API wrapping the [Camoufox](https://github.com/daijro/camoufox) stealth browser engine
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue)](tsconfig.json)
7
+ [![Node](https://img.shields.io/badge/Node-18%2B-green)](package.json)
8
+
9
+ ## Table of Contents
10
+
11
+ - [Why CamoFox?](#why-camofox)
12
+ - [Features](#features)
13
+ - [Quick Start](#quick-start)
14
+ - [Architecture](#architecture)
15
+ - [API Reference](#api-reference)
16
+ - [Search Macros](#search-macros)
17
+ - [Geo Presets](#geo-presets)
18
+ - [Environment Variables](#environment-variables)
19
+ - [Deployment](#deployment)
20
+ - [Used With](#used-with)
21
+ - [Project Structure](#project-structure)
22
+ - [Contributing](#contributing)
23
+ - [Credits](#credits)
24
+ - [License](#license)
25
+
26
+ ## Why CamoFox?
27
+
28
+ **The Problem**: Standard browser automation (Puppeteer, Playwright, Selenium) is easily detected by modern anti-bot systems. JavaScript-level patches are fragile and get bypassed quickly.
29
+
30
+ **The Solution**: CamoFox Browser Server wraps [Camoufox](https://github.com/daijro/camoufox), a Firefox fork with **C++ engine-level fingerprint spoofing**. No JavaScript injection — anti-detection happens at the browser engine level.
31
+
32
+ | Feature | Puppeteer/Playwright | CamoFox Browser Server |
33
+ |---------|---------------------|------------------------|
34
+ | Anti-detection | JavaScript patches (fragile) | C++ engine-level (robust) |
35
+ | Fingerprint spoofing | Limited | Full (engine-level) |
36
+ | Token efficiency | Raw HTML / screenshots | Accessibility snapshots (smaller + structured) |
37
+ | Integration | Direct SDK | REST API for any language / AI agent |
38
+ | AI agent support | Varies | MCP + OpenClaw compatible |
39
+
40
+ ## Features
41
+
42
+ - **C++ Anti-Detection** — fingerprint spoofing at the Camoufox engine level (not JS injection)
43
+ - **REST API** — language-agnostic HTTP endpoints for browser automation and AI agent integration
44
+ - **Multi-Session** — concurrent isolated browser contexts per `userId` (defaults: max 50 sessions, max 10 tabs/session)
45
+ - **Geo Presets** — 8 built-in region presets (locale/timezone/geolocation) + custom presets file
46
+ - **14 Search Macros** — Google, YouTube, Amazon, Reddit (search + subreddit JSON), Wikipedia, Twitter, Yelp, Spotify, Netflix, LinkedIn, Instagram, TikTok, Twitch
47
+ - **Element Refs** — accessibility snapshots annotated with stable `eN` element references for precise interaction
48
+ - **Cookie Persistence** — import Netscape/Playwright-style cookies into a session (optional, gated by API key)
49
+ - **OpenClaw Plugin** — OpenClaw-compatible endpoints (`/start`, `/tabs/open`, `/act`, etc.)
50
+ - **TypeScript** — strict mode, typed request shapes, modular Express routes
51
+
52
+ ## Quick Start
53
+
54
+ ### From Source
55
+
56
+ ```bash
57
+ git clone https://github.com/redf0x1/camofox-browser.git
58
+ cd camofox-browser
59
+ npm install
60
+ npm run build
61
+ npm start
62
+ ```
63
+
64
+ ### Using npm (CLI)
65
+
66
+ This is a **server**, not a browser automation library. Most users should run it from source or Docker.
67
+
68
+ If you want a minimal CLI wrapper that starts the server (via the package `bin`):
69
+
70
+ ```bash
71
+ npm install -g camofox-browser
72
+ camofox-browser
73
+ ```
74
+
75
+ Or one-off:
76
+
77
+ ```bash
78
+ npx camofox-browser
79
+ ```
80
+
81
+ ### Using Docker
82
+
83
+ ```bash
84
+ docker build -t camofox-browser .
85
+ docker run -p 9377:9377 camofox-browser
86
+ ```
87
+
88
+ ### Using Docker Compose
89
+
90
+ ```yaml
91
+ services:
92
+ camofox-browser:
93
+ build: .
94
+ ports:
95
+ - "9377:9377"
96
+ environment:
97
+ CAMOFOX_PORT: "9377"
98
+ # Optional auth gates
99
+ # CAMOFOX_API_KEY: "change-me"
100
+ # CAMOFOX_ADMIN_KEY: "change-me"
101
+ # Optional: proxy routing (also enables Camoufox geoip mode)
102
+ # PROXY_HOST: ""
103
+ # PROXY_PORT: ""
104
+ # PROXY_USERNAME: ""
105
+ # PROXY_PASSWORD: ""
106
+ ```
107
+
108
+ ### Verify
109
+
110
+ ```bash
111
+ curl http://localhost:9377/health
112
+ # {"ok":true,"engine":"camoufox","browserConnected":true}
113
+ ```
114
+
115
+ ## Architecture
116
+
117
+ ```
118
+ AI Agent (MCP / OpenClaw / REST Client)
119
+
120
+ ▼ HTTP REST API (port 9377)
121
+ ┌──────────────────────────────────────────┐
122
+ │ CamoFox Browser Server │
123
+ │ (Express + TypeScript) │
124
+ ├──────────────────────────────────────────┤
125
+ │ Routes Services │
126
+ │ ├── Core API ├── Browser │
127
+ │ └── OpenClaw compat ├── Session │
128
+ │ └── Tab ops │
129
+ ├──────────────────────────────────────────┤
130
+ │ Camoufox Engine (anti-detect) │
131
+ │ Firefox fork + engine-level spoofing │
132
+ └──────────────────────────────────────────┘
133
+ ```
134
+
135
+ ## API Reference
136
+
137
+ Base URL: `http://localhost:9377`
138
+
139
+ ### Core Endpoints
140
+
141
+ Note: For any endpoint that targets an existing tab (`/tabs/:tabId/...`), the server resolves `tabId` **within a `userId` scope**. If you omit `userId`, you will typically get `404 Tab not found`.
142
+
143
+ | Method | Endpoint | Description | Required | Auth |
144
+ |--------|----------|-------------|----------|------|
145
+ | POST | `/sessions/:userId/cookies` | Import cookies into a user session (Playwright cookie objects) | Path: `userId`; Body: `{ "cookies": Cookie[] }` | `Authorization: Bearer $CAMOFOX_API_KEY` |
146
+ | GET | `/health` | Health check (also pre-launches the browser) | None | None |
147
+ | GET | `/presets` | List available geo presets (built-in + custom) | None | None |
148
+ | POST | `/tabs` | Create a new tab (supports `preset` + per-field overrides) | Body: `userId` + (`sessionKey` or `listItemId`) | None |
149
+ | GET | `/tabs?userId=...` | List all tabs for a user (OpenClaw-compatible response shape) | Query: `userId` | None |
150
+ | POST | `/tabs/:tabId/navigate` | Navigate to a URL, or expand a search `macro` + `query` | Body: `userId` + (`url` or `macro`) | None |
151
+ | GET | `/tabs/:tabId/snapshot?userId=...` | Accessibility snapshot annotated with `eN` element refs | Query: `userId` | None |
152
+ | POST | `/tabs/:tabId/wait` | Wait for page readiness (DOM + optional network idle) | Body: `userId` | None |
153
+ | POST | `/tabs/:tabId/click` | Click by `ref` (e.g. `e12`) or CSS `selector` | Body: `userId` + (`ref` or `selector`) | None |
154
+ | POST | `/tabs/:tabId/type` | Type into an element by `ref` or CSS `selector` | Body: `userId` + (`ref` or `selector`) + `text` | None |
155
+ | POST | `/tabs/:tabId/press` | Press a key (e.g. `Enter`, `Escape`) | Body: `userId` + `key` | None |
156
+ | POST | `/tabs/:tabId/scroll` | Scroll up/down by pixels | Body: `userId` | None |
157
+ | POST | `/tabs/:tabId/back` | Go back | Body: `userId` | None |
158
+ | POST | `/tabs/:tabId/forward` | Go forward | Body: `userId` | None |
159
+ | POST | `/tabs/:tabId/refresh` | Refresh | Body: `userId` | None |
160
+ | GET | `/tabs/:tabId/links?userId=...&limit=50&offset=0` | Extract links (paginated) | Query: `userId` | None |
161
+ | GET | `/tabs/:tabId/screenshot?userId=...&fullPage=true` | Screenshot (PNG bytes) | Query: `userId` | None |
162
+ | GET | `/tabs/:tabId/stats?userId=...` | Tab stats + visited URLs | Query: `userId` | None |
163
+ | DELETE | `/tabs/:tabId` | Close a tab (expects JSON body: `{ "userId": "..." }`) | Body: `userId` | None |
164
+ | DELETE | `/tabs/group/:listItemId` | Close a tab group (expects JSON body: `{ "userId": "..." }`) | Body: `userId` | None |
165
+ | DELETE | `/sessions/:userId` | Close all sessions for a user | Path: `userId` | None |
166
+
167
+ ### OpenClaw Endpoints
168
+
169
+ OpenClaw-compatible aliases (used by the OpenClaw plugin).
170
+
171
+ | Method | Endpoint | Description | Required | Auth |
172
+ |--------|----------|-------------|----------|------|
173
+ | GET | `/` | Status (alias of `/health`) | None | None |
174
+ | POST | `/tabs/open` | Open tab (OpenClaw request/response shape) | Body: `userId` + `url` | None |
175
+ | POST | `/start` | Start browser engine | None | None |
176
+ | POST | `/stop` | Stop browser engine | None | `x-admin-key: $CAMOFOX_ADMIN_KEY` |
177
+ | POST | `/navigate` | Navigate (OpenClaw request shape: `targetId` in body) | Body: `userId` + `targetId` + `url` | None |
178
+ | GET | `/snapshot?userId=...&targetId=...` | Snapshot (OpenClaw response shape) | Query: `userId` + `targetId` | None |
179
+ | POST | `/act` | Combined actions (`click`, `type`, `press`, `scroll`, `scrollIntoView`, `hover`, `wait`, `close`) | Body: `userId` + `targetId` + `kind` | None |
180
+
181
+ ## Search Macros
182
+
183
+ Use macros via `POST /tabs/:tabId/navigate` with `{ "macro": "@google_search", "query": "..." }`.
184
+
185
+ | Macro | Engine |
186
+ |-------|--------|
187
+ | `@google_search` | Google |
188
+ | `@youtube_search` | YouTube |
189
+ | `@amazon_search` | Amazon |
190
+ | `@reddit_search` | Reddit (JSON) |
191
+ | `@reddit_subreddit` | Reddit subreddit (JSON) |
192
+ | `@wikipedia_search` | Wikipedia |
193
+ | `@twitter_search` | Twitter/X |
194
+ | `@yelp_search` | Yelp |
195
+ | `@spotify_search` | Spotify |
196
+ | `@netflix_search` | Netflix |
197
+ | `@linkedin_search` | LinkedIn |
198
+ | `@instagram_search` | Instagram tags |
199
+ | `@tiktok_search` | TikTok |
200
+ | `@twitch_search` | Twitch |
201
+
202
+ ## Geo Presets
203
+
204
+ Built-in presets (also exposed via `GET /presets`):
205
+
206
+ | Preset | Locale | Timezone | Location |
207
+ |--------|--------|----------|----------|
208
+ | `us-east` | `en-US` | `America/New_York` | New York (40.7128, -74.0060) |
209
+ | `us-west` | `en-US` | `America/Los_Angeles` | Los Angeles (34.0522, -118.2437) |
210
+ | `japan` | `ja-JP` | `Asia/Tokyo` | Tokyo (35.6895, 139.6917) |
211
+ | `uk` | `en-GB` | `Europe/London` | London (51.5074, -0.1278) |
212
+ | `germany` | `de-DE` | `Europe/Berlin` | Berlin (52.5200, 13.4050) |
213
+ | `vietnam` | `vi-VN` | `Asia/Ho_Chi_Minh` | Ho Chi Minh City (10.8231, 106.6297) |
214
+ | `singapore` | `en-SG` | `Asia/Singapore` | Singapore (1.3521, 103.8198) |
215
+ | `australia` | `en-AU` | `Australia/Sydney` | Sydney (-33.8688, 151.2093) |
216
+
217
+ Create a tab with a preset:
218
+
219
+ ```bash
220
+ curl -X POST http://localhost:9377/tabs \
221
+ -H 'Content-Type: application/json' \
222
+ -d '{"userId":"agent1","sessionKey":"task1","preset":"japan","url":"https://example.com"}'
223
+ ```
224
+
225
+ Custom presets: set `CAMOFOX_PRESETS_FILE=/path/to/presets.json` (JSON object; keys become preset names).
226
+
227
+ ## Environment Variables
228
+
229
+ | Variable | Default | Description |
230
+ |----------|---------|-------------|
231
+ | `CAMOFOX_PORT` | `9377` | Server port |
232
+ | `PORT` | (optional) | Alternative port env var (common in PaaS) |
233
+ | `NODE_ENV` | `development` | Node environment |
234
+ | `CAMOFOX_ADMIN_KEY` | (empty) | Required for `POST /stop` (sent via `x-admin-key`) |
235
+ | `CAMOFOX_API_KEY` | (empty) | Enables cookie import endpoint; sent via `Authorization: Bearer ...` |
236
+ | `CAMOFOX_COOKIES_DIR` | `~/.camofox/cookies` | Directory used by the OpenClaw plugin cookie tool |
237
+ | `CAMOFOX_PRESETS_FILE` | (unset) | Optional JSON file defining/overriding geo presets |
238
+ | `CAMOFOX_SESSION_TIMEOUT` | `1800000` | Session idle timeout in ms (min `60000`) |
239
+ | `CAMOFOX_MAX_SESSIONS` | `50` | Maximum concurrent sessions |
240
+ | `CAMOFOX_MAX_TABS` | `10` | Maximum tabs per session |
241
+ | `PROXY_HOST` | (empty) | Proxy host (enables proxy routing) |
242
+ | `PROXY_PORT` | (empty) | Proxy port |
243
+ | `PROXY_USERNAME` | (empty) | Proxy username |
244
+ | `PROXY_PASSWORD` | (empty) | Proxy password |
245
+
246
+ ## Deployment
247
+
248
+ ### Docker (Recommended)
249
+
250
+ ```bash
251
+ docker build -t camofox-browser .
252
+ docker run -p 9377:9377 \
253
+ -e CAMOFOX_PORT=9377 \
254
+ camofox-browser
255
+ ```
256
+
257
+ ### Fly.io
258
+
259
+ This repo includes a starter `fly.toml` for one-command deploys.
260
+
261
+ ```bash
262
+ fly launch
263
+ fly deploy
264
+ ```
265
+
266
+ ### Railway
267
+
268
+ - Create a new project → deploy from this GitHub repo
269
+ - Set `CAMOFOX_PORT=9377` (Railway will also provide `PORT`, which is supported)
270
+ - Ensure the service exposes port `9377`
271
+
272
+ ### Render
273
+
274
+ - Create a new Web Service → deploy from this GitHub repo
275
+ - Use Docker (recommended) and expose port `9377`
276
+ - Set `CAMOFOX_PORT=9377` (or rely on Render `PORT`)
277
+
278
+ ### System Requirements
279
+
280
+ - Node.js 18+ (20+ recommended)
281
+ - 2GB+ RAM (browser + contexts require significant memory)
282
+ - Linux recommended for production; macOS is fine for development
283
+
284
+ ## Used With
285
+
286
+ | Project | Description |
287
+ |---------|-------------|
288
+ | [CamoFox MCP](https://github.com/redf0x1/camofox-mcp) | MCP (Model Context Protocol) server for Claude, Cursor, VS Code |
289
+ | [OpenClaw](https://openclaw.ai) | Open-source AI agent framework (compat endpoints included) |
290
+ | [Camoufox](https://github.com/daijro/camoufox) | Anti-detection Firefox browser engine |
291
+
292
+ ## Project Structure
293
+
294
+ ```text
295
+ src/
296
+ ├── server.ts # Express app entry point
297
+ ├── types.ts # Shared TypeScript interfaces
298
+ ├── routes/
299
+ │ ├── core.ts # Core REST API (~21 endpoints)
300
+ │ └── openclaw.ts # OpenClaw compatibility (~7 endpoints)
301
+ ├── services/
302
+ │ ├── browser.ts # Browser lifecycle (singleton)
303
+ │ ├── session.ts # Session management + limits
304
+ │ └── tab.ts # Tab operations (snapshot/click/type/etc.)
305
+ ├── middleware/ # Auth, logging, errors
306
+ └── utils/ # Config, presets, macros
307
+ ```
308
+
309
+ ## Contributing
310
+
311
+ See [CONTRIBUTING.md](CONTRIBUTING.md)
312
+
313
+ ## Credits
314
+
315
+ This project is based on [camofox-browser](https://github.com/jo-inc/camofox-browser) by [Jo Inc](https://github.com/jo-inc) (YC W24) and the [Camoufox](https://github.com/daijro/camoufox) anti-detection browser engine by [daijro](https://github.com/daijro).
316
+
317
+ - [Camoufox](https://camoufox.com) - Firefox-based browser with C++ anti-detection
318
+ - [Donate to Camoufox's original creator daijro](https://camoufox.com/about/)
319
+ - [OpenClaw](https://openclaw.ai) - Open-source AI agent framework
320
+
321
+ ## License
322
+
323
+ [MIT](LICENSE)
324
+
325
+ ## Crypto Scam Warning
326
+
327
+ Sketchy people are doing sketchy things with crypto tokens named "Camofox" now that this project is getting attention. **Camofox is not a crypto project and will never be one.** Any token, coin, or NFT using the Camofox name has nothing to do with us.
328
+
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Minimal CLI entrypoint for running the server via `npx camofox-browser`.
4
+ // The server starts listening as a side effect of importing the built entry.
5
+
6
+ require('../dist/src/server.js');
@@ -0,0 +1,6 @@
1
+ import type { Request } from 'express';
2
+ export declare function timingSafeCompare(a: unknown, b: unknown): boolean;
3
+ export declare function getBearerToken(req: Request): string | null;
4
+ export declare function isAuthorizedWithApiKey(req: Request, apiKey: string): boolean;
5
+ export declare function isAuthorizedWithAdminKey(req: Request, adminKey: string): boolean;
6
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/middleware/auth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CAUjE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAI1D;AAED,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAI5E;AAED,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAKhF"}
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.timingSafeCompare = timingSafeCompare;
7
+ exports.getBearerToken = getBearerToken;
8
+ exports.isAuthorizedWithApiKey = isAuthorizedWithApiKey;
9
+ exports.isAuthorizedWithAdminKey = isAuthorizedWithAdminKey;
10
+ const node_crypto_1 = __importDefault(require("node:crypto"));
11
+ function timingSafeCompare(a, b) {
12
+ if (typeof a !== 'string' || typeof b !== 'string')
13
+ return false;
14
+ const bufA = Buffer.from(a);
15
+ const bufB = Buffer.from(b);
16
+ if (bufA.length !== bufB.length) {
17
+ // Keep timing similar even for length mismatch.
18
+ node_crypto_1.default.timingSafeEqual(bufA, bufA);
19
+ return false;
20
+ }
21
+ return node_crypto_1.default.timingSafeEqual(bufA, bufB);
22
+ }
23
+ function getBearerToken(req) {
24
+ const auth = String(req.headers['authorization'] || '');
25
+ const match = auth.match(/^Bearer\s+(.+)$/i);
26
+ return match ? match[1] : null;
27
+ }
28
+ function isAuthorizedWithApiKey(req, apiKey) {
29
+ const token = getBearerToken(req);
30
+ if (!token)
31
+ return false;
32
+ return timingSafeCompare(token, apiKey);
33
+ }
34
+ function isAuthorizedWithAdminKey(req, adminKey) {
35
+ const header = req.headers['x-admin-key'];
36
+ if (!header)
37
+ return false;
38
+ const value = Array.isArray(header) ? header[0] : String(header);
39
+ return timingSafeCompare(value, adminKey);
40
+ }
41
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../../src/middleware/auth.ts"],"names":[],"mappings":";;;;;AAGA,8CAUC;AAED,wCAIC;AAED,wDAIC;AAED,4DAKC;AAhCD,8DAAiC;AAGjC,SAAgB,iBAAiB,CAAC,CAAU,EAAE,CAAU;IACvD,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACjE,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5B,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5B,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC;QACjC,gDAAgD;QAChD,qBAAM,CAAC,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACnC,OAAO,KAAK,CAAC;IACd,CAAC;IACD,OAAO,qBAAM,CAAC,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AAC3C,CAAC;AAED,SAAgB,cAAc,CAAC,GAAY;IAC1C,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,IAAI,EAAE,CAAC,CAAC;IACxD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC;IAC7C,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAChC,CAAC;AAED,SAAgB,sBAAsB,CAAC,GAAY,EAAE,MAAc;IAClE,MAAM,KAAK,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IACzB,OAAO,iBAAiB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;AACzC,CAAC;AAED,SAAgB,wBAAwB,CAAC,GAAY,EAAE,QAAgB;IACtE,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;IAC1C,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC1B,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACjE,OAAO,iBAAiB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;AAC3C,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { NextFunction, Request, Response } from 'express';
2
+ export declare function safeError(err: unknown): string;
3
+ export declare function errorMiddleware(err: unknown, req: Request, res: Response, _next: NextFunction): void;
4
+ export declare function installCrashHandlers(): void;
5
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/middleware/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAO/D,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAS9C;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,YAAY,GAAG,IAAI,CAIpG;AAED,wBAAgB,oBAAoB,IAAI,IAAI,CAQ3C"}
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.safeError = safeError;
4
+ exports.errorMiddleware = errorMiddleware;
5
+ exports.installCrashHandlers = installCrashHandlers;
6
+ const config_1 = require("../utils/config");
7
+ const logging_1 = require("./logging");
8
+ const CONFIG = (0, config_1.loadConfig)();
9
+ function safeError(err) {
10
+ const message = err instanceof Error ? err.message : String(err);
11
+ const stack = err instanceof Error ? err.stack : undefined;
12
+ if (CONFIG.nodeEnv === 'production') {
13
+ (0, logging_1.log)('error', 'internal error', { error: message, stack });
14
+ return 'Internal server error';
15
+ }
16
+ return message;
17
+ }
18
+ function errorMiddleware(err, req, res, _next) {
19
+ (0, logging_1.log)('error', 'request error', { reqId: req.reqId, path: req.path, error: safeError(err) });
20
+ if (res.headersSent)
21
+ return;
22
+ res.status(500).json({ error: safeError(err) });
23
+ }
24
+ function installCrashHandlers() {
25
+ process.on('uncaughtException', (err) => {
26
+ (0, logging_1.log)('error', 'uncaughtException', { error: err.message, stack: err.stack });
27
+ process.exit(1);
28
+ });
29
+ process.on('unhandledRejection', (reason) => {
30
+ (0, logging_1.log)('error', 'unhandledRejection', { reason: String(reason) });
31
+ });
32
+ }
33
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../../src/middleware/errors.ts"],"names":[],"mappings":";;AAOA,8BASC;AAED,0CAIC;AAED,oDAQC;AA9BD,4CAA6C;AAC7C,uCAAgC;AAEhC,MAAM,MAAM,GAAG,IAAA,mBAAU,GAAE,CAAC;AAE5B,SAAgB,SAAS,CAAC,GAAY;IACrC,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACjE,MAAM,KAAK,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IAE3D,IAAI,MAAM,CAAC,OAAO,KAAK,YAAY,EAAE,CAAC;QACrC,IAAA,aAAG,EAAC,OAAO,EAAE,gBAAgB,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QAC1D,OAAO,uBAAuB,CAAC;IAChC,CAAC;IACD,OAAO,OAAO,CAAC;AAChB,CAAC;AAED,SAAgB,eAAe,CAAC,GAAY,EAAE,GAAY,EAAE,GAAa,EAAE,KAAmB;IAC7F,IAAA,aAAG,EAAC,OAAO,EAAE,eAAe,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC3F,IAAI,GAAG,CAAC,WAAW;QAAE,OAAO;IAC5B,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;AACjD,CAAC;AAED,SAAgB,oBAAoB;IACnC,OAAO,CAAC,EAAE,CAAC,mBAAmB,EAAE,CAAC,GAAU,EAAE,EAAE;QAC9C,IAAA,aAAG,EAAC,OAAO,EAAE,mBAAmB,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC;QAC5E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACjB,CAAC,CAAC,CAAC;IACH,OAAO,CAAC,EAAE,CAAC,oBAAoB,EAAE,CAAC,MAAe,EAAE,EAAE;QACpD,IAAA,aAAG,EAAC,OAAO,EAAE,oBAAoB,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { NextFunction, Request, Response } from 'express';
2
+ export type LogLevel = 'info' | 'warn' | 'error';
3
+ export declare function log(level: LogLevel, msg: string, fields?: Record<string, unknown>): void;
4
+ export declare function loggingMiddleware(req: Request, res: Response, next: NextFunction): void;
5
+ export interface StatsBeaconFields {
6
+ sessions: number;
7
+ tabs: number;
8
+ rssBytes: number;
9
+ heapUsedBytes: number;
10
+ uptimeSeconds: number;
11
+ browserConnected: boolean;
12
+ }
13
+ export declare function startStatsBeacon(getFields: () => StatsBeaconFields): NodeJS.Timeout;
14
+ //# sourceMappingURL=logging.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logging.d.ts","sourceRoot":"","sources":["../../../src/middleware/logging.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAE/D,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEjD,wBAAgB,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,IAAI,CAa5F;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CAqBvF;AAED,MAAM,WAAW,iBAAiB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,OAAO,CAAC;CAC1B;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,iBAAiB,GAAG,MAAM,CAAC,OAAO,CAanF"}
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.log = log;
7
+ exports.loggingMiddleware = loggingMiddleware;
8
+ exports.startStatsBeacon = startStatsBeacon;
9
+ const node_crypto_1 = __importDefault(require("node:crypto"));
10
+ function log(level, msg, fields = {}) {
11
+ const entry = {
12
+ ts: new Date().toISOString(),
13
+ level,
14
+ msg,
15
+ ...fields,
16
+ };
17
+ const line = JSON.stringify(entry);
18
+ if (level === 'error') {
19
+ process.stderr.write(line + '\n');
20
+ }
21
+ else {
22
+ process.stdout.write(line + '\n');
23
+ }
24
+ }
25
+ function loggingMiddleware(req, res, next) {
26
+ if (req.path === '/health')
27
+ return next();
28
+ const reqId = node_crypto_1.default.randomUUID().slice(0, 8);
29
+ req.reqId = reqId;
30
+ req.startTime = Date.now();
31
+ const bodyUserId = req.body?.userId;
32
+ const queryUserId = req.query?.userId;
33
+ const userId = (bodyUserId ?? queryUserId ?? '-');
34
+ log('info', 'req', { reqId, method: req.method, path: req.path, userId });
35
+ const origEnd = res.end.bind(res);
36
+ res.end = function patchedEnd(...args) {
37
+ const ms = Date.now() - (req.startTime ?? Date.now());
38
+ log('info', 'res', { reqId, status: res.statusCode, ms });
39
+ return origEnd(...args);
40
+ };
41
+ next();
42
+ }
43
+ function startStatsBeacon(getFields) {
44
+ return setInterval(() => {
45
+ const mem = process.memoryUsage();
46
+ const fields = getFields();
47
+ log('info', 'stats', {
48
+ sessions: fields.sessions,
49
+ tabs: fields.tabs,
50
+ rssBytes: mem.rss,
51
+ heapUsedBytes: mem.heapUsed,
52
+ uptimeSeconds: Math.floor(process.uptime()),
53
+ browserConnected: fields.browserConnected,
54
+ });
55
+ }, 5 * 60_000);
56
+ }
57
+ //# sourceMappingURL=logging.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logging.js","sourceRoot":"","sources":["../../../src/middleware/logging.ts"],"names":[],"mappings":";;;;;AAKA,kBAaC;AAED,8CAqBC;AAWD,4CAaC;AAjED,8DAAiC;AAKjC,SAAgB,GAAG,CAAC,KAAe,EAAE,GAAW,EAAE,SAAkC,EAAE;IACrF,MAAM,KAAK,GAAG;QACb,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QAC5B,KAAK;QACL,GAAG;QACH,GAAG,MAAM;KACT,CAAC;IACF,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACnC,IAAI,KAAK,KAAK,OAAO,EAAE,CAAC;QACvB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IACnC,CAAC;SAAM,CAAC;QACP,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IACnC,CAAC;AACF,CAAC;AAED,SAAgB,iBAAiB,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IAChF,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,IAAI,EAAE,CAAC;IAE1C,MAAM,KAAK,GAAG,qBAAM,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC9C,GAAG,CAAC,KAAK,GAAG,KAAK,CAAC;IAClB,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAE3B,MAAM,UAAU,GAAI,GAAG,CAAC,IAAoD,EAAE,MAAM,CAAC;IACrF,MAAM,WAAW,GAAI,GAAG,CAAC,KAAqD,EAAE,MAAM,CAAC;IACvF,MAAM,MAAM,GAAG,CAAC,UAAU,IAAI,WAAW,IAAI,GAAG,CAAY,CAAC;IAE7D,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;IAE1E,MAAM,OAAO,GAAG,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAClC,GAAG,CAAC,GAAG,GAAG,SAAS,UAAU,CAAC,GAAG,IAAe;QAC/C,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QACtD,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,UAAU,EAAE,EAAE,EAAE,CAAC,CAAC;QAC1D,OAAO,OAAO,CAAC,GAAI,IAAoC,CAAC,CAAC;IAC1D,CAAC,CAAC;IAEF,IAAI,EAAE,CAAC;AACR,CAAC;AAWD,SAAgB,gBAAgB,CAAC,SAAkC;IAClE,OAAO,WAAW,CAAC,GAAG,EAAE;QACvB,MAAM,GAAG,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;QAClC,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;QAC3B,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE;YACpB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,QAAQ,EAAE,GAAG,CAAC,GAAG;YACjB,aAAa,EAAE,GAAG,CAAC,QAAQ;YAC3B,aAAa,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;YAC3C,gBAAgB,EAAE,MAAM,CAAC,gBAAgB;SACzC,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC;AAChB,CAAC"}
@@ -0,0 +1,3 @@
1
+ declare const router: import("express-serve-static-core").Router;
2
+ export default router;
3
+ //# sourceMappingURL=core.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../../src/routes/core.ts"],"names":[],"mappings":"AA4CA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA0iBxB,eAAe,MAAM,CAAC"}