@askjo/camofox-browser 1.7.4 → 1.8.1

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.
package/README.md CHANGED
@@ -42,7 +42,7 @@ This project wraps that engine in a REST API built for agents: accessibility sna
42
42
  - **C++ Anti-Detection** - bypasses Google, Cloudflare, and most bot detection
43
43
  - **Element Refs** - stable `e1`, `e2`, `e3` identifiers for reliable interaction
44
44
  - **Token-Efficient** - accessibility snapshots are ~90% smaller than raw HTML
45
- - **Runs on Anything** - lazy browser launch + idle shutdown keeps memory at ~40MB when idle. Designed to share a box with the rest of your stack — Raspberry Pi, $5 VPS, shared Railway infra.
45
+ - **Runs on Anything** - lazy browser launch + idle shutdown keeps memory at ~40MB when idle. Designed to share a box with the rest of your stack — Raspberry Pi, $5 VPS, shared infra.
46
46
  - **Session Isolation** - separate cookies/storage per user
47
47
  - **Cookie Import** - inject Netscape-format cookie files for authenticated browsing
48
48
  - **Proxy + GeoIP** - route traffic through residential proxies with automatic locale/timezone
@@ -113,9 +113,24 @@ make up VERSION=135.0.1 RELEASE=beta.24
113
113
 
114
114
  > **⚠️ Do not run `docker build` directly.** The Dockerfile uses bind mounts to pull pre-downloaded binaries from `dist/`. Always use `make up` (or `make fetch` then `make build`) — it downloads the binaries first.
115
115
 
116
- ### Fly.io / Railway
116
+ ### Fly.io
117
117
 
118
- `railway.toml` is included. For Fly.io or other remote CI, you'll need a Dockerfile that downloads binaries at build time instead of using bind mounts — see [jo-browser](https://github.com/jo-inc/jo-browser) for an example.
118
+ For Fly.io or other remote CI, you'll need a Dockerfile that downloads binaries at build time instead of using bind mounts.
119
+
120
+ ### Railway
121
+
122
+ A `railway.toml` is included. It uses `Dockerfile.ci` (which downloads binaries at build time) and maps Railway's `PORT` env var to `CAMOFOX_PORT` automatically.
123
+
124
+ ```bash
125
+ # Install Railway CLI, then:
126
+ railway link
127
+ railway up
128
+ ```
129
+
130
+ Set secrets via the Railway dashboard or CLI:
131
+ ```bash
132
+ railway variables set CAMOFOX_API_KEY="your-generated-key"
133
+ ```
119
134
 
120
135
  ## Usage
121
136
 
@@ -252,7 +267,7 @@ curl -X POST http://localhost:9377/sessions/agent1/cookies \
252
267
  -d '{"cookies":[{"name":"foo","value":"bar","domain":"example.com","path":"/","expires":-1,"httpOnly":false,"secure":false}]}'
253
268
  ```
254
269
 
255
- #### Docker / Fly.io
270
+ #### Docker / Fly.io / Railway
256
271
 
257
272
  ```bash
258
273
  docker run -p 9377:9377 \
@@ -266,6 +281,11 @@ For Fly.io:
266
281
  fly secrets set CAMOFOX_API_KEY="your-generated-key"
267
282
  ```
268
283
 
284
+ For Railway:
285
+ ```bash
286
+ railway variables set CAMOFOX_API_KEY="your-generated-key"
287
+ ```
288
+
269
289
  ### Proxy + GeoIP
270
290
 
271
291
  Route all browser traffic through a proxy with automatic locale, timezone, and geolocation derived from the proxy's IP address via Camoufox's built-in GeoIP.
@@ -480,9 +500,10 @@ Reddit macros return JSON directly (no HTML parsing needed):
480
500
  | Variable | Description | Default |
481
501
  |----------|-------------|---------|
482
502
  | `CAMOFOX_PORT` | Server port | `9377` |
483
- | `PORT` | Server port (fallback, for platforms like Fly.io) | `9377` |
503
+ | `PORT` | Server port (fallback, for platforms like Fly.io, Railway) | `9377` |
484
504
  | `CAMOFOX_API_KEY` | Enable cookie import endpoint (disabled if unset) | - |
485
505
  | `CAMOFOX_ADMIN_KEY` | Required for `POST /stop` | - |
506
+ | `CAMOFOX_ACCESS_KEY` | If set, all routes (except `/health`, cookie import, and `/stop`) require `Authorization: Bearer <key>`. Lets you safely expose the server beyond loopback. | - |
486
507
  | `CAMOFOX_COOKIES_DIR` | Directory for cookie files | `~/.camofox/cookies` |
487
508
  | `CAMOFOX_PROFILE_DIR` | Directory for persisted session profiles | `~/.camofox/profiles` |
488
509
  | `CAMOFOX_TRACES_DIR` | Directory for session trace zips | `~/.camofox/traces` |
package/lib/auth.js CHANGED
@@ -4,10 +4,16 @@
4
4
  * Extracts the duplicated auth pattern from cookie/storage_state endpoints
5
5
  * into a reusable Express middleware factory.
6
6
  *
7
- * Policy:
7
+ * Policy (requireAuth / per-route):
8
8
  * - If CAMOFOX_API_KEY is set, require Bearer token match (timing-safe).
9
- * - If not set and NODE_ENV !== production, allow loopback (127.0.0.1 / ::1).
9
+ * - If CAMOFOX_ACCESS_KEY is set, also accept it as an alternative (superkey).
10
+ * - If neither key set and NODE_ENV !== production, allow loopback (127.0.0.1 / ::1).
10
11
  * - Otherwise, reject.
12
+ *
13
+ * Policy (accessKeyMiddleware / global):
14
+ * - If CAMOFOX_ACCESS_KEY is set, require Bearer match on all routes except
15
+ * /health, cookie import (when CAMOFOX_API_KEY set), and /stop (when CAMOFOX_ADMIN_KEY set).
16
+ * - If not set, pass through (backward-compatible).
11
17
  */
12
18
 
13
19
  import crypto from 'crypto';
@@ -38,7 +44,12 @@ function isLoopbackAddress(address) {
38
44
  /**
39
45
  * Create an Express middleware that enforces API key auth.
40
46
  *
41
- * @param {object} config - Must have { apiKey, nodeEnv }
47
+ * Accepts CAMOFOX_API_KEY as primary token. When CAMOFOX_ACCESS_KEY is also
48
+ * configured, it is accepted as an alternative ("superkey") so that routes
49
+ * gated by both the global access-key middleware AND this per-route middleware
50
+ * don't require two different tokens in a single Authorization header.
51
+ *
52
+ * @param {object} config - Must have { apiKey, nodeEnv }; optionally { accessKey }
42
53
  * @param {object} [options]
43
54
  * @param {string} [options.errorMessage] - Custom error message when rejecting unauthenticated requests
44
55
  * @returns {function} Express middleware (req, res, next)
@@ -47,16 +58,27 @@ export function requireAuth(config, options = {}) {
47
58
  const errorMessage = options.errorMessage ||
48
59
  'This endpoint requires CAMOFOX_API_KEY except for loopback requests in non-production environments.';
49
60
 
50
- return (req, res, next) => {
51
- if (config.apiKey) {
52
- const auth = String(req.headers['authorization'] || '');
53
- const match = auth.match(/^Bearer\s+(.+)$/i);
54
- if (!match || !timingSafeCompare(match[1], config.apiKey)) {
55
- return res.status(403).json({ error: 'Forbidden' });
56
- }
61
+ return function requireAuthCheck(req, res, next) {
62
+ const auth = String(req.headers['authorization'] || '');
63
+ const match = auth.match(/^Bearer\s+(.+)$/i);
64
+ const token = match ? match[1]?.trim() : null;
65
+
66
+ // Accept API key
67
+ if (config.apiKey && token && timingSafeCompare(token, config.apiKey)) {
68
+ return next();
69
+ }
70
+
71
+ // Accept access key as alternative (superkey)
72
+ if (config.accessKey && token && timingSafeCompare(token, config.accessKey)) {
57
73
  return next();
58
74
  }
59
75
 
76
+ // If any key is configured, a valid token was required — reject
77
+ if (config.apiKey || config.accessKey) {
78
+ return res.status(403).json({ error: 'Forbidden' });
79
+ }
80
+
81
+ // No keys configured — allow loopback in non-production
60
82
  const remoteAddress = req.socket?.remoteAddress || '';
61
83
  const allowUnauthedLocal = config.nodeEnv !== 'production' && isLoopbackAddress(remoteAddress);
62
84
  if (!allowUnauthedLocal) {
@@ -67,5 +89,46 @@ export function requireAuth(config, options = {}) {
67
89
  };
68
90
  }
69
91
 
92
+ /**
93
+ * Global access-key middleware factory.
94
+ *
95
+ * When CAMOFOX_ACCESS_KEY is set, requires `Authorization: Bearer <key>` on
96
+ * every route except:
97
+ * - GET /health (Docker/Fly healthcheck)
98
+ * - POST /sessions/:userId/cookies (only when CAMOFOX_API_KEY is also set — has its own gate)
99
+ * - POST /stop (only when CAMOFOX_ADMIN_KEY is also set — has its own gate)
100
+ *
101
+ * When a route's dedicated key is NOT configured, the access-key middleware
102
+ * does NOT exempt it — defense-in-depth prevents unprotected endpoints.
103
+ *
104
+ * When CAMOFOX_ACCESS_KEY is not set, passes through (backward-compatible).
105
+ *
106
+ * @param {object} config - Must have { accessKey }; optionally { apiKey, adminKey }
107
+ * @returns {function} Express middleware (req, res, next)
108
+ */
109
+ export function accessKeyMiddleware(config) {
110
+ return function accessKeyCheck(req, res, next) {
111
+ if (!config.accessKey) return next();
112
+
113
+ // Exempt healthcheck
114
+ if (req.path === '/health') return next();
115
+
116
+ // Exempt routes with their own dedicated auth — but only when their key is configured.
117
+ // If the dedicated key is NOT set, the access key gates the route (defense-in-depth).
118
+ if (config.apiKey && req.method === 'POST' && /^\/sessions\/[^/]+\/cookies$/.test(req.path)) return next();
119
+ if (config.adminKey && req.method === 'POST' && req.path === '/stop') return next();
120
+
121
+ const auth = String(req.headers['authorization'] || '');
122
+ const match = auth.match(/^Bearer\s+(.+)$/i);
123
+ const token = match ? match[1]?.trim() : null;
124
+ if (!token || !timingSafeCompare(token, config.accessKey)) {
125
+ return res.status(401)
126
+ .set('WWW-Authenticate', 'Bearer realm="camofox"')
127
+ .json({ error: 'Unauthorized' });
128
+ }
129
+ next();
130
+ };
131
+ }
132
+
70
133
  // Re-export utilities so server.js can still use them directly
71
134
  export { timingSafeCompare, isLoopbackAddress };
package/lib/config.js CHANGED
@@ -58,6 +58,7 @@ function loadConfig() {
58
58
  flyApiToken: process.env.FLY_API_TOKEN || '',
59
59
  adminKey: process.env.CAMOFOX_ADMIN_KEY || '',
60
60
  apiKey: process.env.CAMOFOX_API_KEY || '',
61
+ accessKey: (process.env.CAMOFOX_ACCESS_KEY || '').trim(),
61
62
  cookiesDir: process.env.CAMOFOX_COOKIES_DIR || join(os.homedir(), '.camofox', 'cookies'),
62
63
  profileDir: process.env.CAMOFOX_PROFILE_DIR || join(os.homedir(), '.camofox', 'profiles'),
63
64
  tracesDir: process.env.CAMOFOX_TRACES_DIR || join(os.homedir(), '.camofox', 'traces'),
@@ -97,6 +98,7 @@ function loadConfig() {
97
98
  NODE_ENV: process.env.NODE_ENV,
98
99
  CAMOFOX_ADMIN_KEY: process.env.CAMOFOX_ADMIN_KEY,
99
100
  CAMOFOX_API_KEY: process.env.CAMOFOX_API_KEY,
101
+ CAMOFOX_ACCESS_KEY: process.env.CAMOFOX_ACCESS_KEY,
100
102
  CAMOFOX_COOKIES_DIR: process.env.CAMOFOX_COOKIES_DIR,
101
103
  CAMOFOX_TRACES_DIR: process.env.CAMOFOX_TRACES_DIR,
102
104
  CAMOFOX_TRACES_MAX_BYTES: process.env.CAMOFOX_TRACES_MAX_BYTES,
package/lib/openapi.js CHANGED
@@ -52,7 +52,12 @@ const swaggerDefinition = {
52
52
  BearerAuth: {
53
53
  type: 'http',
54
54
  scheme: 'bearer',
55
- description: 'Bearer token matching CAMOFOX_API_KEY.',
55
+ description: 'Bearer token matching CAMOFOX_API_KEY (per-route auth for sensitive endpoints like cookie import and traces).',
56
+ },
57
+ AccessKeyAuth: {
58
+ type: 'http',
59
+ scheme: 'bearer',
60
+ description: 'Bearer token matching CAMOFOX_ACCESS_KEY. When set, gates all routes except /health, cookie import, and /stop. Acts as a superkey — also accepted by endpoints that normally require CAMOFOX_API_KEY.',
56
61
  },
57
62
  },
58
63
  schemas: {
@@ -2,7 +2,7 @@
2
2
  "id": "camofox-browser",
3
3
  "name": "Camofox Browser",
4
4
  "description": "Anti-detection browser automation for AI agents using Camoufox (Firefox-based)",
5
- "version": "1.7.4",
5
+ "version": "1.8.1",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "properties": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askjo/camofox-browser",
3
- "version": "1.7.4",
3
+ "version": "1.8.1",
4
4
  "description": "Headless browser automation server and OpenClaw plugin for AI agents - anti-detection, element refs, and session isolation",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -10,7 +10,7 @@ import { loadConfig } from './lib/config.js';
10
10
  import { normalizePlaywrightProxy, createProxyPool, buildProxyUrl } from './lib/proxy.js';
11
11
  import { createFlyHelpers } from './lib/fly.js';
12
12
  import { createPluginEvents, loadPlugins } from './lib/plugins.js';
13
- import { requireAuth, timingSafeCompare as _timingSafeCompare, isLoopbackAddress as _isLoopbackAddress } from './lib/auth.js';
13
+ import { requireAuth, accessKeyMiddleware, timingSafeCompare as _timingSafeCompare, isLoopbackAddress as _isLoopbackAddress } from './lib/auth.js';
14
14
  import { windowSnapshot } from './lib/snapshot.js';
15
15
  import {
16
16
  MAX_DOWNLOAD_INLINE_BYTES,
@@ -142,6 +142,12 @@ const FLY_MACHINE_ID = fly.machineId;
142
142
  // Route tab requests to the owning machine via fly-replay header.
143
143
  app.use('/tabs/:tabId', fly.replayMiddleware(log));
144
144
 
145
+ // Access-key middleware: gates every route when CAMOFOX_ACCESS_KEY is set.
146
+ // Exempts /health (Docker healthcheck) and routes that have their own
147
+ // dedicated keys (cookie import → CAMOFOX_API_KEY, /stop → CAMOFOX_ADMIN_KEY)
148
+ // so each key gates a distinct surface. When unset, behavior is unchanged.
149
+ app.use(accessKeyMiddleware(CONFIG));
150
+
145
151
  const ALLOWED_URL_SCHEMES = ['http:', 'https:'];
146
152
 
147
153
  // Interactive roles to include - exclude combobox to avoid opening complex widgets