Commit Graph
180 Commits
Author SHA1 Message Date
Pradeep Elankumaran 138614b517 feat: add opt-in interactive desktop browser 2026-08-19 12:45:32 -07:00
Pradeep Elankumaran a210adeee7 fix: harden browser navigation and actions 2026-08-19 11:04:13 -07:00
Pradeep Elankumaran 74d164dbbf fix(vnc): clean up Xvfb display files after exit 2026-08-19 11:03:51 -07:00
Pradeep Elankumaran dfaf188f84 fix: make consent dismissal explicit and scoped 2026-08-19 09:54:17 -07:00
Pradeep ElankumaranandD Yiapanis 78b649c3e0 merge: isolate navigation health recovery per user
Co-authored-by: D Yiapanis <d@yiapanis.co>
2026-08-19 09:41:03 -07:00
Pradeep ElankumaranandErick Garcia f3e4ffa145 merge: apply health-probe viewport compatibility fix
Co-authored-by: Erick Garcia <erick@egarcia.dev>
2026-08-19 09:40:49 -07:00
D Yiapanis 8ec4b9f694 fix: per-user nav health tracking, session-scoped recovery, single-flight race
- Replace process-global healthState.consecutiveNavFailures with
  Map<userId, UserNavHealth> for per-user failure tracking (#9321)
- Wire recordNavSuccess(userId)/recordNavFailure(userId) into all
  navigation routes: POST /tabs, POST /tabs/:tabId/navigate,
  POST /tabs/open, POST /navigate, and handleRouteError for /act (#9323)
- Add recoverUserSession(userId) for session-scoped recovery instead
  of restartBrowser() killing all sessions on nav failures (#9322)
- Remove manual browserLaunchPromise = null in restartBrowser() —
  ensureBrowser() owns the single-flight primitive (#8554)
- Clean up per-user nav health on destroySession()
- Aggregate per-user failures in /health endpoint
- Add tests/unit/navHealthRecovery.test.js (8 tests)

Fixes #9321, #9322, #9323
Relates to #8554
2026-08-13 09:09:53 +10:00
Erick Garcia 192fa202c8 fix: pass viewport: null on the health-probe browser context too
#7266 fixed the "Found property <root>.viewport.isMobile ... not
described in this scheme" juggler rejection by switching context
creation to viewport: null in probeGoogleSearch() and getSession(), but
missed a third call site: the active health probe's setInterval calls
browser.newContext() with no arguments, which still hits Playwright's
implicit default viewport and the same isMobile rejection.

In practice this means the health probe itself can trip the very
failure it's meant to detect, log it as "health probe failed", and
call restartBrowser('health probe failed') -- restarting a browser
that was otherwise fine, repeatedly, on whatever interval the probe
runs at.

Extends tests/unit/launchCompat.test.js (added in #7266, same
source-contract idiom as tests/unit/noSecrets.test.js) with a fourth
case covering this call site. Confirmed the new test fails against the
unpatched line and passes with the fix.
2026-08-11 20:33:23 -06:00
Pradeep Elankumaran 8daab78f4c Merge branch 'fix/evaluate-body-limit' into master 2026-08-01 18:26:35 -07:00
Pradeep Elankumaran 08e9989442 Merge branch 'fix/session-page-leases' into master
Fixes #8555
2026-08-01 17:47:46 -07:00
Pradeep ElankumaranandLeone Parise 7b2dfe68af fix(upload): contain file paths within upload directory
Resolve upload paths before attaching files. Reject paths outside CAMOFOX_UPLOADS_DIR, including symlink escapes, and require regular files.

Co-authored-by: Leone Parise <1442927+leoneparise@users.noreply.github.com>
2026-08-01 17:20:35 -07:00
Pradeep ElankumaranandLeone Parise 8e3cd9b3da Merge remote-tracking branch 'origin/pr-8273' into fix/upload-sandbox
Co-authored-by: Leone Parise <1442927+leoneparise@users.noreply.github.com>
2026-08-01 17:19:43 -07:00
Pradeep ElankumaranandFrancois Botha d805b4218c fix(evaluate): isolate large request bodies
Fixes #8397
Refs #8398

Co-authored-by: Francois Botha <igitur@users.noreply.github.com>
2026-08-01 17:14:19 -07:00
Pradeep Elankumaran 6dccc63422 Merge remote-tracking branch 'origin/pr-8398' into fix/evaluate-body-limit 2026-08-01 17:14:19 -07:00
Pradeep Elankumaran 700a3bf4f5 fix(sessions): lease pages during creation
Refines the session-reaping approach from #8319 without adding admission queues or HTTP 429 behavior.\n\nFixes #8555\nRefs #8319\n\nCo-authored-by: batumilove <batumilove@users.noreply.github.com>
2026-08-01 17:10:58 -07:00
Mattia Trapani 0b472b94b3 fix(evaluate): route errors through handleRouteError 2026-07-30 11:48:46 +02:00
Francois Botha bc7dbb3569 fix: add CAMOFOX_MAX_BODY_SIZE env var to control body parser limit
Both the global express.json() parser and the /evaluate endpoint now
read from CAMOFOX_MAX_BODY_SIZE (default: 100kb) via CONFIG.maxBodySize.
The setting is defined in lib/config.js alongside all other env vars.

Closes #8397
2026-07-26 11:08:11 +02:00
Leone Parise 4b1a33ade1 refactor(upload): extract timeout magic numbers into constants + timeout arg
The upload route had inline millisecond literals (4000, 12000, 3000,
10000, 500, 1500) scattered through its two attach strategies. Replace
them with named UPLOAD_*_MS constants declared next to the route, and
expose the overall wait budget as an optional `timeout` request field.

- UPLOAD_UI_TIMEOUT_MS (default 12000) backs the request's `timeout`:
  the budget to wait for an upload UI (panel input or native chooser).
  Non-numeric / <= 0 values fall back to the default.
- The panel-poll window derives from that budget minus
  UPLOAD_PANEL_MARGIN_MS, preserving the original 10000/12000 split so a
  late native chooser is still caught after polling stops.
- UPLOAD_INPUT/FOCUS/CLICK/REFS/POLL/SETTLE_MS name the per-call bounds.

Defaults reproduce the previous behavior exactly. OpenAPI documents the
new `timeout` field; tests cover timeout resolution and assert the route
carries no bare millisecond literals.
2026-07-20 17:12:24 -03:00
Leone Parise 36b00359ef feat: add /tabs/:tabId/upload endpoint for file attachment
Attach a file to an upload control without going through the native OS
file dialog. Two strategies are tried in order:

  1. If an <input type="file"> is already present, call Playwright
     setInputFiles on it directly (works for hidden inputs).
  2. Otherwise arm a filechooser listener, activate the trigger element
     (ref or selector) via keyboard (focus + Enter) with a forced click
     as fallback, and setFiles on the resulting chooser. Also polls for
     an in-app panel <input type=file> that mounts after activation.

The panel-input path is preferred over the native chooser so a control
that surfaces both (e.g. LinkedIn's media picker) attaches the file
exactly once rather than producing a duplicate.

Paths must be visible inside the container (e.g. a bind-mounted dir);
the route guards with fs.existsSync and returns 400 file_not_found
otherwise. Runs under the same per-user and per-tab locks as the other
interaction routes.

Includes OpenAPI documentation and unit tests (request validation +
source-contract assertions).
2026-07-20 16:58:18 -03:00
Love-JourneYandPradeep Elankumaran f15148b05d fix: recover zombie browser contexts during tab creation (#5416)
Bound new-page creation with a configurable 10-second deadline, replace only the affected user context on timeout/dead-context failures, and retry once.

Co-authored-by: Pradeep Elankumaran <pradeep@askjo.ai>
2026-07-19 22:44:17 -07:00
PluginsKersandPluginsKers bd2a071a3d feat(persistence): reset complete session storage state (#6972)
Add an authenticated storage-state reset endpoint that closes the live browser context without checkpointing, waits for in-flight persistence writes, and removes the saved state so the next session starts fresh.

Co-authored-by: PluginsKers <ikers@foxmail.com>
2026-07-19 21:37:15 -07:00
snapssandSnapsNoCaps 948c1c43f7 feat: support configurable server bind host (#8096)
Add optional CAMOFOX_BIND_HOST configuration, forward it to launched server subprocesses, document the setting, and report the effective listener address at startup.

Co-authored-by: SnapsNoCaps <139733767+HODL-Community@users.noreply.github.com>
2026-07-19 20:59:16 -07:00
Pradeep Elankumaran e45aac282c Merge PR #5591: skip unstable bot-detection iframes 2026-07-17 23:52:38 -07:00
Pradeep Elankumaran e018394207 Merge PR #5663: bound click fallback element lookup 2026-07-17 23:52:38 -07:00
Pradeep Elankumaran 6da9c7b7fc Merge PR #7879: scope browser cleanup to server ownership
# Conflicts:
#	server.js
2026-07-17 23:52:30 -07:00
Pradeep Elankumaran 6f38544203 Remove jo-browser-specific integrations 2026-07-17 23:26:26 -07:00
Pradeep Elankumaran 56974db43b Merge PR #7163: persist storage before shutdown (fixes #7162) 2026-07-17 23:00:00 -07:00
Pradeep Elankumaran 5a035c33f1 Merge PR #5078: allow disabling default UBO addon (fixes #5067)
# Conflicts:
#	tests/unit/config.test.js
2026-07-17 22:48:41 -07:00
Pradeep Elankumaran 3fab9c629a Merge PR #7266: fix Linux launch regressions 2026-07-17 22:43:20 -07:00
Leo Mercer b5acf489ad fix: isolate browser cleanup by server ownership 2026-07-11 16:19:24 -04:00
Matt Van Horn 993f707e91 fix: size default virtual display 1280x720 for non-VNC launches 2026-07-04 00:40:39 -07:00
Matt Van Horn afd7638d9e fix: await virtual display and drop fixed viewport for camoufox compatibility 2026-07-04 00:40:39 -07:00
Joyjit Nath 1563b6274c Disable Playwright's own signal handlers on browser launch
The previous two commits fixed the ordering of our own shutdown
sequence (server:shutdown now blocks on emitAsync, forceTimeout/close
run before it), but production verification showed the storage-state
race still reproduced 2/2 times even with both fixes deployed.

Root cause: Playwright's launcher defaults handleSIGTERM/SIGINT/SIGHUP
to true, registering its own process signal handlers independent of
ours. On SIGTERM, that handler sends Browser.close straight to Firefox
over its debug transport -- outside pluginEvents entirely, racing
ahead of gracefulShutdown() and the persistence plugin's checkpoint
regardless of how carefully our own code is sequenced.

Set all three handleSIG* options to false so gracefulShutdown() is the
sole authority over shutdown; closeBrowserFully() already does an
explicit browser.close() plus a force-kill fallback, so nothing is
lost by disabling Playwright's own signal-driven close path.

Verified against a real deployment: reproduced the original failure
2/2 times with only the prior two commits applied, then 2/2 successful
"storage state persisted" after adding this fix, via docker restart
immediately (and with a 5s settle) after creating a real session.

No new unit test added for this specific change -- firefox.launch()
isn't currently unit-testable in isolation without substantial mocking
scaffolding disproportionate to a 3-line options change, and the
production verification above is stronger evidence than a mocked unit
test would provide.
2026-07-02 11:37:11 -07:00
Joyjit Nath df2e1cb28b Arm shutdown watchdog before awaiting server:shutdown listeners
The previous commit made server:shutdown blocking (emitAsync) to fix
the storage-state persist race, but that reordering had three side
effects: forceTimeout was armed only after the awaited emit resolved
(a hung listener disables the force-exit watchdog entirely), a
rejecting listener could abort the rest of gracefulShutdown via
Promise.all's fail-fast semantics with no top-level catch, and
server.close() ran later, widening the window where new sessions
could still be created during shutdown.

Move forceTimeout and server.close() back to running immediately, and
catch a rejecting server:shutdown listener so it can't skip the rest
of cleanup. The checkpoint-before-close ordering from the previous
commit is unchanged.
2026-07-02 10:50:36 -07:00
Joyjit Nath ff6f378a71 Await server:shutdown listeners before closing sessions
gracefulShutdown() fired the server:shutdown event with plain
EventEmitter#emit, which does not wait for async listeners. It then
called closeAllSessions() immediately after, racing the persistence
plugin's in-flight context.storageState() checkpoint against
context.close() for the same session -- intermittently failing with
"Target page, context or browser has been closed" and silently
dropping unsaved storage state on restart.

closeSession() already gets this right for session:destroying via
emitAsync(); server:shutdown just wasn't using the same helper.

Fixes #7162
2026-07-02 10:17:29 -07:00
Pradeep Elankumaran 1eb6ae857a Handle non-fillable type targets 2026-06-18 10:40:16 -07:00
Pradeep Elankumaran e92337d4b4 Pin Camoufox dependency for Docker deploys 2026-06-15 00:20:22 -07:00
Leone Parise 64ad36e0da fix(click): bound boundingBox() in mouse-sequence fallback to prevent 30s handler timeout 500s
When a normal click attempt fails because the element detached after a page
change (SPA re-render between snapshot and click), the mouse-sequence
fallback called locator.boundingBox() with no timeout. Playwright waited its
default 30s for the element to resolve, blowing the entire HANDLER_TIMEOUT_MS
budget and surfacing as:

  click failed: action timed out after 30000ms -> 500 internal error

Worse, that generic 'timed out after' error is classified by isTimeoutError()
as a navigation timeout, so handleRouteError() destroyed the whole user
session (fresh proxy/context) over a stale ref.

Fix:
- boundingBox() is now bounded to min(3s, remaining handler budget),
  floored at 500ms.
- On timeout it throws a 422 'Element not actionable' error whose message
  deliberately avoids the 'timed out after' phrase, so the session survives
  and the client gets an actionable hint (snapshot + retry) in ~3s
  instead of a 500 after 30s.

Adds tests/unit/clickBoundingBoxTimeout.test.js covering the source
contract (no bare boundingBox() calls), the budget math, and the error
classification.
2026-06-11 18:51:25 -03:00
Leone Parise 556090ef3c fix(buildRefs): skip PerimeterX/captcha iframes to prevent 30s hang on authenticated clicks
buildRefs() traverses child frames and runs ariaSnapshot on each one not matched
by IFRAME_SKIP_PATTERNS. Some sites inject a bot-detection iframe (PerimeterX
px-iframe-*/px-captcha, hCaptcha, Arkose/FunCaptcha, DataDome) that is short-lived
and detaches mid-ariaSnapshot. The ariaSnapshot call then hangs until the handler
timeout (30s), and the click/snapshot that triggered the ref rebuild returns a
spurious 500 even though the underlying action succeeded.

Concretely on LinkedIn: PerimeterX is injected on authenticated actions such as
the connection-invite 'Add a note' modal, so every such click 500s and write
flows silently fail.

Fix: add px-iframe/px-captcha/perimeterx/captcha/hcaptcha/arkose/funcaptcha/datadome
to IFRAME_SKIP_PATTERNS so buildRefs skips them (same treatment recaptcha already
gets). Both iframe loops reference this constant, so the single edit covers ref
building and snapshot YAML.

Tested: an automated LinkedIn connection-invite flow that previously 500'd on
profiles showing the intermediate 'Add a note' step now completes and verifies
Pending.
2026-06-10 14:46:45 -03:00
Pradeep Elankumaran 45aac9bda9 Generalize auth session endpoints
ref JO-2738
2026-06-03 09:09:46 -07:00
Pradeep Elankumaran f0a51b482d Add Amazon auth session endpoints
ref JO-2738
2026-06-03 09:05:57 -07:00
Matt Van Horn 26e2501ea2 fix: add CAMOFOX_DISABLE_DEFAULT_ADDONS knob to exclude default UBO addon 2026-06-03 00:55:48 -07:00
Pradeep Elankumaran dac76d5b56 Normalize browser operational failures
ref JO-2731
2026-06-02 15:33:16 -07:00
Matt Van Horn 7a0c479161 fix: honor BROWSER_IDLE_TIMEOUT_MS=0 as never
parseInt(...) || 300000 treated an explicit 0 as falsy and fell back to
the 5-minute default, and scheduleBrowserIdleShutdown armed a timer
regardless. Parse with Number.isFinite so 0 is preserved, and skip
scheduling when the timeout is <= 0, matching the README's documented
0 = never contract.

Closes #4942
2026-06-02 08:40:44 -07:00
Pradeep Elankumaran 066870c7f7 Return conflict for page-changed browser actions 2026-06-01 07:52:50 -07:00
Pradeep Elankumaran 80a28281ec Recycle wedged idle browsers sooner 2026-05-31 01:25:49 -07:00
Pradeep Elankumaran 3d94707f5c Normalize browser crash errors ref JO-2632 2026-05-28 07:59:20 -07:00
Pradeep Elankumaran cf78bc1a54 Add safePageUrl helper, fix popup .url() crash
Adds safePageUrl() that catches destroyed page access. Applied to popup
handler where popupPage.url() can throw if popup closes immediately.
Addresses JO-BROWSER-18 TypeError: Cannot read properties of undefined
(reading 'url').
2026-05-24 13:09:23 -07:00
Pradeep Elankumaran 3f6ecfec0f Fix tabNotFoundResponse to handle Fly-prefixed tab IDs
Tab IDs on Fly are '{machineId}_{uuid}'. The UUID regex check was
testing the full prefixed string, always failing, so stale tabs after
browser restart got 404 instead of 410. Now extracts the UUID portion
before validation.
2026-05-24 09:42:00 -07:00
Pradeep Elankumaran 7e623869eb Health endpoint: return 503 for unexpected browser absence
Tracks _lastBrowserStopReason to distinguish intentional idle/admin
stops (200) from unexpected deaths like browser_disconnected,
memory_pressure, browser_rss_pressure (503 + warm retry).

Fly health checks will now detect and route around machines with
unexpectedly dead browsers, while intentional idle shutdown still
passes health checks as before.
2026-05-24 09:40:18 -07:00