@askjo/camofox-browser 1.8.9 → 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/server.js CHANGED
@@ -144,7 +144,7 @@ app.use('/tabs/:tabId', fly.replayMiddleware(log));
144
144
 
145
145
  // Access-key middleware: gates every route when CAMOFOX_ACCESS_KEY is set.
146
146
  // Exempts /health (Docker healthcheck) and routes that have their own
147
- // dedicated keys (cookie import CAMOFOX_API_KEY, /stop CAMOFOX_ADMIN_KEY)
147
+ // dedicated keys (cookie import -> CAMOFOX_API_KEY, /stop -> CAMOFOX_ADMIN_KEY)
148
148
  // so each key gates a distinct surface. When unset, behavior is unchanged.
149
149
  app.use(accessKeyMiddleware(CONFIG));
150
150
 
@@ -167,7 +167,7 @@ const SKIP_PATTERNS = [
167
167
  const timingSafeCompare = _timingSafeCompare;
168
168
  const isLoopbackAddress = _isLoopbackAddress;
169
169
 
170
- // Custom error for stale/unknown element refs returned as 422 instead of 500
170
+ // Custom error for stale/unknown element refs -- returned as 422 instead of 500
171
171
  class StaleRefsError extends Error {
172
172
  constructor(ref, maxRef, totalRefs) {
173
173
  super(`Unknown ref: ${ref} (valid refs: e1-${maxRef}, ${totalRefs} total). Refs reset after navigation - call snapshot first.`);
@@ -208,7 +208,7 @@ function validateUrl(url) {
208
208
  }
209
209
  }
210
210
 
211
- // isLoopbackAddress now imported from lib/auth.js (see top of file)
211
+ // isLoopbackAddress -- now imported from lib/auth.js (see top of file)
212
212
 
213
213
  // Import cookies into a user's browser context (Playwright cookies format)
214
214
  // POST /sessions/:userId/cookies { cookies: Cookie[] }
@@ -634,7 +634,7 @@ function getTotalTabCount() {
634
634
 
635
635
  // Virtual display for WebGL support and anti-detection.
636
636
  // Xvfb gives Firefox a real X display with GLX, enabling software-rendered WebGL
637
- // via Mesa llvmpipe. Without this, WebGL returns "no context" a massive bot signal.
637
+ // via Mesa llvmpipe. Without this, WebGL returns "no context" -- a massive bot signal.
638
638
  let virtualDisplay = null;
639
639
  let browserLaunchProxy = null;
640
640
 
@@ -680,8 +680,8 @@ function attachBrowserCleanup(candidateBrowser, localVirtualDisplay) {
680
680
  *
681
681
  * Serialized: concurrent callers await the same promise (no double-close).
682
682
  *
683
- * Order: capture PID close browser force-kill survivors
684
- * clean temp profiles verify FD/handle drop.
683
+ * Order: capture PID -> close browser -> force-kill survivors ->
684
+ * clean temp profiles -> verify FD/handle drop.
685
685
  */
686
686
  async function closeBrowserFully(reason) {
687
687
  if (_browserClosePromise) return _browserClosePromise;
@@ -697,7 +697,7 @@ async function _closeBrowserFullyImpl(reason) {
697
697
  const b = browser;
698
698
  if (!b) return;
699
699
 
700
- // Capture PID before nulling browser ref we need it for force-kill
700
+ // Capture PID before nulling browser ref -- we need it for force-kill
701
701
  const pid = _lastBrowserPid;
702
702
  const preCloseFds = _countOpenFds();
703
703
  const preCloseHandles = _countActiveHandles();
@@ -1032,7 +1032,7 @@ async function getSession(userId, { trace = false } = {}) {
1032
1032
  // Check if existing session's context is still alive
1033
1033
  if (session) {
1034
1034
  if (session._closing) {
1035
- // Session is being torn down by reaper/expiry treat as dead
1035
+ // Session is being torn down by reaper/expiry -- treat as dead
1036
1036
  session = null;
1037
1037
  } else {
1038
1038
  try {
@@ -1157,7 +1157,7 @@ function handleRouteError(err, req, res, extraFields = {}) {
1157
1157
  if (userId && isDeadContextError(err)) {
1158
1158
  destroySession(userId);
1159
1159
  }
1160
- // Proxy errors mean the session is dead rotate at context level.
1160
+ // Proxy errors mean the session is dead -- rotate at context level.
1161
1161
  // Destroy the user's session so the next request gets a fresh context with a new proxy.
1162
1162
  if (isProxyError(err) && proxyPool?.canRotateSessions && userId) {
1163
1163
  log('warn', 'proxy error detected, destroying user session for fresh proxy on next request', {
@@ -1331,7 +1331,7 @@ function createTabState(page) {
1331
1331
  async function isGoogleUnavailable(page) {
1332
1332
  if (!page || page.isClosed()) return false;
1333
1333
  const bodyText = await page.evaluate(() => document.body?.innerText?.slice(0, 600) || '').catch(() => '');
1334
- return /Unable to connect|502 Bad Gateway or Proxy Error|Camoufox cant establish a connection/.test(bodyText);
1334
+ return /Unable to connect|502 Bad Gateway or Proxy Error|Camoufox can't establish a connection/.test(bodyText);
1335
1335
  }
1336
1336
 
1337
1337
  async function rotateGoogleTab(userId, sessionKey, tabId, previousTabState, reason, reqId) {
@@ -1340,7 +1340,7 @@ async function rotateGoogleTab(userId, sessionKey, tabId, previousTabState, reas
1340
1340
 
1341
1341
  browserRestartsTotal.labels(reason).inc(); // track rotation events (not a full restart)
1342
1342
 
1343
- // Rotate at context level create a fresh context with a new proxy session
1343
+ // Rotate at context level -- create a fresh context with a new proxy session
1344
1344
  // instead of restarting the entire browser (which kills ALL sessions/tabs).
1345
1345
  const key = normalizeUserId(userId);
1346
1346
  const oldSession = sessions.get(key);
@@ -1672,7 +1672,7 @@ async function buildRefs(page) {
1672
1672
  return refs;
1673
1673
  }
1674
1674
 
1675
- // Google SERP fast path skip ariaSnapshot entirely
1675
+ // Google SERP fast path -- skip ariaSnapshot entirely
1676
1676
  const url = page.url();
1677
1677
  if (isGoogleSerp(url)) {
1678
1678
  const { refs: googleRefs } = await extractGoogleSerp(page);
@@ -2207,7 +2207,7 @@ app.post('/tabs/:tabId/navigate', async (req, res) => {
2207
2207
  const recreateTabOnFreshContext = async () => {
2208
2208
  const previousRetryCount = tabState.googleRetryCount || 0;
2209
2209
  browserRestartsTotal.labels('google_search_block').inc();
2210
- // Rotate at context level destroy this user's session and create
2210
+ // Rotate at context level -- destroy this user's session and create
2211
2211
  // a fresh one with a new proxy session. Does NOT restart the browser.
2212
2212
  const key = normalizeUserId(userId);
2213
2213
  const oldSession = sessions.get(key);
@@ -2243,7 +2243,7 @@ app.post('/tabs/:tabId/navigate', async (req, res) => {
2243
2243
  }
2244
2244
 
2245
2245
  // For Google SERP: skip eager ref building during navigate.
2246
- // Results render asynchronously after DOMContentLoaded the snapshot
2246
+ // Results render asynchronously after DOMContentLoaded -- the snapshot
2247
2247
  // call will wait for and extract them.
2248
2248
  if (isGoogleSerp(tabState.page.url())) {
2249
2249
  tabState.refs = new Map();
@@ -2383,7 +2383,7 @@ app.get('/tabs/:tabId/snapshot', async (req, res) => {
2383
2383
 
2384
2384
  const pageUrl = tabState.page.url();
2385
2385
 
2386
- // Google SERP fast path DOM extraction instead of ariaSnapshot
2386
+ // Google SERP fast path -- DOM extraction instead of ariaSnapshot
2387
2387
  if (isGoogleSerp(pageUrl)) {
2388
2388
  const { refs: googleRefs, snapshot: googleSnapshot } = await extractGoogleSerp(tabState.page);
2389
2389
  tabState.refs = googleRefs;
@@ -2617,7 +2617,7 @@ app.post('/tabs/:tabId/click', async (req, res) => {
2617
2617
  const clickStart = Date.now();
2618
2618
  const remainingBudget = () => Math.max(0, HANDLER_TIMEOUT_MS - 2000 - (Date.now() - clickStart));
2619
2619
  // Full mouse event sequence for stubborn JS click handlers (mirrors Swift WebView.swift)
2620
- // Dispatches: mouseover mouseenter mousedown mouseup click
2620
+ // Dispatches: mouseover -> mouseenter -> mousedown -> mouseup -> click
2621
2621
  const dispatchMouseSequence = async (locator) => {
2622
2622
  const box = await locator.boundingBox();
2623
2623
  if (!box) throw new Error('Element not visible (no bounding box)');
@@ -2638,7 +2638,7 @@ app.post('/tabs/:tabId/click', async (req, res) => {
2638
2638
  };
2639
2639
 
2640
2640
  // On Google SERPs, skip the normal click attempt (always intercepted by overlays)
2641
- // and go directly to force click saves 5s timeout per click
2641
+ // and go directly to force click -- saves 5s timeout per click
2642
2642
  const onGoogleSerp = isGoogleSerp(tabState.page.url());
2643
2643
 
2644
2644
  const doClick = async (locatorOrSelector, isLocator) => {
@@ -2710,7 +2710,7 @@ app.post('/tabs/:tabId/click', async (req, res) => {
2710
2710
  await tabState.page.waitForLoadState('domcontentloaded', { timeout: 3000 });
2711
2711
  } catch {}
2712
2712
  await tabState.page.waitForTimeout(200);
2713
- // Skip buildRefs here SERP clicks typically navigate to a new page,
2713
+ // Skip buildRefs here -- SERP clicks typically navigate to a new page,
2714
2714
  // and the caller always requests /snapshot next which rebuilds refs.
2715
2715
  tabState.lastSnapshot = null;
2716
2716
  tabState.refs = new Map();
@@ -2721,7 +2721,7 @@ app.post('/tabs/:tabId/click', async (req, res) => {
2721
2721
  await tabState.page.waitForTimeout(500);
2722
2722
  }
2723
2723
  tabState.lastSnapshot = null;
2724
- // buildRefs after click use remaining budget (min 2s) so we don't blow the handler timeout.
2724
+ // buildRefs after click -- use remaining budget (min 2s) so we don't blow the handler timeout.
2725
2725
  // If it times out, return without refs (caller's next /snapshot will rebuild them).
2726
2726
  const postClickBudget = Math.max(2000, remainingBudget());
2727
2727
  try {
@@ -2867,7 +2867,7 @@ app.post('/tabs/:tabId/type', async (req, res) => {
2867
2867
  await tabState.page.fill(selector, text, { timeout: 10000 });
2868
2868
  }
2869
2869
  } else {
2870
- // keyboard mode char-by-char real key events (required for Ember/contenteditable)
2870
+ // keyboard mode -- char-by-char real key events (required for Ember/contenteditable)
2871
2871
  if (locator) {
2872
2872
  await locator.focus({ timeout: 10000 });
2873
2873
  } else if (selector) {
@@ -3099,7 +3099,7 @@ app.post('/tabs/:tabId/back', async (req, res) => {
3099
3099
  await tabState.page.goBack({ timeout: 10000 });
3100
3100
  } catch (navErr) {
3101
3101
  // NS_BINDING_CANCELLED_OLD_LOAD: Firefox cancels the old load when going back.
3102
- // The navigation itself succeeded just the prior page's load was interrupted.
3102
+ // The navigation itself succeeded -- just the prior page's load was interrupted.
3103
3103
  if (navErr.message && navErr.message.includes('NS_BINDING_CANCELLED')) {
3104
3104
  log('info', 'goBack cancelled old load (expected)', { reqId: req.reqId, tabId });
3105
3105
  } else {
@@ -3767,7 +3767,7 @@ app.post('/tabs/:tabId/evaluate', express.json({ limit: '1mb' }), async (req, re
3767
3767
  * schema:
3768
3768
  * $ref: '#/components/schemas/Error'
3769
3769
  * 409:
3770
- * description: No refs available call snapshot first.
3770
+ * description: No refs available -- call snapshot first.
3771
3771
  * content:
3772
3772
  * application/json:
3773
3773
  * schema:
@@ -3818,7 +3818,7 @@ app.post('/tabs/:tabId/extract', express.json({ limit: '256kb' }), async (req, r
3818
3818
 
3819
3819
  if (!tabState.refs || tabState.refs.size === 0) {
3820
3820
  return res.status(409).json({
3821
- error: 'no refs available call GET /tabs/:tabId/snapshot first to build the ref table',
3821
+ error: 'no refs available -- call GET /tabs/:tabId/snapshot first to build the ref table',
3822
3822
  snapshot: tabState.lastSnapshot || null,
3823
3823
  });
3824
3824
  }
@@ -4247,7 +4247,7 @@ setInterval(() => {
4247
4247
  refreshTabLockQueueDepth();
4248
4248
  }, 60_000);
4249
4249
 
4250
- // Per-tab inactivity reaper close tabs idle for TAB_INACTIVITY_MS
4250
+ // Per-tab inactivity reaper -- close tabs idle for TAB_INACTIVITY_MS
4251
4251
  setInterval(() => {
4252
4252
  const now = Date.now();
4253
4253
  for (const [userId, session] of sessions) {
@@ -4278,7 +4278,7 @@ setInterval(() => {
4278
4278
  session.tabGroups.delete(listItemId);
4279
4279
  }
4280
4280
  }
4281
- // Clean up sessions with zero tabs remaining free browser context memory
4281
+ // Clean up sessions with zero tabs remaining -- free browser context memory
4282
4282
  if (session.tabGroups.size === 0) {
4283
4283
  session._closing = true;
4284
4284
  log('info', 'session empty after tab reaper, closing', { userId });
@@ -4294,7 +4294,7 @@ setInterval(() => {
4294
4294
  // These allow camoufox to be used as a profile backend for OpenClaw's browser tool
4295
4295
  // =============================================================================
4296
4296
 
4297
- // GET / - Status (passive does not launch browser)
4297
+ // GET / - Status (passive -- does not launch browser)
4298
4298
  /**
4299
4299
  * @openapi
4300
4300
  * /:
@@ -5076,12 +5076,12 @@ setInterval(() => {
5076
5076
  });
5077
5077
  }, 5 * 60_000);
5078
5078
 
5079
- // Active health probe detect hung browser even when isConnected() lies
5079
+ // Active health probe -- detect hung browser even when isConnected() lies
5080
5080
  setInterval(async () => {
5081
5081
  if (!browser || healthState.isRecovering) return;
5082
5082
  const timeSinceSuccess = Date.now() - healthState.lastSuccessfulNav;
5083
5083
  // Skip probe if operations are in flight AND last success was recent.
5084
- // If it's been >120s since any successful operation, probe anyway
5084
+ // If it's been >120s since any successful operation, probe anyway --
5085
5085
  // active ops are likely stuck on a frozen browser and will time out eventually.
5086
5086
  if (healthState.activeOps > 0 && timeSinceSuccess < 120000) {
5087
5087
  log('info', 'health probe skipped, operations active', { activeOps: healthState.activeOps });
@@ -5150,7 +5150,7 @@ async function gracefulShutdown(signal) {
5150
5150
  process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
5151
5151
  process.on('SIGINT', () => gracefulShutdown('SIGINT'));
5152
5152
 
5153
- // Idle self-shutdown REMOVED it was racing with min_machines_running=2
5153
+ // Idle self-shutdown REMOVED -- it was racing with min_machines_running=2
5154
5154
  // and stopping machines that Fly couldn't auto-restart fast enough, leaving
5155
5155
  // only 1 machine to handle all browser traffic (causing timeouts for users).
5156
5156
  // Fly's auto_stop_machines=false + min_machines_running=2 handles scaling.
@@ -5181,7 +5181,7 @@ const pluginCtx = {
5181
5181
  createMetric,
5182
5182
  /** Factory for Xvfb virtual display. Plugins can replace this to customise resolution/args. */
5183
5183
  createVirtualDisplay: () => new VirtualDisplay(),
5184
- /** The upstream VirtualDisplay class plugins can subclass it. */
5184
+ /** The upstream VirtualDisplay class -- plugins can subclass it. */
5185
5185
  VirtualDisplay,
5186
5186
  };
5187
5187
  const loadedPlugins = await loadPlugins(app, pluginCtx);
@@ -5235,7 +5235,7 @@ const server = app.listen(PORT, async () => {
5235
5235
  log('error', 'browser pre-warm failed (will retry in background)', { error: err.message });
5236
5236
  scheduleBrowserWarmRetry();
5237
5237
  }
5238
- // Idle self-shutdown removed Fly manages machine lifecycle via fly.toml.
5238
+ // Idle self-shutdown removed -- Fly manages machine lifecycle via fly.toml.
5239
5239
  });
5240
5240
 
5241
5241
  server.on('error', (err) => {