Commit Graph
100 Commits
Author SHA1 Message Date
SGavrl eab2a5eea1 Merge pull request #535 from h4ckf0r0day/fix/492-style-sync
Fix/492 style sync
2026-08-01 19:58:18 +00:00
SGavrl 108e777418 Merge remote-tracking branch 'origin/main' into maintainer/pr-492-fix 2026-08-01 19:51:52 +00:00
SGavrl 4b41d7c746 Merge pull request #534 from h4ckf0r0day/fix/517-string-timers
Fix/517 string timers
2026-08-01 19:51:23 +00:00
SGavrl 037e321a79 Merge remote-tracking branch 'origin/main' into maintainer/pr-517-tests 2026-08-01 19:47:51 +00:00
SGavrl 92576571ef Merge pull request #533 from h4ckf0r0day/fix/509-performance-clock
Fix/509 performance clock
2026-08-01 19:46:49 +00:00
SGavrl 2275d8fea0 Merge remote-tracking branch 'origin/main' into maintainer/pr-509-fix
# Conflicts:
#	crates/obscura-js/src/runtime.rs
2026-08-01 19:39:24 +00:00
SGavrl bbc4980eba Merge pull request #510 from mnaza/fix/childnode-coerce-498
fix(js): coerce non-string primitives in before/after/replaceWith
2026-08-01 17:18:11 +00:00
SGavrl 389de72d1b Merge remote-tracking branch 'origin/main' into maintainer/pr-510-review 2026-08-01 17:14:05 +00:00
SGavrl 31ac297a48 Merge pull request #508 from mnaza/fix/history-preserve-url-496
fix(js): keep the current URL when pushState/replaceState omit url
2026-08-01 17:12:59 +00:00
SGavrl e9f783b3cf Merge pull request #511 from mnaza/fix/quirks-mode-selectors-499
fix(dom): honor quirks mode for class/id selector case-sensitivity
2026-08-01 17:12:44 +00:00
SGavrl ec7334d91b Merge pull request #512 from mnaza/fix/remove-double-free-500
fix(dom): guard DomTree::remove() against OOB and double-free
2026-08-01 17:12:28 +00:00
SGavrl 0428099b0f Merge remote-tracking branch 'origin/main' into maintainer/pr-509-fix 2026-08-01 16:04:37 +00:00
SGavrl 907e15d85d perf(js): avoid per-element style sync objects 2026-08-01 15:46:25 +00:00
SGavrl 82aa320084 Merge remote-tracking branch 'origin/main' into maintainer/pr-492-fix 2026-08-01 15:44:40 +00:00
SGavrl 16b6a09546 Merge remote-tracking branch 'origin/main' into maintainer/pr-517-tests 2026-08-01 15:37:17 +00:00
SGavrl dbc87e86f9 test(js): cover string timer handlers 2026-08-01 15:37:17 +00:00
SGavrl b5c2f9cc4e Merge branch 'maintainer/pr-508-review' into maintainer/pr-510-review
# Conflicts:
#	crates/obscura-js/src/runtime.rs
2026-08-01 15:30:32 +00:00
SGavrl caf51975bb Merge remote-tracking branch 'origin/main' into maintainer/pr-508-review 2026-08-01 15:28:07 +00:00
SGavrl 3f5dd23778 Merge remote-tracking branch 'origin/main' into maintainer/pr-510-review 2026-08-01 15:28:02 +00:00
SGavrl d93ff517fc fix(js): keep performance clock bounded 2026-08-01 15:20:35 +00:00
SGavrl d66e5ec110 Merge pull request #526 from marcoripa96/native-ca-roots
feat(net): trust platform CA roots alongside bundled webpki roots
2026-07-31 17:44:01 +00:00
SGavrl bb21452f90 Merge pull request #532 from alectimison-maker/fix/dynamic-script-data-urls
fix(js): execute dynamic data URL scripts
2026-07-31 17:43:45 +00:00
SGavrl 0def838088 Merge pull request #523 from stack3mpty/fix/missing-event-constructors-521-522
fix(js): add global PromiseRejectionEvent and StorageEvent constructors
2026-07-31 17:43:25 +00:00
SGavrl f46f741e8c fix(net): load configured CA roots without default cost 2026-07-30 21:57:18 +00:00
SGavrl 0ff1ba0ea9 fix(js): match event constructor semantics 2026-07-30 21:43:33 +00:00
SGavrl f84120591e fix(js): match Chromium data URL handling 2026-07-30 21:40:13 +00:00
SGavrl 3a68457c0b Merge pull request #530 from lisa0314/fix/524-react-controlled-inputs
fix(mcp): fill React controlled inputs
2026-07-29 22:24:20 +00:00
SGavrl f153487030 Update README.md 2026-07-29 01:31:08 +02:00
SGavrl 450eaa3b88 Added sponsor logo 2026-07-28 23:27:45 +00:00
SGavrl e78b5e6026 Merge pull request #516 from h4ckf0r0day/fix/macos-stealth-sdk-506
ci(release): use active macOS SDK for stealth builds
2026-07-26 11:50:20 +00:00
SGavrl 0670e738a0 ci(release): use active macOS SDK for stealth builds 2026-07-26 11:47:15 +00:00
SGavrl 8078cc3070 Merge pull request #506 from h4ckf0r0day/fix/release-stealth-build-483
ci(release): publish explicit stealth artifacts
2026-07-26 11:21:54 +00:00
SGavrl 19aeb1eddb Merge pull request #505 from h4ckf0r0day/fix/mcp-read-timeout-501
fix(mcp): bound HTTP request read time
2026-07-26 11:21:22 +00:00
SGavrl 78a3e997e7 ci(release): publish explicit stealth artifacts 2026-07-26 11:05:13 +00:00
SGavrl 750ffa62a9 fix(mcp): bound HTTP request read time 2026-07-26 11:05:04 +00:00
SGavrl 6ad8eb9040 Merge pull request #502 from h4ckf0r0day/fix/op-reentrancy-453-v2
fix(js): scope fetch clients to browser contexts
2026-07-26 10:32:16 +00:00
SGavrl ab6fa0e39b fix(js): scope fetch clients to browser contexts 2026-07-26 10:20:25 +00:00
SGavrl 4af658351d Merge pull request #493 from mnaza/fix/mcp-sse-wedge-489
fix(mcp): detach SSE keep-alive so it cannot wedge the HTTP server
2026-07-26 10:18:38 +00:00
SGavrl 08d7b5e254 Merge pull request #485 from ulises2k/fix/svg-parsed-namespace
fix(js,dom): keep parsed SVG in the SVG namespace so href reflects an SVGAnimatedString
2026-07-26 10:18:19 +00:00
SGavrl e023cc87cb Merge pull request #487 from ulises2k/security/quinn-proto-GHSA-4w2j-m93h-cj5j
security(deps): bump quinn-proto 0.11.14 -> 0.11.15 (GHSA-4w2j-m93h-cj5j)
2026-07-26 10:17:59 +00:00
SGavrl e314b9b0f3 Merge pull request #471 from mnaza/fix/treewalker-movers-469
fix(js): descend on FILTER_SKIP in TreeWalker child/sibling movers (#469)
2026-07-24 15:21:58 +00:00
SGavrl 0df158ec9d Merge remote-tracking branch 'origin/main' into pr471-push
# Conflicts:
#	crates/obscura-js/js/bootstrap.js
2026-07-24 15:19:29 +00:00
SGavrl dd9f3ec41f Merge pull request #480 from h4ckf0r0day/fix/template-serialize-perf
perf(dom): avoid per-element node clone when serializing template contents
2026-07-24 15:16:02 +00:00
SGavrl becb3d941f perf(dom): avoid per-element node clone when serializing template contents
The template.content bridge (#463) routed every element through
content_source(), which called get_node().clone() to read the Copy
template_contents field. That deep-cloned each element's Node (its attrs
Vec plus a String per attribute) on every innerHTML/outerHTML/dump-html
serialization, once per non-void element.

template_contents is already destructured from the node in the serialize
loop, so read it there directly. content_source() (inner_html path) now
reads it through with_node() by reference. Serialization output is
byte-identical; ~10% faster on serialization-heavy pages.
2026-07-24 14:37:42 +00:00
SGavrl 98f4efae1c Merge pull request #470 from mnaza/fix/window-scroll-468
fix(js): make window-level scrolling actually scroll (#468)
2026-07-24 14:31:00 +00:00
SGavrl 3f820c48c9 Merge pull request #458 from asap-cool/fix/scroll-event-on-assignment
fix(js): fire a scroll event when scrollTop/scrollLeft are assigned
2026-07-24 14:30:47 +00:00
SGavrl 4c8213a62c Merge pull request #479 from j00jun/fix/iframe-event-dispatch
fix(js): make iframe events reach registered listeners
2026-07-24 14:30:31 +00:00
SGavrl 85a6b7bc85 Merge pull request #477 from lisa0314/fix/474-dynamic-script-settle
fix(js): allow pending dynamic scripts to finish settling
2026-07-24 14:30:13 +00:00
SGavrl a9c25976b2 Merge pull request #476 from mnaza/fix/treewalker-parentnode-475
fix(js): implement DOM parentNode() traversal in TreeWalker (#475)
2026-07-24 14:29:57 +00:00
SGavrl 3c94d69870 Merge pull request #472 from mnaza/fix/node-iterator-467
fix(js): implement a real NodeIterator instead of aliasing TreeWalker (#467)
2026-07-24 14:29:41 +00:00
SGavrl be18346a96 Merge pull request #466 from mnaza/fix/template-content-463
fix(dom): bridge <template> contents to JS so .content is not always empty (#463)
2026-07-24 14:29:23 +00:00
SGavrl 485497393d Merge pull request #465 from mnaza/fix/treewalker-previousnode-462
fix(js): walk reverse document order in TreeWalker.previousNode() (#462)
2026-07-23 10:32:49 +00:00
SGavrl 61dd2f764e Merge pull request #473 from lisa0314/test/js-runtime-page-init
test(js): initialize runtime fixtures before evaluation
2026-07-23 10:31:10 +00:00
SGavrl 36d8e43140 Update AGENTS.md 2026-07-22 19:12:44 +02:00
SGavrl 45aba21165 Update README.md 2026-07-22 19:11:35 +02:00
SGavrl 2974a71f3c Merge pull request #456 from h4ckf0r0day/fix/cdp-context-isolation-449
Isolate CDP browser contexts
2026-07-22 11:36:49 +00:00
SGavrl 68d5824c8b Merge pull request #455 from h4ckf0r0day/fix/networkinformation-eventtarget-277
Fix NetworkInformation event listeners
2026-07-22 11:35:52 +00:00
SGavrl 409ac781d2 Isolate CDP browser contexts 2026-07-22 10:45:36 +00:00
SGavrl fc9f5248f7 Fix NetworkInformation event listeners 2026-07-22 10:29:51 +00:00
SGavrl 730bd01f57 Merge pull request #454 from lisa0314/fix/452-document-fragment-insertion
fix(js): flatten DocumentFragment during node insertion
2026-07-22 10:13:02 +00:00
SGavrl 43d542da37 Merge pull request #435 from asap-cool/upstream-430
fix(cdp): confine V8 isolates per connection to end the #430 concurrency abort
2026-07-22 10:12:35 +00:00
SGavrl 72d66bc5c0 Merge pull request #447 from h4ckf0r0day/fix/treewalker-432
fix(js): keep TreeWalker traversal in document order (#432)
2026-07-19 19:50:04 +00:00
SGavrl c12915a1a6 fix(js): keep TreeWalker traversal in document order (#432) 2026-07-19 19:40:27 +00:00
SGavrl 113b492ed5 Update README.md 2026-07-16 12:07:50 +02:00
SGavrl 99ece0e12d feat(net): make passive request/response callbacks detachable (#416)
on_request/on_response stored callbacks in append-only Vecs with no way
to remove one, so a crawler that installed a response-body capture hook
for one phase could never detach it: it fired for the shared client's
whole lifetime, re-registering across navigations piled up duplicates,
and (because the client is shared per context) it saw sibling pages'
requests too.

Tag each callback with a stable id. on_request/on_response now return
the id and off_request/off_response (remove_on_request/remove_on_response
on the client) detach a specific one. Storage is Vec<(u64, Callback)>;
all fire sites destructure the tuple. Closes #408.
2026-07-14 13:57:45 +00:00
SGavrl d0b6fc4ffd fix(cdp): emit Network events for script-initiated requests (#415)
Requests made by page JS (fetch/XHR/dynamic script and stylesheet loads)
run in the V8 op layer and never produced a NetworkEvent, so CDP emitted
Network.requestWillBeSent / responseReceived only for the static
navigation subresources. Puppeteer/Playwright page.on('request') and
on('response') therefore missed every XHR/JSON response, which breaks
response capture (and is the root cause of the Aviasales half of #394).

op_fetch_url now records a JsNetworkEvent keyed by the same fetch-{N} id
under which it already stores the response body. The Page drains these
into its network_events before the CDP layer emits, so they flow through
emit_navigation_events as requestWillBeSent / responseReceived /
loadingFinished alongside the document subresources, and
Network.getResponseBody resolves for them via the shared id. Applied to
both navigation paths (server process_navigation and in-process
do_navigate). The queue is capped at 4096 events per page.

Closes #406.
2026-07-14 13:23:55 +00:00
SGavrl 358100aa63 fix(cdp): prune valid_context_ids on executionContextsCleared (#413)
Runtime.executionContextsCleared on navigation told clients every prior
context was gone, but valid_context_ids was insert-only, so a
Runtime.callFunctionOn targeting a pre-navigation context id still ran
instead of being rejected with Chrome's 'Cannot find context with
specified id'. On a long-lived CDP connection the set also grew by one
id per navigation and was never pruned.

Clear valid_context_ids when executionContextsCleared is emitted, then
re-register the default world (id 2) and the isolated worlds as their
executionContextCreated events are emitted. Closes #407.
2026-07-14 12:23:29 +00:00
SGavrl c8c78ba229 fix(js): fire load/error for dynamically inserted stylesheet links (#412)
A <link rel=stylesheet href> appended via JS (createElement +
appendChild) was never fetched and fired neither load nor error, so any
Promise.all awaiting the link's onload (lazy CSS loaders, antd/bootstrap
fetchers) hung forever and the page stalled at its pre-load stage.

appendChild already drives this for <script src>; mirror it for links:
resolve the href against <base>/document URL, fetch it, and dispatch
load on success or error on failure. CSS is not applied (no layout
engine yet) but the load-event contract matches Chrome.

Read the stylesheet intent from both the rel attribute and the rel
property, since obscura does not yet reflect the rel IDL attribute back
to the content attribute. Closes #409.
2026-07-14 12:13:25 +00:00
SGavrl 1ca6047351 feat(cdp): stream large response bodies via takeResponseBodyAsStream + IO (#381)
Network.getResponseBody returns the whole body in one base64 blob and the
cache drops anything over 2 MiB, so large downloads (images, PDFs,
archives) either failed or forced the long-running server to hold the
full body in memory (issue #360).

Add Fetch.takeResponseBodyAsStream + IO.read + IO.close. The body is moved
out of the page cache into a stream handle and delivered in bounded chunks
via IO.read, then freed on IO.close, so it is held once and released after
the download. Binary bodies are byte-exact (stored base64). To stream a
large body, raise OBSCURA_NETWORK_BODY_BUFFER_BYTES so it is cached.

This is entirely opt-in: nothing runs unless a client calls the new
methods, so the default fetch/render/scrape path is unchanged (verified:
obstacle course 33/33, obscura-vs-Chrome head-to-head within noise of the
pre-change baseline).

Fixes #360.
2026-07-10 20:38:28 +00:00
SGavrl d3a8b9a482 fix(js): return module eval on completion, not loop idle (Vite mount) (#380)
load_module and load_inline_module drove run_event_loop to full idle
before reading the module's mod_evaluate result. A page timer keeps the
loop busy forever (Vite's HMR / React-Refresh client installs a
setInterval), so the preamble inline module burned the entire budget
waiting for an idle that never came, and the later module that mounts the
app was skipped, leaving #root empty while the production preview rendered
fine (issue #374).

Drive the event loop and the mod_evaluate result together and return as
soon as the module finishes evaluating, so a busy background timer no
longer starves the remaining modules. Post-eval async work is still
drained by the normal post-load settle.

Fixes #374.
2026-07-10 20:36:08 +00:00
SGavrl 4bc69c5209 Update README.md 2026-07-10 19:44:18 +00:00
SGavrl fa9ba7f13d fix(browser): raise script-execution budget so heavy SPAs boot (#371)
The script-execution phase had a 10s soft deadline. On a heavy SPA the
remaining scripts were skipped before the app finished booting, so it
never fired its XHR/fetch calls and page.on('response') captured zero API
responses (issue #361). The knob to extend it, OBSCURA_SCRIPT_DEADLINE_MS,
existed but was undocumented, so it was undiscoverable.

Raise the default to 30s and document the env var. Only pages that keep
running past the deadline are affected; fast pages finish and return well
before it, so the larger budget costs them nothing, and the per-phase
watchdog (armed at deadline + 1s) still bounds a real synchronous hang.
Very heavy SPAs on slow networks can raise it further with
OBSCURA_SCRIPT_DEADLINE_MS, paired with a matching client nav timeout.

Fixes #361.
2026-07-09 11:07:34 +02:00
SGavrl f5d7e11692 fix(cli): pass worker proxy via env, not argv (credential leak) (#370)
obscura serve --workers N --proxy ... spawned each worker with the proxy
as a --proxy CLI flag, so the plaintext credential was visible to any
local user via ps / /proc/<pid>/cmdline (issue #366). scrape already
avoids this by passing the proxy in OBSCURA_PROXY, which is only readable
by the owner.

Pass the proxy to serve workers via OBSCURA_PROXY too, and have the serve
command read that env as a fallback when no --proxy flag is given. The
worker argv no longer contains the credential.

Fixes #366.
2026-07-09 10:52:06 +02:00
SGavrl 6788996b0b feat(cdp): implement DOM.setFileInputFiles for file uploads (#369)
Puppeteer's ElementHandle.uploadFile and Playwright's setInputFiles drive
an <input type=file> through CDP DOM.setFileInputFiles, which obscura
rejected as an unknown method, so any upload flow failed (issue #359).

Implement it: resolve the node, read each local file, and hand the bytes
(base64) to the JS layer, which builds real File objects (backed by the
bytes, so page code can read them via FileReader or upload them via
fetch/FormData) and fires input+change like a genuine selection. The file
input exposes a FileList via .files, and .value reads C:\fakepath\<name>
to match Chrome.

Fixes #359.
2026-07-09 10:51:18 +02:00
SGavrl 8f26dfa331 fix(cli): make --dump text extraction iterative (deep-DOM crash) (#368)
extract_readable_text recursed once per DOM nesting level, so a deeply
nested page overflowed the call stack and aborted the process (exit 134)
on --dump text. This is the CLI counterpart of the serialize/textContent
walks made iterative in obscura-dom; that fix did not cover this function.

Rewrite it as an iterative DFS over an explicit work stack, with a
Newline work item for a block element's trailing newline so output is
byte-for-byte unchanged, plus the same defense-in-depth node cap the DOM
walkers use. Relates to #362.
2026-07-09 10:49:44 +02:00
SGavrl 5c3d560d8e test(js): lock script-error isolation for the babel-polyfill case (#358)
Regression test for #355: an uncaught `throw` in one inline <script> must
not prevent later independent <script>s from running. This was fixed by
the #147 poison-guard (present on main) but reported again against 0.1.9
with an explicit throw; this pins the exact scenario so it cannot regress.

Closes #355.
2026-07-07 09:35:49 +02:00
SGavrl ad7a7a93d1 fix(js): support in/Object.keys on style and dataset (CSSOM parity) (#357)
el.style (CSSStyleDeclaration) and el.dataset (DOMStringMap) were plain
Proxies with only get/set traps, so `'color' in el.style`, `Object.keys
(el.style)`, `'foo' in el.dataset`, and `Object.keys(el.dataset)` all
failed, and Object.keys(el.style) leaked the internal `_props` (issue
#356). cssText also serialized camelCase names without a trailing
semicolon.

Add has / ownKeys / getOwnPropertyDescriptor traps to both proxies so the
`in` operator and enumeration behave like real CSSOM. CSSStyleDeclaration
now stores properties by their dashed name (matching the spec), so
el.style.fontSize and getPropertyValue('font-size') stay in sync, and
cssText serializes dashed names with a trailing semicolon. A known CSS
property list backs `in`/enumeration so feature detection ('object-fit'
in el.style) works for unset properties.

Closes #356.
2026-07-07 09:26:23 +02:00
SGavrl 14286e7ad6 Update README.md 2026-07-06 23:25:37 +02:00
SGavrl 205eefaf6b Added sponsor logo 2026-07-06 21:15:57 +00:00
SGavrl ca71ce3c2d docs: add Integrations section linking the Hermes agent plugin (#354)
Point users at hermes-plugin-obscura, which runs Hermes agent browser
tasks on Obscura over CDP as a local backend.
2026-07-03 14:20:54 +02:00
SGavrl 08c1f0d721 fix(cdp): fire onChange for React/Vue controlled inputs (#353)
Typing over CDP into a React controlled input never fired onChange, so
the value never reached component state (issue #324). Two root causes,
both needed:

1. Value tracker. React/Preact/Vue install a value tracker by redefining
   `value` on the element instance to record the last value they wrote.
   obscura's CDP typing assigned `el.value = ...` directly, which runs
   that wrapper and keeps the tracker in sync, so React saw no change on
   the input event. Synthetic value/backspace writes now go through a new
   __obscura_setFieldValue helper that writes via the element's prototype
   setter, bypassing the tracker. The tracker stays stale, so the input
   event reads as a real edit.

2. Event handler feature detection. React decides whether to use the
   modern `input` event via `("oninput" in document)`. obscura exposed the
   GlobalEventHandlers on* attributes only on window, so that check failed
   and React fell back to a legacy change-detection path that ignores
   input events. The on* attributes are now also present on Document and
   Element, matching the HTML spec (GlobalEventHandlers is mixed into
   Window, Document, and HTMLElement).

Verified end to end over CDP against the real React 18 UMD build: typing
into controlled inputs updates state, the component re-renders, and a
submit handler reads the captured values.

Closes #324.
2026-07-03 14:10:03 +02:00
SGavrl b5039a8a7f feat(cli): add --file and --concurrency batch mode to fetch (#352)
fetch handled one URL at a time, so checking many raw resources meant
wrapping it in an external loop. Add --file <path> to read newline-
delimited URLs (- for stdin; blank lines and # comments skipped) and
--concurrency <N> to fetch them in parallel bounded by a semaphore.

Batch mode is raw HTTP only (--dump original, the default in this mode)
and prints one JSON status line per URL (url, ok, status, content_type,
bytes, elapsed_ms, or error) in input order. Rendered/DOM batch work
still belongs to scrape. A single positional URL is unchanged.

Fixes #349.
2026-07-03 13:25:17 +02:00
SGavrl a5a8de7c8c fix(js): make new Image() a real HTMLImageElement so .style works
new Image() used a bare class shim with no .style, so new Image().style
returned undefined and libraries touching style on a detached image threw
(issue #350). Build the image from document.createElement('img') instead,
which gives a working CSSStyleDeclaration, attribute reflection, and event
dispatch. Assigning .src still emulates a successful decode: it flips
complete and fires load (both the onload property and load listeners) so
lazy loaders and preloaders do not hang.
2026-07-03 13:24:20 +02:00
SGavrl 99d95fcdb4 Update README.md 2026-07-03 10:47:53 +02:00
SGavrl 1042d646af Add sponsor logo 2026-07-03 08:47:01 +00:00
SGavrl 016387021b feat(js): implement WebCrypto SubtleCrypto secret-key algorithms
feat(js): implement WebCrypto SubtleCrypto secret-key algorithms
2026-07-01 16:03:31 +00:00
SGavrl f72234d99e fix: harden comment serialization and DOMSnapshot recursion depth
fix: harden comment serialization and DOMSnapshot recursion depth
2026-07-01 16:02:55 +00:00
SGavrl e7300c5df7 fix(cdp): make DOMSnapshot walk iterative to bound recursion depth
The DOMSnapshot walk recursed one frame per nesting level. The MAX_NODES
cap bounds the node count but not the depth, so a deeply nested linear
chain (script can build thousands of nested elements) could overflow the
stack before the count guard triggered. Rewrite it with an explicit stack
so depth lives on the heap. Pre-order traversal and the MAX_NODES cap are
preserved.

Fixes #341.
2026-07-01 15:58:49 +00:00
SGavrl 4603c05ab2 fix(dom): neutralize "-->" when serializing comment contents
The comment serializer wrote its contents verbatim. The HTML parser can
never produce a comment whose data contains "-->" (it ends the comment),
but script can via document.createComment("a-->b"). Serializing that
verbatim closed the comment early and let the trailing text escape into
markup. Replace "-->" with "--&gt;" so the output round-trips as a single
comment.

Fixes #342.
2026-07-01 15:58:49 +00:00
SGavrl ae684fae9c fix(cdp): expose main-resource response body and metadata on navigation 2026-07-01 07:02:32 +00:00
SGavrl 7440b534f3 fix(cdp): stub setDownloadBehavior, detachFromTarget, activateTarget
These Chrome CDP methods logged "Unknown method" warnings. obscura runs
one page per session and does not download to disk, so there is nothing
to do, but clients that call them should get a success ack instead of a
warning. Added as no-op stubs.

Refs #340.
2026-07-01 06:55:37 +00:00
SGavrl 630f9894f6 fix(cdp): expose main-resource response body and real mime on navigation
Navigating straight to an image, PDF, or other resource could not read the
main response body over CDP, and Page.frameNavigated always reported
text/html.

Two problems: the main resource's body was stored under its internal
request id, but the client sees it as the navigation loaderId (Chrome's
requestId === loaderId convention), so Network.getResponseBody(loaderId)
missed. And binary bodies were stored as UTF-8-lossy text, corrupting the
bytes. Alias the stored body to the loaderId, store non-text-like content
types (images, PDF, octet-stream) base64, and report the response's real
Content-Type in frameNavigated. HTML pages are unchanged.

Fixes #340.
2026-07-01 06:55:37 +00:00
SGavrl d5d9b7788f fix(js): DOM conformance for checkbox value and NodeList
Two standard-conformance fixes surfaced by #335 (the issue itself is a commercial anti-bot challenge and out of scope; these are the legitimate browser-compatibility gaps from it).

• checkbox/radio value: an input with no value attribute returns "on" in a real browser; obscura returned "". An explicit value attribute still wins.
• NodeList: childNodes returned a plain Array, and NodeList extended Array, so Array.isArray(list) was true and Object.prototype.toString reported "[object Array]". A real browser reports "[object NodeList]" with Array.isArray false. NodeList is now its own type with the array-like surface scripts use (indexed access, length, item, forEach, entries/keys/values, iteration), and childNodes goes through it.

Verified: checkbox/radio default "on" (explicit value preserved), childNodes and querySelectorAll are real NodeLists (constructor "NodeList", "[object NodeList]", Array.isArray false), and spread / for..of / Array.from still work. Obstacle course 33/33, obscura-dom 33/33.

Refs #335.
2026-06-30 15:27:05 +00:00
SGavrl 25ce541ec8 fix(js): make NodeList a real type and childNodes a NodeList
childNodes returned a plain Array (constructor "Array"), and NodeList
extended Array, so Array.isArray(list) was true and
Object.prototype.toString reported "[object Array]". A real browser
reports "[object NodeList]" and Array.isArray is false, which
feature-detection and fingerprinting scripts check.

NodeList is now its own type with the array-like surface scripts use
(indexed access, length, item, forEach, entries/keys/values, iteration),
and childNodes goes through it. Verified spread, for..of, and Array.from
still work.

Refs #335.
2026-06-30 15:18:21 +00:00
SGavrl a16e8d4fe3 fix(js): default checkbox and radio value to "on"
A checkbox or radio input with no value attribute returns "on" in a real
browser; obscura returned the empty string. Page scripts that read
input.value to build form submissions (and browser-environment checks)
saw the wrong default. An explicit value attribute still wins.

Refs #335.
2026-06-30 15:18:21 +00:00
SGavrl 1826570927 fix(cli): bind multi-worker load balancer to --host
The single-worker serve path honored --host, but run_multi_worker_serve hardcoded the load balancer to 127.0.0.1. With --host 0.0.0.0 and --workers >1 (the common Docker setup) the balancer bound loopback only, so the mapped port was refused from outside the container. Bind the balancer to the requested host. Workers stay on loopback and are reached only by the balancer.

Closes #336.
2026-06-30 08:10:54 +00:00
SGavrl 9837262af8 fix(cli): bind multi-worker load balancer to --host
The single-worker serve path honored --host, but run_multi_worker_serve
hardcoded the load balancer to 127.0.0.1. With --host 0.0.0.0 and
--workers >1 (the common Docker setup) the balancer bound loopback only,
so the mapped port was refused from outside the container. Bind the
balancer to the requested host. Workers stay on loopback and are reached
only by the balancer.

Fixes #336.
2026-06-30 08:05:48 +00:00
SGavrl 50ef1eecbd fix: SVGAnimatedString, :has() selectors, and cookie persistence on serve 2026-06-29 11:00:22 +00:00
SGavrl b63125faed cdp: flush cookies on SIGTERM in serve (#333)
serve --storage-dir saved cookies only on Ctrl-C (SIGINT). docker stop
and kill send SIGTERM, which was not caught, so the save_cookies() call
at shutdown never ran and cookies were lost on restart. Watch SIGTERM as
well as Ctrl-C on Unix.
2026-06-29 10:48:20 +00:00
SGavrl 2840b08b56 dom: support :has() selectors (#331)
querySelectorAll / --selector silently returned nothing for :has().

Two fixes were needed:
- Enable :has() parsing: the selectors crate gates it on the parser's
  parse_has() (default false), so a:has(p) failed to parse and the error
  was swallowed into an empty match set.
- Fix OpaqueElement stability: opaque() was keyed off the transient
  DomElement (which is Copy and gets a fresh stack address each traversal
  step), so the same node produced a different id every call. :has anchor
  matching compares the anchor's opaque against the element reached by
  walking up the tree, so it never matched. Key opaque() off the node's
  stable slot in the tree's Vec instead.
2026-06-29 10:48:20 +00:00