@askjo/camofox-browser 1.5.2 → 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/Dockerfile +17 -2
- package/README.md +138 -8
- package/camofox.config.json +18 -0
- package/lib/auth.js +71 -0
- package/lib/config.js +27 -1
- package/lib/cookies.js +38 -1
- package/lib/downloads.js +10 -2
- package/lib/extract.js +74 -0
- package/lib/inflight.js +16 -0
- package/lib/metrics.js +29 -0
- package/lib/openapi.js +100 -0
- package/lib/persistence.js +89 -0
- package/lib/plugins.js +175 -0
- package/lib/reporter.js +751 -0
- package/lib/tmp-cleanup.js +40 -0
- package/lib/tracing.js +137 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +8 -2
- package/plugins/persistence/AGENTS.md +37 -0
- package/plugins/persistence/README.md +48 -0
- package/plugins/persistence/index.js +124 -0
- package/plugins/persistence/persistence.test.js +117 -0
- package/plugins/persistence/plugin.test.js +98 -0
- package/plugins/vnc/AGENTS.md +42 -0
- package/plugins/vnc/README.md +165 -0
- package/plugins/vnc/apt.txt +7 -0
- package/plugins/vnc/index.js +142 -0
- package/plugins/vnc/spawn.js +8 -0
- package/plugins/vnc/vnc-launcher.js +64 -0
- package/plugins/vnc/vnc-watcher.sh +82 -0
- package/plugins/vnc/vnc.test.js +204 -0
- package/plugins/youtube/AGENTS.md +25 -0
- package/plugins/youtube/apt.txt +1 -0
- package/plugins/youtube/index.js +206 -0
- package/plugins/youtube/post-install.sh +5 -0
- package/plugins/youtube/youtube.test.js +41 -0
- package/scripts/exec.js +8 -0
- package/scripts/generate-openapi.js +24 -0
- package/scripts/install-plugin-deps.sh +63 -0
- package/scripts/plugin.js +342 -0
- package/scripts/plugin.test.js +117 -0
- package/server.js +2124 -355
- /package/{lib → plugins/youtube}/youtube.js +0 -0
package/Dockerfile
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
FROM node:20-slim
|
|
1
|
+
FROM node:20-slim AS camofox-browser
|
|
2
2
|
|
|
3
3
|
# Pinned Camoufox version for reproducible builds
|
|
4
4
|
# Update these when upgrading Camoufox
|
|
@@ -36,6 +36,7 @@ RUN apt-get update && apt-get install -y \
|
|
|
36
36
|
fontconfig \
|
|
37
37
|
# Utils
|
|
38
38
|
ca-certificates \
|
|
39
|
+
curl \
|
|
39
40
|
unzip \
|
|
40
41
|
# yt-dlp runtime dependency
|
|
41
42
|
python3-minimal \
|
|
@@ -60,11 +61,25 @@ COPY package.json ./
|
|
|
60
61
|
RUN npm install --production
|
|
61
62
|
|
|
62
63
|
COPY server.js ./
|
|
64
|
+
COPY camofox.config.json ./
|
|
63
65
|
COPY lib/ ./lib/
|
|
66
|
+
COPY plugins/ ./plugins/
|
|
67
|
+
COPY scripts/ ./scripts/
|
|
68
|
+
|
|
69
|
+
# Install default plugin dependencies (apt packages + post-install hooks)
|
|
70
|
+
RUN scripts/install-plugin-deps.sh
|
|
64
71
|
|
|
65
72
|
ENV NODE_ENV=production
|
|
66
|
-
ENV CAMOFOX_PORT=
|
|
73
|
+
ENV CAMOFOX_PORT=9377
|
|
67
74
|
|
|
68
75
|
EXPOSE 9377
|
|
69
76
|
|
|
70
77
|
CMD ["sh", "-c", "node --max-old-space-size=${MAX_OLD_SPACE_SIZE:-128} server.js"]
|
|
78
|
+
|
|
79
|
+
# Optional: rebuild plugin deps after adding third-party plugins
|
|
80
|
+
# Usage: docker build --target with-plugins -t camofox-browser .
|
|
81
|
+
FROM camofox-browser AS with-plugins
|
|
82
|
+
COPY plugins/ ./plugins/
|
|
83
|
+
COPY camofox.config.json ./
|
|
84
|
+
COPY scripts/install-plugin-deps.sh /tmp/install-plugin-deps.sh
|
|
85
|
+
RUN /tmp/install-plugin-deps.sh && rm /tmp/install-plugin-deps.sh
|
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
|
|
@@ -50,6 +54,11 @@ This project wraps that engine in a REST API built for agents: accessibility sna
|
|
|
50
54
|
- **Download Capture** - capture browser downloads and fetch them via API (optional inline base64)
|
|
51
55
|
- **DOM Image Extraction** - list `<img>` src/alt and optionally return inline data URLs
|
|
52
56
|
- **Deploy Anywhere** - Docker, Fly.io, Railway
|
|
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`.
|
|
53
62
|
|
|
54
63
|
## Optional Dependencies
|
|
55
64
|
|
|
@@ -102,11 +111,11 @@ make up ARCH=x86_64
|
|
|
102
111
|
make up VERSION=135.0.1 RELEASE=beta.24
|
|
103
112
|
```
|
|
104
113
|
|
|
105
|
-
|
|
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.
|
|
106
115
|
|
|
107
116
|
### Fly.io / Railway
|
|
108
117
|
|
|
109
|
-
`
|
|
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.
|
|
110
119
|
|
|
111
120
|
## Usage
|
|
112
121
|
|
|
@@ -178,6 +187,62 @@ Camoufox browser session (authenticated browsing)
|
|
|
178
187
|
- Max 500 cookies per request, 5MB file size limit
|
|
179
188
|
- Cookie objects are sanitized to an allowlist of Playwright fields
|
|
180
189
|
|
|
190
|
+
### Session Persistence
|
|
191
|
+
|
|
192
|
+
By default, camofox persists each user's cookies and localStorage to `~/.camofox/profiles/`. Sessions survive browser restarts — log in once (via cookies or VNC), and subsequent sessions restore the authenticated state automatically.
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
~/.camofox/
|
|
196
|
+
├── cookies/ # Bootstrap cookie files (Netscape format)
|
|
197
|
+
└── profiles/ # Persisted session state (auto-managed)
|
|
198
|
+
└── <hashed-userId>/
|
|
199
|
+
└── storage_state.json
|
|
200
|
+
```
|
|
201
|
+
|
|
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`.
|
|
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
|
+
|
|
181
246
|
#### Standalone server usage
|
|
182
247
|
|
|
183
248
|
```bash
|
|
@@ -247,6 +312,60 @@ When a proxy is configured:
|
|
|
247
312
|
- Browser fingerprint (language, timezone, coordinates) is consistent with the proxy location
|
|
248
313
|
- Without a proxy, defaults to `en-US`, `America/Los_Angeles`, San Francisco coordinates
|
|
249
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
|
+
|
|
250
369
|
### Structured Logging
|
|
251
370
|
|
|
252
371
|
All log output is JSON (one object per line) for easy parsing by log aggregators:
|
|
@@ -346,6 +465,7 @@ Uses [yt-dlp](https://github.com/yt-dlp/yt-dlp) when available (fast, no browser
|
|
|
346
465
|
| Method | Endpoint | Description |
|
|
347
466
|
|--------|----------|-------------|
|
|
348
467
|
| `POST` | `/sessions/:userId/cookies` | Add cookies to a user session (Playwright cookie objects) |
|
|
468
|
+
| `GET` | `/sessions/:userId/storage_state` | Export cookies + localStorage ([VNC plugin](plugins/vnc/)) |
|
|
349
469
|
|
|
350
470
|
## Search Macros
|
|
351
471
|
|
|
@@ -364,6 +484,10 @@ Reddit macros return JSON directly (no HTML parsing needed):
|
|
|
364
484
|
| `CAMOFOX_API_KEY` | Enable cookie import endpoint (disabled if unset) | - |
|
|
365
485
|
| `CAMOFOX_ADMIN_KEY` | Required for `POST /stop` | - |
|
|
366
486
|
| `CAMOFOX_COOKIES_DIR` | Directory for cookie files | `~/.camofox/cookies` |
|
|
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` |
|
|
367
491
|
| `MAX_SESSIONS` | Max concurrent browser sessions | `50` |
|
|
368
492
|
| `MAX_TABS_PER_SESSION` | Max tabs per session | `10` |
|
|
369
493
|
| `SESSION_TIMEOUT_MS` | Session inactivity timeout | `1800000` (30min) |
|
|
@@ -382,6 +506,12 @@ Reddit macros return JSON directly (no HTML parsing needed):
|
|
|
382
506
|
| `PROXY_COUNTRY` | Target country for proxy geo-targeting | - |
|
|
383
507
|
| `PROXY_STATE` | Target state/region for proxy geo-targeting | - |
|
|
384
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` |
|
|
512
|
+
| `ENABLE_VNC` | Enable VNC plugin for interactive browser access (`1`) | - |
|
|
513
|
+
| `VNC_PASSWORD` | Password for VNC access (recommended in production) | - |
|
|
514
|
+
| `NOVNC_PORT` | noVNC web UI port | `6080` |
|
|
385
515
|
|
|
386
516
|
## Architecture
|
|
387
517
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "camofox-browser",
|
|
3
|
+
"name": "Camofox Browser",
|
|
4
|
+
"version": "1.6.0",
|
|
5
|
+
"plugins": {
|
|
6
|
+
"youtube": { "enabled": true },
|
|
7
|
+
"persistence": { "enabled": true },
|
|
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=="
|
|
17
|
+
}
|
|
18
|
+
}
|
package/lib/auth.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared auth middleware for camofox-browser.
|
|
3
|
+
*
|
|
4
|
+
* Extracts the duplicated auth pattern from cookie/storage_state endpoints
|
|
5
|
+
* into a reusable Express middleware factory.
|
|
6
|
+
*
|
|
7
|
+
* Policy:
|
|
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).
|
|
10
|
+
* - Otherwise, reject.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import crypto from 'crypto';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Timing-safe string comparison.
|
|
17
|
+
*/
|
|
18
|
+
function timingSafeCompare(a, b) {
|
|
19
|
+
if (typeof a !== 'string' || typeof b !== 'string') return false;
|
|
20
|
+
const bufA = Buffer.from(a);
|
|
21
|
+
const bufB = Buffer.from(b);
|
|
22
|
+
if (bufA.length !== bufB.length) {
|
|
23
|
+
// Compare against self to burn constant time, then return false
|
|
24
|
+
crypto.timingSafeEqual(bufA, bufA);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return crypto.timingSafeEqual(bufA, bufB);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if an address is loopback.
|
|
32
|
+
*/
|
|
33
|
+
function isLoopbackAddress(address) {
|
|
34
|
+
if (!address) return false;
|
|
35
|
+
return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create an Express middleware that enforces API key auth.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} config - Must have { apiKey, nodeEnv }
|
|
42
|
+
* @param {object} [options]
|
|
43
|
+
* @param {string} [options.errorMessage] - Custom error message when rejecting unauthenticated requests
|
|
44
|
+
* @returns {function} Express middleware (req, res, next)
|
|
45
|
+
*/
|
|
46
|
+
export function requireAuth(config, options = {}) {
|
|
47
|
+
const errorMessage = options.errorMessage ||
|
|
48
|
+
'This endpoint requires CAMOFOX_API_KEY except for loopback requests in non-production environments.';
|
|
49
|
+
|
|
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
|
+
}
|
|
57
|
+
return next();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const remoteAddress = req.socket?.remoteAddress || '';
|
|
61
|
+
const allowUnauthedLocal = config.nodeEnv !== 'production' && isLoopbackAddress(remoteAddress);
|
|
62
|
+
if (!allowUnauthedLocal) {
|
|
63
|
+
return res.status(403).json({ error: errorMessage });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
next();
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Re-export utilities so server.js can still use them directly
|
|
71
|
+
export { timingSafeCompare, isLoopbackAddress };
|
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").
|
|
@@ -46,6 +59,10 @@ function loadConfig() {
|
|
|
46
59
|
adminKey: process.env.CAMOFOX_ADMIN_KEY || '',
|
|
47
60
|
apiKey: process.env.CAMOFOX_API_KEY || '',
|
|
48
61
|
cookiesDir: process.env.CAMOFOX_COOKIES_DIR || join(os.homedir(), '.camofox', 'cookies'),
|
|
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),
|
|
49
66
|
handlerTimeoutMs: parseInt(process.env.HANDLER_TIMEOUT_MS) || 30000,
|
|
50
67
|
maxConcurrentPerUser: parseInt(process.env.MAX_CONCURRENT_PER_USER) || 3,
|
|
51
68
|
sessionTimeoutMs: parseInt(process.env.SESSION_TIMEOUT_MS) || 600000,
|
|
@@ -81,6 +98,9 @@ function loadConfig() {
|
|
|
81
98
|
CAMOFOX_ADMIN_KEY: process.env.CAMOFOX_ADMIN_KEY,
|
|
82
99
|
CAMOFOX_API_KEY: process.env.CAMOFOX_API_KEY,
|
|
83
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,
|
|
84
104
|
PROXY_STRATEGY: process.env.PROXY_STRATEGY,
|
|
85
105
|
PROXY_PROVIDER: process.env.PROXY_PROVIDER,
|
|
86
106
|
PROXY_HOST: process.env.PROXY_HOST,
|
|
@@ -96,6 +116,12 @@ function loadConfig() {
|
|
|
96
116
|
PROXY_ZIP: process.env.PROXY_ZIP,
|
|
97
117
|
PROXY_SESSION_DURATION_MINUTES: process.env.PROXY_SESSION_DURATION_MINUTES,
|
|
98
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(),
|
|
99
125
|
};
|
|
100
126
|
}
|
|
101
127
|
|
package/lib/cookies.js
CHANGED
|
@@ -79,4 +79,41 @@ async function readCookieFile({ cookiesDir, cookiesPath, domainSuffix, maxBytes
|
|
|
79
79
|
}));
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Import all cookies from the default bootstrap cookie file into a Playwright context.
|
|
84
|
+
* Intended for first-run session seeding before any persistent storage state exists.
|
|
85
|
+
* Missing file is treated as a no-op.
|
|
86
|
+
* @param {object} opts
|
|
87
|
+
* @param {string} opts.cookiesDir - Base directory for cookie files
|
|
88
|
+
* @param {object} opts.context - Playwright BrowserContext
|
|
89
|
+
* @param {string} [opts.cookiesPath='cookies.txt'] - Relative cookie file path within cookiesDir
|
|
90
|
+
* @param {object} [opts.logger=console] - Logger with warn()
|
|
91
|
+
* @returns {Promise<{imported: number, source: string|null}>}
|
|
92
|
+
*/
|
|
93
|
+
async function importBootstrapCookies({ cookiesDir, context, cookiesPath = 'cookies.txt', logger = console }) {
|
|
94
|
+
if (!cookiesDir || !context) {
|
|
95
|
+
return { imported: 0, source: null };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const resolved = path.resolve(cookiesDir, cookiesPath);
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const cookies = await readCookieFile({ cookiesDir, cookiesPath });
|
|
102
|
+
if (cookies.length === 0) {
|
|
103
|
+
return { imported: 0, source: resolved };
|
|
104
|
+
}
|
|
105
|
+
await context.addCookies(cookies);
|
|
106
|
+
return { imported: cookies.length, source: resolved };
|
|
107
|
+
} catch (err) {
|
|
108
|
+
if (err?.code === 'ENOENT') {
|
|
109
|
+
return { imported: 0, source: null };
|
|
110
|
+
}
|
|
111
|
+
logger?.warn?.('failed to import bootstrap cookies', {
|
|
112
|
+
cookiesPath: resolved,
|
|
113
|
+
error: err?.message || String(err),
|
|
114
|
+
});
|
|
115
|
+
return { imported: 0, source: resolved };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export { parseNetscapeCookieFile, readCookieFile, importBootstrapCookies };
|
package/lib/downloads.js
CHANGED
|
@@ -60,7 +60,7 @@ async function clearSessionDownloads(session) {
|
|
|
60
60
|
await Promise.all(tasks);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
function attachDownloadListener(tabState, tabId, log) {
|
|
63
|
+
function attachDownloadListener(tabState, tabId, log, pluginEvents, userId) {
|
|
64
64
|
if (tabState.downloadListenerAttached) return;
|
|
65
65
|
tabState.downloadListenerAttached = true;
|
|
66
66
|
|
|
@@ -69,6 +69,11 @@ function attachDownloadListener(tabState, tabId, log) {
|
|
|
69
69
|
const suggestedFilename = sanitizeFilename(download.suggestedFilename?.() || `download-${downloadId}.bin`);
|
|
70
70
|
const filePath = path.join(os.tmpdir(), `camofox-download-${downloadId}-${suggestedFilename}`);
|
|
71
71
|
|
|
72
|
+
const url = String(download.url?.() || '').trim();
|
|
73
|
+
if (pluginEvents) {
|
|
74
|
+
pluginEvents.emit('tab:download:start', { userId: userId || null, tabId, filename: suggestedFilename, url });
|
|
75
|
+
}
|
|
76
|
+
|
|
72
77
|
let failure = null;
|
|
73
78
|
let bytes = null;
|
|
74
79
|
|
|
@@ -86,7 +91,6 @@ function attachDownloadListener(tabState, tabId, log) {
|
|
|
86
91
|
failure = reportedFailure;
|
|
87
92
|
}
|
|
88
93
|
|
|
89
|
-
const url = String(download.url?.() || '').trim();
|
|
90
94
|
if (url) {
|
|
91
95
|
tabState.visitedUrls.add(url);
|
|
92
96
|
}
|
|
@@ -104,6 +108,10 @@ function attachDownloadListener(tabState, tabId, log) {
|
|
|
104
108
|
failure,
|
|
105
109
|
});
|
|
106
110
|
|
|
111
|
+
if (pluginEvents && !failure) {
|
|
112
|
+
pluginEvents.emit('tab:download:complete', { userId: userId || null, tabId, filename: suggestedFilename, path: filePath, size: bytes });
|
|
113
|
+
}
|
|
114
|
+
|
|
107
115
|
await trimTabDownloads(tabState);
|
|
108
116
|
log('info', 'download captured', {
|
|
109
117
|
tabId, downloadId, suggestedFilename, mimeType, bytes,
|
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/inflight.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
async function coalesceInflight(map, key, factory) {
|
|
2
|
+
const existing = map.get(key);
|
|
3
|
+
if (existing) return existing;
|
|
4
|
+
|
|
5
|
+
const promise = (async () => {
|
|
6
|
+
try {
|
|
7
|
+
return await factory();
|
|
8
|
+
} finally {
|
|
9
|
+
map.delete(key);
|
|
10
|
+
}
|
|
11
|
+
})();
|
|
12
|
+
map.set(key, promise);
|
|
13
|
+
return promise;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { coalesceInflight };
|
package/lib/metrics.js
CHANGED
|
@@ -12,6 +12,27 @@ const noopCounter = { inc() {}, labels() { return this; } };
|
|
|
12
12
|
const noopHistogram = { observe() {}, startTimer() { return () => {}; }, labels() { return this; } };
|
|
13
13
|
const noopGauge = { set() {}, inc() {}, dec() {}, labels() { return this; } };
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Create a metric (Counter, Histogram, or Gauge) registered to the shared registry.
|
|
17
|
+
* Returns a no-op stub when Prometheus is disabled — plugins never need to check.
|
|
18
|
+
*
|
|
19
|
+
* @param {'counter'|'histogram'|'gauge'} type
|
|
20
|
+
* @param {object} opts - prom-client options: { name, help, labelNames, buckets, ... }
|
|
21
|
+
* @returns {object} The metric instance or a no-op stub
|
|
22
|
+
*/
|
|
23
|
+
export async function createMetric(type, opts) {
|
|
24
|
+
if (!_register) {
|
|
25
|
+
if (type === 'histogram') return noopHistogram;
|
|
26
|
+
if (type === 'gauge') return noopGauge;
|
|
27
|
+
return noopCounter;
|
|
28
|
+
}
|
|
29
|
+
const client = (await import('prom-client')).default;
|
|
30
|
+
const MetricClass = type === 'histogram' ? client.Histogram
|
|
31
|
+
: type === 'gauge' ? client.Gauge
|
|
32
|
+
: client.Counter;
|
|
33
|
+
return new MetricClass({ ...opts, registers: [_register] });
|
|
34
|
+
}
|
|
35
|
+
|
|
15
36
|
function buildNoopMetrics() {
|
|
16
37
|
return {
|
|
17
38
|
requestsTotal: noopCounter,
|
|
@@ -24,6 +45,7 @@ function buildNoopMetrics() {
|
|
|
24
45
|
tabsRecycledTotal: noopCounter,
|
|
25
46
|
requestDuration: noopHistogram,
|
|
26
47
|
pageLoadDuration: noopHistogram,
|
|
48
|
+
snapshotBytes: noopHistogram,
|
|
27
49
|
activeTabsGauge: noopGauge,
|
|
28
50
|
tabLockQueueDepth: noopGauge,
|
|
29
51
|
memoryUsageBytes: noopGauge,
|
|
@@ -93,6 +115,13 @@ async function buildRealMetrics() {
|
|
|
93
115
|
buckets: [0.5, 1, 2, 5, 10, 20, 30, 60],
|
|
94
116
|
registers: [_register],
|
|
95
117
|
}),
|
|
118
|
+
snapshotBytes: new client.Histogram({
|
|
119
|
+
name: 'camofox_snapshot_bytes',
|
|
120
|
+
help: 'Size of accessibility tree snapshots in bytes (before windowing)',
|
|
121
|
+
labelNames: ['type'],
|
|
122
|
+
buckets: [1000, 5000, 10000, 25000, 50000, 80000, 120000, 200000, 500000],
|
|
123
|
+
registers: [_register],
|
|
124
|
+
}),
|
|
96
125
|
activeTabsGauge: new client.Gauge({
|
|
97
126
|
name: 'camofox_active_tabs',
|
|
98
127
|
help: 'Current number of open browser tabs',
|