@askjo/camofox-browser 1.6.0 → 1.7.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 +118 -8
- package/camofox.config.json +8 -0
- package/lib/config.js +26 -1
- package/lib/extract.js +74 -0
- package/lib/openapi.js +100 -0
- package/lib/plugins.js +1 -0
- package/lib/reporter.js +743 -0
- package/lib/tracing.js +137 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -2
- package/plugins/persistence/index.js +7 -3
- package/plugins/persistence/plugin.test.js +2 -2
- package/plugins/vnc/spawn.js +8 -0
- package/plugins/vnc/vnc-launcher.js +2 -2
- package/scripts/exec.js +8 -0
- package/scripts/generate-openapi.js +24 -0
- package/scripts/plugin.js +1 -1
- package/scripts/plugin.test.js +1 -1
- package/server.js +1846 -25
package/README.md
CHANGED
|
@@ -3,20 +3,24 @@
|
|
|
3
3
|
<h1>camofox-browser</h1>
|
|
4
4
|
<p><strong>Anti-detection browser server for AI agents, powered by Camoufox</strong></p>
|
|
5
5
|
<p>
|
|
6
|
-
<a href="
|
|
7
|
-
<a href="https://
|
|
8
|
-
<a href="https://
|
|
9
|
-
<a href="https://
|
|
6
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
|
|
7
|
+
<a href="https://github.com/jo-inc/camofox-browser/stargazers"><img src="https://img.shields.io/github/stars/jo-inc/camofox-browser" alt="GitHub stars" /></a>
|
|
8
|
+
<a href="https://www.npmjs.com/package/camofox-browser"><img src="https://img.shields.io/npm/v/camofox-browser" alt="npm version" /></a>
|
|
9
|
+
<a href="https://github.com/jo-inc/camofox-browser/commits"><img src="https://img.shields.io/github/last-commit/jo-inc/camofox-browser" alt="GitHub last commit" /></a>
|
|
10
10
|
</p>
|
|
11
11
|
<p>
|
|
12
12
|
Standing on the mighty shoulders of <a href="https://camoufox.com">Camoufox</a> - a Firefox fork with fingerprint spoofing at the C++ level.
|
|
13
|
-
<br/><br/>
|
|
14
|
-
The same engine behind <a href="https://askjo.ai?ref=camofox">Jo</a> — an AI assistant that doesn't need you to babysit it. Runs half on your Mac, half on a dedicated cloud machine that only you use. Available on macOS, Telegram, and WhatsApp. <a href="https://askjo.ai?ref=camofox">Try the beta free →</a>
|
|
15
13
|
</p>
|
|
16
14
|
</div>
|
|
17
15
|
|
|
18
16
|
<br/>
|
|
19
17
|
|
|
18
|
+
> <a href="https://askjo.ai?ref=camofox"><img src="jo-logo.png" alt="Jo" width="80" height="80" align="left" /></a>
|
|
19
|
+
>
|
|
20
|
+
> Built by the team behind <a href="https://askjo.ai?ref=camofox"><strong>jo — a personal AI agent</strong></a> that runs half on your Mac, half on a dedicated cloud machine just for you — with zero maintenance needed. Available on macOS, Telegram, WhatsApp, and email. <a href="https://askjo.ai?ref=camofox">Try the beta free →</a>
|
|
21
|
+
|
|
22
|
+
<br/>
|
|
23
|
+
|
|
20
24
|
```bash
|
|
21
25
|
git clone https://github.com/jo-inc/camofox-browser && cd camofox-browser
|
|
22
26
|
npm install && npm start
|
|
@@ -51,6 +55,10 @@ This project wraps that engine in a REST API built for agents: accessibility sna
|
|
|
51
55
|
- **DOM Image Extraction** - list `<img>` src/alt and optionally return inline data URLs
|
|
52
56
|
- **Deploy Anywhere** - Docker, Fly.io, Railway
|
|
53
57
|
- **VNC Interactive Login** - log into sites visually via noVNC, export storage state for agent reuse
|
|
58
|
+
- **OpenAPI Docs** - auto-generated spec at [`/openapi.json`](http://localhost:9377/openapi.json) and interactive docs at [`/docs`](http://localhost:9377/docs)
|
|
59
|
+
- **Structured Extract** - `POST /tabs/:tabId/extract` with a JSON Schema that maps properties to snapshot refs via `x-ref`
|
|
60
|
+
- **Session Tracing** - opt-in per-session Playwright trace capture (screenshots + DOM snapshots + network) with API endpoints to list, fetch, and delete trace zips
|
|
61
|
+
- **Crash Reporter** - automatic [anonymized crash/hang reporting](lib/reporter.js#L28-L290) via GitHub Issues. Identifies which sites cause failures and common failure patterns. Private domains are HMAC-hashed, paths/params stripped, tokens/IPs redacted. Opt-out with `CAMOFOX_CRASH_REPORT_ENABLED=false`.
|
|
54
62
|
|
|
55
63
|
## Optional Dependencies
|
|
56
64
|
|
|
@@ -103,11 +111,11 @@ make up ARCH=x86_64
|
|
|
103
111
|
make up VERSION=135.0.1 RELEASE=beta.24
|
|
104
112
|
```
|
|
105
113
|
|
|
106
|
-
|
|
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.
|
|
107
115
|
|
|
108
116
|
### Fly.io / Railway
|
|
109
117
|
|
|
110
|
-
`
|
|
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.
|
|
111
119
|
|
|
112
120
|
## Usage
|
|
113
121
|
|
|
@@ -193,6 +201,48 @@ By default, camofox persists each user's cookies and localStorage to `~/.camofox
|
|
|
193
201
|
|
|
194
202
|
Override the directory with `CAMOFOX_PROFILE_DIR` or set `"profileDir"` in the persistence plugin config. To disable persistence, set `"persistence": { "enabled": false }` in `camofox.config.json`.
|
|
195
203
|
|
|
204
|
+
### Session Tracing
|
|
205
|
+
|
|
206
|
+
Capture a Playwright trace of every action in a session: page screenshots, DOM snapshots, network requests, and console output. Output is a single `.zip` file you can open in Playwright's built-in Trace Viewer.
|
|
207
|
+
|
|
208
|
+
Opt-in per session by passing `trace: true` when opening the first tab:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
curl -X POST http://localhost:9377/tabs \
|
|
212
|
+
-H 'Content-Type: application/json' \
|
|
213
|
+
-d '{"userId":"agent1","sessionKey":"task1","url":"https://example.com","trace":true}'
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The trace is written when the session closes. Close the session to flush it, then list, fetch, and view:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
# Close the session to flush the trace
|
|
220
|
+
curl -X DELETE http://localhost:9377/sessions/agent1
|
|
221
|
+
|
|
222
|
+
# List trace files
|
|
223
|
+
curl http://localhost:9377/sessions/agent1/traces
|
|
224
|
+
# {"traces":[{"filename":"trace-2026-04-18T04-05-00-...zip","sizeBytes":42810,"createdAt":...}]}
|
|
225
|
+
|
|
226
|
+
# Download (Content-Type: application/zip)
|
|
227
|
+
curl http://localhost:9377/sessions/agent1/traces/trace-2026-04-18T04-05-00-abc.zip > session.zip
|
|
228
|
+
|
|
229
|
+
# View it in Playwright's Trace Viewer
|
|
230
|
+
npx playwright show-trace session.zip
|
|
231
|
+
|
|
232
|
+
# Delete
|
|
233
|
+
curl -X DELETE http://localhost:9377/sessions/agent1/traces/trace-2026-04-18T04-05-00-abc.zip
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Why traces instead of video: Camoufox is Firefox-based, and Playwright's `recordVideo` is Chromium-only. Traces work on Firefox and give you more than video (network + DOM + console + screenshots).
|
|
237
|
+
|
|
238
|
+
Tracing cannot be toggled on an existing session. `DELETE /sessions/:userId` first if you need to change the flag.
|
|
239
|
+
|
|
240
|
+
Storage defaults to `~/.camofox/traces/<hashed-userId>/` and is swept on server startup:
|
|
241
|
+
|
|
242
|
+
- `CAMOFOX_TRACES_DIR` - base directory (default: `~/.camofox/traces`)
|
|
243
|
+
- `CAMOFOX_TRACES_MAX_BYTES` - max size per trace, removed at next startup if exceeded (default: 50MB)
|
|
244
|
+
- `CAMOFOX_TRACES_TTL_HOURS` - traces older than this are removed at next startup (default: 24)
|
|
245
|
+
|
|
196
246
|
#### Standalone server usage
|
|
197
247
|
|
|
198
248
|
```bash
|
|
@@ -262,6 +312,60 @@ When a proxy is configured:
|
|
|
262
312
|
- Browser fingerprint (language, timezone, coordinates) is consistent with the proxy location
|
|
263
313
|
- Without a proxy, defaults to `en-US`, `America/Los_Angeles`, San Francisco coordinates
|
|
264
314
|
|
|
315
|
+
### Crash Reporter
|
|
316
|
+
|
|
317
|
+
Browser automation fails in ways that are hard to predict — Cloudflare challenges, site redesigns breaking selectors, redirect loops, dialog storms, renderer crashes. The scope is wide and the failure modes are diverse. Without telemetry, the only signal is "it didn't work."
|
|
318
|
+
|
|
319
|
+
The crash reporter gives us structured data on *which sites fail*, *how they fail*, and *how often*, so we can prioritize fixes for the patterns that actually affect users. It files GitHub Issues automatically when:
|
|
320
|
+
|
|
321
|
+
- **Uncaught exceptions** crash the process
|
|
322
|
+
- **Event loop stalls** exceed 5 seconds (watchdog detection)
|
|
323
|
+
- **Frustration patterns** — 3+ consecutive failures (timeout, dead context, navigation abort) on the same tab
|
|
324
|
+
|
|
325
|
+
Each report includes the failure type, stack trace, tab health counters (HTTP status histogram, console errors, request failures, redirect depth), and the target URL — all anonymized.
|
|
326
|
+
|
|
327
|
+
#### Privacy
|
|
328
|
+
|
|
329
|
+
All reported data goes through paranoid anonymization ([`lib/reporter.js` L28–290](lib/reporter.js#L28-L290)) before leaving the process:
|
|
330
|
+
|
|
331
|
+
- **URLs** — well-known public domains (Google, Amazon, Reddit, Cloudflare, etc.) are shown verbatim so we can identify which sites cause problems. Private/unknown domains are replaced with a stable HMAC hash (`site-a1b2c3d4`) — same hash across reports for correlation, but not reversible to the original domain. Path segments become `•/•/•` (depth only). Query params become `?[3]` (count only). No keys, values, or path content is ever included.
|
|
332
|
+
- **File paths** → stripped to filename only (`<path>/server.js`)
|
|
333
|
+
- **Tokens, secrets, API keys** → `<token>`
|
|
334
|
+
- **IPs, emails, env vars** → redacted
|
|
335
|
+
- **Docker/Fly machine IDs** → `<id>`
|
|
336
|
+
- **Tab health** — pure counters (crash count, error count, status code histogram). No page content, no URLs, no user data.
|
|
337
|
+
|
|
338
|
+
Duplicate issues are detected by stack signature and get a `+1` comment instead of a new issue.
|
|
339
|
+
|
|
340
|
+
Uses a dedicated GitHub App ([Camofox Crash/Stuck Reporter](https://github.com/apps/camofox-crash-stuck-reporter)) with issues-only permissions — no PAT or configuration required.
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
# Disable crash reporting
|
|
344
|
+
export CAMOFOX_CRASH_REPORT_ENABLED=false
|
|
345
|
+
|
|
346
|
+
# Report to a different repo (default: jo-inc/camofox-browser)
|
|
347
|
+
export CAMOFOX_CRASH_REPORT_REPO=your-org/your-repo
|
|
348
|
+
|
|
349
|
+
# Adjust rate limit (default: 10 per hour)
|
|
350
|
+
export CAMOFOX_CRASH_REPORT_RATE_LIMIT=5
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
#### Reporting to your own repo
|
|
354
|
+
|
|
355
|
+
By default, reports go to `jo-inc/camofox-browser`. To file issues in your own repo instead, create a GitHub App:
|
|
356
|
+
|
|
357
|
+
1. Go to **Settings → Developer settings → GitHub Apps → New GitHub App**
|
|
358
|
+
2. Set permissions: **Repository → Issues → Read & Write**. Uncheck **Webhook → Active**.
|
|
359
|
+
3. Click **Generate a private key** — downloads a `.pem` file
|
|
360
|
+
4. Install the app on your target repo (Install App → select repo)
|
|
361
|
+
5. Note your **App ID** (number on the app's settings page) and **Installation ID** (from the URL after installing: `github.com/settings/installations/{id}`)
|
|
362
|
+
6. Base64-encode the private key and split it into two halves:
|
|
363
|
+
```bash
|
|
364
|
+
base64 < your-app.pem | tr -d '\n' | fold -w $(($(base64 < your-app.pem | tr -d '\n' | wc -c) / 2)) | head -2
|
|
365
|
+
```
|
|
366
|
+
7. Replace `_GH_APP_ID`, `_GH_INSTALL_ID`, `_K_A`, and `_K_B` in `lib/reporter.js` with your values
|
|
367
|
+
8. Set `CAMOFOX_CRASH_REPORT_REPO=your-org/your-repo`
|
|
368
|
+
|
|
265
369
|
### Structured Logging
|
|
266
370
|
|
|
267
371
|
All log output is JSON (one object per line) for easy parsing by log aggregators:
|
|
@@ -381,6 +485,9 @@ Reddit macros return JSON directly (no HTML parsing needed):
|
|
|
381
485
|
| `CAMOFOX_ADMIN_KEY` | Required for `POST /stop` | - |
|
|
382
486
|
| `CAMOFOX_COOKIES_DIR` | Directory for cookie files | `~/.camofox/cookies` |
|
|
383
487
|
| `CAMOFOX_PROFILE_DIR` | Directory for persisted session profiles | `~/.camofox/profiles` |
|
|
488
|
+
| `CAMOFOX_TRACES_DIR` | Directory for session trace zips | `~/.camofox/traces` |
|
|
489
|
+
| `CAMOFOX_TRACES_MAX_BYTES` | Max size per trace, removed on next startup if exceeded | `52428800` (50MB) |
|
|
490
|
+
| `CAMOFOX_TRACES_TTL_HOURS` | Traces older than this are swept on startup | `24` |
|
|
384
491
|
| `MAX_SESSIONS` | Max concurrent browser sessions | `50` |
|
|
385
492
|
| `MAX_TABS_PER_SESSION` | Max tabs per session | `10` |
|
|
386
493
|
| `SESSION_TIMEOUT_MS` | Session inactivity timeout | `1800000` (30min) |
|
|
@@ -399,6 +506,9 @@ Reddit macros return JSON directly (no HTML parsing needed):
|
|
|
399
506
|
| `PROXY_COUNTRY` | Target country for proxy geo-targeting | - |
|
|
400
507
|
| `PROXY_STATE` | Target state/region for proxy geo-targeting | - |
|
|
401
508
|
| `TAB_INACTIVITY_MS` | Close tabs idle longer than this | `300000` (5min) |
|
|
509
|
+
| `CAMOFOX_CRASH_REPORT_ENABLED` | Enable anonymized crash/hang reporter (`false` to disable) | `true` |
|
|
510
|
+
| `CAMOFOX_CRASH_REPORT_REPO` | GitHub repo for issue reports | `jo-inc/camofox-browser` |
|
|
511
|
+
| `CAMOFOX_CRASH_REPORT_RATE_LIMIT` | Max reports per hour | `10` |
|
|
402
512
|
| `ENABLE_VNC` | Enable VNC plugin for interactive browser access (`1`) | - |
|
|
403
513
|
| `VNC_PASSWORD` | Password for VNC access (recommended in production) | - |
|
|
404
514
|
| `NOVNC_PORT` | noVNC web UI port | `6080` |
|
package/camofox.config.json
CHANGED
|
@@ -6,5 +6,13 @@
|
|
|
6
6
|
"youtube": { "enabled": true },
|
|
7
7
|
"persistence": { "enabled": true },
|
|
8
8
|
"vnc": { "resolution": "1920x1080" }
|
|
9
|
+
},
|
|
10
|
+
"crashReporter": {
|
|
11
|
+
"appId": "3503870",
|
|
12
|
+
"installationId": "127089401",
|
|
13
|
+
"repo": "jo-inc/camofox-browser",
|
|
14
|
+
"userAgent": "camofox-crash-reporter",
|
|
15
|
+
"keyA": "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb2dJQkFBS0NBUUVBMDRQRDBWWkNRb1pYdkpCNmYwUHdGRk1scEVzbjF2blFYaHJNSkxlSDdBWElmdGhCCmtpbElUb0xNeWFCVU9aWEdUbXNNYnE2MEd5R1p4RGFQVSt4bGRQdmc2Y2QrUElUNmxSbjB2TCsrd2FNT0llMDQKUEdrallSWW1YZzhqZHFCNEhQcmlKbkQwa2srU0EwVUxBYm5jUHVhTitKNEVFYjBDZUZQRUpzNEJqd0dvcENZMwpaM1VBS2ZBVjh5Slpzc3JVM0lvVlBzckc4dHVnRFhya3JTc2Y0K1Jmdm5lejRibGk1YXJKQ1h3cXJPVE0xRERNCk9mUFhOMENQMWNIWW1XL1p0TVNiRlpsbFRobVdMb05NUXNNdlFEZ3lmWjRXbG9rL1doaWVhTWttWFB6YzFuUkkKMVltNCs0YzZVS2kzOURFSS9MODA5RUZ6OHU0K2lnN1dzaEdsZHdJREFRQUJBb0lCQVFDcC8xQWw4cjhrZXBjUApqY3QyZCtNQVl1ZHhDWnFHbEplYzJzclNnOU94cGVCRDJvbXc4SThWMHRqSEFKNVEvZ2k1UkI1azR2TU1qMC9uCnZMWXJqR2Jxdy9vN3lzT3gzbXNMNVNXbmdqRE5yc0NRRWZuTnkrN01mQ0h3SFJpeW9qeUhoamkzRHJmeTFCTVYKbjZzK0F1UjZoWkQ3amZ6VlNPVXdVcHJuV1ZFMVhuaVBaZmZSd29BQmxEQi92Zm9wWWZpKzhKaXpYQTNVaytlMwp1TEdIZHc2bzJlUkp1bW90RFBubWtsVkVSKy8vcUZpa1BqTGJBSHprNGRTM3hkMWF1ZEFUNU5hczdiUkhGeXFoCkt2eGk2blM4cTAzanBIS0JMQ0hsN212Y1JQUWhYek1XcHVQblVsNGpvRFVDQ0RITG5CcWNmZ3FjTnlzOGhuSkIKaFpueE5pbGhBb0dCQVBKNm04QXo5KytnbzZjMGoxNnFsSXk3V3NyU0liSW80bXZSUEtRc0lnQUxOYTB3TXluNgozWms3NHlTdWJVaWY5c0pwM0pCNFEzdlBqVm",
|
|
16
|
+
"keyB": "9pSHBDaVZ3ZEttNFFLeC9mMVJITUtCTzVDOFlHK0VsaTlUSWllCkVjWFJMN2VyUmpValIvNjJoZVAvVG96ZXdPckxYbHFlVEEySEJ2S2RJbTFrL2ozNnlWNFVUdm1SQW9HQkFOOVAKSkRpWCt4Y083RnMwOUxxWlYxRFVhK1Rja1RQR1BMMVdlelBEeHc0M0lLVERyL2F1RWs0Mm1pK0NYY0huQk5CagpDT0Y3bEVhRTVENzdVVlZFL3JxR0E3dGdLVER3M2N5cDhQb2x3T1IwK01LUmh5MFpjeG1wcFhlb1dlZm9LVEJaCmtyQjlRbXZrZTMvdWhHUWNqemt3M2dxcE1QRmdFdFlNUUdBY3RtcUhBb0dCQUtobGNnbFhqaGJERHlTdUlldHkKdDl2TXVjOGxnL1ZBNDQ1Ukw3WXNXQ2lEb0hGNGllL2JvMDRxQXlPVVo1MEtTc3JWempJZTgyN213NW9YRy9jQwpaMEpQRkJYdGp0YXJaVEFuZ3lrZElMQWtHb1c2WVk1M2lJeERMTXAzamppVkdnalJKY2NqcForN2kyc0VkYkNsClF0Z2FNRDhKMWNEM1pJSVN5d29sUEh1aEFuOHYrZERPVjlpYUc1cXIvYlNXWWx0Z0FrTXI2RGRKNkUwa1lIQVgKcnZnVlRNSzJvMVFxcXp0RHlYZFd2YXRtL1RzTGlqdGVOaTZrOStnUm4relpaUGxWR1hXenkvVU5qcklZUm1wLwpVNTBkZUFQNXlVcEJaalpVVFI0L2x1dTU1eWJ5UEV4SG5xR21qRy84REVKbFA3MkZpL29vVURFenFuQmhqRUJJClplTExBb0dBWUQxSkp2S0d5U2NtZWVCU0J3TTRlSUF2b3RCMWR2L0NvaThDNVR2Z1F6QWRySE1BTGhIbDNtbVEKajJ3ZnpPOFlEbW45ZmdreDVvVTMyWjJNQTEyVS9mQklJLzVFYWFLK045MVVJanlFUHd1ZU42emR3c0MwT0NXWQovTVYyaFI3dlk1bmN5SUxubG1yRHNRU3htTzIrZmFhUlAxQUVtUit5aUpOUUw2ZlE1RGc9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg=="
|
|
9
17
|
}
|
|
10
18
|
}
|
package/lib/config.js
CHANGED
|
@@ -5,9 +5,22 @@
|
|
|
5
5
|
* flag plugin.ts or server.js for env-harvesting (env + network in same file).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { join } from 'path';
|
|
8
|
+
import { join, dirname } from 'path';
|
|
9
|
+
import { readFileSync } from 'fs';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
9
11
|
import os from 'os';
|
|
10
12
|
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const ROOT_DIR = join(__dirname, '..');
|
|
15
|
+
|
|
16
|
+
/** Read crashReporter section from camofox.config.json (if present). */
|
|
17
|
+
function readCrashReporterConfig() {
|
|
18
|
+
try {
|
|
19
|
+
const raw = readFileSync(join(ROOT_DIR, 'camofox.config.json'), 'utf-8');
|
|
20
|
+
return JSON.parse(raw).crashReporter || {};
|
|
21
|
+
} catch { return {}; }
|
|
22
|
+
}
|
|
23
|
+
|
|
11
24
|
/**
|
|
12
25
|
* Parse PROXY_PORTS env var into an array of port numbers.
|
|
13
26
|
* Supports range ("10001-10010") or comma-separated ("10001,10002,10003").
|
|
@@ -47,6 +60,9 @@ function loadConfig() {
|
|
|
47
60
|
apiKey: process.env.CAMOFOX_API_KEY || '',
|
|
48
61
|
cookiesDir: process.env.CAMOFOX_COOKIES_DIR || join(os.homedir(), '.camofox', 'cookies'),
|
|
49
62
|
profileDir: process.env.CAMOFOX_PROFILE_DIR || join(os.homedir(), '.camofox', 'profiles'),
|
|
63
|
+
tracesDir: process.env.CAMOFOX_TRACES_DIR || join(os.homedir(), '.camofox', 'traces'),
|
|
64
|
+
tracesMaxBytes: parseInt(process.env.CAMOFOX_TRACES_MAX_BYTES || String(50 * 1024 * 1024), 10),
|
|
65
|
+
tracesTtlHours: parseInt(process.env.CAMOFOX_TRACES_TTL_HOURS || '24', 10),
|
|
50
66
|
handlerTimeoutMs: parseInt(process.env.HANDLER_TIMEOUT_MS) || 30000,
|
|
51
67
|
maxConcurrentPerUser: parseInt(process.env.MAX_CONCURRENT_PER_USER) || 3,
|
|
52
68
|
sessionTimeoutMs: parseInt(process.env.SESSION_TIMEOUT_MS) || 600000,
|
|
@@ -82,6 +98,9 @@ function loadConfig() {
|
|
|
82
98
|
CAMOFOX_ADMIN_KEY: process.env.CAMOFOX_ADMIN_KEY,
|
|
83
99
|
CAMOFOX_API_KEY: process.env.CAMOFOX_API_KEY,
|
|
84
100
|
CAMOFOX_COOKIES_DIR: process.env.CAMOFOX_COOKIES_DIR,
|
|
101
|
+
CAMOFOX_TRACES_DIR: process.env.CAMOFOX_TRACES_DIR,
|
|
102
|
+
CAMOFOX_TRACES_MAX_BYTES: process.env.CAMOFOX_TRACES_MAX_BYTES,
|
|
103
|
+
CAMOFOX_TRACES_TTL_HOURS: process.env.CAMOFOX_TRACES_TTL_HOURS,
|
|
85
104
|
PROXY_STRATEGY: process.env.PROXY_STRATEGY,
|
|
86
105
|
PROXY_PROVIDER: process.env.PROXY_PROVIDER,
|
|
87
106
|
PROXY_HOST: process.env.PROXY_HOST,
|
|
@@ -97,6 +116,12 @@ function loadConfig() {
|
|
|
97
116
|
PROXY_ZIP: process.env.PROXY_ZIP,
|
|
98
117
|
PROXY_SESSION_DURATION_MINUTES: process.env.PROXY_SESSION_DURATION_MINUTES,
|
|
99
118
|
},
|
|
119
|
+
// Crash reporter (opt-in, uses embedded GitHub App credentials)
|
|
120
|
+
// Crash reporter (opt-in, credentials from camofox.config.json)
|
|
121
|
+
crashReportEnabled: process.env.CAMOFOX_CRASH_REPORT_ENABLED !== 'false',
|
|
122
|
+
crashReportRepo: process.env.CAMOFOX_CRASH_REPORT_REPO,
|
|
123
|
+
crashReportRateLimit: parseInt(process.env.CAMOFOX_CRASH_REPORT_RATE_LIMIT, 10) || 10,
|
|
124
|
+
crashReporterConfig: readCrashReporterConfig(),
|
|
100
125
|
};
|
|
101
126
|
}
|
|
102
127
|
|
package/lib/extract.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const SUPPORTED_TYPES = new Set(['string', 'number', 'integer', 'boolean', 'object', 'null']);
|
|
2
|
+
|
|
3
|
+
export function validateSchema(schema) {
|
|
4
|
+
if (!schema || typeof schema !== 'object') {
|
|
5
|
+
return { ok: false, error: 'schema must be an object' };
|
|
6
|
+
}
|
|
7
|
+
if (schema.type !== 'object') {
|
|
8
|
+
return { ok: false, error: 'top-level schema must have type: object' };
|
|
9
|
+
}
|
|
10
|
+
if (!schema.properties || typeof schema.properties !== 'object') {
|
|
11
|
+
return { ok: false, error: 'schema must have a properties object' };
|
|
12
|
+
}
|
|
13
|
+
for (const [prop, def] of Object.entries(schema.properties)) {
|
|
14
|
+
if (!def || typeof def !== 'object') {
|
|
15
|
+
return { ok: false, error: `property "${prop}" must be an object` };
|
|
16
|
+
}
|
|
17
|
+
if (def.type && !SUPPORTED_TYPES.has(def.type)) {
|
|
18
|
+
return { ok: false, error: `property "${prop}" has unsupported type "${def.type}"` };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return { ok: true };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function coerceValue(raw, type) {
|
|
25
|
+
if (raw == null) return null;
|
|
26
|
+
if (type === 'string' || !type) return String(raw).trim();
|
|
27
|
+
if (type === 'number') {
|
|
28
|
+
const n = parseFloat(String(raw).replace(/[^0-9.eE+-]/g, ''));
|
|
29
|
+
return Number.isFinite(n) ? n : null;
|
|
30
|
+
}
|
|
31
|
+
if (type === 'integer') {
|
|
32
|
+
const n = parseInt(String(raw).replace(/[^0-9-]/g, ''), 10);
|
|
33
|
+
return Number.isFinite(n) ? n : null;
|
|
34
|
+
}
|
|
35
|
+
if (type === 'boolean') {
|
|
36
|
+
const s = String(raw).toLowerCase().trim();
|
|
37
|
+
if (s === 'true' || s === 'yes' || s === '1') return true;
|
|
38
|
+
if (s === 'false' || s === 'no' || s === '0') return false;
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return raw;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function extractFromRef(refs, refId) {
|
|
45
|
+
const info = refs.get(refId);
|
|
46
|
+
if (!info) return null;
|
|
47
|
+
return info.name || null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function extractDeterministic({ schema, refs }) {
|
|
51
|
+
const check = validateSchema(schema);
|
|
52
|
+
if (!check.ok) throw new Error(check.error);
|
|
53
|
+
|
|
54
|
+
const result = {};
|
|
55
|
+
for (const [prop, def] of Object.entries(schema.properties)) {
|
|
56
|
+
const refId = def['x-ref'];
|
|
57
|
+
|
|
58
|
+
let value = null;
|
|
59
|
+
if (refId) {
|
|
60
|
+
value = extractFromRef(refs, refId);
|
|
61
|
+
if (value != null && def.type && def.type !== 'object') {
|
|
62
|
+
value = coerceValue(value, def.type);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (value == null && Array.isArray(schema.required) && schema.required.includes(prop)) {
|
|
67
|
+
throw new Error(`required property "${prop}" could not be extracted (x-ref=${refId || 'n/a'})`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
result[prop] = value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return result;
|
|
74
|
+
}
|
package/lib/openapi.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI spec generation via swagger-jsdoc + docs UI (swagger-stripey).
|
|
3
|
+
*
|
|
4
|
+
* swagger-jsdoc scans JSDoc `@openapi` comments on route handlers in server.js
|
|
5
|
+
* (and any file passed in `apis`) to build the spec at startup.
|
|
6
|
+
* Docs UI lives in docs/api.html (swagger-stripey: Stripe-style 3-panel renderer).
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* import { mountDocs } from './lib/openapi.js';
|
|
10
|
+
* // After all routes are registered:
|
|
11
|
+
* mountDocs(app);
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import swaggerJsdoc from 'swagger-jsdoc';
|
|
15
|
+
import express from 'express';
|
|
16
|
+
import { readFileSync } from 'fs';
|
|
17
|
+
import { dirname, join } from 'path';
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
|
|
22
|
+
let version = 'unknown';
|
|
23
|
+
try {
|
|
24
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
25
|
+
version = pkg.version;
|
|
26
|
+
} catch { /* ignore */ }
|
|
27
|
+
|
|
28
|
+
const swaggerDefinition = {
|
|
29
|
+
openapi: '3.0.3',
|
|
30
|
+
info: {
|
|
31
|
+
title: 'camofox-browser',
|
|
32
|
+
version,
|
|
33
|
+
description:
|
|
34
|
+
'Anti-detection browser automation server for AI agents. ' +
|
|
35
|
+
'Accessibility snapshots, element refs, session isolation, cookie import, proxy rotation, and structured logs.',
|
|
36
|
+
license: { name: 'MIT', url: 'https://opensource.org/licenses/MIT' },
|
|
37
|
+
contact: { name: 'Jo Inc', url: 'https://askjo.ai', email: 'oss@askjo.ai' },
|
|
38
|
+
},
|
|
39
|
+
servers: [{ url: 'http://localhost:9377', description: 'Local development' }],
|
|
40
|
+
tags: [
|
|
41
|
+
{ name: 'System', description: 'Server health, metrics, and status.' },
|
|
42
|
+
{ name: 'Tabs', description: 'Create, list, inspect, and destroy browser tabs.' },
|
|
43
|
+
{ name: 'Navigation', description: 'Navigate tabs to URLs or via search macros.' },
|
|
44
|
+
{ name: 'Interaction', description: 'Click, type, scroll, press keys, evaluate JS.' },
|
|
45
|
+
{ name: 'Content', description: 'Accessibility snapshots, screenshots, links, images, downloads.' },
|
|
46
|
+
{ name: 'Sessions', description: 'Per-user session state: cookies, teardown.' },
|
|
47
|
+
{ name: 'Browser', description: 'Global browser lifecycle (start/stop).' },
|
|
48
|
+
{ name: 'Legacy', description: 'OpenClaw-compatible endpoints (deprecated).' },
|
|
49
|
+
],
|
|
50
|
+
components: {
|
|
51
|
+
securitySchemes: {
|
|
52
|
+
BearerAuth: {
|
|
53
|
+
type: 'http',
|
|
54
|
+
scheme: 'bearer',
|
|
55
|
+
description: 'Bearer token matching CAMOFOX_API_KEY.',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
schemas: {
|
|
59
|
+
Error: {
|
|
60
|
+
type: 'object',
|
|
61
|
+
required: ['error'],
|
|
62
|
+
properties: { error: { type: 'string' } },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Mount GET /openapi.json and GET /docs on the Express app.
|
|
70
|
+
* Call AFTER all routes are registered so swagger-jsdoc can scan them.
|
|
71
|
+
*
|
|
72
|
+
* @param {import('express').Application} app
|
|
73
|
+
* @param {Object} [opts]
|
|
74
|
+
* @param {string[]} [opts.apis] - Glob patterns for files with @openapi JSDoc (default: ['./server.js'])
|
|
75
|
+
*/
|
|
76
|
+
export function mountDocs(app, opts = {}) {
|
|
77
|
+
const apis = opts.apis || ['./server.js'];
|
|
78
|
+
|
|
79
|
+
const spec = swaggerJsdoc({
|
|
80
|
+
definition: swaggerDefinition,
|
|
81
|
+
apis,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
app.get('/openapi.json', (_req, res) => {
|
|
85
|
+
res.json(spec);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Serve docs static assets (api.html, fox.png, openapi.json)
|
|
89
|
+
const docsDir = join(__dirname, '..', 'docs');
|
|
90
|
+
app.use('/docs', express.static(docsDir, { index: 'api.html' }));
|
|
91
|
+
|
|
92
|
+
// Also serve fox.png at root for backward compat with old Swagger UI HTML
|
|
93
|
+
app.get('/fox.png', (_req, res) => {
|
|
94
|
+
res.sendFile(join(docsDir, 'fox.png'));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return spec;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export { swaggerDefinition };
|
package/lib/plugins.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* SESSION LIFECYCLE
|
|
18
18
|
* session:creating { userId, contextOptions } — mutate context options
|
|
19
19
|
* session:created { userId, context } — after context stored
|
|
20
|
+
* session:destroying { userId, reason } — before context close (context still alive)
|
|
20
21
|
* session:destroyed { userId, reason } — after cleanup
|
|
21
22
|
* session:expired { userId, idleMs } — reaper triggered
|
|
22
23
|
*
|