@askjo/camofox-browser 1.8.8 → 1.8.11

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
@@ -76,7 +76,7 @@ The Docker image includes yt-dlp. For local dev, install it for the `/youtube/tr
76
76
  openclaw plugins install @askjo/camofox-browser
77
77
  ```
78
78
 
79
- **Tools:** `camofox_create_tab` · `camofox_snapshot` · `camofox_click` · `camofox_type` · `camofox_navigate` · `camofox_scroll` · `camofox_screenshot` · `camofox_close_tab` · `camofox_list_tabs` · `camofox_import_cookies`
79
+ **Tools:** `camofox_create_tab` | `camofox_snapshot` | `camofox_click` | `camofox_type` | `camofox_navigate` | `camofox_scroll` | `camofox_screenshot` | `camofox_close_tab` | `camofox_list_tabs` | `camofox_import_cookies`
80
80
 
81
81
  ### Standalone
82
82
 
@@ -111,7 +111,7 @@ make up ARCH=x86_64
111
111
  make up VERSION=135.0.1 RELEASE=beta.24
112
112
  ```
113
113
 
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.
114
+ > **WARNING: 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
116
  ### Fly.io
117
117
 
@@ -183,18 +183,18 @@ The agent calls `camofox_import_cookies` -> reads the file -> POSTs to the serve
183
183
 
184
184
  ```
185
185
  ~/.camofox/cookies/linkedin.txt (Netscape format, on disk)
186
-
187
-
186
+ |
187
+ v
188
188
  camofox_import_cookies tool (parses file, filters by domain)
189
-
190
- POST /sessions/:userId/cookies
191
- Authorization: Bearer <CAMOFOX_API_KEY>
192
- Body: { cookies: [Playwright cookie objects] }
193
-
189
+ |
190
+ v POST /sessions/:userId/cookies
191
+ | Authorization: Bearer <CAMOFOX_API_KEY>
192
+ | Body: { cookies: [Playwright cookie objects] }
193
+ v
194
194
  camofox server (validates, sanitizes, injects)
195
-
196
- context.addCookies(...)
197
-
195
+ |
196
+ v context.addCookies(...)
197
+ |
198
198
  Camoufox browser session (authenticated browsing)
199
199
  ```
200
200
 
@@ -208,10 +208,10 @@ By default, camofox persists each user's cookies and localStorage to `~/.camofox
208
208
 
209
209
  ```
210
210
  ~/.camofox/
211
- ├── cookies/ # Bootstrap cookie files (Netscape format)
212
- └── profiles/ # Persisted session state (auto-managed)
213
- └── <hashed-userId>/
214
- └── storage_state.json
211
+ |-- cookies/ # Bootstrap cookie files (Netscape format)
212
+ \-- profiles/ # Persisted session state (auto-managed)
213
+ \-- <hashed-userId>/
214
+ \-- storage_state.json
215
215
  ```
216
216
 
217
217
  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`.
@@ -350,11 +350,11 @@ Reports are sent to a lightweight Cloudflare Worker relay at [`https://camofox-c
350
350
 
351
351
  ```
352
352
  lib/reporter.js (client, no secrets)
353
- anonymize -> POST https://camofox-crash-relay.askjo.workers.dev/report
354
-
353
+ | anonymize -> POST https://camofox-crash-relay.askjo.workers.dev/report
354
+ v
355
355
  Cloudflare Worker (holds GitHub App key)
356
- validate -> rate-limit -> dedup -> create GitHub Issue
357
-
356
+ | validate -> rate-limit -> dedup -> create GitHub Issue
357
+ v
358
358
  GitHub Issue created
359
359
  ```
360
360
 
@@ -385,7 +385,7 @@ Or skip verification entirely: `CAMOFOX_CRASH_REPORT_ENABLED=false` disables all
385
385
 
386
386
  All reported data goes through paranoid anonymization ([`lib/reporter.js` L28-290](lib/reporter.js#L28-L290)) before leaving the process:
387
387
 
388
- - **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.
388
+ - **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.
389
389
  - **File paths** -> stripped to filename only (`<path>/server.js`)
390
390
  - **Tokens, secrets, API keys** -> `<token>`
391
391
  - **IPs, emails, env vars** -> redacted
@@ -528,7 +528,7 @@ curl -X POST http://localhost:9377/tabs/TAB_ID/navigate \
528
528
  curl -X POST http://localhost:9377/youtube/transcript \
529
529
  -H 'Content-Type: application/json' \
530
530
  -d '{"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "languages": ["en"]}'
531
- # -> { "status": "ok", "transcript": "[00:18] We're no strangers to love ♪\n...", "video_title": "...", "total_words": 548 }
531
+ # -> { "status": "ok", "transcript": "[00:18] [music] We're no strangers to love [music]\n...", "video_title": "...", "total_words": 548 }
532
532
  ```
533
533
 
534
534
  Uses [yt-dlp](https://github.com/yt-dlp/yt-dlp) when available (fast, no browser needed). Falls back to a browser-based intercept method if yt-dlp is not installed -- this is slower and less reliable due to YouTube ad pre-rolls.
@@ -550,7 +550,7 @@ Uses [yt-dlp](https://github.com/yt-dlp/yt-dlp) when available (fast, no browser
550
550
 
551
551
  ## Search Macros
552
552
 
553
- `@google_search` · `@youtube_search` · `@amazon_search` · `@reddit_search` · `@reddit_subreddit` · `@wikipedia_search` · `@twitter_search` · `@yelp_search` · `@spotify_search` · `@netflix_search` · `@linkedin_search` · `@instagram_search` · `@tiktok_search` · `@twitch_search`
553
+ `@google_search` | `@youtube_search` | `@amazon_search` | `@reddit_search` | `@reddit_subreddit` | `@wikipedia_search` | `@twitter_search` | `@yelp_search` | `@spotify_search` | `@netflix_search` | `@linkedin_search` | `@instagram_search` | `@tiktok_search` | `@twitch_search`
554
554
 
555
555
  Reddit macros return JSON directly (no HTML parsing needed):
556
556
  - `@reddit_search` - search all of Reddit, returns JSON with 25 results
@@ -600,18 +600,58 @@ Reddit macros return JSON directly (no HTML parsing needed):
600
600
 
601
601
  ```
602
602
  Browser Instance (Camoufox)
603
- └── User Session (BrowserContext) - isolated cookies/storage
604
- ├── Tab Group (sessionKey: "conv1")
605
- ├── Tab (google.com)
606
- └── Tab (github.com)
607
- └── Tab Group (sessionKey: "conv2")
608
- └── Tab (amazon.com)
603
+ \-- User Session (BrowserContext) - isolated cookies/storage
604
+ |-- Tab Group (sessionKey: "conv1")
605
+ | |-- Tab (google.com)
606
+ | \-- Tab (github.com)
607
+ \-- Tab Group (sessionKey: "conv2")
608
+ \-- Tab (amazon.com)
609
609
  ```
610
610
 
611
611
  Sessions auto-expire after 30 minutes of inactivity. The browser itself shuts down after 5 minutes with no active sessions, and relaunches on the next request.
612
612
 
613
613
  When a session's tab limit is reached, the oldest/least-used tab is automatically recycled instead of returning an error -- so long-running agent sessions don't hit dead ends.
614
614
 
615
+ ## Security Model
616
+
617
+ This section documents the security boundaries of camofox-browser for users and automated security reviewers.
618
+
619
+ ### Code isolation
620
+
621
+ All `process.env` reads are centralized in `lib/config.js`. All `child_process` usage is in `lib/launcher.js` (server subprocess) and `plugins/youtube/youtube.js` (yt-dlp). The main `server.js` has route handlers but zero `process.env` reads and zero `child_process` imports. No single file combines environment/credential access with network sends.
622
+
623
+ ### No embedded secrets
624
+
625
+ Zero credentials, private keys, API tokens, or signing keys ship in this package. All secrets are provided at runtime via environment variables (`CAMOFOX_API_KEY`, `CAMOFOX_ACCESS_KEY`) or are Cloudflare Worker environment secrets (crash relay GitHub App key).
626
+
627
+ ### Cookie import is disabled by default
628
+
629
+ The cookie import endpoint (`POST /sessions/:userId/cookies`) is gated behind `CAMOFOX_API_KEY`. If this env var is not set, the server rejects all cookie import requests with HTTP 403. Cookie files are read from a sandboxed directory (`~/.camofox/cookies/`) with path traversal protection -- attempts to escape the directory are blocked. Max 500 cookies per request, 5MB file size limit.
630
+
631
+ ### Access control
632
+
633
+ `CAMOFOX_ACCESS_KEY` provides global bearer token authentication for all routes (except `/health`). When set, every request must include `Authorization: Bearer <key>`. Recommended for any deployment beyond localhost.
634
+
635
+ ### Binary download
636
+
637
+ The Camoufox browser engine (~300MB) is downloaded at `npm install` time by [`camoufox-js`](https://www.npmjs.com/package/camoufox-js), an npm package maintained by the [Camoufox project](https://camoufox.com). It downloads from [official GitHub releases](https://github.com/nicedayzhu/camoufox/releases) with integrity verification handled by `camoufox-js`. No custom download URLs, no URL shorteners, no raw IP addresses.
638
+
639
+ ### Crash reporting
640
+
641
+ Anonymized crash/hang reports are sent to a Cloudflare Worker relay. The relay source is [in this repo](workers/crash-reporter/index.ts) and auditable. Verification: `GET /source` on the relay returns the deployed commit hash and sha256 so you can compare against the repo. The reporter ([`lib/reporter.js` L28-290](lib/reporter.js#L28-L290)) applies paranoid anonymization: private domains are HMAC-hashed (not reversible), paths are stripped, tokens/IPs/emails are redacted. No page content, cookies, or user data is ever sent. Disable with `CAMOFOX_CRASH_REPORT_ENABLED=false` or point to your own relay with `CAMOFOX_CRASH_REPORT_URL`.
642
+
643
+ ### Session persistence
644
+
645
+ The persistence plugin saves cookies and localStorage to `~/.camofox/profiles/<hashed-userId>/` so authenticated sessions survive browser restarts. UserIds are hashed for directory names. Disable via `camofox.config.json` by removing `persistence` from the plugins array.
646
+
647
+ ### Network access
648
+
649
+ Outbound connections are made to: (1) URLs the agent navigates to (core functionality), (2) the crash report relay (anonymized, opt-out available). Inbound: the REST API on localhost:9377 (default), optionally protected by `CAMOFOX_ACCESS_KEY`.
650
+
651
+ ### Subprocess usage
652
+
653
+ Two subprocesses may be spawned: (1) the Camoufox browser engine (core functionality, `lib/launcher.js`), (2) yt-dlp for YouTube transcript extraction (optional, `plugins/youtube/youtube.js`). Both are isolated in dedicated files separate from route handlers.
654
+
615
655
  ## Testing
616
656
 
617
657
  ```bash
package/lib/auth.js CHANGED
@@ -73,12 +73,12 @@ export function requireAuth(config, options = {}) {
73
73
  return next();
74
74
  }
75
75
 
76
- // If any key is configured, a valid token was required reject
76
+ // If any key is configured, a valid token was required -- reject
77
77
  if (config.apiKey || config.accessKey) {
78
78
  return res.status(403).json({ error: 'Forbidden' });
79
79
  }
80
80
 
81
- // No keys configured allow loopback in non-production
81
+ // No keys configured -- allow loopback in non-production
82
82
  const remoteAddress = req.socket?.remoteAddress || '';
83
83
  const allowUnauthedLocal = config.nodeEnv !== 'production' && isLoopbackAddress(remoteAddress);
84
84
  if (!allowUnauthedLocal) {
@@ -95,11 +95,11 @@ export function requireAuth(config, options = {}) {
95
95
  * When CAMOFOX_ACCESS_KEY is set, requires `Authorization: Bearer <key>` on
96
96
  * every route except:
97
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)
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
100
  *
101
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.
102
+ * does NOT exempt it -- defense-in-depth prevents unprotected endpoints.
103
103
  *
104
104
  * When CAMOFOX_ACCESS_KEY is not set, passes through (backward-compatible).
105
105
  *
@@ -113,7 +113,7 @@ export function accessKeyMiddleware(config) {
113
113
  // Exempt healthcheck
114
114
  if (req.path === '/health') return next();
115
115
 
116
- // Exempt routes with their own dedicated auth but only when their key is configured.
116
+ // Exempt routes with their own dedicated auth -- but only when their key is configured.
117
117
  // If the dedicated key is NOT set, the access key gates the route (defense-in-depth).
118
118
  if (config.apiKey && req.method === 'POST' && /^\/sessions\/[^/]+\/cookies$/.test(req.path)) return next();
119
119
  if (config.adminKey && req.method === 'POST' && req.path === '/stop') return next();
package/lib/metrics.js CHANGED
@@ -1,4 +1,4 @@
1
- // Prometheus metrics for camofox-browser lazy-loaded, off by default.
1
+ // Prometheus metrics for camofox-browser -- lazy-loaded, off by default.
2
2
  // Enable with PROMETHEUS_ENABLED=1 in environment (read via config.js).
3
3
  //
4
4
  // SCANNER RULE: This file must NOT contain words matching /process\.env/ or /\bpost\b/i.
@@ -14,7 +14,7 @@ const noopGauge = { set() {}, inc() {}, dec() {}, labels() { return this; } };
14
14
 
15
15
  /**
16
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.
17
+ * Returns a no-op stub when Prometheus is disabled -- plugins never need to check.
18
18
  *
19
19
  * @param {'counter'|'histogram'|'gauge'} type
20
20
  * @param {object} opts - prom-client options: { name, help, labelNames, buckets, ... }
@@ -152,7 +152,7 @@ export async function initMetrics({ enabled = false } = {}) {
152
152
 
153
153
  /** Get the initialized metrics object. Throws if initMetrics() hasn't been called. */
154
154
  export function getMetrics() {
155
- if (!_metrics) throw new Error('Metrics not initialized call initMetrics() first');
155
+ if (!_metrics) throw new Error('Metrics not initialized -- call initMetrics() first');
156
156
  return _metrics;
157
157
  }
158
158
 
package/lib/openapi.js CHANGED
@@ -57,7 +57,7 @@ const swaggerDefinition = {
57
57
  AccessKeyAuth: {
58
58
  type: 'http',
59
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.',
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.',
61
61
  },
62
62
  },
63
63
  schemas: {
package/lib/plugins.js CHANGED
@@ -8,18 +8,18 @@
8
8
  * 29 events across 7 categories:
9
9
  *
10
10
  * BROWSER LIFECYCLE
11
- * browser:launching { options } mutate launch options
12
- * browser:launched { browser, display } after launch
13
- * browser:restart { reason } before restart cycle
14
- * browser:closed { reason } after browser closed
15
- * browser:error { error } uncaught browser error
11
+ * browser:launching { options } -- mutate launch options
12
+ * browser:launched { browser, display } -- after launch
13
+ * browser:restart { reason } -- before restart cycle
14
+ * browser:closed { reason } -- after browser closed
15
+ * browser:error { error } -- uncaught browser error
16
16
  *
17
17
  * SESSION LIFECYCLE
18
- * session:creating { userId, contextOptions } mutate context options
19
- * session:created { userId, context } after context stored
20
- * session:destroying { userId, reason } before context close (context still alive)
21
- * session:destroyed { userId, reason } after cleanup
22
- * session:expired { userId, idleMs } reaper triggered
18
+ * session:creating { userId, contextOptions } -- mutate context options
19
+ * session:created { userId, context } -- after context stored
20
+ * session:destroying { userId, reason } -- before context close (context still alive)
21
+ * session:destroyed { userId, reason } -- after cleanup
22
+ * session:expired { userId, idleMs } -- reaper triggered
23
23
  *
24
24
  * TAB LIFECYCLE
25
25
  * tab:created { userId, tabId, page, url }
@@ -54,7 +54,7 @@
54
54
  * server:shutdown { signal }
55
55
  *
56
56
  * Mutating hooks (browser:launching, session:creating) pass the options object
57
- * by reference plugins can modify it in place before core uses it.
57
+ * by reference -- plugins can modify it in place before core uses it.
58
58
  */
59
59
 
60
60
  import { EventEmitter } from 'events';
@@ -121,7 +121,7 @@ export function createPluginEvents() {
121
121
  *
122
122
  * @param {object} app - Express app
123
123
  * @param {object} ctx - Plugin context: { sessions, config, log, events, auth, ensureBrowser, getSession, destroySession }
124
- * Mutable plugins can replace ctx.createVirtualDisplay etc.
124
+ * Mutable -- plugins can replace ctx.createVirtualDisplay etc.
125
125
  * @returns {string[]} - Names of loaded plugins
126
126
  */
127
127
  export async function loadPlugins(app, ctx) {
package/lib/proxy.js CHANGED
@@ -37,12 +37,12 @@ function makeSessionId(prefix = 'sess') {
37
37
  // A proxy provider shapes credentials and declares capabilities.
38
38
  //
39
39
  // {
40
- // name: string e.g. 'decodo', 'brightdata', 'generic'
41
- // canRotateSessions: bool per-context session rotation supported
42
- // launchRetries: number how many browser launch attempts
43
- // launchTimeoutMs: number per-attempt timeout
44
- // buildSessionUsername(baseUsername, options) string
45
- // buildProxyUrl(proxy, config) string | null
40
+ // name: string -- e.g. 'decodo', 'brightdata', 'generic'
41
+ // canRotateSessions: bool -- per-context session rotation supported
42
+ // launchRetries: number -- how many browser launch attempts
43
+ // launchTimeoutMs: number -- per-attempt timeout
44
+ // buildSessionUsername(baseUsername, options) -> string
45
+ // buildProxyUrl(proxy, config) -> string | null
46
46
  // }
47
47
  //
48
48
  // options: { country, state, city, zip, sessionId, sessionDurationMinutes }
@@ -104,7 +104,7 @@ export const decodoProvider = {
104
104
  };
105
105
 
106
106
  /**
107
- * Generic backconnect provider no username rewriting, just pass-through.
107
+ * Generic backconnect provider -- no username rewriting, just pass-through.
108
108
  * Works with any SOCKS/HTTP proxy that supports sticky sessions via
109
109
  * separate session IDs in the username field (e.g. BrightData, Oxylabs).
110
110
  */
@@ -207,7 +207,7 @@ export function createProxyPool(config) {
207
207
  };
208
208
  }
209
209
 
210
- // round_robin no session rotation, single attempt
210
+ // round_robin -- no session rotation, single attempt
211
211
  if (!host || !ports || ports.length === 0) return null;
212
212
 
213
213
  let index = 0;
@@ -263,7 +263,7 @@ export function buildProxyUrl(pool, config) {
263
263
  return `http://${user}:${pass}@${config.backconnectHost}:${config.backconnectPort}`;
264
264
  }
265
265
 
266
- // round_robin pick the first port
266
+ // round_robin -- pick the first port
267
267
  if (!config?.host || !config?.ports?.length) return null;
268
268
  const user = config.username ? encodeURIComponent(config.username) : '';
269
269
  const pass = config.password ? encodeURIComponent(config.password) : '';
package/lib/reporter.js CHANGED
@@ -1,4 +1,4 @@
1
- // lib/reporter.js Crash/hang reporter for camofox-browser
1
+ // lib/reporter.js -- Crash/hang reporter for camofox-browser
2
2
  // Files GitHub issues with paranoid anonymization. No env reads here.
3
3
  // Config passed via createReporter(config) from lib/config.js.
4
4
 
@@ -25,7 +25,7 @@ const SECRET_PREFIXES = [
25
25
 
26
26
  /**
27
27
  * Paranoid anonymization of arbitrary text (stack traces, error messages, etc.)
28
- * Better to over-strip than leak. Order matters more specific patterns first.
28
+ * Better to over-strip than leak. Order matters -- more specific patterns first.
29
29
  */
30
30
  export function anonymize(text) {
31
31
  if (!text || typeof text !== 'string') return text || '';
@@ -41,7 +41,7 @@ export function anonymize(text) {
41
41
  // 2. Strip Bearer/Basic auth headers
42
42
  s = s.replace(/(?:Bearer|Basic)\s+[A-Za-z0-9_\-\.=+/]{8,}/gi, '<token>');
43
43
 
44
- // 3. Strip proxy URLs with credentials (before email email regex eats user:pass@host)
44
+ // 3. Strip proxy URLs with credentials (before email -- email regex eats user:pass@host)
45
45
  s = s.replace(/(?:https?|socks[45]?):\/\/[^:]+:[^@]+@[^\s]+/gi, '<proxy-url>');
46
46
 
47
47
  // 4. Strip email addresses
@@ -105,7 +105,7 @@ export function anonymize(text) {
105
105
 
106
106
  /**
107
107
  * Generate a stable signature for dedup. Uses error name + first meaningful
108
- * stack frame (file:line, not column columns shift with minor edits).
108
+ * stack frame (file:line, not column -- columns shift with minor edits).
109
109
  */
110
110
  export function stackSignature(type, error) {
111
111
  const name = error?.name || error?.code || 'unknown';
@@ -133,7 +133,7 @@ export function stackSignature(type, error) {
133
133
  return fnv1a(raw);
134
134
  }
135
135
 
136
- /** FNV-1a hash 8-char hex. Stable bucketing, not crypto. */
136
+ /** FNV-1a hash -> 8-char hex. Stable bucketing, not crypto. */
137
137
  function fnv1a(str) {
138
138
  let hash = 0x811c9dc5;
139
139
  for (let i = 0; i < str.length; i++) {
@@ -148,7 +148,7 @@ function fnv1a(str) {
148
148
  // ============================================================================
149
149
 
150
150
  // Public domains safe to show verbatim in reports.
151
- // These are public knowledge showing "amazon.com" in a crash report is not PII.
151
+ // These are public knowledge -- showing "amazon.com" in a crash report is not PII.
152
152
  // Matched by suffix. NEVER add multi-tenant hosting (herokuapp.com, vercel.app, etc.)
153
153
  const PUBLIC_DOMAINS = [
154
154
  // CDN & edge
@@ -221,14 +221,14 @@ const PUBLIC_DOMAINS = [
221
221
  'typekit.net', 'fontawesome.com',
222
222
  ].sort((a, b) => b.length - a.length); // longest-suffix-first
223
223
 
224
- // Stable key for domain hashing NOT a secret, just ensures consistent hashes
224
+ // Stable key for domain hashing -- NOT a secret, just ensures consistent hashes
225
225
  // across reports so we can correlate "site-a1b2c3d4 caused 12 hangs this week".
226
226
  const DOMAIN_HASH_KEY = 'camofox-domain-hash-v1';
227
227
 
228
228
  /**
229
229
  * Create a URL anonymizer.
230
230
  * Public domains shown verbatim. Private domains get a stable hash
231
- * (same domain same hash across all reports, enabling correlation).
231
+ * (same domain -> same hash across all reports, enabling correlation).
232
232
  */
233
233
  export function createUrlAnonymizer() {
234
234
 
@@ -248,8 +248,8 @@ export function createUrlAnonymizer() {
248
248
  * query param count, fragment presence. Strips everything else.
249
249
  *
250
250
  * Examples:
251
- * https://challenges.cloudflare.com/•/•/•
252
- * https://site-a1b2c3d4:8443/•/• ?[3] #[frag]
251
+ * https://challenges.cloudflare.com/[path]/[path]/[path]
252
+ * https://site-a1b2c3d4:8443/[path]/[path] ?[3] #[frag]
253
253
  */
254
254
  function anonymizeUrl(rawUrl) {
255
255
  if (!rawUrl || typeof rawUrl !== 'string') return '[empty]';
@@ -334,7 +334,7 @@ export function detectBotProtection(response) {
334
334
  * Create a health tracker for a tab. Attaches to Playwright page events.
335
335
  * Tracks: crashes, page errors, request failures, redirect status codes,
336
336
  * HTTP status histogram (4xx+), and anti-bot challenge detection.
337
- * All count-based no URLs or content stored.
337
+ * All count-based -- no URLs or content stored.
338
338
  */
339
339
  export function createTabHealthTracker(page) {
340
340
  const health = {
@@ -372,7 +372,7 @@ export function createTabHealthTracker(page) {
372
372
  if (s >= 400) health.statusCounts[s] = (health.statusCounts[s] || 0) + 1;
373
373
  });
374
374
 
375
- // Auto-dismiss dialogs to prevent page hangs (not tracked as a metric noise)
375
+ // Auto-dismiss dialogs to prevent page hangs (not tracked as a metric -- noise)
376
376
  page.on('dialog', async (dialog) => {
377
377
  try { await dialog.dismiss(); } catch { /* page might be closed */ }
378
378
  });
@@ -415,7 +415,7 @@ export function createTabHealthTracker(page) {
415
415
 
416
416
  /**
417
417
  * Get document.readyState from the page. Returns null if page is unresponsive.
418
- * Use a tight timeout if the renderer is crashed, evaluate will hang.
418
+ * Use a tight timeout -- if the renderer is crashed, evaluate will hang.
419
419
  */
420
420
  async function getReadyState() {
421
421
  try {
@@ -460,7 +460,7 @@ class RateLimiter {
460
460
  // ============================================================================
461
461
 
462
462
  // Reports are sent to a Cloudflare Worker relay. All credentials are
463
- // environment secrets on the relay nothing sensitive ships in this package.
463
+ // environment secrets on the relay -- nothing sensitive ships in this package.
464
464
  //
465
465
  // Default relay: https://camofox-crash-relay.askjo.workers.dev
466
466
  // Override: CAMOFOX_CRASH_REPORT_URL=https://your-own-relay/report
@@ -483,7 +483,7 @@ function fetchWithTimeout(url, options) {
483
483
 
484
484
  /**
485
485
  * Send a crash report to the relay. Returns true if accepted.
486
- * Never throws reporter must never crash the server.
486
+ * Never throws -- reporter must never crash the server.
487
487
  */
488
488
  export async function sendToRelay(payload) {
489
489
  try {
@@ -556,10 +556,10 @@ function formatIssueBody(type, detail) {
556
556
  sections.push(`- **HTTP status:** ${b.httpStatus || '?'}`);
557
557
  if (b.responseBodySizeKb != null) sections.push(`- **response size:** ${b.responseBodySizeKb} KB`);
558
558
  if (b.redirectChainLength != null) sections.push(`- **redirect chain:** ${b.redirectChainLength} hops`);
559
- if (b.redirectStatusCodes?.length) sections.push(`- **redirect statuses:** ${b.redirectStatusCodes.join(' ')}`);
559
+ if (b.redirectStatusCodes?.length) sections.push(`- **redirect statuses:** ${b.redirectStatusCodes.join(' -> ')}`);
560
560
  }
561
561
 
562
- // Proxy info (safe fields only no IPs, credentials, or hostnames)
562
+ // Proxy info (safe fields only -- no IPs, credentials, or hostnames)
563
563
  if (detail.proxy) {
564
564
  const p = detail.proxy;
565
565
  sections.push('', '## Proxy');
@@ -581,7 +581,7 @@ function formatIssueBody(type, detail) {
581
581
  if (s.cpuElapsedS != null) sections.push(`- **CPU time during stall:** ${s.cpuElapsedS}s`);
582
582
  if (s.cpuRatio != null) sections.push(`- **CPU/wall ratio:** ${s.cpuRatio}`);
583
583
  if (s.sigcontInWindow != null) sections.push(`- **SIGCONT in window:** ${s.sigcontInWindow}`);
584
- if (s.hrtimeWallDriftS != null) sections.push(`- **hrtimewall drift:** ${s.hrtimeWallDriftS}s`);
584
+ if (s.hrtimeWallDriftS != null) sections.push(`- **hrtime<->wall drift:** ${s.hrtimeWallDriftS}s`);
585
585
  if (s.eventLoopDelay) {
586
586
  const eld = s.eventLoopDelay;
587
587
  sections.push(`- **event loop delay:** p50=${eld.p50Ms}ms p99=${eld.p99Ms}ms max=${eld.maxMs}ms`);
@@ -682,7 +682,7 @@ export function createReporter(config) {
682
682
  version,
683
683
  });
684
684
  } catch {
685
- // Swallow reporter must never crash the server
685
+ // Swallow -- reporter must never crash the server
686
686
  }
687
687
  })();
688
688
 
@@ -814,11 +814,11 @@ export function createReporter(config) {
814
814
  const NATIVE_MEM_LEAK_THRESHOLD_MB = 200; // alert if native mem exceeds baseline by this much
815
815
  let nativeMemAlertFired = false;
816
816
 
817
- // SIGCONT detection macOS sends SIGCONT on wake from sleep/suspend
817
+ // SIGCONT detection -- macOS sends SIGCONT on wake from sleep/suspend
818
818
  let lastSigcont = 0;
819
819
  try { process.on('SIGCONT', () => { lastSigcont = Date.now(); }); } catch { /* unavailable */ }
820
820
 
821
- // Event loop delay histogram (perf_hooks) correlating evidence
821
+ // Event loop delay histogram (perf_hooks) -- correlating evidence
822
822
  let elHistogram = null;
823
823
  try {
824
824
  elHistogram = monitorEventLoopDelay({ resolution: 20 });
@@ -937,7 +937,7 @@ export function createReporter(config) {
937
937
  // Remove resourceOpts from extra so it doesn't end up in context
938
938
  delete extra.resourceOpts;
939
939
 
940
- // Don't report idle-server stalls no user impact
940
+ // Don't report idle-server stalls -- no user impact
941
941
  if ((resources.activeTabs || 0) === 0 && (resources.browserContexts || 0) === 0) {
942
942
  return;
943
943
  }
@@ -960,7 +960,7 @@ export function createReporter(config) {
960
960
 
961
961
  fileReport('stuck:event-loop', labels, {
962
962
  message: `Event loop stalled for ${Math.round(drift / 1000)}s (threshold: ${Math.round(thresholdMs / 1000)}s)`,
963
- // Stable signature: duration is NOT included all stalls on the same route dedup
963
+ // Stable signature: duration is NOT included -- all stalls on the same route dedup
964
964
  error: { name: 'EventLoopStall', message: _lastRoute || 'idle', stack: '' },
965
965
  uptimeMinutes: typeof process !== 'undefined'
966
966
  ? Math.round(process.uptime() / 60) : undefined,
@@ -1006,7 +1006,7 @@ export function createReporter(config) {
1006
1006
  * browser session measures from a fresh baseline, not the old one.
1007
1007
  */
1008
1008
  function resetNativeMemBaseline() {
1009
- // These are closure vars in startWatchdog we need to reach them.
1009
+ // These are closure vars in startWatchdog -- we need to reach them.
1010
1010
  // Since this runs in the same module, we set a flag the watchdog reads.
1011
1011
  _resetNativeMemBaseline = true;
1012
1012
  }
@@ -1,4 +1,4 @@
1
- // HTTP request classification helpers kept separate from metrics.js
1
+ // HTTP request classification helpers -- kept separate from metrics.js
2
2
  // to avoid scanner rule triggers (this file contains HTTP method strings).
3
3
 
4
4
  /**
package/lib/resources.js CHANGED
@@ -1,9 +1,8 @@
1
- // lib/resources.js Process resource metrics and proxy error classification.
1
+ // lib/resources.js -- Process resource metrics and proxy error classification.
2
2
  // Isolated from reporter.js so that fs reads and network sends are never
3
3
  // in the same file (avoids OpenClaw scanner "potential-exfiltration" pattern).
4
4
 
5
5
  import fs from 'fs';
6
- import { execSync } from 'child_process';
7
6
 
8
7
  // ============================================================================
9
8
  // Process resource snapshot (memory, handles, FDs, browser RSS)
@@ -11,7 +10,7 @@ import { execSync } from 'child_process';
11
10
 
12
11
  /**
13
12
  * Collect process-level resource metrics. Safe to call at any time.
14
- * Returns anonymized metrics no PIDs, paths, or user data.
13
+ * Returns anonymized metrics -- no PIDs, paths, or user data.
15
14
  */
16
15
  export function collectResourceSnapshot(opts = {}) {
17
16
  const mem = process.memoryUsage();
@@ -38,16 +37,13 @@ export function collectResourceSnapshot(opts = {}) {
38
37
  }
39
38
  } catch { /* not available or permission denied */ }
40
39
 
41
- // Browser process RSS (the one people miss browser OOMs, not Node)
40
+ // Browser process RSS (the one people miss -- browser OOMs, not Node)
42
41
  if (opts.browserPid && Number.isInteger(opts.browserPid) && opts.browserPid > 0) {
43
42
  try {
44
43
  if (process.platform === 'linux') {
45
44
  const status = fs.readFileSync(`/proc/${opts.browserPid}/status`, 'utf8');
46
45
  const match = status.match(/VmRSS:\s+(\d+)\s+kB/);
47
46
  if (match) snap.browserRssMb = Math.round(parseInt(match[1], 10) / 1024);
48
- } else if (process.platform === 'darwin') {
49
- const out = execSync(`ps -o rss= -p ${opts.browserPid}`, { timeout: 1000 }).toString().trim();
50
- if (out) snap.browserRssMb = Math.round(parseInt(out, 10) / 1024);
51
47
  }
52
48
  } catch { /* process gone or permission denied */ }
53
49
  }
@@ -65,7 +61,7 @@ export function collectResourceSnapshot(opts = {}) {
65
61
 
66
62
  /**
67
63
  * Classify proxy errors from Playwright navigation error messages.
68
- * Returns { proxyError: string|null, proxyTlsError: bool } no IPs or credentials.
64
+ * Returns { proxyError: string|null, proxyTlsError: bool } -- no IPs or credentials.
69
65
  */
70
66
  export function classifyProxyError(errorMessage) {
71
67
  if (!errorMessage || typeof errorMessage !== 'string') return { proxyError: null, proxyTlsError: false };
package/lib/snapshot.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Snapshot windowing truncate large accessibility snapshots while
2
+ * Snapshot windowing -- truncate large accessibility snapshots while
3
3
  * preserving pagination/navigation links at the tail.
4
4
  */
5
5
 
@@ -81,7 +81,7 @@ export function cleanupStaleFirefoxProfiles({ tmpDir, minAgeMs = 2 * 60 * 1000,
81
81
  result.removed++;
82
82
  result.bytes += dirBytes;
83
83
  } catch {
84
- // directory vanished, permission denied, or in-use skip
84
+ // directory vanished, permission denied, or in-use -- skip
85
85
  }
86
86
  }
87
87