@askjo/camofox-browser 1.6.0 → 1.7.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.
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="https://github.com/jo-inc/camofox-browser/actions"><img src="https://img.shields.io/badge/build-passing-brightgreen" alt="Build" /></a>
7
- <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-blue" alt="License" /></a>
8
- <a href="https://camoufox.com"><img src="https://img.shields.io/badge/engine-Camoufox-red" alt="Camoufox" /></a>
9
- <a href="https://hub.docker.com"><img src="https://img.shields.io/badge/docker-ready-blue" alt="Docker" /></a>
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
- Note: `make fetch` (or `make build`) must be run first the Dockerfile expects pre-downloaded binaries in `dist/`.
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
- `fly.toml` and `railway.toml` are included. Deploy with `fly deploy` or connect the repo to Railway.
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` |
@@ -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
  *