98 Commits
Author SHA1 Message Date
germondai 693d81b84e chore(deps): refresh release dependencies 2026-08-10 19:21:02 +02:00
germondai ff5756ca46 fix(browser): update pinned runtime assets 2026-08-10 19:20:57 +02:00
germondai ac79af388f perf(api): accelerate cold starts 2026-08-09 23:34:47 +02:00
germondai b37979996c fix(tiers): prevent stale challenge responses 2026-08-09 19:59:11 +02:00
germondai c0769af181 fix(browser): bound memory with rolling recycling 2026-08-09 18:15:48 +02:00
germondai 2547daa41b fix(browser): clarify Gluetun startup failures 2026-08-08 22:58:10 +02:00
germondai c2ab109fe5 fix: report effective URL after redirects 2026-08-08 18:24:29 +02:00
germondai 097565a19f chore(release): prepare v1.3.1 2026-08-02 15:51:35 +02:00
germondai 8b54abd4a1 fix(browser): keep persistent contexts pool-owned 2026-08-02 15:37:47 +02:00
GermondandGitHub 1f38465ddb Merge pull request #47 from nandyalu/fix/tier1-full-body-response
fix(tiers): return full body from Tier 1, not the 4 KiB detection preview
2026-08-02 03:20:51 +02:00
germondai 3a3226ed20 chore(release): prepare v1.3.0 dependencies 2026-08-02 03:07:04 +02:00
nandyaluandClaude Fable 5 de0bc788fc test(tiers): add full-body regression tests; reuse preview when body fits
Address review feedback:
- Reuse previewText on success when the body fits inside the preview
  window, avoiding a redundant second decode for small responses.
- Add regression tests for #46: full html for >4 KiB responses, a
  multi-byte character straddling the preview boundary, and small
  responses returned unchanged. Verified the large-body test fails
  against the pre-fix Tier 1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 00:30:13 -05:00
nandyaluandClaude Fable 5 53154aabe1 fix(tiers): return full body from Tier 1, not the 4 KiB detection preview
Since d1ebbdd (1.2.0), Tier 1 reuses previewText — the 4096-byte slice
decoded for challenge detection — as the html field on success. The
/v1 and /scrape routes serve html as solution.response, so any
non-challenged text response larger than 4 KiB comes back silently
truncated: HTTP 200, status ok, no error anywhere.

Decode the full rawBytes buffer for html instead. previewText remains
bounded and is still used only for challenge/block detection. Tiers
2-4 are unaffected (their html is the browser-rendered DOM).

Fixes #46

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 00:23:37 -05:00
germondai 78836d6ed7 fix(proxy): support authenticated browser proxies 2026-07-27 03:08:16 +02:00
germondai 3a4d7491b3 Merge pull request #37 from funkypenguin/fix/bound-restart-and-reclaim-stalled 2026-07-27 02:40:41 +02:00
germondai 98e61d3770 Merge pull request #33 from edasque/feat/akamai-support 2026-07-26 17:27:02 +02:00
germondai f08f5a2b44 chore(release): prepare v1.2.0 2026-07-26 16:18:47 +02:00
germondai 748d89dfc5 refactor(proxy): normalize proxy pool fallbacks 2026-07-25 08:34:12 +02:00
germondai 1d752aee92 feat(proxy): add transparent header sanitization 2026-07-24 18:19:44 +02:00
germondai fa2bcbb64f refactor(solvers): simplify optional results 2026-07-24 16:51:07 +02:00
germondai d1ebbddf38 feat(tiers): preserve raw response payloads 2026-07-24 14:26:31 +02:00
germondai defade4324 refactor(browser): simplify optional pool state 2026-07-24 12:08:55 +02:00
germondai 3e26a7e990 feat(browser): add persistent context cache 2026-07-24 10:43:18 +02:00
germondai a62019cd55 refactor(types): add raw response metadata 2026-07-24 09:17:42 +02:00
germondai 90fc9b59dd chore(tiers): export isChallengeWall + SolveResult type from public API 2026-07-22 23:15:09 +02:00
germondai 2840af1d13 chore(release): bump all packages to v1.1.0 2026-07-22 23:13:17 +02:00
germondai 000b48d2e7 feat(tiers): extract isChallengeWall as universal challenge predicate 2026-07-22 23:08:05 +02:00
germondai 6223a4e593 perf: release v1.0.1 - slimmer image, faster boot, firefox telemetry/dead-feature prefs 2026-07-21 18:31:26 +02:00
David Young 7dae357103 fix(browser): bound every await in restartEntry; reclaim stalled checkouts
`BrowserPool.restartEntry` awaited `context.close()`, `browser.close()` and the
Camoufox launch with no timeout on any of them. Camoufox hangs on close when a
content process is wedged — tiers/3.ts and tiers/4.ts already guard their
*temporary* contexts against exactly this with a 5s `Promise.race` — but the
persistent context and browser the pool owns had no such guard, and launches can
hang too.

When any one of those hangs, the entry is pinned at `restarting = true` forever.
From then on the health check hits its own `if (entry.restarting) return` guard,
so every 30s tick logs "browser N disconnected, restarting" and does nothing.
The pool silently loses that slot permanently: `restartCount` never increments,
so the restart counter sits frozen while the log implies furious activity. With
enough uptime every entry ends up in this state and the pool is inert.

Changes:

  * every await in `restartEntry`, `init()` and `shutdown()` is bounded. On
    timeout the entry is left unhealthy with `restarting` cleared, so the next
    health-check tick retries it from scratch instead of wedging.
  * `runHealthCheck` reclaims checkouts past their deadline. Previously busy
    entries were skipped entirely, so an entry whose request wedged was never
    examined again.
  * a per-checkout `lease`, returned on the handle and passed back to
    `release()`, so a request that outlives its checkout cannot free — or
    recycle, via `noteTemporaryContext` — a browser the pool has since handed to
    someone else.
  * `release()` hands its in-flight page closes to `restartEntry` rather than
    racing them, since closing a context underneath in-flight `page.close()`
    calls is one way to wedge the transport in the first place.
  * abandoned launches are counted and capped. A timeout can only stop *waiting*
    for a launch, not cancel it, so retrying without a cap could pile up hung
    Firefox processes; past the cap the entry stays down and `live` reflects it.

Timeouts are configurable (`closeTimeoutMs`, `launchTimeoutMs`, `stallAfterMs`,
`healthIntervalMs`) with the API exposing them as BROWSER_*_MS env vars.

Adds regression tests for the hung close, the hung launch, stall accounting,
budget-aware stall deadlines, disconnected-but-busy entries, and stale releases.
The hung-close and hung-launch tests both fail against the unpatched pool.
2026-07-21 14:47:53 +12:00
David Young 961579724a fix(api): gate /health on real pool capacity, not available + busy
`/health` returns 200 as soon as `pool` is non-null, which happens before
`await pool.init()` has warmed any browser — so a readiness probe on /health
passes before the process can solve anything.

The obvious fix, `available + busy > 0`, is also wrong, and fails in a much
worse way. A request that hangs mid-solve never reaches the orchestrator's
`finally`, so it never calls `release()` and its entry stays `busy` for the life
of the process. `busy` therefore counts dead entries as capacity, and /health
can report 200/"ok" indefinitely on a pool with zero usable browsers — the
failure is completely invisible to any external check.

Adds `stalled` and `live` to PoolStats:

  * an entry is `stalled` once its checkout outlives the caller's own budget
    (req.maxTimeout, threaded through acquire()) plus a grace period, so a slow
    but genuinely live request is never miscounted
  * `live` counts entries that can serve work now or are genuinely mid-request:
    idle-and-connected, plus busy-and-connected-and-not-stalled

/health now gates on `live > 0`. A fully utilised pool still reports ready, so
this does not flap under load, but a wedged one cannot report ready at all.

`isUsable()` also checks `browser.isConnected()` rather than trusting the
`healthy` flag, which is only refreshed on the 30s health-check tick and is
never refreshed at all for busy entries.
2026-07-21 14:47:53 +12:00
Erik Dasque 4ac9e74bab feat(tiers): add Akamai Bot Manager (behavioral / sec-cpt) challenge support
trawl returned some Akamai-fronted pages as 200 'success' with only the
~2KB sec-cpt behavioral interstitial as content, because tier detection
knew Cloudflare/Imperva but not Akamai.

- detect.ts: hasAkamaiChallenge() + 'akamai' ChallengeType (sec-if-cpt-container
  / behavioral-content markers, size-gated sensor fallback); wired into
  detectChallengeType/isBlocked/needsJs.
- akamaiWait.ts (new): Akamai analogue of challengeWait/impervaWait — drives
  human-like mouse motion, press-and-hold on the behavioral widget, waits for
  the sensor's location.reload() into real content.
- tiers 1-4: escalate the 200 interstitial (needs-js), invalidate a stale
  cached-session interstitial, dispatch the resolver, report akamai-persistent.

Additive; Cloudflare/Imperva paths untouched. Verified against Edmunds.
2026-07-19 00:17:17 -04:00
germondai 6f96285544 chore(release): bump to 1.0.0 and backfill versioned changelog history
Splits the single [Unreleased] CHANGELOG block into dated 0.1.0-1.0.0
sections matching the milestone commits being tagged for issue #24
(numeric release tags), and bumps every package.json to 1.0.0.
2026-07-10 21:11:04 +02:00
germondai 040c7352fb chore(deps): bump typescript to v7 and update workspace dependencies 2026-07-10 19:19:35 +02:00
germondai 7a36a6c2c9 refactor(tiers): split into tiers/ and utils/, dedupe cookie and network-failure helpers 2026-07-09 03:23:30 +02:00
germondai 25fe9d739a feat(types): centralize BrowserHandle, BrowserFingerprint, SupportedMethod 2026-07-08 20:06:24 +02:00
germondai 4dfe5be680 fix(tiers): escalate JS-shell challenge pages from Tier 1 to browser 2026-07-08 19:40:38 +02:00
germondai 16499ce2fc feat(tiers): add audio STT fallback to hCaptcha solver 2026-07-08 19:40:24 +02:00
germondai 973505b7f3 chore(browser): suppress noExplicitAny lint in pool.ts 2026-07-08 03:49:12 +02:00
Erik Dasque 7d3204351c fix(tiers): recognize more block/error page variants, add Tier 4 captcha parity, surface proxy/timing info
Found while running trawl against a large batch of real-world URLs: several
cases where the API returned 200 with content that was actually a blocked
page, an empty challenge stub, or Firefox's own error page. Each was a
detection gap where a tier didn't recognize the failure and reported it as a
successful scrape.

- Recognize Firefox's about:neterror/about:certerror page (browser never
  reached a server), Cloudflare's static "you have been blocked" WAF-deny
  page, and a lean CF challenge stub (blank title/body, just the bootstrap
  script) — the stub check is gated on page size since the same script
  snippet also appears on ordinary, fully-loaded CF pages as bot-management
  telemetry.
- Wire the existing isBlocked() status-code check (403/429/202) into Tiers 2
  and 3 — previously only Tier 1 checked status code, so a generic non-CF WAF
  deny that escalated to a browser tier was reported as a success.
- Bring Tier 4 up to parity with Tier 3: captcha solving and the same block
  detection. Sites that need Tier 4 for IP reputation can just as easily have
  an in-page captcha widget.
- Add proxyUsed: boolean to the response, set from the actual proxy used by
  the winning tier — previously the only signal was inferring from tier === 4,
  which doesn't distinguish "no proxy" from Tier 3's datacenter proxy.
- Attach the per-tier timings array to thrown errors via a new ScrapeError,
  and return it in /scrape's error response. The array was already being
  built in memory; it just never survived the throw, so failed requests gave
  a flat error string with no way to see which tier failed or why.
- Add process-level uncaughtException/unhandledRejection handlers. One target
  site's page threw a JS error that Camoufox/Firefox reports in a shape
  playwright-core's dispatcher doesn't expect, which crashed the entire
  process and dropped every in-flight request across all clients.
- Update the native API docs for the new response fields and error shape.

All additive — no existing fields changed shape. Full existing test suite
passes (58/58), and this is rebuilt/smoke-tested against latest dev.
2026-07-07 15:58:54 +00:00
germondai d7476274a2 feat(browser): randomize OS/screen/window per browser and match HTTP UA to platform 2026-07-07 02:32:12 +02:00
germondai e42582e6b6 ci: use native bun test instead of npm-script wrapper in CI 2026-07-06 20:52:59 +02:00
germondai bb062c6bd8 test(browser): cover recycle-on-blocked and contentProcesses options 2026-07-06 20:10:19 +02:00
germondai 7b57cd4f80 refactor(tiers): add normalizeSameSite helper for cookie sameSite 2026-07-06 20:10:13 +02:00
germondai a307fe835d feat(browser): cap Firefox content processes via dom.ipc.processCount 2026-07-06 20:08:56 +02:00
germondai 1c90df92d0 feat(tiers): recycle browser only when tier returns blocked 2026-07-06 20:08:32 +02:00
GermondandGitHub 47fc0fa647 Merge pull request #14 from CoolDotty/codex/fix-browser-process-leak
Recycle browser pool after temporary contexts
2026-07-06 19:41:42 +02:00
germondai 1e5acb7975 test(tiers): cover normalizeProxy for string and object inputs 2026-07-06 13:55:25 +02:00
germondai 4adf1e0347 feat(tiers): add normalizeProxy helper for proxy field normalization 2026-07-06 13:55:02 +02:00
germondai ff1804bcb7 fix(types): accept Prowlarr object form for FlareSolverrRequest.proxy 2026-07-06 13:53:53 +02:00
CoolDotty 68ad2e0f1a Recycle browsers after temporary contexts 2026-07-06 00:47:32 -07:00