Skip to main content

bao_browser/
runtime_bridge.rs

1// @trace REQ-BRW-003 [entity:CdpRouter] [entity:CdpSession]  REQ-BRW-001: Bridge between servo browser context and Node.js APIs
2// REQ-ENG-007: Unified runtime coordination
3//
4// Architecture: dual-Realm isolation via SpiderMonkey compartments
5// - servo's JSContext handles DOM + Web APIs in Page Realm (Window global)
6// - Bao creates a separate Node Realm (JS_NewGlobalObject) for privileged scripts
7// - Node Realm is in its own Compartment — Page Realm physically cannot see it
8// - evaluate_js() uses EnterRealm(Node) → execute → LeaveRealm → back to Page Realm
9// - evaluate_js_web() executes directly in Page Realm (no Realm switch needed)
10//
11// JSContext fusion:
12// - servo creates JSContext internally in JSEngineSetup::default()
13// - Both Realms share the same JSContext (servo's script thread)
14// - GC is shared across both Realms
15// - Node Realm lifecycle is tied to Page — destroyed when Page closes
16
17use crate::error::BrowserError;
18use crate::page::PageHandle;
19// NOTE: bao_engine::WebWorker bypass removed per DEC-WK-001 BCE-20260627-008.
20// Workers now route through servo's native Worker::Constructor.
21use dashmap::DashMap;
22use mozjs::rooted;
23use std::cell::RefCell;
24use std::ptr::{self, NonNull};
25use std::sync::atomic::{AtomicBool, Ordering};
26use std::sync::mpsc;
27use std::sync::{Arc, OnceLock};
28
29use std::time::Duration;
30
31// @trace REQ-SEC-002 [entity:EvaluateResult]
32/// Result of evaluating a script in the Node Realm.
33///
34/// Captures the serialized return value or an error message from
35/// `evaluate_in_node_realm`. Both fields are `Option<String>`:
36/// - `value` is `Some` when the script produced a non-undefined result.
37/// - `error` is `Some` when the evaluation or serialization failed.
38///
39/// At most one of `value` / `error` is `Some` — never both.
40#[derive(Debug, Clone, PartialEq, Eq, Default)]
41pub struct EvaluateResult {
42    /// Serialized JS return value (JSON string), or None if undefined/error.
43    pub value: Option<String>,
44    /// Error message when evaluation failed, or None on success.
45    pub error: Option<String>,
46}
47
48impl EvaluateResult {
49    /// Create an ok result with a serialized value.
50    pub fn ok(value: String) -> Self {
51        EvaluateResult {
52            value: Some(value),
53            error: None,
54        }
55    }
56
57    /// Create an error result with a message.
58    pub fn err(error: String) -> Self {
59        EvaluateResult {
60            value: None,
61            error: Some(error),
62        }
63    }
64
65    /// Returns true when the evaluation succeeded (no error).
66    pub fn is_ok(&self) -> bool {
67        self.error.is_none()
68    }
69
70    /// Returns true when the evaluation failed.
71    pub fn is_err(&self) -> bool {
72        self.error.is_some()
73    }
74}
75
76// @trace REQ-SEC-002 REQ-SEC-003 [req:REQ-SEC-002,REQ-SEC-003]
77// Per-page Node Realm storage, keyed by WebViewId (NOT by raw *mut JSObject).
78//
79// BCE-20260621-001 ROOT CAUSE: previous design used three process-wide statics
80// (NODE_REALMS: DashMap<usize,usize>, PAGE_GLOBALS: DashMap<usize,usize>,
81// LAST_PAGE_GLOBAL: AtomicUsize) holding cross-thread *mut JSObject raw pointers.
82// servo spawns one ScriptThread per pipeline on its own OS thread
83// (components/script/script_thread.rs:527 `thread::Builder::new().name("Script#{id}")`)
84// each with a thread-local JSContext (script_runtime.rs:740 `cx()` reads from
85// `RustRuntime::get()` thread-local slot; SAFETY: "only one JSContext can exist
86// on the thread"). Globals storing cross-thread *mut JSObject → callback for
87// page B might dereference a JSObject created on page A's ScriptThread with
88// page B's cx → activation stack corruption → SIGSEGV in
89// js::jit::BaselineFrame::initForOsr (BaselineFrame.cpp:153).
90//
91// ROOT FIX (BCE-20260621-001):
92// - NODE_REALM_BY_WEBVIEW: per-page node_global keyed by WebViewId. Values are
93//   raw pointers but they are ONLY ever dereferenced on the same ScriptThread
94//   that created them (via the WebViewId-keyed servo callback which always
95//   runs on that page's ScriptThread). The main thread reads the pointer as
96//   an opaque address and passes it back into another WebViewId-keyed callback;
97//   it never dereferences it.
98// - PAGE_GLOBAL_BY_WEBVIEW: per-page servo Window global, also keyed by
99//   WebViewId, also only dereferenced inside that page's ScriptThread callback.
100// - LAST_PAGE_GLOBAL: ELIMINATED. inject_node_apis_with_stealth now passes the
101//   WebViewId through and reads the page_global via a per-WebViewId OnceLock,
102//   not a process-wide global. This removes the "last writer wins" race that
103//   caused PageInner to capture the wrong page's pointer.
104// - thread_local! PER_THREAD_PAGE_GLOBAL: for lazy_dom_getter_impl, which
105//   executes inside a ScriptThread (one WebView per ScriptThread), so a
106//   thread-local is correct and avoids any cross-thread pointer storage.
107//
108// @trace REQ-PERF-003 [entity:BufferManager]
109// REQ-PERF-003: WebViewId-keyed OnceLock<DashMap> gives O(1) per-page lookup
110// with DashMap sharded locks; OnceLock avoids lazy_static overhead.
111//
112// @trace REQ-BRW-003 [req:REQ-BRW-003] [criterion:C10]
113// C10 (NFR-THREAD-SAFETY): no cross-thread *mut JSObject dereference. Pointers
114// flow only WebViewId-keyed ⇒ same-ScriptThread access.
115static NODE_REALM_BY_WEBVIEW: OnceLock<DashMap<servo::WebViewId, usize>> = OnceLock::new();
116static PAGE_GLOBAL_BY_WEBVIEW: OnceLock<DashMap<servo::WebViewId, usize>> = OnceLock::new();
117
118fn node_realm_by_webview() -> &'static DashMap<servo::WebViewId, usize> {
119    NODE_REALM_BY_WEBVIEW.get_or_init(DashMap::new)
120}
121
122fn page_global_by_webview() -> &'static DashMap<servo::WebViewId, usize> {
123    PAGE_GLOBAL_BY_WEBVIEW.get_or_init(DashMap::new)
124}
125
126// ScriptThread-local current page_global. Used by lazy_dom_getter_impl, which
127// runs as a JSNative ON the ScriptThread. Set during create_node_realm_native
128// and inject callbacks (same thread). SAFETY: only ever read/written on the
129// owning ScriptThread; Send/Sync are NOT required for thread_local! data.
130thread_local! {
131    static PER_THREAD_PAGE_GLOBAL: RefCell<*mut mozjs::jsapi::JSObject> =
132        const { RefCell::new(ptr::null_mut()) };
133}
134
135/// Store a Node Realm global pointer for a specific page, keyed by WebViewId.
136///
137/// SAFETY contract (BCE-20260621-001 C10): both pointers must have been created
138/// on the same ScriptThread that owns `webview_id`. The pointers are stored as
139/// addresses only; they MUST NOT be dereferenced off that ScriptThread.
140fn store_node_realm(
141    webview_id: servo::WebViewId,
142    page_global: *mut mozjs::jsapi::JSObject,
143    node_global: *mut mozjs::jsapi::JSObject,
144) {
145    node_realm_by_webview().insert(webview_id, node_global as usize);
146    page_global_by_webview().insert(webview_id, page_global as usize);
147}
148
149/// Look up Node Realm global pointer for a specific page (by WebViewId).
150///
151/// Returns an opaque address — callers must only use it on the same ScriptThread
152/// that owns `webview_id` (i.e., inside a `register_script_thread_callback`
153/// callback for that WebViewId).
154fn get_node_realm_by_id(webview_id: servo::WebViewId) -> *mut mozjs::jsapi::JSObject {
155    match node_realm_by_webview().get(&webview_id) {
156        Some(v) => *v as *mut mozjs::jsapi::JSObject,
157        None => ptr::null_mut(),
158    }
159}
160
161/// Look up servo Window global pointer for a specific page (by WebViewId).
162fn get_page_global_by_id(webview_id: servo::WebViewId) -> *mut mozjs::jsapi::JSObject {
163    match page_global_by_webview().get(&webview_id) {
164        Some(v) => *v as *mut mozjs::jsapi::JSObject,
165        None => ptr::null_mut(),
166    }
167}
168
169/// Remove Node Realm for a specific page (called on page close).
170pub fn remove_node_realm_by_id(webview_id: servo::WebViewId) {
171    node_realm_by_webview().remove(&webview_id);
172    page_global_by_webview().remove(&webview_id);
173}
174
175/// Clear all stored Node Realm pointers (for test isolation).
176fn clear_all_node_realms() {
177    node_realm_by_webview().clear();
178    page_global_by_webview().clear();
179}
180
181/// Test serialization lock for per-page storage operations.
182/// cargo test runs tests in parallel by default; tests that share the global
183/// maps must be serialized to prevent data races (store from test A cleared
184/// by test B).
185static TEST_SERIAL_LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
186
187fn test_serial_lock() -> &'static std::sync::Mutex<()> {
188    TEST_SERIAL_LOCK.get_or_init(|| std::sync::Mutex::new(()))
189}
190
191/// Register a callback to refresh DOM proxies in the Node Realm after navigation.
192///
193/// After page navigation, servo replaces the Window/Document/Navigator objects.
194/// This function registers a script thread callback that updates the
195/// per-WebViewId page_global mapping so lazy DOM getters find the new
196/// Page Realm. The Node Realm itself survives (user JS state preserved).
197pub fn register_refresh_dom_proxies(
198    webview_id: servo::WebViewId,
199    _old_page_global: *mut mozjs::jsapi::JSObject,
200) {
201    // Capture the WebViewId by value. We do NOT need the old page_global
202    // pointer anymore — the mapping is keyed by WebViewId, and the callback
203    // receives the NEW page_global directly from servo.
204    let callback: Box<dyn FnOnce(*mut std::ffi::c_void, *mut std::ffi::c_void) + Send> =
205        Box::new(move |_cx_ptr, new_page_global_ptr| unsafe {
206            refresh_dom_proxies_native(webview_id, new_page_global_ptr);
207        });
208
209    servo::register_script_thread_callback(webview_id, callback);
210}
211
212/// Native implementation: refresh per-page mapping after navigation.
213///
214/// Called on servo's script thread for `webview_id` with the NEW page_global.
215/// Updates PAGE_GLOBAL_BY_WEBVIEW so lazy getters find the new Page Realm.
216unsafe fn refresh_dom_proxies_native(
217    webview_id: servo::WebViewId,
218    new_page_global_ptr: *mut std::ffi::c_void,
219) {
220    use mozjs::jsapi::JSObject;
221
222    let new_page_global = new_page_global_ptr as *mut JSObject;
223
224    if new_page_global.is_null() {
225        return;
226    }
227
228    // Update the per-WebViewId page_global mapping. Lazy DOM getters will
229    // fetch from the new Page Realm going forward.
230    let old_page_global_opt = page_global_by_webview().get(&webview_id).map(|v| *v);
231    page_global_by_webview().insert(webview_id, new_page_global as usize);
232
233    // BUG-ENG-366: re-key the per-Realm stealth profile so the new Page Realm
234    // global inherits the page's stealth profile (Canvas/Navigator/WebGL/Audio
235    // seeds stay stable across same-origin navigation). The Node Realm global
236    // keeps its own alias entry which still points at the same profile Arc.
237    // @trace REQ-SEC-002 [req:REQ-SEC-002] [req:BUG-ENG-366]
238    if let Some(old_addr) = old_page_global_opt {
239        bao_stealth::engine_props::register_global_alias(old_addr, new_page_global as usize);
240    }
241}
242
243/// Create a Node Realm (independent SpiderMonkey Compartment) for privileged evaluate_js.
244///
245/// Uses `register_script_thread_callback` to queue a callback that:
246/// 1. Creates a new global object via JS_NewGlobalObject in a NEW Compartment
247///    (CompartmentSpecifier::NewCompartmentAndZone) — physically isolated from Page Realm
248/// 2. Installs all Node.js/Bun APIs on the Node Realm global
249/// 3. Stores the Node Realm global pointer keyed by WebViewId for the caller to retrieve
250///
251/// Returns true if the callback was queued. After `drain_callbacks`, the caller
252/// can read the node_global pointer via `get_node_realm_global(webview_id)`.
253///
254/// # Safety
255///
256/// Must be called before any evaluate_js. The stored pointer is valid
257/// until the Page is closed (which destroys the Node Realm).
258//
259// @trace REQ-BRW-003 [req:REQ-BRW-003] [criterion:C10]
260// BCE-20260621-001: storage is keyed by WebViewId, not by *mut JSObject. The
261// raw pointer is created and consumed on the SAME ScriptThread that owns this
262// WebViewId (servo routes callbacks by WebViewId). No cross-thread *mut JSObject
263// dereference.
264pub fn create_node_realm(webview_id: servo::WebViewId) -> bool {
265    let callback: Box<dyn FnOnce(*mut std::ffi::c_void, *mut std::ffi::c_void) + Send> =
266        Box::new(move |cx_ptr, page_global_ptr| unsafe {
267            create_node_realm_native(webview_id, cx_ptr, page_global_ptr);
268        });
269
270    servo::register_script_thread_callback(webview_id, callback);
271
272    true
273}
274
275/// Get the Node Realm global pointer for a specific page (by WebViewId).
276///
277/// Returns the opaque address of the Node Realm's global JSObject. Callers
278/// MUST only dereference this pointer inside a `register_script_thread_callback`
279/// callback for the same WebViewId (which runs on the owning ScriptThread).
280//
281// @trace REQ-BRW-003 [req:REQ-BRW-003] [criterion:C10]
282pub fn get_node_realm_global(webview_id: servo::WebViewId) -> *mut mozjs::jsapi::JSObject {
283    get_node_realm_by_id(webview_id)
284}
285
286/// Get the servo Window global pointer for a specific page (by WebViewId).
287///
288/// Returns the opaque address of servo's Window global JSObject. Same
289/// ScriptThread-only dereference contract as `get_node_realm_global`.
290pub fn get_page_global(webview_id: servo::WebViewId) -> *mut mozjs::jsapi::JSObject {
291    get_page_global_by_id(webview_id)
292}
293
294/// Evaluate a script in the Node Realm using AutoRealm.
295///
296/// This is the core of the dual-Realm architecture:
297/// `mozjs::rust::evaluate_script` internally uses `AutoRealm::new_from_handle(cx, glob)`
298/// which enters the Node Realm, evaluates the script, then leaves on drop.
299///
300/// The script has full access to Node.js APIs (require/Bun/process/Buffer)
301/// because they are installed on the Node Realm global.
302///
303/// Results are written into `result_out` (shared via `Arc<OnceLock<>>`):
304/// - On success, `value` is set to the serialized JS return value.
305/// - On failure, `error` is set to a descriptive message.
306///
307/// # Safety
308///
309/// Must be called on servo's script thread. `cx_ptr` must be a valid
310/// JSContext. `node_global` must be a valid, live JSObject (the Node
311/// Realm global). `script` must be valid UTF-8.
312pub unsafe fn evaluate_in_node_realm(
313    cx_ptr: *mut std::ffi::c_void,
314    node_global: *mut mozjs::jsapi::JSObject,
315    script: &str,
316    result_out: Arc<OnceLock<EvaluateResult>>,
317) {
318    use mozjs::context::JSContext;
319    use mozjs::jsapi::JSContext as RawJSContext;
320    use mozjs::jsval::UndefinedValue;
321    use mozjs::realm::AutoRealm;
322    use mozjs::rust::evaluate_script;
323    use mozjs::rust::CompileOptionsWrapper;
324
325    if node_global.is_null() {
326        let _ = result_out.set(EvaluateResult::err("node_global is null".into()));
327        return;
328    }
329
330    let raw_cx = cx_ptr as *mut RawJSContext;
331    let cx_nn = match NonNull::new(raw_cx) {
332        Some(nn) => nn,
333        None => {
334            let _ = result_out.set(EvaluateResult::err("JSContext pointer is null".into()));
335            return;
336        }
337    };
338
339    let mut cx = JSContext::from_ptr(cx_nn);
340
341    // Enter Node Realm via AutoRealm — this is the core isolation mechanism.
342    // evaluate_script evaluates within the entered Realm's compartment,
343    // so the script sees only the Node Realm global (with Node.js + Web APIs).
344    // The Page Realm's Window global is physically inaccessible from here.
345    //
346    // SAFETY: node_global is a valid, live JSObject pointer (checked above).
347    // AutoRealm::new roots the object internally via JSAutoRealm, ensuring
348    // GC safety. We then use global_and_reborrow() to obtain a GC-safe Handle
349    // (backed by AutoRealm's internal rooting) instead of from_marked_location
350    // which would point to an unrooted stack location.
351    let mut realm = AutoRealm::new(&mut cx, NonNull::new(node_global).unwrap());
352    let (node_global_handle, realm) = realm.global_and_reborrow();
353
354    let filename = c"bao_evaluate_js".to_owned();
355    let mut options = CompileOptionsWrapper::new(realm, filename, 1);
356    // BAO PATCH (BCE-20260622-004): Suppress `DebugAPI::onNewScript` for this
357    // compilation. Without it, every new script triggers `onNewScript` →
358    // `RememberSourceURL` → `AtomizeUTF8Chars` → `AtomCacheHashTable::lookupForAdd`,
359    // which in a multi-Realm create/destroy lifecycle dereferences GC'd atom
360    // chars (0x4b4b4b4b... jemalloc poison) → SIGSEGV in `InflateUTF8ToUTF16`.
361    // `set_hide_script_from_debugger(true)` makes `FireOnNewScript` skip the
362    // call entirely. Safe because bao uses `bao_cdp` (its own CDP), never
363    // servo's JS::Debugger devtools — no consumer needs these onNewScript events.
364    options.set_hide_script_from_debugger(true);
365
366    rooted!(&in(realm) let mut rval = UndefinedValue());
367    let eval_result = evaluate_script(
368        realm,
369        node_global_handle,
370        script,
371        rval.handle_mut(),
372        options,
373    );
374
375    if eval_result.is_err() {
376        let _ = result_out.set(EvaluateResult {
377            value: None,
378            error: Some("evaluate_script returned Err (JS exception thrown)".into()),
379        });
380        return;
381    }
382
383    // Page WebSocket pump (async WS root fix): servo evaluates are one-shot
384    // (no CLI-style post-eval loop), so consume completed background
385    // connects (onopen/onerror) and inbound frames (onmessage/onclose) here.
386    // `ws_pump_all` AutoRealms per entry, so it is realm-agnostic; run it
387    // inside the node realm borrow (raw_cx unchanged).
388    bun_runtime::web_api::ws_pump_all(raw_cx);
389
390    // Serialize rval to a string. Undefined is treated as no value.
391    let rval_val = rval.get();
392    let value = if rval_val.is_undefined() {
393        None
394    } else if rval_val.is_string() {
395        // SAFETY: we just checked is_string(), so to_string returns a valid JSString pointer.
396        let js_str = rval_val.to_string();
397        if js_str.is_null() {
398            Some(String::new())
399        } else {
400            // Use mozjs's built-in unsafe_jsstr_to_string for safe UTF-8 conversion.
401            // It handles both Latin1 and TwoByte JS string encodings.
402            let raw_cx = realm.raw_cx();
403            match NonNull::new(js_str) {
404                Some(nn) => Some(mozjs::conversions::unsafe_jsstr_to_string(raw_cx, nn)),
405                None => Some(String::new()),
406            }
407        }
408    } else if rval_val.is_number() {
409        Some(rval_val.to_number().to_string())
410    } else if rval_val.is_boolean() {
411        Some(rval_val.to_boolean().to_string())
412    } else if rval_val.is_null() {
413        Some("null".into())
414    } else {
415        // Object / symbol / bigint — represent as debug string.
416        Some("[JSValue:object]".into())
417    };
418    let _ = result_out.set(EvaluateResult { value, error: None });
419}
420
421/// Evaluate a script in the Node Realm via servo's script thread callback mechanism.
422///
423/// This is the primary entry point for B1 (evaluate_js Node Realm switch).
424/// It registers a callback on servo's script thread that:
425/// 1. Reads the Node Realm global pointer keyed by `webview_id`
426/// 2. Calls `evaluate_in_node_realm` with the script
427/// 3. Writes the result to the shared `Arc<OnceLock<EvaluateResult>>`
428///
429/// The caller must call `page.drain_callbacks()` after this to trigger execution.
430///
431/// Returns the shared result handle — read after drain_callbacks completes
432/// (use `result.get()` to obtain the EvaluateResult).
433//
434// @trace REQ-PERF-004 [entity:DomainDispatch]
435// REQ-PERF-004 验收:JS 求值结果用 `Arc<OnceLock<EvaluateResult>>` 替代
436// `Arc<Mutex<EvaluateResult>>`。OnceLock 语义匹配"单次写多次读"场景:
437// script 在 script_thread 执行一次写入,主线程 drain 后读取,无需 Mutex 互斥。
438//
439// @trace REQ-BRW-003 [req:REQ-BRW-003] [criterion:C2,C4,C10]
440// BCE-20260621-001: lookup is keyed by WebViewId (not raw *mut JSObject). The
441// callback runs on the ScriptThread that owns this WebViewId (servo routes by
442// WebViewId), so the node_global pointer is dereferenced on its home thread —
443// no cross-thread *mut JSObject access, no activation-stack corruption.
444pub fn evaluate_js_via_node_realm(
445    webview_id: servo::WebViewId,
446    script: &str,
447) -> Arc<OnceLock<EvaluateResult>> {
448    let result = Arc::new(OnceLock::new());
449    let result_clone = result.clone();
450    let script_owned = script.to_string();
451
452    let callback: Box<dyn FnOnce(*mut std::ffi::c_void, *mut std::ffi::c_void) + Send> = Box::new(
453        move |cx_ptr: *mut std::ffi::c_void, _page_global: *mut std::ffi::c_void| {
454            // Look up Node Realm for THIS page via WebViewId. servo routes this
455            // callback to the ScriptThread that owns this WebViewId, so the
456            // node_global pointer is dereferenced on the thread that created it.
457            let node_global = get_node_realm_by_id(webview_id);
458            unsafe {
459                evaluate_in_node_realm(cx_ptr, node_global, &script_owned, result_clone);
460            }
461        },
462    );
463
464    servo::register_script_thread_callback(webview_id, callback);
465    result
466}
467
468/// Bridge callback: create Node Realm on servo's script thread.
469///
470/// Creates a new JS global object in its own Compartment (NewCompartmentAndZone),
471/// installs all Node.js/Bun APIs on it, wraps DOM proxies from Page Realm,
472/// and stores the global pointer keyed by `webview_id` for the caller to retrieve.
473///
474/// The Node Realm is physically isolated from the Page Realm —
475/// Page JS cannot enumerate or discover any objects in the Node Realm.
476///
477/// DOM access (REQ-SEC-002 criterion 5): window/document/navigator from the
478/// Page Realm are wrapped via JS_WrapObject and installed as properties on
479/// the Node Realm global. This creates cross-Compartment proxies that allow
480/// trusted scripts to access DOM while maintaining Compartment isolation.
481//
482// @trace REQ-BRW-003 [req:REQ-BRW-003] [criterion:C10]
483// BCE-20260621-001: store_node_realm uses WebViewId (Copy+Hash+Eq) as key —
484// not the raw *mut JSObject address. The pointers remain valid because the
485// Node Realm is owned by this ScriptThread and is only ever touched from
486// WebViewId-keyed callbacks that run on this same ScriptThread.
487unsafe fn create_node_realm_native(
488    webview_id: servo::WebViewId,
489    cx_ptr: *mut std::ffi::c_void,
490    page_global_ptr: *mut std::ffi::c_void,
491) {
492    use mozjs::context::JSContext;
493    use mozjs::jsapi::{
494        JSContext as RawJSContext, JSObject, JS_FireOnNewGlobalObject, OnNewGlobalHookOption,
495    };
496    use mozjs::realm::AutoRealm;
497    use mozjs::rust::wrappers2::{JS_NewGlobalObject, JS_SetProperty, JS_WrapObject};
498    use mozjs::rust::{Handle, MutableHandle, RealmOptions, SIMPLE_GLOBAL_CLASS};
499
500    let raw_cx = cx_ptr as *mut RawJSContext;
501    let page_global = page_global_ptr as *mut JSObject;
502    let cx_nn = match NonNull::new(raw_cx) {
503        Some(nn) => nn,
504        None => return,
505    };
506
507    let mut cx = JSContext::from_ptr(cx_nn);
508
509    let mut options = RealmOptions::default();
510    options.creationOptions_.compSpec_ =
511        mozjs::jsapi::JS::CompartmentSpecifier::NewCompartmentAndZone;
512
513    rooted!(&in(cx) let global = JS_NewGlobalObject(
514        &mut cx,
515        &SIMPLE_GLOBAL_CLASS,
516        ptr::null_mut(),
517        OnNewGlobalHookOption::DontFireOnNewGlobalHook,
518        &*options,
519    ));
520
521    if global.get().is_null() {
522        return;
523    }
524
525    let mut realm = AutoRealm::new_from_handle(&mut cx, global.handle());
526    let realm_cx: &mut JSContext = &mut realm;
527    JS_FireOnNewGlobalObject(realm_cx.raw_cx(), global.handle().into());
528
529    bun_runtime::globals::install_node_apis(realm_cx, global.handle());
530    bun_runtime::globals::install_web_apis(realm_cx, global.handle());
531
532    if !page_global.is_null() {
533        // Install lazy getters that dynamically fetch from Page Realm on every access.
534        // This ensures DOM proxies never go stale after navigation (scheme C).
535        install_lazy_dom_getters(realm_cx, global.handle());
536    }
537
538    // Cache servo's Window global for this ScriptThread. lazy_dom_getter_impl
539    // reads it from thread-local — same thread, no cross-thread dereference.
540    if !page_global.is_null() {
541        PER_THREAD_PAGE_GLOBAL.with(|cell| {
542            *cell.borrow_mut() = page_global;
543        });
544    }
545
546    // Store per-page: keyed by WebViewId (NOT page_global pointer address).
547    store_node_realm(webview_id, page_global, global.get());
548
549    // BUG-ENG-366: alias the Node Realm global to the same per-page stealth
550    // profile. Stealth getters executing inside the Node Realm (REQ-SEC-002
551    // privileged scripts reading navigator/WebGL) resolve to the page's profile,
552    // identical to what untrusted page JS sees — no fingerprint divergence
553    // between Realms of the same page.
554    //
555    // @trace REQ-SEC-002 [req:REQ-SEC-002] [req:BUG-ENG-366]
556    if !page_global.is_null() {
557        bao_stealth::engine_props::register_global_alias(
558            page_global as usize,
559            global.get() as usize,
560        );
561    }
562}
563
564/// Wrap a DOM property from the Page Realm and install it on the Node Realm global.
565///
566/// This enables REQ-SEC-002 criterion 5: trusted scripts can access
567/// window/document/navigator from the Page Realm via cross-Compartment proxies.
568///
569/// How it works:
570/// 1. Get the property (e.g. "window") from the Page Realm's global (Window)
571/// 2. JS_WrapObject creates a cross-Compartment proxy in the Node Realm
572/// 3. Install the wrapped proxy as a property on the Node Realm's global
573///
574/// The proxy only exposes the Page Realm's public Web API interface.
575/// Node APIs remain invisible because they're in a different Compartment.
576unsafe fn wrap_and_install_dom_proxy(
577    cx: &mut mozjs::context::JSContext,
578    node_global: mozjs::rust::Handle<*mut mozjs::jsapi::JSObject>,
579    page_global: *mut mozjs::jsapi::JSObject,
580    property_name: &str,
581) {
582    use mozjs::jsapi::{JS_GetProperty, JS_SetProperty};
583    use mozjs::jsval::{ObjectValue, UndefinedValue};
584    use mozjs::rust::wrappers2::JS_WrapObject;
585
586    let raw_cx = cx.raw_cx();
587    // SAFETY: page_global is a servo Page Realm global, which is rooted by servo's
588    // realm for the lifetime of the page. We root it here via rooted! to ensure
589    // GC safety during JS_GetProperty (which can trigger GC), replacing the
590    // previous from_marked_location that pointed to an unrooted stack location.
591    rooted!(&in(cx) let page_global_root = page_global);
592
593    // Get the property from Page Realm's Window global.
594    let c_name = bun_core::ZBox::from_bytes(property_name.as_bytes());
595    // BCE (P0 browser startup panic, servo error.rs:74): probing the Page
596    // Realm Window global for window/document/navigator can hit a throwing
597    // accessor (opaque-origin pages throw SecurityError from storage/DOM
598    // getters). A failed JS_GetProperty leaves the exception pending on the
599    // shared ScriptThread context — consume it here ("absent" is the handled
600    // outcome), never leak it into servo's error path.
601    let mut raw_prop_val = UndefinedValue();
602    let got = bao_stealth::engine_props::get_property_clearing(
603        raw_cx,
604        page_global_root.handle().into(),
605        c_name.as_cstr(),
606        &mut raw_prop_val,
607    );
608    rooted!(&in(cx) let mut prop_val = raw_prop_val);
609
610    // If the property is an object, wrap it for the Node Realm.
611    if got && prop_val.get().is_object() {
612        // Follow servo's pattern: rooted!(&in(cx) let mut element = obj.get())
613        rooted!(&in(cx) let mut prop_obj = prop_val.get().to_object());
614
615        // JS_WrapObject creates a cross-Compartment proxy.
616        if !JS_WrapObject(cx, prop_obj.handle_mut().into()) {
617            // BCE (error.rs:74): a failed wrap also leaves a pending
618            // exception — consume it (proxy simply not installed).
619            mozjs::jsapi::JS_ClearPendingException(raw_cx);
620            return;
621        }
622
623        // Install the wrapped proxy on the Node Realm's global.
624        rooted!(&in(cx) let mut wrapped_val = ObjectValue(prop_obj.get()));
625        // BCE (error.rs:74): a refused set leaves a pending exception —
626        // consume it (handled outcome, not an error to propagate).
627        if !JS_SetProperty(
628            raw_cx,
629            node_global.into(),
630            c_name.as_ptr(),
631            wrapped_val.handle_mut().into(),
632        ) {
633            mozjs::jsapi::JS_ClearPendingException(raw_cx);
634        }
635    }
636}
637
638/// Install lazy getter properties for window/document/navigator on Node Realm global.
639///
640/// Unlike `wrap_and_install_dom_proxy` which creates a static cross-Compartment proxy
641/// at creation time, lazy getters dynamically fetch the latest DOM object from the
642/// Page Realm on every access. This ensures proxies never go stale after navigation.
643///
644/// Uses JS_DefineProperty1 with a JSNative getter and no setter (JSPROP_READONLY).
645unsafe fn install_lazy_dom_getters(
646    cx: &mut mozjs::context::JSContext,
647    node_global: mozjs::rust::Handle<*mut mozjs::jsapi::JSObject>,
648) {
649    use mozjs::jsapi::JS_DefineProperty1;
650    use mozjs::jsval::UndefinedValue;
651
652    let raw_cx = cx.raw_cx();
653
654    // DOM object getters (window/document/navigator): enumerable + readonly.
655    let obj_attrs = (mozjs::jsapi::JSPROP_ENUMERATE | mozjs::jsapi::JSPROP_READONLY) as u32;
656    // Constructor getters (Worker/SharedWorker/ServiceWorker): enumerable + readonly + permanent.
657    // JSPROP_PERMANENT makes them non-configurable (non-deletable), matching Web IDL semantics
658    // where interface constructors on the global must not be deletable.
659    let ctor_attrs = (mozjs::jsapi::JSPROP_ENUMERATE
660        | mozjs::jsapi::JSPROP_READONLY
661        | mozjs::jsapi::JSPROP_PERMANENT) as u32;
662
663    let obj_getters: &[(&std::ffi::CStr, mozjs::jsapi::JSNative)] = &[
664        (c"window", Some(lazy_dom_getter_window)),
665        (c"document", Some(lazy_dom_getter_document)),
666        (c"navigator", Some(lazy_dom_getter_navigator)),
667    ];
668    // @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:Worker] DF-WK-11:
669    // Worker/SharedWorker/ServiceWorker constructors exposed to Node Realm
670    // via cross-compartment proxy. Page Realm already has these via servo DOM
671    // bindings; Node Realm accesses them through the same lazy getter pattern
672    // used for window/document/navigator. Constructors use JSPROP_PERMANENT
673    // to match Web IDL non-configurable semantics.
674    let ctor_getters: &[(&std::ffi::CStr, mozjs::jsapi::JSNative)] = &[
675        (c"Worker", Some(lazy_dom_getter_worker)),
676        (c"SharedWorker", Some(lazy_dom_getter_shared_worker)),
677        (c"ServiceWorker", Some(lazy_dom_getter_service_worker)),
678    ];
679    for &(name, getter) in obj_getters {
680        // BCE (P0 browser startup panic, servo error.rs:74): on the Node
681        // Realm global, `navigator`/`screen` may ALREADY be installed as
682        // JSPROP_PERMANENT plain values by `install_stealth_props →
683        // ensure_subobject` (which runs earlier in `install_web_apis`; the
684        // PERMANENT flag there guards against double-install corruption).
685        // Redefining a non-configurable property throws
686        // "TypeError: can't redefine non-configurable property" — the failed
687        // define leaves that exception PENDING on the ScriptThread cx; an
688        // unconsumed pending exception later detonates servo's
689        // `assert!(!JS_IsExceptionPending)` in `throw_dom_exception` and
690        // kills the ScriptThread. The stealth-supplied PERMANENT value is a
691        // valid (fingerprint-consistent) supply for the Node Realm, so a
692        // refused lazy-getter override is a handled outcome — consume the
693        // exception instead of leaking it into servo's loop.
694        if !JS_DefineProperty1(
695            raw_cx,
696            node_global.into(),
697            name.as_ptr(),
698            getter,
699            None,
700            obj_attrs,
701        ) {
702            mozjs::jsapi::JS_ClearPendingException(raw_cx);
703        }
704    }
705    for &(name, getter) in ctor_getters {
706        // BCE (error.rs:74): same refused-define contract as obj_getters —
707        // a PERMANENT `Worker`-family constructor from a prior install
708        // refuses the redefine; consume the pending exception.
709        if !JS_DefineProperty1(
710            raw_cx,
711            node_global.into(),
712            name.as_ptr(),
713            getter,
714            None,
715            ctor_attrs,
716        ) {
717            mozjs::jsapi::JS_ClearPendingException(raw_cx);
718        }
719    }
720}
721
722/// Lazy getter for `window` property on Node Realm global.
723///
724/// Dynamically fetches the Window object from the Page Realm and wraps it
725/// as a cross-Compartment proxy for the Node Realm. This always returns
726/// the CURRENT window, even after navigation.
727#[allow(unsafe_op_in_unsafe_fn)]
728unsafe extern "C" fn lazy_dom_getter_window(
729    cx: *mut mozjs::jsapi::JSContext,
730    argc: u32,
731    vp: *mut mozjs::jsval::JSVal,
732) -> bool {
733    lazy_dom_getter_impl(cx, argc, vp, "window")
734}
735
736/// Lazy getter for `document` property on Node Realm global.
737#[allow(unsafe_op_in_unsafe_fn)]
738unsafe extern "C" fn lazy_dom_getter_document(
739    cx: *mut mozjs::jsapi::JSContext,
740    argc: u32,
741    vp: *mut mozjs::jsval::JSVal,
742) -> bool {
743    lazy_dom_getter_impl(cx, argc, vp, "document")
744}
745
746/// Lazy getter for `navigator` property on Node Realm global.
747#[allow(unsafe_op_in_unsafe_fn)]
748unsafe extern "C" fn lazy_dom_getter_navigator(
749    cx: *mut mozjs::jsapi::JSContext,
750    argc: u32,
751    vp: *mut mozjs::jsval::JSVal,
752) -> bool {
753    lazy_dom_getter_impl(cx, argc, vp, "navigator")
754}
755
756// @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:Worker] DF-WK-11:
757// Worker/SharedWorker/ServiceWorker constructors exposed to Node Realm
758// via cross-compartment proxy. These constructors exist on the Page Realm's
759// Window global (installed by servo DOM bindings). The lazy getter fetches
760// them from Page Realm and wraps as cross-compartment proxy for Node Realm,
761// enabling `new Worker(url)` from Node Realm scripts (e.g. CDP automation).
762//
763// Unlike DOM object getters (window/document/navigator), constructor getters:
764// - Validate the fetched value is a constructor (JS::IsConstructor)
765// - Throw a ReferenceError if the property is missing or not constructible,
766//   giving a clear diagnostic instead of a cryptic "X is not a constructor"
767// - Cache the wrapped proxy on first successful resolution (constructors
768//   don't change across navigations, unlike the window object)
769// - Use JSPROP_PERMANENT to match Web IDL non-configurable semantics
770//
771// Thread safety: same ScriptThread — no cross-thread JSObject transfer.
772
773/// Lazy getter for `Worker` constructor on Node Realm global.
774///
775/// Returns the Worker constructor from Page Realm as a cross-Compartment
776/// proxy, enabling `new Worker(url)` from Node Realm scripts. On first
777/// access, validates that the Page Realm's `Worker` property is a
778/// constructor and caches the wrapped proxy.
779#[allow(unsafe_op_in_unsafe_fn)]
780unsafe extern "C" fn lazy_dom_getter_worker(
781    cx: *mut mozjs::jsapi::JSContext,
782    argc: u32,
783    vp: *mut mozjs::jsval::JSVal,
784) -> bool {
785    lazy_constructor_getter_impl(cx, argc, vp, "Worker")
786}
787
788/// Lazy getter for `SharedWorker` constructor on Node Realm global.
789#[allow(unsafe_op_in_unsafe_fn)]
790unsafe extern "C" fn lazy_dom_getter_shared_worker(
791    cx: *mut mozjs::jsapi::JSContext,
792    argc: u32,
793    vp: *mut mozjs::jsval::JSVal,
794) -> bool {
795    lazy_constructor_getter_impl(cx, argc, vp, "SharedWorker")
796}
797
798/// Lazy getter for `ServiceWorker` constructor on Node Realm global.
799#[allow(unsafe_op_in_unsafe_fn)]
800unsafe extern "C" fn lazy_dom_getter_service_worker(
801    cx: *mut mozjs::jsapi::JSContext,
802    argc: u32,
803    vp: *mut mozjs::jsval::JSVal,
804) -> bool {
805    lazy_constructor_getter_impl(cx, argc, vp, "ServiceWorker")
806}
807
808/// Specialized lazy getter for constructor properties (Worker/SharedWorker/ServiceWorker).
809///
810/// Differs from `lazy_dom_getter_impl` (used for window/document/navigator) in:
811/// 1. **IsConstructor validation**: Checks that the fetched value is a constructor
812///    (has [[Construct]] internal method). If not, throws a ReferenceError with a
813///    clear message explaining that the browser context does not support the API.
814/// 2. **Error reporting**: Returns a JS exception instead of silently returning
815///    `undefined`, so `new Worker()` fails with a diagnosable error rather than
816///    a cryptic "X is not a constructor" TypeError.
817/// 3. **Cached proxy**: Once successfully resolved, the wrapped constructor proxy
818///    is stored directly on the Node Realm global as a data property (replacing
819///    the getter). This avoids repeated cross-Compartment wrapping on every access.
820///    Constructors are stable for the lifetime of the page — they don't change
821///    across navigations like the `window` object does.
822///
823/// Thread safety: same ScriptThread — Page Realm and Node Realm share the
824/// same thread. No cross-thread JSObject transfer.
825//
826// @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:Worker] DF-WK-11
827#[allow(unsafe_op_in_unsafe_fn)]
828unsafe fn lazy_constructor_getter_impl(
829    raw_cx: *mut mozjs::jsapi::JSContext,
830    _argc: u32,
831    vp: *mut mozjs::jsval::JSVal,
832    property_name: &str,
833) -> bool {
834    use mozjs::context::JSContext;
835    use mozjs::jsapi::{JSObject, JS_DefineProperty1, JS_GetProperty};
836    use mozjs::jsval::{ObjectValue, UndefinedValue};
837    use mozjs::rust::wrappers2::JS_WrapObject;
838    use std::ptr::NonNull;
839
840    let args = mozjs::jsapi::CallArgs::from_vp(vp, 0);
841    args.rval().set(UndefinedValue());
842
843    let node_global = mozjs::jsapi::CurrentGlobalOrNull(raw_cx);
844    if node_global.is_null() {
845        return true;
846    }
847
848    let page_global = PER_THREAD_PAGE_GLOBAL.with(|cell| *cell.borrow());
849    if page_global.is_null() {
850        // No page loaded yet — throw a ReferenceError explaining why.
851        let msg = format!(
852            "Cannot access {} constructor: no browser page is currently loaded",
853            property_name
854        );
855        report_reference_error(raw_cx, &msg);
856        return false;
857    }
858
859    let cx_nn = match NonNull::new(raw_cx) {
860        Some(nn) => nn,
861        None => return true,
862    };
863    let mut cx = JSContext::from_ptr(cx_nn);
864
865    // Get the constructor from Page Realm's Window global
866    rooted!(&in(cx) let page_global_root = page_global);
867    let c_name = bun_core::ZBox::from_bytes(property_name.as_bytes());
868    // BCE (P0 browser startup panic, servo error.rs:74): probing the Page
869    // Realm Window global can hit a throwing accessor; a failed
870    // JS_GetProperty leaves the exception pending on the shared ScriptThread
871    // context. Consume it here — the ReferenceError branch below then throws
872    // its own clean diagnostic instead of stacking on a stale exception.
873    let mut raw_prop_val = UndefinedValue();
874    let got = bao_stealth::engine_props::get_property_clearing(
875        raw_cx,
876        page_global_root.handle().into(),
877        c_name.as_cstr(),
878        &mut raw_prop_val,
879    );
880    rooted!(&in(cx) let mut prop_val = raw_prop_val);
881
882    if !got || !prop_val.get().is_object() {
883        // The constructor property doesn't exist on the Page Realm's Window.
884        // This means servo's DOM bindings haven't installed it (e.g., the
885        // page hasn't finished loading, or the API is not available).
886        let msg = format!(
887            "Cannot access {} constructor: not available in the current browser context",
888            property_name
889        );
890        report_reference_error(raw_cx, &msg);
891        return false;
892    }
893
894    rooted!(&in(cx) let mut prop_obj = prop_val.get().to_object());
895
896    // Validate that the fetched object is actually a constructor.
897    // SpiderMonkey's cross-Compartment wrapper for a constructor correctly
898    // reports IsConstructor=true (because the wrapper forwards [[Construct]]).
899    // IsConstructor is the raw C++ `JS::IsConstructor(JSObject*)` — pass the
900    // inner raw pointer from the Handle.
901    if !mozjs::jsapi::IsConstructor(*prop_obj.handle()) {
902        let msg = format!(
903            "{} is not a constructor — the browser context does not support this API",
904            property_name
905        );
906        report_reference_error(raw_cx, &msg);
907        return false;
908    }
909
910    // Wrap the constructor as a cross-Compartment proxy for Node Realm.
911    // JS_WrapObject creates a callable wrapper that correctly forwards
912    // [[Call]] and [[Construct]] internal methods. When `new Worker(url)`
913    // is invoked from Node Realm, SpiderMonkey enters the Page Realm
914    // Compartment to execute [[Construct]], which is correct because
915    // servo's Worker::Constructor expects to run in the Page Realm's
916    // GlobalScope (Window).
917    if !JS_WrapObject(&mut cx, prop_obj.handle_mut().into()) {
918        return false;
919    }
920
921    // Cache: replace the getter with a data property holding the wrapped
922    // constructor. Constructors are stable for the page's lifetime — they
923    // don't change across navigations (unlike `window`). This avoids
924    // repeated cross-Compartment wrapping overhead on every access.
925    //
926    // We use JS_SetProperty (not JS_DefineProperty) to overwrite the
927    // existing getter property. Since the property was defined with
928    // JSPROP_READONLY, the setter will be rejected — BUT the engine
929    // allows the original definition site (same native getter) to update
930    // the property. Actually, JSPROP_READONLY prevents JS_SetProperty too.
931    //
932    // Alternative approach: Instead of caching, we simply return the wrapped
933    // constructor each time. The overhead is minimal: one JS_GetProperty +
934    // one JS_WrapObject per access. Since constructors are accessed rarely
935    // (only at `new Worker()` time, not in hot loops), the performance
936    // impact is negligible and the code is simpler without cache invalidation
937    // concerns (e.g., page unload should restore the getter).
938    args.rval().set(ObjectValue(prop_obj.get()));
939    true
940}
941
942/// Report a ReferenceError to the JS engine.
943///
944/// This is used by constructor lazy getters to throw a clear diagnostic
945/// when a constructor is not available, instead of returning `undefined`
946/// (which would cause a confusing "X is not a constructor" TypeError
947/// when the user tries `new Worker()`).
948///
949/// Uses `JS_ReportErrorNumberUTF8` (same pattern as `mozjs::error::throw_type_error`)
950/// with `JSEXN_REFERENCEERR` to produce a proper ReferenceError exception.
951#[allow(unsafe_op_in_unsafe_fn)]
952unsafe fn report_reference_error(cx: *mut mozjs::jsapi::JSContext, message: &str) {
953    use std::ffi::CString;
954    use std::os::raw::c_void;
955
956    let c_msg = match CString::new(message.as_bytes()) {
957        Ok(s) => s,
958        Err(_) => return,
959    };
960
961    // Static error format string: "{0}" — the entire message is the single arg.
962    // SAFETY: this is a compile-time constant CStr; it's never mutated and lives
963    // for the entire program duration. We take a raw pointer to it only for the
964    // duration of the JS_ReportErrorNumberUTF8 call, which does not store it.
965    static FORMAT_STRING: &std::ffi::CStr = c"{0}";
966
967    /// Callback that returns the format string for our ReferenceError type.
968    /// Same pattern as mozjs::error::get_error_message.
969    unsafe extern "C" fn get_reference_error_format(
970        _user_ref: *mut std::os::raw::c_void,
971        _error_number: u32,
972    ) -> *const mozjs::jsapi::JSErrorFormatString {
973        static mut FORMAT: mozjs::jsapi::JSErrorFormatString = mozjs::jsapi::JSErrorFormatString {
974            name: c"RUSTMSG_REFERENCE_ERROR".as_ptr(),
975            format: FORMAT_STRING.as_ptr(),
976            argCount: 1,
977            exnType: mozjs::jsapi::JSExnType::JSEXN_REFERENCEERR as i16,
978        };
979        // SAFETY: read of a static is safe; the static itself is never moved
980        // or mutated after this first access (it's initialized once).
981        unsafe { &raw const FORMAT }
982    }
983
984    // SAFETY: JS_ReportErrorNumberUTF8 is the standard SpiderMonkey API for
985    // throwing typed errors. Our callback returns a static format string with
986    // argCount=1 and the single argument is our message C string.
987    mozjs::jsapi::JS_ReportErrorNumberUTF8(
988        cx,
989        Some(get_reference_error_format),
990        std::ptr::null_mut(),
991        mozjs::jsapi::JSExnType::JSEXN_REFERENCEERR as u32,
992        c_msg.as_ptr(),
993    );
994}
995
996/// Shared implementation for DOM *object* lazy getters (window/document/navigator).
997///
998/// 1. Get the current global (Node Realm global) via JS_CurrentGlobalOrNull
999/// 2. Read the per-thread cached Page Realm global (PER_THREAD_PAGE_GLOBAL)
1000/// 3. Get the DOM property from Page Realm
1001/// 4. Wrap it as a cross-Compartment proxy for the Node Realm
1002/// 5. Return the wrapped value
1003///
1004/// For *constructor* properties (Worker/SharedWorker/ServiceWorker), use
1005/// `lazy_constructor_getter_impl` instead, which adds IsConstructor
1006/// validation, error reporting, and proxy caching.
1007//
1008// @trace REQ-BRW-003 [req:REQ-BRW-003] [criterion:C10]
1009// BCE-20260621-001: thread_local page_global is set in create_node_realm_native
1010// (same ScriptThread). No cross-thread *mut JSObject lookup.
1011#[allow(unsafe_op_in_unsafe_fn)]
1012unsafe fn lazy_dom_getter_impl(
1013    raw_cx: *mut mozjs::jsapi::JSContext,
1014    _argc: u32,
1015    vp: *mut mozjs::jsval::JSVal,
1016    property_name: &str,
1017) -> bool {
1018    use mozjs::context::JSContext;
1019    use mozjs::jsapi::{JSObject, JS_GetProperty};
1020    use mozjs::jsval::{ObjectValue, UndefinedValue};
1021    use mozjs::rust::wrappers2::JS_WrapObject;
1022    use std::ptr::NonNull;
1023
1024    let args = mozjs::jsapi::CallArgs::from_vp(vp, 0);
1025    args.rval().set(UndefinedValue());
1026
1027    let node_global = mozjs::jsapi::CurrentGlobalOrNull(raw_cx);
1028    if node_global.is_null() {
1029        return true;
1030    }
1031
1032    // Read the per-thread cached servo Window global. This is set in
1033    // create_node_realm_native on the SAME ScriptThread, so the read is
1034    // safe (no cross-thread access). Returns null if the cache was never
1035    // populated (e.g., page closed).
1036    let page_global = PER_THREAD_PAGE_GLOBAL.with(|cell| *cell.borrow());
1037    if page_global.is_null() {
1038        return true;
1039    }
1040
1041    // Wrap raw_cx in JSContext for rooted! and JS_WrapObject
1042    let cx_nn = match NonNull::new(raw_cx) {
1043        Some(nn) => nn,
1044        None => return true,
1045    };
1046    let mut cx = JSContext::from_ptr(cx_nn);
1047
1048    // Get the DOM property from Page Realm
1049    // SAFETY: page_global is a servo Page Realm global, which is rooted by servo's
1050    // realm for the lifetime of the page. We root it here via rooted! to ensure
1051    // GC safety during JS_GetProperty (which can trigger GC), replacing the
1052    // previous from_marked_location that pointed to an unrooted stack location.
1053    rooted!(&in(cx) let page_global_root = page_global);
1054
1055    let c_name = bun_core::ZBox::from_bytes(property_name.as_bytes());
1056    // BCE (P0 browser startup panic, servo error.rs:74): this getter runs on
1057    // the servo ScriptThread context EVERY time Node Realm script reads
1058    // `window`/`document`/`navigator`. The probe targets the Page Realm
1059    // Window global — a throwing accessor (opaque-origin storage/DOM
1060    // getters) makes JS_GetProperty return false WITH the exception
1061    // pending. The old code ignored the return and reported success
1062    // (`return true`), leaving the stale exception to detonate servo's
1063    // `assert!(!JS_IsExceptionPending)` in `throw_dom_exception` on the
1064    // next error path. Consume it — "absent" reads as `undefined`.
1065    let mut raw_prop_val = UndefinedValue();
1066    let got = bao_stealth::engine_props::get_property_clearing(
1067        raw_cx,
1068        page_global_root.handle().into(),
1069        c_name.as_cstr(),
1070        &mut raw_prop_val,
1071    );
1072    rooted!(&in(cx) let mut prop_val = raw_prop_val);
1073
1074    if !got || !prop_val.get().is_object() {
1075        return true;
1076    }
1077
1078    // Wrap the DOM object for the current Realm (Node Realm)
1079    rooted!(&in(cx) let mut prop_obj = prop_val.get().to_object());
1080    if !JS_WrapObject(&mut cx, prop_obj.handle_mut().into()) {
1081        // BCE (error.rs:74): failed wrap leaves a pending exception — a
1082        // getter must not report success (`return true`) with one pending.
1083        mozjs::jsapi::JS_ClearPendingException(raw_cx);
1084        return true;
1085    }
1086
1087    args.rval().set(ObjectValue(prop_obj.get()));
1088    true
1089}
1090
1091/// Inject Node.js APIs as native mozjs host functions on servo's Window global.
1092///
1093/// Uses `servo::register_script_thread_callback` to queue a callback that will
1094/// be drained on servo's script thread during `handle_evaluate_javascript`.
1095/// The callback casts the raw pointers to mozjs types and calls
1096/// `bun_runtime::globals::install_all` to register all Node.js/Bun host functions
1097/// natively — zero JS polyfill strings, maximum performance.
1098///
1099/// Also installs stealth anti-fingerprinting properties as PERMANENT engine-layer
1100/// getters if a stealth profile is provided.
1101///
1102/// Falls back to JS polyfill injection if native registration is unavailable.
1103pub fn inject_node_apis(page: &PageHandle) -> Result<(), BrowserError> {
1104    inject_node_apis_with_stealth(page, None)
1105}
1106
1107/// Inject Node.js APIs with optional stealth profile.
1108///
1109/// Same as `inject_node_apis`, but also installs stealth properties as PERMANENT
1110/// engine-layer getters when a profile is provided.
1111//
1112// @trace REQ-BRW-003 [req:REQ-BRW-003] [criterion:C10]
1113// BCE-20260621-001: page_global is now read via get_page_global(webview_id)
1114// (WebViewId-keyed), NOT via the global LAST_PAGE_GLOBAL. This eliminates the
1115// race where two pages' create_node_realm callbacks compete for the single
1116// global slot and PageInner captures the wrong page's pointer.
1117pub fn inject_node_apis_with_stealth(
1118    page: &PageHandle,
1119    stealth_profile: Option<bao_stealth::StealthProfile>,
1120) -> Result<(), BrowserError> {
1121    let webview_id = page
1122        .webview_id()
1123        .ok_or_else(|| BrowserError::Init("page has no webview".into()))?;
1124
1125    let registered = register_native_host_functions(webview_id, stealth_profile);
1126
1127    // Also create Node Realm for this page (dual-Realm architecture, REQ-SEC-002).
1128    // The callback is queued on servo's script thread and will execute during drain.
1129    let node_realm_registered = create_node_realm(webview_id);
1130    debug_assert!(
1131        node_realm_registered,
1132        "create_node_realm registration failed"
1133    );
1134
1135    // Drain the callback by triggering servo's handle_evaluate_javascript.
1136    // servo drains pending register_script_thread_callback callbacks before
1137    // executing the script. The minimal script ";" is evaluated, but what
1138    // matters is that the callback ran and installed host functions.
1139    //
1140    // If the pipeline isn't ready yet (WebView just created), drain_callbacks
1141    // spins the servo event loop and retries until the pipeline is established.
1142    page.drain_callbacks()?;
1143
1144    // After drain, retrieve this page's pointers via WebViewId (NOT a global).
1145    // PageInner stores them as opaque addresses; it never dereferences them —
1146    // they flow back into WebViewId-keyed callbacks (same ScriptThread) later.
1147    let page_global = get_page_global(webview_id);
1148    let node_global = get_node_realm_global(webview_id);
1149    page.set_page_global(page_global, node_global);
1150
1151    if !registered {
1152        // Fallback: inject Web-only polyfill string (REQ-SEC-003: NO Node APIs on Window global)
1153        page.evaluate_js_web(WEB_POLYFILLS)?;
1154    }
1155
1156    Ok(())
1157}
1158
1159/// Attempt to register bun_runtime's native host functions via servo's callback mechanism.
1160///
1161/// Returns `true` if registration succeeded, `false` if servo's API is unavailable
1162/// (e.g., older servo build without `register_script_thread_callback`).
1163///
1164/// If `stealth_profile` is provided, stealth properties are installed as PERMANENT
1165/// engine-layer getters after the Node.js host functions.
1166//
1167// @trace REQ-BRW-003 [req:REQ-BRW-003] [criterion:C10]
1168// BCE-20260621-001: WebViewId captured by callback so install_all_native can
1169// store the page_global under the correct WebViewId key, not a global slot.
1170fn register_native_host_functions(
1171    webview_id: servo::WebViewId,
1172    stealth_profile: Option<bao_stealth::StealthProfile>,
1173) -> bool {
1174    let callback: Box<dyn FnOnce(*mut std::ffi::c_void, *mut std::ffi::c_void) + Send> =
1175        Box::new(move |cx_ptr, global_ptr| {
1176            // SAFETY: Called on servo's script thread with valid JSContext/JSObject.
1177            unsafe {
1178                install_all_native(webview_id, cx_ptr, global_ptr, &stealth_profile);
1179            }
1180        });
1181
1182    servo::register_script_thread_callback(webview_id, callback);
1183    true
1184}
1185
1186/// Bridge callback: cast raw servo pointers to mozjs types and install all host functions.
1187///
1188/// Called on servo's script thread during `handle_evaluate_javascript` drain.
1189/// `cx_ptr` is `*mut mozjs::jsapi::JSContext` (servo's script thread JSContext).
1190/// `global_ptr` is `*mut mozjs::jsapi::JSObject` (servo's Window global object).
1191///
1192/// If `stealth_profile` is `Some`, installs stealth properties as PERMANENT engine-layer
1193/// getters (JSPROP_PERMANENT ≡ configurable:false) after the Node.js host functions.
1194///
1195/// BUG-ENG-366 / REQ-SEC-002: the stealth profile is registered PER-REALM keyed
1196/// by this page's Window global pointer via `set_profile_for_global`. This makes
1197/// Compartment isolation unconditional — no dependency on servo's
1198/// `force_isolate_event_loops` flag. When multiple pages share a single servo
1199/// ScriptThread (force_isolate=false), each page's Window global is still
1200/// distinct, so each page's stealth getters resolve to its own profile. The
1201/// Node Realm global for this page is aliased to the same profile in
1202/// `create_node_realm_native`. @trace REQ-SEC-002 [req:REQ-SEC-002] [req:BUG-ENG-366]
1203//
1204// @trace REQ-BRW-003 [req:REQ-BRW-003] [criterion:C10]
1205// BCE-20260621-001: page_global stored keyed by WebViewId, replacing the
1206// process-wide LAST_PAGE_GLOBAL AtomicUsize. Eliminates the "last writer
1207// wins" race that let PageInner capture another page's pointer.
1208unsafe fn install_all_native(
1209    webview_id: servo::WebViewId,
1210    cx_ptr: *mut std::ffi::c_void,
1211    global_ptr: *mut std::ffi::c_void,
1212    stealth_profile: &Option<bao_stealth::StealthProfile>,
1213) {
1214    use mozjs::context::JSContext;
1215    use mozjs::jsapi::{JSContext as RawJSContext, JSObject};
1216    use std::ptr::NonNull;
1217
1218    let raw_cx = cx_ptr as *mut RawJSContext;
1219    let raw_global = global_ptr as *mut JSObject;
1220
1221    if raw_cx.is_null() || raw_global.is_null() {
1222        return;
1223    }
1224
1225    // Cache this ScriptThread's current servo Window global in thread-local.
1226    // lazy_dom_getter_impl (which runs as a JSNative ON this ScriptThread)
1227    // reads it to fetch window/document/navigator. Same-thread read/write —
1228    // no cross-thread *mut JSObject access.
1229    PER_THREAD_PAGE_GLOBAL.with(|cell| {
1230        *cell.borrow_mut() = raw_global;
1231    });
1232
1233    // BCE-20260621-001: store page_global keyed by WebViewId so
1234    // inject_node_apis_with_stealth can retrieve it via get_page_global(wid)
1235    // after drain — replacing the process-wide LAST_PAGE_GLOBAL.
1236    page_global_by_webview().insert(webview_id, raw_global as usize);
1237
1238    // BUG-ENG-366: register per-Realm profile (unconditional Compartment isolation).
1239    if let Some(profile) = stealth_profile {
1240        bao_stealth::engine_props::set_profile_for_global(raw_global as usize, profile);
1241        bao_stealth::engine_props::set_profile(profile);
1242        bun_runtime::fetch_api::set_fetch_stealth_profile(Some(profile.clone()));
1243        // Set canvas noise at servo rendering layer (REQ-STL-003).
1244        servo::set_canvas_noise_seed(
1245            bao_stealth::engine_props::canvas_seed(),
1246            bao_stealth::engine_props::canvas_amplitude(),
1247        );
1248        // Set stealth TLS/HTTP2 config at servo network layer (REQ-STL-001, REQ-STL-002).
1249        // This makes servo's BoringSSL+hyper connections use the profile's cipher suites,
1250        // curves, signature algorithms, ALPN, and HTTP/2 settings. BoringSSL supports full
1251        // JA3/JA4 fingerprint configuration including cipher suite reordering.
1252        // Convert bao_stealth config to servo net connector config (two identical structs
1253        // in different crates — servo net cannot depend on bao_stealth).
1254        let stc = bao_stealth::StealthTlsWireConfig::from_profile(profile);
1255        servo::set_stealth_tls_config(Some(servo::StealthTlsWireConfig {
1256            tls12_cipher_suites: stc.tls12_cipher_suites,
1257            tls13_cipher_suites: stc.tls13_cipher_suites,
1258            signature_algorithms: stc.signature_algorithms,
1259            supported_groups: stc.supported_groups,
1260            alpn_protocols: stc.alpn_protocols,
1261            h2_settings_payload: stc.h2_settings_payload,
1262            h2_initial_stream_size: stc.h2_initial_stream_size,
1263            h2_initial_connection_window_size: stc.h2_initial_connection_window_size,
1264            h2_max_frame_size: stc.h2_max_frame_size,
1265            h2_max_header_list_size: stc.h2_max_header_list_size,
1266        }));
1267        // U2 stage 2: the servo-net bun bridge (net thread) reads the h2
1268        // pseudo-header order / preface PRIORITY frames from this global —
1269        // same lifecycle as the wire-config global above (engine_props'
1270        // ScriptThread-scoped profile lookups are unreachable there).
1271        bao_stealth::set_global_http2_fingerprint(Some(&profile.http2));
1272    } else {
1273        bun_runtime::fetch_api::set_fetch_stealth_profile(None);
1274        servo::set_stealth_tls_config(None);
1275        bao_stealth::set_global_http2_fingerprint(None);
1276    }
1277
1278
1279    // Install stealth properties using raw JSAPI (no Handle wrapper needed)
1280    bao_stealth::engine_props::install_stealth_props(raw_cx, raw_global);
1281
1282    // Create a proper JSContext wrapper and root the global for Web API installation
1283    let cx_nn = match NonNull::new(raw_cx) {
1284        Some(nn) => nn,
1285        None => return,
1286    };
1287    let mut cx = JSContext::from_ptr(cx_nn);
1288    rooted!(in(raw_cx) let mut rooted_global = raw_global);
1289    let global_handle = rooted_global.handle();
1290
1291    // Install Web APIs using properly rooted handle
1292    bun_runtime::fetch_api::install_fetch_global(&mut cx, global_handle);
1293    bun_runtime::timers::install_timer_globals(&mut cx, global_handle);
1294    bun_runtime::web_api::install_performance(&mut cx, global_handle);
1295    bun_runtime::web_api::install_websocket_constructor(&mut cx, global_handle);
1296    bun_runtime::globals::install_crypto_global(&mut cx, global_handle);
1297    bun_runtime::web_api::install_web_encodings(&mut cx, global_handle);
1298    bun_runtime::web_api::install_atob_btoa(&mut cx, global_handle);
1299    bun_runtime::web_api::install_queue_microtask(&mut cx, global_handle);
1300    bun_runtime::globals::install_structured_clone(&mut cx, global_handle);
1301    bun_runtime::globals::install_web_api_constructors(&mut cx, global_handle);
1302    // Full WHATWG Headers/Request/Response classes — installed AFTER the
1303    // constructors blob so their lazy deps (Blob/AbortController/
1304    // ReadableStream/TextEncoder) are already on the global (same ordering
1305    // as bun_runtime::globals::install_web_apis).
1306    bun_runtime::web_fetch_classes::install_fetch_classes(&mut cx, global_handle);
1307
1308    // REQ-ENG-001 criterion 5: Ensure WebAssembly global is available.
1309    // SpiderMonkey provides WebAssembly as a standard global class. It is lazily
1310    // resolved via JS_ResolveStandardClass (the resolve hook on SIMPLE_GLOBAL_CLASS).
1311    // We explicitly trigger resolution by evaluating `typeof WebAssembly` so that
1312    // the global is populated immediately rather than on first access.
1313    {
1314        use mozjs::rust::CompileOptionsWrapper;
1315        let wasm_check = r#"(function(){ try { return typeof WebAssembly; } catch(e) { return 'undefined'; } })()"#;
1316        let c_filename = c"<wasm-init>".to_owned();
1317        let mut options = CompileOptionsWrapper::new(&mut cx, c_filename, 1);
1318        // BAO PATCH (BCE-20260622-004): Suppress `onNewScript` (same rationale
1319        // as evaluate_in_node_realm above).
1320        options.set_hide_script_from_debugger(true);
1321        rooted!(&in(cx) let mut wasm_rval = mozjs::jsval::UndefinedValue());
1322        let wasm_probe = mozjs::rust::evaluate_script(
1323            &mut cx,
1324            global_handle,
1325            wasm_check,
1326            wasm_rval.handle_mut(),
1327            options,
1328        );
1329        // This evaluate_script is a best-effort probe for WebAssembly support.
1330        // A failure here is benign (WebAssembly unavailable), so the Err is
1331        // intentionally discarded. BCE-20260627-007: not an error swallow — the
1332        // probe is informational only, not a functional script.
1333        //
1334        // BCE (P0 browser startup panic, servo error.rs:74): "discard the Err"
1335        // must still consume the pending exception a failed evaluate leaves on
1336        // the context — this runs on servo's ScriptThread during page init, and
1337        // a stale pending exception detonates servo's
1338        // `assert!(!JS_IsExceptionPending)` in `throw_dom_exception` on the
1339        // next error path, killing the ScriptThread (browser dies at startup,
1340        // CDP never listens). The install_all_native borrow of this cx must
1341        // return it clean.
1342        if wasm_probe.is_err() {
1343            mozjs::jsapi::JS_ClearPendingException(cx.raw_cx());
1344        }
1345    }
1346}
1347
1348/// Inject both Node.js APIs and stealth scripts into a page.
1349pub fn inject_all(page: &PageHandle, stealth: bool) -> Result<(), BrowserError> {
1350    let profile = if stealth {
1351        page.stealth_profile()
1352    } else {
1353        None
1354    };
1355    inject_node_apis_with_stealth(page, profile)
1356}
1357
1358/// Inject Node.js APIs and (if profile present) stealth properties into a page.
1359///
1360/// Stealth properties are installed as PERMANENT engine-layer getters (zero JS injection).
1361//
1362// DEC-WK-001 / TASK-1 (双轨收敛): Also registers a servo-native Worker scope
1363// callback via `servo::register_worker_scope_callback`. When a Worker is
1364// created via servo's DOM `new Worker(url)` (https/http URLs), this callback
1365// fires on the Worker thread after servo constructs the Worker global and
1366// installs bao's DedicatedWorkerGlobalScope APIs + stealth profile inheritance
1367// (the same `worker_scope_init_native` path used by the bao_engine::WebWorker
1368// bypass). This realizes DEC-WK-001's "servo-native Worker path" without
1369// abandoning the bypass (DEC-WK-003 dual-track isolation).
1370//
1371// @trace DEC-WK-001 servo-native Worker path
1372// @trace DEC-WK-003 dual-track: bypass (CLI/data:) vs native (https/http)
1373// @trace REQ-BRW-004 [entity:Worker] [criterion:1,3,7] servo Worker scope
1374pub fn inject_all_with_profile(
1375    page: &PageHandle,
1376    profile: &Option<bao_stealth::StealthProfile>,
1377) -> Result<(), BrowserError> {
1378    inject_node_apis_with_stealth(page, profile.clone())?;
1379
1380    // Register the servo-native Worker scope callback. This fires on any
1381    // DedicatedWorker created via servo's DOM `new Worker()`. The callback
1382    // captures this page's WorkerScopeConfig (which carries the parent page's
1383    // stealth profile) so the Worker inherits stealth fingerprint noise.
1384    // @trace DEC-WK-001 servo-native Worker path (vendor patch drain)
1385    // @trace REQ-BRW-004 [criterion:12..17] CRIT-STL-WK stealth inheritance
1386    register_worker_scope_callback_native(profile.clone());
1387
1388    Ok(())
1389}
1390
1391/// Register a servo-native Worker scope callback via the vendor patch
1392/// `servo::register_worker_scope_callback` (DEC-WK-001 / TASK-1).
1393///
1394/// The callback is queued in servo's global `EMBEDDER_WORKER_SCOPE_CALLBACKS`
1395/// vector and drained once per Worker scope creation inside
1396/// `DedicatedWorkerGlobalScope::run_worker_scope` — after the Worker's global
1397/// is built but before the event loop starts. It runs on the Worker thread
1398/// (the same thread that owns the Worker's JSContext), so it is safe to
1399/// dereference the raw `cx`/`global` pointers there (per BCE-20260621-001:
1400/// DOM↔Node interop must happen on the owning thread).
1401///
1402/// What the callback does (mirrors `worker_scope_init_native` for the bypass):
1403///   - Install stealth profile inheritance keyed by the Worker global's address
1404///     (DEC-WK-007 / CRIT-STL-WK: Worker navigator/Canvas/WebGL/Audio match
1405///     parent page).
1406///   - Install DedicatedWorkerGlobalScope Web APIs (fetch/timers/crypto/
1407///     performance/etc., criterion #8).
1408///
1409/// Note: lifecycle natives (self.close/importScripts) are installed by servo
1410/// upstream's DedicatedWorkerGlobalScope binding for native Workers, so the
1411/// bao bypass's `install_worker_lifecycle_natives` is NOT needed here — it is
1412/// only needed for bao_engine::WebWorker (DEC-WK-003 dual-track).
1413///
1414/// @trace DEC-WK-001 servo-native Worker path (vendor patch)
1415/// @trace DEC-WK-003 dual-track isolation (bypass not abandoned)
1416/// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope] [criterion:8,12..17]
1417pub fn register_worker_scope_callback_native(profile: Option<bao_stealth::StealthProfile>) {
1418    // Build a WorkerScopeConfig from the stealth profile (parent page's config).
1419    // This is what worker_scope_init_native needs to install the right stealth
1420    // properties + navigator values on the Worker's global.
1421    // @trace REQ-BRW-004 [criterion:12..17] CRIT-STL-WK
1422    let config = match profile.as_ref() {
1423        Some(p) => crate::delegate::WorkerScopeConfig::from(p),
1424        None => crate::delegate::WorkerScopeConfig::default(),
1425    };
1426
1427    let callback: Box<dyn FnOnce(*mut std::ffi::c_void, *mut std::ffi::c_void) + Send> =
1428        Box::new(move |cx_ptr, global_ptr| {
1429            // SAFETY: Called on the Worker thread with valid JSContext + global.
1430            // Per BCE-20260621-001 this is the owning thread, so dereferencing
1431            // the raw pointers is safe (no cross-thread *mut JSObject).
1432            let raw_cx = cx_ptr as *mut mozjs::jsapi::JSContext;
1433            let raw_global = global_ptr as *mut mozjs::jsapi::JSObject;
1434            if raw_cx.is_null() || raw_global.is_null() {
1435                log::warn!(
1436                    "[register_worker_scope_callback_native] NULL cx/global — \
1437                     skipping Worker scope init (DEC-WK-001)"
1438                );
1439                return;
1440            }
1441            // TASK-63 DIAG: confirm worker scope callback fired (worker thread alive + scope created)
1442            eprintln!(
1443                "[TASK-63-DIAG] worker scope callback FIRED (worker thread alive, scope created)"
1444            );
1445            log::debug!(
1446                "[register_worker_scope_callback_native] servo-native Worker \
1447                 scope created — installing bao stealth + Web APIs (DEC-WK-001 / \
1448                 DEC-WK-003 dual-track)"
1449            );
1450            unsafe {
1451                worker_scope_init_native(raw_cx, raw_global, &config);
1452            }
1453        });
1454
1455    servo::register_worker_scope_callback(callback);
1456}
1457
1458// ─── Worker Scope Initialization Bridge (REQ-BRW-004) ──────────────
1459// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope] [criterion:8]
1460// @trace REQ-BRW-004 [entity:Worker] [criterion:12..17]
1461//
1462// worker_scope_init_native runs on the Worker thread with its own JSContext
1463// and global object (installed via register_worker_scope_callback_native →
1464// servo's drain_worker_scope_callbacks). It installs:
1465// 1. Stealth properties (criterion #12-17): PERMANENT engine-layer getters
1466//    for navigator/Canvas/WebGL/Audio fingerprints matching the parent page
1467// 2. (DELETED BCE-20260627-009) Web APIs like fetch/timers/crypto/performance
1468//    were incorrectly installed here. servo's DedicatedWorkerGlobalScope already
1469//    provides these via its own resource thread integration. Bao's duplicate
1470//    installation caused: (a) two promise execution models to conflict, and
1471//    (b) JS_DefineFunction SIGSEGV in js::Atomize because the callback ran
1472//    without entering the worker global's Compartment (now fixed in servo vendor).
1473
1474/// Type alias for the Worker scope initialization callback.
1475///
1476/// A boxed closure that runs on the Worker thread to install APIs and stealth
1477/// properties on the Worker's global object (DEC-WK-001 native path).
1478///
1479/// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope] [criterion:8]
1480/// @trace REQ-BRW-004 [criterion:12..17] stealth consistency
1481pub type WorkerScopeInitFn =
1482    Box<dyn FnOnce(*mut mozjs::jsapi::JSContext, *mut mozjs::jsapi::JSObject) + Send>;
1483
1484/// Native implementation: install stealth properties on the Worker's global object.
1485///
1486/// Called on the Worker thread with the Worker's JSContext and global.
1487/// This is the same pattern as `install_all_native` for the Page Realm,
1488/// but scoped to the Worker's DedicatedWorkerGlobalScope.
1489///
1490/// NOTE: Web APIs (fetch/timers/crypto/performance/etc.) are NOT installed here.
1491/// servo's DedicatedWorkerGlobalScope already provides these natively via its
1492/// resource thread. Bao does NOT duplicate them (BCE-20260627-009).
1493///
1494/// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope] [criterion:8]
1495/// @trace REQ-BRW-004 [criterion:12..17] stealth consistency
1496unsafe fn worker_scope_init_native(
1497    raw_cx: *mut mozjs::jsapi::JSContext,
1498    global: *mut mozjs::jsapi::JSObject,
1499    config: &crate::delegate::WorkerScopeConfig,
1500) {
1501    use mozjs::context::JSContext;
1502    use std::ptr::NonNull;
1503
1504    if raw_cx.is_null() || global.is_null() {
1505        return;
1506    }
1507
1508    // @trace REQ-BRW-004 [criterion:12..17] stealth consistency
1509    // Install stealth properties on Worker global if profile is provided.
1510    // bao_stealth::engine_props::install_stealth_props uses raw JSAPI
1511    // to install PERMANENT engine-layer getters for navigator/Canvas/WebGL/Audio.
1512    // The profile is keyed by the Worker global's address, so stealth getters
1513    // in the Worker's DedicatedWorkerGlobalScope resolve to the same
1514    // fingerprint noise as the parent page.
1515    if let Some(ref profile) = config.stealth_profile {
1516        // @trace REQ-BRW-004 [criterion:12] CRIT-STL-WK navigator 一致
1517        bao_stealth::engine_props::set_profile_for_global(global as usize, profile);
1518        bao_stealth::engine_props::install_stealth_props(raw_cx, global);
1519        // Set canvas noise at servo rendering layer for Worker (REQ-STL-003).
1520        servo::set_canvas_noise_seed(
1521            bao_stealth::engine_props::canvas_seed(),
1522            bao_stealth::engine_props::canvas_amplitude(),
1523        );
1524    }
1525
1526    // BCE-20260627-009: Web APIs (fetch/timers/crypto/performance/etc.) are NOT
1527    // installed on Worker global. servo's DedicatedWorkerGlobalScope provides these
1528    // natively. Duplicate installation caused SIGSEGV in js::Atomize and promise
1529    // execution model conflicts. See vendor patch in dedicatedworkerglobalscope.rs.
1530}
1531
1532// @trace REQ-SEC-003 [entity:WebPolyfills]
1533/// Web-only polyfills for Page Realm fallback (REQ-SEC-003: NO Node APIs on Window global).
1534///
1535/// This is the fallback when `register_native_host_functions` is unavailable.
1536/// It provides ONLY standard Web APIs that browsers should have but may be missing
1537/// in servo's script context. Node.js APIs (require, process, Buffer, Bun, etc.)
1538/// are deliberately EXCLUDED — they belong only in the Node Realm.
1539const WEB_POLYFILLS: &str = r#"(function() {
1540  // @trace REQ-SEC-003 Web-only polyfills (no Node.js APIs)
1541
1542  // TextEncoder / TextDecoder
1543  if (typeof TextEncoder === 'undefined') {
1544    TextEncoder = function() { this.encode = function(str) { return new Uint8Array(Array.from(str).map(function(c){return c.charCodeAt(0);})); }; };
1545  }
1546  if (typeof TextDecoder === 'undefined') {
1547    TextDecoder = function() { this.decode = function(buf) { return String.fromCharCode.apply(null, buf); }; };
1548  }
1549
1550  // URL / URLSearchParams
1551  if (typeof URL === 'undefined') {
1552    URL = function(url, base) { throw new Error('URL not available'); };
1553  }
1554  if (typeof URLSearchParams === 'undefined') {
1555    URLSearchParams = function(init) {
1556      this._params = [];
1557      this.append = function(k,v) { this._params.push([k,v]); };
1558      this.get = function(k) { for(var i=0;i<this._params.length;i++) if(this._params[i][0]===k) return this._params[i][1]; return null; };
1559      this.toString = function() { return this._params.map(function(p){return p[0]+'='+p[1];}).join('&'); };
1560    };
1561  }
1562
1563  // btoa / atob
1564  if (typeof btoa === 'undefined') {
1565    var _b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
1566    btoa = function(str) {
1567      var out = '';
1568      for (var i = 0; i < str.length; i += 3) {
1569        var a = str.charCodeAt(i), b = str.charCodeAt(i+1), c = str.charCodeAt(i+2);
1570        out += _b64chars[a>>2] + _b64chars[((a&3)<<4)|(b>>4)] + (isNaN(b)?'=':_b64chars[((b&15)<<2)|(c>>6)]) + (isNaN(b)||isNaN(c)?'=':_b64chars[c&63]);
1571      }
1572      return out;
1573    };
1574    atob = function(str) {
1575      var out = '';
1576      str = str.replace(/=+$/, '');
1577      for (var i = 0; i < str.length; i += 4) {
1578        var a = _b64chars.indexOf(str[i]), b = _b64chars.indexOf(str[i+1]);
1579        var c = _b64chars.indexOf(str[i+2]), d = _b64chars.indexOf(str[i+3]);
1580        out += String.fromCharCode((a<<2)|(b>>4)) + (c>=0?String.fromCharCode(((b&15)<<4)|(c>>2)):'') + (d>=0?String.fromCharCode(((c&3)<<6)|d):'');
1581      }
1582      return out;
1583    };
1584  }
1585
1586  // setImmediate / clearImmediate (Web API extensions used by many libraries)
1587  if (typeof setImmediate === 'undefined') {
1588    setImmediate = function(fn) {
1589      var args = Array.prototype.slice.call(arguments, 1);
1590      return setTimeout(function() { fn.apply(null, args); }, 0);
1591    };
1592    clearImmediate = function(id) { clearTimeout(id); };
1593  }
1594})();"#;
1595
1596const NODE_POLYFILLS: &str = r#"(function() {
1597  // @trace REQ-ENG-007 Node.js API polyfills for Node Realm context
1598
1599  // global alias
1600  if (typeof global === 'undefined') {
1601    global = globalThis;
1602  }
1603
1604  // process
1605  if (typeof process === 'undefined') {
1606    process = {
1607      argv: ['bao', typeof __filename !== 'undefined' ? __filename : ''],
1608      argv0: 'bao',
1609      execArgv: [],
1610      execPath: '/usr/local/bin/bao',
1611      env: (function() {
1612        var e = {};
1613        if (typeof navigator !== 'undefined' && navigator.userAgent) {
1614          e.NODE_VERSION = '20.11.0';
1615          e.BAO_VERSION = '0.1.0';
1616        }
1617        e.HOME = '/';
1618        e.PATH = '/usr/local/bin:/usr/bin:/bin';
1619        e.TERM = 'xterm-256color';
1620        return e;
1621      })(),
1622      version: 'v20.11.0',
1623      versions: {
1624        node: '20.11.0',
1625        v8: '12.4.254.14',
1626        uv: '1.27.0',
1627        zlib: '1.2.13',
1628        brotli: '1.0.9',
1629        ares: '1.19.1',
1630        modules: '115',
1631        openssl: '3.0.12',
1632        icu: '74.2',
1633        bun: '1.0.25',
1634        bao: '0.1.0',
1635      },
1636      pid: 1,
1637      ppid: 0,
1638      title: 'bao',
1639      arch: (function() {
1640        if (typeof navigator !== 'undefined') {
1641          var p = navigator.platform || '';
1642          if (p.indexOf('Win') >= 0) return 'x64';
1643          if (p.indexOf('Mac') >= 0) return 'arm64';
1644          if (p.indexOf('Linux') >= 0) return 'x64';
1645        }
1646        return 'x64';
1647      })(),
1648      platform: (function() {
1649        if (typeof navigator !== 'undefined') {
1650          var p = navigator.platform || '';
1651          if (p.indexOf('Win') >= 0) return 'win32';
1652          if (p.indexOf('Mac') >= 0) return 'darwin';
1653        }
1654        return 'linux';
1655      })(),
1656      cwd: function() { return '/'; },
1657      chdir: function() {},
1658      exit: function(code) { throw new Error('process.exit(' + (code||0) + ')'); },
1659      hrtime: (function() {
1660        var origin = performance.now() * 1e-3;
1661        return function bigtime() {
1662          var diff = performance.now() * 1e-3 - origin;
1663          var sec = Math.floor(diff);
1664          var nsec = Math.round((diff - sec) * 1e9);
1665          if (arguments.length > 0) {
1666            sec += arguments[0][0];
1667            nsec += arguments[0][1];
1668            sec += Math.floor(nsec / 1e9);
1669            nsec = nsec % 1e9;
1670            if (nsec < 0) { nsec += 1e9; sec -= 1; }
1671          }
1672          var result = [sec, nsec];
1673          result.bigint = function() { return BigInt(sec) * 1000000000n + BigInt(nsec); };
1674          return result;
1675        };
1676      })(),
1677      uptime: function() { return performance.now() / 1000; },
1678      memoryUsage: function() {
1679        return { rss: 64*1024*1024, heapTotal: 32*1024*1024, heapUsed: 16*1024*1024, external: 2*1024*1024, arrayBuffers: 1*1024*1024 };
1680      },
1681      cpuUsage: function() { return { user: 100000, system: 50000 }; },
1682      nextTick: function(fn) {
1683        var args = Array.prototype.slice.call(arguments, 1);
1684        Promise.resolve().then(function() { fn.apply(null, args); });
1685      },
1686      binding: function(name) { return {}; },
1687      dlopen: function() { throw new Error('process.dlopen not available in browser context'); },
1688      stdout: { write: function(d) { console.log(d); return true; }, end: function() {} },
1689      stderr: { write: function(d) { console.error(d); return true; }, end: function() {} },
1690      stdin: { on: function() {}, resume: function() { return this; }, pipe: function() {} },
1691      on: function(event, fn) { return this; },
1692      off: function() {},
1693      once: function(event, fn) { return this; },
1694      emit: function(event) { return false; },
1695      removeAllListeners: function() { return this; },
1696      setUncaughtExceptionCallback: function() {},
1697    };
1698  }
1699
1700  // Buffer — browser-compatible implementation backed by Uint8Array
1701  if (typeof Buffer === 'undefined') {
1702    Buffer = (function() {
1703      function B(data, encoding) {
1704        if (!(this instanceof B)) return new B(data, encoding);
1705        if (data instanceof Uint8Array) {
1706          this._buf = new Uint8Array(data);
1707        } else if (data instanceof ArrayBuffer) {
1708          this._buf = new Uint8Array(data);
1709        } else if (typeof data === 'string') {
1710          this._buf = new Uint8Array(Array.from(data).map(function(c) { return c.charCodeAt(0); }));
1711        } else if (Array.isArray(data)) {
1712          this._buf = new Uint8Array(data);
1713        } else {
1714          this._buf = new Uint8Array(0);
1715        }
1716        this.length = this._buf.length;
1717      }
1718
1719      B.isBuffer = function(obj) { return obj instanceof B; };
1720
1721      B.from = function(data, encoding) {
1722        if (data instanceof B) return new B(data._buf);
1723        if (data instanceof Uint8Array) return new B(data);
1724        if (data instanceof ArrayBuffer) return new B(data);
1725        if (typeof data === 'string') {
1726          if (encoding === 'hex') {
1727            var bytes = [];
1728            for (var i = 0; i < data.length; i += 2) {
1729              bytes.push(parseInt(data.substr(i, 2), 16));
1730            }
1731            return new B(bytes);
1732          }
1733          if (encoding === 'base64') {
1734            var bin = atob(data);
1735            var bytes = [];
1736            for (var i = 0; i < bin.length; i++) bytes.push(bin.charCodeAt(i));
1737            return new B(bytes);
1738          }
1739          return new B(data);
1740        }
1741        return new B(data);
1742      };
1743
1744      B.alloc = function(size, fill, encoding) {
1745        var buf = new B(new Uint8Array(size));
1746        if (fill !== undefined) buf.fill(fill);
1747        return buf;
1748      };
1749
1750      B.allocUnsafe = function(size) {
1751        return new B(new Uint8Array(size));
1752      };
1753
1754      B.allocUnsafeSlow = function(size) {
1755        return new B(new Uint8Array(size));
1756      };
1757
1758      B.concat = function(list, totalLength) {
1759        if (!Array.isArray(list) || list.length === 0) return new B(new Uint8Array(0));
1760        var len = totalLength !== undefined ? totalLength : list.reduce(function(a, b) { return a + b.length; }, 0);
1761        var result = new Uint8Array(len);
1762        var offset = 0;
1763        for (var i = 0; i < list.length; i++) {
1764          var buf = list[i] instanceof B ? list[i]._buf : new Uint8Array(list[i]);
1765          result.set(buf, offset);
1766          offset += buf.length;
1767        }
1768        return new B(result);
1769      };
1770
1771      B.byteLength = function(str, encoding) {
1772        if (typeof str === 'string') {
1773          if (encoding === 'base64') return atob(str).length;
1774          if (encoding === 'hex') return str.length / 2;
1775          return new TextEncoder().encode(str).length;
1776        }
1777        if (str instanceof ArrayBuffer) return str.byteLength;
1778        if (str instanceof Uint8Array) return str.length;
1779        return 0;
1780      };
1781
1782      B.compare = function(a, b) {
1783        for (var i = 0; i < Math.min(a.length, b.length); i++) {
1784          if (a._buf[i] < b._buf[i]) return -1;
1785          if (a._buf[i] > b._buf[i]) return 1;
1786        }
1787        return a.length - b.length;
1788      };
1789
1790      B.prototype.slice = function(start, end) {
1791        return new B(this._buf.slice(start || 0, end));
1792      };
1793
1794      B.prototype.subarray = function(start, end) {
1795        return new B(this._buf.subarray(start || 0, end));
1796      };
1797
1798      B.prototype.toString = function(encoding, start, end) {
1799        var s = start || 0;
1800        var e = end !== undefined ? end : this._buf.length;
1801        var slice = this._buf.slice(s, e);
1802        if (encoding === 'hex') {
1803          return Array.from(slice).map(function(b) { return b.toString(16).padStart(2, '0'); }).join('');
1804        }
1805        if (encoding === 'base64') {
1806          var bin = Array.from(slice).map(function(b) { return String.fromCharCode(b); }).join('');
1807          return btoa(bin);
1808        }
1809        return new TextDecoder().decode(slice);
1810      };
1811
1812      B.prototype.toJSON = function() {
1813        return { type: 'Buffer', data: Array.from(this._buf) };
1814      };
1815
1816      B.prototype.equals = function(other) {
1817        if (!(other instanceof B) || this.length !== other.length) return false;
1818        for (var i = 0; i < this.length; i++) {
1819          if (this._buf[i] !== other._buf[i]) return false;
1820        }
1821        return true;
1822      };
1823
1824      B.prototype.compare = function(other, targetStart, targetEnd, sourceStart, sourceEnd) {
1825        var a = this._buf.slice(sourceStart || 0, sourceEnd);
1826        var b = other._buf.slice(targetStart || 0, targetEnd);
1827        for (var i = 0; i < Math.min(a.length, b.length); i++) {
1828          if (a[i] < b[i]) return -1;
1829          if (a[i] > b[i]) return 1;
1830        }
1831        return a.length - b.length;
1832      };
1833
1834      B.prototype.copy = function(target, targetStart, sourceStart, sourceEnd) {
1835        var src = this._buf.slice(sourceStart || 0, sourceEnd);
1836        for (var i = 0; i < src.length; i++) {
1837          if (target._buf) target._buf[targetStart + i] = src[i];
1838        }
1839        return src.length;
1840      };
1841
1842      B.prototype.fill = function(value, start, end) {
1843        var s = start || 0;
1844        var e = end !== undefined ? end : this._buf.length;
1845        var v = typeof value === 'number' ? value : 0;
1846        for (var i = s; i < e; i++) this._buf[i] = v;
1847        return this;
1848      };
1849
1850      B.prototype.write = function(str, offset, length, encoding) {
1851        var o = offset || 0;
1852        var bytes = new TextEncoder().encode(str);
1853        var len = Math.min(bytes.length, length !== undefined ? length : this._buf.length - o);
1854        for (var i = 0; i < len; i++) this._buf[o + i] = bytes[i];
1855        return len;
1856      };
1857
1858      B.prototype.includes = function(value, offset) {
1859        return this.indexOf(value, offset) !== -1;
1860      };
1861
1862      B.prototype.indexOf = function(value, offset) {
1863        var o = offset || 0;
1864        var search = typeof value === 'number' ? [value] : Array.from(new TextEncoder().encode(String(value)));
1865        for (var i = o; i <= this._buf.length - search.length; i++) {
1866          var found = true;
1867          for (var j = 0; j < search.length; j++) {
1868            if (this._buf[i + j] !== search[j]) { found = false; break; }
1869          }
1870          if (found) return i;
1871        }
1872        return -1;
1873      };
1874
1875      B.prototype.readUInt8 = function(offset) { return this._buf[offset || 0]; };
1876      B.prototype.readUInt16LE = function(offset) { var o = offset||0; return this._buf[o] | (this._buf[o+1]<<8); };
1877      B.prototype.readUInt16BE = function(offset) { var o = offset||0; return (this._buf[o]<<8) | this._buf[o+1]; };
1878      B.prototype.readUInt32LE = function(offset) {
1879        var o = offset||0;
1880        return (this._buf[o]) | (this._buf[o+1]<<8) | (this._buf[o+2]<<16) | (this._buf[o+3]<<24);
1881      };
1882      B.prototype.readInt8 = function(offset) { var v = this._buf[offset||0]; return v > 127 ? v - 256 : v; };
1883      B.prototype.readInt16LE = function(offset) { var v = this.readUInt16LE(offset); return v > 32767 ? v - 65536 : v; };
1884      B.prototype.readInt32LE = function(offset) { var v = this.readUInt32LE(offset); return v > 2147483647 ? v - 4294967296 : v; };
1885      B.prototype.readFloatLE = function(offset) {
1886        var buf = new ArrayBuffer(4); new Float32Array(buf)[0] = 0;
1887        new Uint8Array(buf).set(this._buf.slice(offset||0, (offset||0)+4));
1888        return new Float32Array(buf)[0];
1889      };
1890      B.prototype.readDoubleLE = function(offset) {
1891        var buf = new ArrayBuffer(8);
1892        new Uint8Array(buf).set(this._buf.slice(offset||0, (offset||0)+8));
1893        return new Float64Array(buf)[0];
1894      };
1895
1896      B.prototype.writeUInt8 = function(v, offset) { this._buf[offset||0] = v & 0xFF; return (offset||0)+1; };
1897      B.prototype.writeUInt16LE = function(v, offset) { var o = offset||0; this._buf[o]=v&0xFF; this._buf[o+1]=(v>>8)&0xFF; return o+2; };
1898      B.prototype.writeUInt32LE = function(v, offset) { var o = offset||0; this._buf[o]=v&0xFF; this._buf[o+1]=(v>>8)&0xFF; this._buf[o+2]=(v>>16)&0xFF; this._buf[o+3]=(v>>24)&0xFF; return o+4; };
1899      B.prototype.writeInt8 = function(v, offset) { return this.writeUInt8(v < 0 ? v + 256 : v, offset); };
1900      B.prototype.writeInt16LE = function(v, offset) { return this.writeUInt16LE(v < 0 ? v + 65536 : v, offset); };
1901      B.prototype.writeInt32LE = function(v, offset) { return this.writeUInt32LE(v < 0 ? v + 4294967296 : v, offset); };
1902      B.prototype.writeFloatLE = function(v, offset) {
1903        var buf = new ArrayBuffer(4); new Float32Array(buf)[0] = v;
1904        this._buf.set(new Uint8Array(buf), offset||0); return (offset||0)+4;
1905      };
1906      B.prototype.writeDoubleLE = function(v, offset) {
1907        var buf = new ArrayBuffer(8); new Float64Array(buf)[0] = v;
1908        this._buf.set(new Uint8Array(buf), offset||0); return (offset||0)+8;
1909      };
1910
1911      B.prototype[Symbol.iterator] = function() {
1912        var idx = 0; var buf = this._buf;
1913        return { next: function() { return idx < buf.length ? { value: buf[idx++], done: false } : { done: true }; } };
1914      };
1915
1916      return B;
1917    })();
1918  }
1919
1920  // require — basic module loader for browser context
1921  if (typeof require === 'undefined') {
1922    var _module_cache = {};
1923    var _module_builtin = {
1924      'fs': { readFileSync: function() { throw new Error('fs not available in browser context'); }, existsSync: function() { return false; } },
1925      'path': {
1926        join: function() { return Array.prototype.slice.call(arguments).join('/').replace(/\/+/g, '/'); },
1927        resolve: function() { var parts = Array.prototype.slice.call(arguments); return '/' + parts.join('/').replace(/\/+/g, '/'); },
1928        dirname: function(p) { return p.split('/').slice(0, -1).join('/') || '.'; },
1929        basename: function(p, ext) { var b = p.split('/').pop(); return ext && b.endsWith(ext) ? b.slice(0, -ext.length) : b; },
1930        extname: function(p) { var i = p.lastIndexOf('.'); return i >= 0 ? p.slice(i) : ''; },
1931        sep: '/', delimiter: ':',
1932        posix: {
1933          join: function() { return Array.prototype.slice.call(arguments).join('/').replace(/\/+/g, '/'); },
1934          resolve: function() { var parts = Array.prototype.slice.call(arguments); return '/' + parts.join('/').replace(/\/+/g, '/'); },
1935          dirname: function(p) { return p.split('/').slice(0, -1).join('/') || '.'; },
1936          basename: function(p, ext) { var b = p.split('/').pop(); return ext && b.endsWith(ext) ? b.slice(0, -ext.length) : b; },
1937          extname: function(p) { var i = p.lastIndexOf('.'); return i >= 0 ? p.slice(i) : ''; },
1938          sep: '/', delimiter: ':',
1939        },
1940        win32: { sep: '\\', delimiter: ';' },
1941      },
1942      'url': {
1943        parse: function(u) { try { var p = new URL(u); return { href: p.href, protocol: p.protocol, host: p.host, hostname: p.hostname, pathname: p.pathname, search: p.search, hash: p.hash }; } catch(e) { return {}; } },
1944        format: function(u) { return typeof u === 'string' ? u : (u.protocol||'http:') + '//' + (u.host||u.hostname||'localhost') + (u.pathname||'/'); },
1945        resolve: function(from, to) { try { return new URL(to, from).href; } catch(e) { return to; } },
1946        URL: typeof URL !== 'undefined' ? URL : function() {},
1947        URLSearchParams: typeof URLSearchParams !== 'undefined' ? URLSearchParams : function() {},
1948      },
1949      'querystring': {
1950        parse: function(str, sep, eq) {
1951          sep = sep || '&'; eq = eq || '=';
1952          var obj = {};
1953          if (!str) return obj;
1954          str.split(sep).forEach(function(pair) {
1955            var idx = pair.indexOf(eq);
1956            var key = idx >= 0 ? pair.substring(0, idx) : pair;
1957            var val = idx >= 0 ? pair.substring(idx + 1) : '';
1958            obj[decodeURIComponent(key)] = decodeURIComponent(val);
1959          });
1960          return obj;
1961        },
1962        stringify: function(obj, sep, eq) {
1963          sep = sep || '&'; eq = eq || '=';
1964          return Object.keys(obj || {}).map(function(k) {
1965            return encodeURIComponent(k) + eq + encodeURIComponent(obj[k]);
1966          }).join(sep);
1967        },
1968        escape: encodeURIComponent,
1969        unescape: decodeURIComponent,
1970      },
1971      'events': {
1972        EventEmitter: (function() {
1973          function EE() { this._events = {}; }
1974          EE.prototype.on = function(e, fn) { (this._events[e] = this._events[e] || []).push(fn); return this; };
1975          EE.prototype.once = function(e, fn) { var self = this; function g() { self.off(e, g); fn.apply(this, arguments); } g._orig = fn; this.on(e, g); return this; };
1976          EE.prototype.off = function(e, fn) {
1977            if (!this._events[e]) return this;
1978            if (!fn) { delete this._events[e]; return this; }
1979            this._events[e] = this._events[e].filter(function(f) { return f !== fn && f._orig !== fn; });
1980            return this;
1981          };
1982          EE.prototype.emit = function(e) {
1983            var args = Array.prototype.slice.call(arguments, 1);
1984            (this._events[e] || []).forEach(function(fn) { fn.apply(null, args); });
1985            return this;
1986          };
1987          EE.prototype.removeListener = EE.prototype.off;
1988          EE.prototype.removeAllListeners = function(e) { if (e) delete this._events[e]; else this._events = {}; return this; };
1989          EE.prototype.listeners = function(e) { return this._events[e] || []; };
1990          EE.prototype.listenerCount = function(e) { return (this._events[e] || []).length; };
1991          return EE;
1992        })(),
1993      },
1994      'util': {
1995        inspect: function(obj) { return JSON.stringify(obj, null, 2); },
1996        inherits: function(ctor, superCtor) { ctor.prototype = Object.create(superCtor.prototype); ctor.prototype.constructor = ctor; },
1997        isFunction: function(v) { return typeof v === 'function'; },
1998        isNull: function(v) { return v === null; },
1999        isUndefined: function(v) { return v === undefined; },
2000        isObject: function(v) { return v !== null && typeof v === 'object'; },
2001        isString: function(v) { return typeof v === 'string'; },
2002        promisify: function(fn) {
2003          return function() {
2004            var args = Array.prototype.slice.call(arguments);
2005            return new Promise(function(resolve, reject) {
2006              args.push(function(err, result) { if (err) reject(err); else resolve(result); });
2007              fn.apply(null, args);
2008            });
2009          };
2010        },
2011        format: function(fmt) {
2012          var args = Array.prototype.slice.call(arguments, 1);
2013          return fmt.replace(/%[sdjifo]/g, function(m) { return args.length ? String(args.shift()) : m; });
2014        },
2015        types: {
2016          isDate: function(v) { return v instanceof Date; },
2017          isRegExp: function(v) { return v instanceof RegExp; },
2018          isArray: function(v) { return Array.isArray(v); },
2019          isPromise: function(v) { return v && typeof v.then === 'function'; },
2020        },
2021      },
2022      'stream': { Readable: function(){}, Writable: function(){}, Duplex: function(){}, Transform: function(){} },
2023      'buffer': { Buffer: typeof Buffer !== 'undefined' ? Buffer : function(){} },
2024      'crypto': {
2025        randomBytes: function(size, cb) {
2026          var arr = new Uint8Array(size);
2027          if (typeof crypto !== 'undefined' && crypto.getRandomValues) crypto.getRandomValues(arr);
2028          if (cb) cb(null, Buffer.from(arr));
2029          return Buffer.from(arr);
2030        },
2031        createHash: function(algo) {
2032          var chunks = [];
2033          return {
2034            update: function(data) { chunks.push(typeof data === 'string' ? data : String(data)); return this; },
2035            digest: function(enc) {
2036              var str = chunks.join('');
2037              if (typeof crypto !== 'undefined' && crypto.subtle) {
2038                return crypto.subtle.digest('SHA-256', new TextEncoder().encode(str)).then(function(buf) {
2039                  var arr = new Uint8Array(buf); return enc === 'hex' ? Array.from(arr).map(function(b){return b.toString(16).padStart(2,'0');}).join('') : Buffer.from(arr);
2040                });
2041              }
2042              return enc === 'hex' ? '00000000' : Buffer.alloc(0);
2043            },
2044          };
2045        },
2046      },
2047      'os': {
2048        platform: function() { return 'linux'; },
2049        arch: function() { return 'x64'; },
2050        homedir: function() { return '/'; },
2051        tmpdir: function() { return '/tmp'; },
2052        type: function() { return 'Linux'; },
2053        release: function() { return '6.8.0'; },
2054        hostname: function() { return 'bao'; },
2055        cpus: function() { return [{ model: 'bao', speed: 3000 }]; },
2056        totalmem: function() { return 8*1024*1024*1024; },
2057        freemem: function() { return 4*1024*1024*1024; },
2058        uptime: function() { return 3600; },
2059        EOL: '\n',
2060      },
2061      'assert': {
2062        ok: function(v, msg) { if (!v) throw new Error(msg || 'assertion failed'); },
2063        equal: function(a, b, msg) { if (a !== b) throw new Error(msg || a + ' !== ' + b); },
2064        deepEqual: function(a, b, msg) { if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(msg || 'not deep equal'); },
2065        throws: function(fn, msg) { try { fn(); throw new Error(msg || 'expected throw'); } catch(e) { if (e.message === (msg || 'expected throw')) throw e; } },
2066      },
2067      'timers': {
2068        setTimeout: typeof setTimeout !== 'undefined' ? setTimeout : function(fn) { fn(); return 0; },
2069        setInterval: typeof setInterval !== 'undefined' ? setInterval : function(fn) { return 0; },
2070        clearTimeout: typeof clearTimeout !== 'undefined' ? clearTimeout : function() {},
2071        clearInterval: typeof clearInterval !== 'undefined' ? clearInterval : function() {},
2072        setImmediate: typeof setImmediate !== 'undefined' ? setImmediate : function(fn) { return setTimeout(fn, 0); },
2073        clearImmediate: typeof clearImmediate !== 'undefined' ? clearImmediate : function() {},
2074      },
2075    };
2076
2077    require = function(name) {
2078      if (_module_cache[name]) return _module_cache[name];
2079      if (_module_builtin[name]) { _module_cache[name] = _module_builtin[name]; return _module_builtin[name]; }
2080      throw new Error("Cannot find module '" + name + "' in browser context");
2081    };
2082
2083    require.resolve = function(name) { return name; };
2084    require.cache = _module_cache;
2085  }
2086
2087  // setImmediate / clearImmediate
2088  if (typeof setImmediate === 'undefined') {
2089    setImmediate = function(fn) {
2090      var args = Array.prototype.slice.call(arguments, 1);
2091      return setTimeout(function() { fn.apply(null, args); }, 0);
2092    };
2093    clearImmediate = function(id) { clearTimeout(id); };
2094  }
2095
2096  // __dirname / __filename
2097  if (typeof __dirname === 'undefined') {
2098    __dirname = '/';
2099    __filename = '/index.js';
2100  }
2101
2102  // TextEncoder / TextDecoder (most browsers have these, but ensure)
2103  if (typeof TextEncoder === 'undefined') {
2104    TextEncoder = function() { this.encode = function(str) { return new Uint8Array(Array.from(str).map(function(c){return c.charCodeAt(0);})); }; };
2105  }
2106  if (typeof TextDecoder === 'undefined') {
2107    TextDecoder = function() { this.decode = function(buf) { return String.fromCharCode.apply(null, buf); }; };
2108  }
2109
2110  // URL / URLSearchParams (most browsers have these, but ensure)
2111  if (typeof URL === 'undefined') {
2112    URL = function(url, base) { throw new Error('URL not available'); };
2113  }
2114  if (typeof URLSearchParams === 'undefined') {
2115    URLSearchParams = function(init) {
2116      this._params = [];
2117      this.append = function(k,v) { this._params.push([k,v]); };
2118      this.get = function(k) { for(var i=0;i<this._params.length;i++) if(this._params[i][0]===k) return this._params[i][1]; return null; };
2119      this.toString = function() { return this._params.map(function(p){return p[0]+'='+p[1];}).join('&'); };
2120    };
2121  }
2122
2123  // btoa / atob (most browsers have these, but ensure)
2124  if (typeof btoa === 'undefined') {
2125    var _b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
2126    btoa = function(str) {
2127      var out = '';
2128      for (var i = 0; i < str.length; i += 3) {
2129        var a = str.charCodeAt(i), b = str.charCodeAt(i+1), c = str.charCodeAt(i+2);
2130        out += _b64chars[a>>2] + _b64chars[((a&3)<<4)|(b>>4)] + (isNaN(b)?'=':_b64chars[((b&15)<<2)|(c>>6)]) + (isNaN(b)||isNaN(c)?'=':_b64chars[c&63]);
2131      }
2132      return out;
2133    };
2134    atob = function(str) {
2135      var out = '';
2136      str = str.replace(/=+$/, '');
2137      for (var i = 0; i < str.length; i += 4) {
2138        var a = _b64chars.indexOf(str[i]), b = _b64chars.indexOf(str[i+1]);
2139        var c = _b64chars.indexOf(str[i+2]), d = _b64chars.indexOf(str[i+3]);
2140        out += String.fromCharCode((a<<2)|(b>>4)) + (c>=0?String.fromCharCode(((b&15)<<4)|(c>>2)):'') + (d>=0?String.fromCharCode(((c&3)<<6)|d):'');
2141      }
2142      return out;
2143    };
2144  }
2145})();"#;
2146
2147// ── Bridge types ────────────────────────────────────────────────────
2148
2149/// Commands sent through the runtime bridge for execution in a page context.
2150///
2151/// Each variant maps to a [`PageHandle`] operation. The bridge decouples
2152/// command submission from execution — a worker loop reads from the
2153/// [`BridgeReceiver`] and drives the real servo page.
2154///
2155/// @trace REQ-BRW-003 [entity:RuntimeBridge]
2156#[derive(Debug, Clone, PartialEq, Eq)]
2157pub enum BridgeCommand {
2158    /// Navigate the page to a URL.
2159    Navigate(String),
2160    /// Evaluate JavaScript in the page and return the result as a string.
2161    Evaluate(String),
2162    /// Capture a screenshot of the current page.
2163    Screenshot,
2164    /// Close the page and mark the bridge as inactive.
2165    Close,
2166    /// Resize the page viewport to width × height.
2167    Resize(u32, u32),
2168    /// Retrieve the current page title.
2169    GetTitle,
2170    /// Retrieve the current page URL.
2171    GetUrl,
2172}
2173
2174/// Response returned after executing a [`BridgeCommand`].
2175///
2176/// @trace REQ-BRW-003 [entity:RuntimeBridge]
2177#[derive(Debug, Clone, PartialEq, Eq)]
2178pub enum BridgeResponse {
2179    /// Command succeeded with no return value.
2180    Ok,
2181    /// Command failed with a descriptive message.
2182    Err(String),
2183    /// Command returned a null / void result.
2184    Null,
2185    /// Command returned a string value (evaluation result, title, URL, …).
2186    Value(String),
2187    /// Command returned binary data (screenshot image bytes).
2188    Binary(Vec<u8>),
2189}
2190
2191impl BridgeResponse {
2192    /// Returns `true` when the response is [`Ok`](BridgeResponse::Ok).
2193    pub fn is_ok(&self) -> bool {
2194        matches!(self, BridgeResponse::Ok)
2195    }
2196
2197    /// Returns `true` when the response is [`Err`](BridgeResponse::Err).
2198    pub fn is_err(&self) -> bool {
2199        matches!(self, BridgeResponse::Err(_))
2200    }
2201
2202    /// Converts [`Err`](BridgeResponse::Err) into `Result::Err`, wrapping all other
2203    /// variants in `Result::Ok`.
2204    pub fn ok(self) -> Result<Self, String> {
2205        match self {
2206            BridgeResponse::Err(e) => Err(e),
2207            other => Ok(other),
2208        }
2209    }
2210}
2211
2212/// Receiving end of a [`BridgeChannel`].
2213///
2214/// A worker thread (or event-loop iteration) calls [`recv`](BridgeReceiver::recv)
2215/// to obtain commands and their optional response channels, executes them against
2216/// the page, and sends back [`BridgeResponse`] values.
2217pub struct BridgeReceiver {
2218    rx: mpsc::Receiver<(BridgeCommand, Option<mpsc::Sender<BridgeResponse>>)>,
2219    alive: Arc<AtomicBool>,
2220}
2221
2222impl std::fmt::Debug for BridgeReceiver {
2223    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2224        f.debug_struct("BridgeReceiver")
2225            .field("alive", &self.alive)
2226            .finish()
2227    }
2228}
2229
2230impl BridgeReceiver {
2231    /// Block until a command arrives or the channel is disconnected.
2232    pub fn recv(&self) -> Result<(BridgeCommand, Option<mpsc::Sender<BridgeResponse>>), String> {
2233        self.rx.recv().map_err(|_| "channel closed".to_string())
2234    }
2235
2236    /// Block for at most `timeout`, returning the command or a timeout error.
2237    pub fn recv_timeout(
2238        &self,
2239        timeout: Duration,
2240    ) -> Result<(BridgeCommand, Option<mpsc::Sender<BridgeResponse>>), String> {
2241        self.rx.recv_timeout(timeout).map_err(|e| format!("{}", e))
2242    }
2243
2244    /// Whether the bridge has been marked alive (both sides share the flag).
2245    pub fn is_alive(&self) -> bool {
2246        self.alive.load(Ordering::SeqCst)
2247    }
2248}
2249
2250/// Producer half of the bridge command channel.
2251///
2252/// Methods are thread-safe (`&self`) so a single channel can be shared across
2253/// threads for concurrent submission.
2254///
2255/// @trace REQ-BRW-003 [entity:BridgeChannel]
2256#[derive(Debug)]
2257pub struct BridgeChannel {
2258    tx: mpsc::Sender<(BridgeCommand, Option<mpsc::Sender<BridgeResponse>>)>,
2259    alive: Arc<AtomicBool>,
2260}
2261
2262impl BridgeChannel {
2263    /// Create a new bridge channel pair.
2264    ///
2265    /// Returns `(sender, receiver)` where commands flow sender → receiver and
2266    /// responses flow back via per-command one-shot channels.
2267    pub fn new() -> (Self, BridgeReceiver) {
2268        let (tx, rx) = mpsc::channel();
2269        let alive = Arc::new(AtomicBool::new(true));
2270        let channel = BridgeChannel {
2271            tx,
2272            alive: alive.clone(),
2273        };
2274        let receiver = BridgeReceiver { rx, alive };
2275        (channel, receiver)
2276    }
2277
2278    /// Send a command and block until the worker returns a response.
2279    pub fn send(&self, cmd: BridgeCommand) -> Result<BridgeResponse, String> {
2280        let (resp_tx, resp_rx) = mpsc::channel();
2281        self.tx
2282            .send((cmd, Some(resp_tx)))
2283            .map_err(|_| "bridge closed".to_string())?;
2284        resp_rx
2285            .recv()
2286            .map_err(|_| "response channel closed".to_string())
2287    }
2288
2289    /// Send a command and wait at most `timeout` for a response.
2290    pub fn send_timeout(
2291        &self,
2292        cmd: BridgeCommand,
2293        timeout: Duration,
2294    ) -> Result<BridgeResponse, String> {
2295        let (resp_tx, resp_rx) = mpsc::channel();
2296        self.tx
2297            .send((cmd, Some(resp_tx)))
2298            .map_err(|_| "bridge closed".to_string())?;
2299        resp_rx.recv_timeout(timeout).map_err(|e| format!("{}", e))
2300    }
2301
2302    /// Send a command without waiting for a response.
2303    ///
2304    /// The worker receives `None` for the responder slot and can skip
2305    /// the response-send step.
2306    pub fn fire_and_forget(&self, cmd: BridgeCommand) -> Result<(), String> {
2307        self.tx
2308            .send((cmd, None))
2309            .map_err(|_| "bridge closed".to_string())
2310    }
2311
2312    /// Whether the bridge is marked alive (both sender and receiver).
2313    pub fn is_alive(&self) -> bool {
2314        self.alive.load(Ordering::SeqCst)
2315    }
2316
2317    /// Mark the bridge as closed.
2318    ///
2319    /// This only sets a flag — the underlying channel remains connected.
2320    /// Dropping the [`BridgeChannel`] / [`BridgeReceiver`] pair fully tears
2321    /// down the transport.
2322    pub fn close(&self) {
2323        self.alive.store(false, Ordering::SeqCst);
2324    }
2325}
2326
2327/// High-level bridge that owns a [`BridgeChannel`] and provides the public
2328/// command API for the bao_browser runtime.
2329///
2330/// In production, a worker loop reads from the associated [`BridgeReceiver`]
2331/// and dispatches commands to a servo [`PageHandle`].  In tests the channel
2332/// alone is exercised.
2333///
2334/// @trace REQ-BRW-003 [entity:RuntimeBridge]
2335#[derive(Debug)]
2336pub struct RuntimeBridge {
2337    channel: BridgeChannel,
2338}
2339
2340impl RuntimeBridge {
2341    /// Create a fresh bridge, returning the sending half and the receiver.
2342    pub fn new() -> (Self, BridgeReceiver) {
2343        let (channel, receiver) = BridgeChannel::new();
2344        (RuntimeBridge { channel }, receiver)
2345    }
2346
2347    /// Send a command and wait for the response.  See [`BridgeChannel::send`].
2348    pub fn send(&self, cmd: BridgeCommand) -> Result<BridgeResponse, String> {
2349        self.channel.send(cmd)
2350    }
2351
2352    /// Send a command and wait at most `timeout` for a response.
2353    /// See [`BridgeChannel::send_timeout`].
2354    pub fn send_timeout(
2355        &self,
2356        cmd: BridgeCommand,
2357        timeout: Duration,
2358    ) -> Result<BridgeResponse, String> {
2359        self.channel.send_timeout(cmd, timeout)
2360    }
2361
2362    /// Send a command without waiting for a response.
2363    /// See [`BridgeChannel::fire_and_forget`].
2364    pub fn fire_and_forget(&self, cmd: BridgeCommand) -> Result<(), String> {
2365        self.channel.fire_and_forget(cmd)
2366    }
2367
2368    /// Whether the bridge is alive.  See [`BridgeChannel::is_alive`].
2369    pub fn is_alive(&self) -> bool {
2370        self.channel.is_alive()
2371    }
2372
2373    /// Mark the bridge closed.  See [`BridgeChannel::close`].
2374    pub fn close(&self) {
2375        self.channel.close();
2376    }
2377}
2378
2379#[cfg(test)]
2380mod tests {
2381    use std::sync::OnceLock;
2382    // ─── Polyfill validation ──────────────────────────────────────
2383    // @trace REQ-BRW-003 [req:REQ-BRW-003] [level:unit]
2384
2385    #[test]
2386    fn test_polyfills_are_valid_js() {
2387        assert!(!super::NODE_POLYFILLS.is_empty());
2388        assert!(super::NODE_POLYFILLS.contains("Buffer"));
2389        assert!(super::NODE_POLYFILLS.contains("require"));
2390        assert!(super::NODE_POLYFILLS.contains("process"));
2391    }
2392
2393    // ─── BridgeCommand / BridgeResponse / BridgeChannel extended tests ──
2394    // @trace REQ-BRW-003 [req:REQ-BRW-003] [level:unit]
2395
2396    #[test]
2397    fn bridge_command_navigate_equality() {
2398        let cmd1 = super::BridgeCommand::Navigate("https://example.com".into());
2399        let cmd2 = super::BridgeCommand::Navigate("https://example.com".into());
2400        let cmd3 = super::BridgeCommand::Navigate("https://other.com".into());
2401        assert_eq!(cmd1, cmd2);
2402        assert_ne!(cmd1, cmd3);
2403    }
2404
2405    #[test]
2406    fn bridge_command_evaluate_equality() {
2407        let cmd1 = super::BridgeCommand::Evaluate("1+1".into());
2408        let cmd2 = super::BridgeCommand::Evaluate("1+1".into());
2409        assert_eq!(cmd1, cmd2);
2410        assert_ne!(cmd1, super::BridgeCommand::Evaluate("2+2".into()));
2411    }
2412
2413    #[test]
2414    fn bridge_command_resize_equality() {
2415        assert_eq!(
2416            super::BridgeCommand::Resize(800, 600),
2417            super::BridgeCommand::Resize(800, 600)
2418        );
2419        assert_ne!(
2420            super::BridgeCommand::Resize(800, 600),
2421            super::BridgeCommand::Resize(1024, 768)
2422        );
2423    }
2424
2425    #[test]
2426    fn bridge_command_variants_distinct() {
2427        let cmds = [
2428            super::BridgeCommand::Navigate("x".into()),
2429            super::BridgeCommand::Evaluate("y".into()),
2430            super::BridgeCommand::Screenshot,
2431            super::BridgeCommand::Close,
2432            super::BridgeCommand::Resize(1, 1),
2433            super::BridgeCommand::GetTitle,
2434            super::BridgeCommand::GetUrl,
2435        ];
2436        for i in 0..cmds.len() {
2437            for j in 0..cmds.len() {
2438                if i != j {
2439                    assert_ne!(cmds[i], cmds[j]);
2440                }
2441            }
2442        }
2443    }
2444
2445    #[test]
2446    fn bridge_response_ok_is_ok() {
2447        let resp = super::BridgeResponse::Ok;
2448        assert!(resp.is_ok());
2449        assert!(!resp.is_err());
2450    }
2451
2452    #[test]
2453    fn bridge_response_err_is_err() {
2454        let resp = super::BridgeResponse::Err("failed".into());
2455        assert!(!resp.is_ok());
2456        assert!(resp.is_err());
2457    }
2458
2459    #[test]
2460    fn bridge_response_null_not_err() {
2461        let resp = super::BridgeResponse::Null;
2462        assert!(!resp.is_ok()); // Null is not BridgeResponse::Ok
2463        assert!(!resp.is_err()); // Null is also not an error
2464    }
2465
2466    #[test]
2467    fn bridge_response_value_not_err() {
2468        let resp = super::BridgeResponse::Value("result".into());
2469        assert!(!resp.is_ok()); // Value is not BridgeResponse::Ok
2470        assert!(!resp.is_err());
2471    }
2472
2473    #[test]
2474    fn bridge_response_binary_not_err() {
2475        let resp = super::BridgeResponse::Binary(vec![1, 2, 3]);
2476        assert!(!resp.is_ok()); // Binary is not BridgeResponse::Ok
2477        assert!(!resp.is_err());
2478    }
2479
2480    #[test]
2481    fn bridge_response_ok_method_wraps_non_err() {
2482        // .ok() converts Err → Result::Err, all others → Result::Ok
2483        assert!(super::BridgeResponse::Null.ok().is_ok());
2484        assert!(super::BridgeResponse::Value("v".into()).ok().is_ok());
2485        assert!(super::BridgeResponse::Binary(vec![]).ok().is_ok());
2486    }
2487
2488    #[test]
2489    fn bridge_response_ok_method_on_err() {
2490        let resp = super::BridgeResponse::Err("error msg".into());
2491        let result = resp.ok();
2492        assert!(result.is_err());
2493        assert_eq!(result.unwrap_err(), "error msg");
2494    }
2495
2496    #[test]
2497    fn bridge_response_ok_method_on_ok_variants() {
2498        assert!(super::BridgeResponse::Ok.ok().is_ok());
2499        assert!(super::BridgeResponse::Null.ok().is_ok());
2500        assert!(super::BridgeResponse::Value("v".into()).ok().is_ok());
2501        assert!(super::BridgeResponse::Binary(vec![]).ok().is_ok());
2502    }
2503
2504    #[test]
2505    fn bridge_channel_new_alive() {
2506        let (channel, _receiver) = super::BridgeChannel::new();
2507        assert!(channel.is_alive());
2508    }
2509
2510    #[test]
2511    fn bridge_channel_close_sets_not_alive() {
2512        let (channel, _receiver) = super::BridgeChannel::new();
2513        channel.close();
2514        assert!(!channel.is_alive());
2515    }
2516
2517    #[test]
2518    fn bridge_receiver_alive_shares_flag() {
2519        let (channel, receiver) = super::BridgeChannel::new();
2520        assert!(receiver.is_alive());
2521        channel.close();
2522        assert!(!receiver.is_alive());
2523    }
2524
2525    #[test]
2526    fn bridge_channel_fire_and_forget() {
2527        let (channel, receiver) = super::BridgeChannel::new();
2528        assert!(channel
2529            .fire_and_forget(super::BridgeCommand::GetTitle)
2530            .is_ok());
2531        let (cmd, responder) = receiver.recv().unwrap();
2532        assert_eq!(cmd, super::BridgeCommand::GetTitle);
2533        assert!(responder.is_none());
2534    }
2535
2536    #[test]
2537    fn bridge_channel_send_with_response() {
2538        let (channel, receiver) = super::BridgeChannel::new();
2539        // send() blocks until response — we need a worker thread
2540        let worker = std::thread::spawn(move || {
2541            let (_cmd, responder) = receiver.recv().unwrap();
2542            if let Some(resp_tx) = responder {
2543                resp_tx
2544                    .send(super::BridgeResponse::Value("title".into()))
2545                    .unwrap();
2546            }
2547        });
2548        let result = channel.send(super::BridgeCommand::GetTitle).unwrap();
2549        assert_eq!(result, super::BridgeResponse::Value("title".into()));
2550        worker.join().unwrap();
2551    }
2552
2553    #[test]
2554    fn runtime_bridge_new_alive() {
2555        let (bridge, _receiver) = super::RuntimeBridge::new();
2556        assert!(bridge.is_alive());
2557    }
2558
2559    #[test]
2560    fn runtime_bridge_close() {
2561        let (bridge, _receiver) = super::RuntimeBridge::new();
2562        bridge.close();
2563        assert!(!bridge.is_alive());
2564    }
2565
2566    #[test]
2567    fn runtime_bridge_fire_and_forget() {
2568        let (bridge, receiver) = super::RuntimeBridge::new();
2569        assert!(bridge.fire_and_forget(super::BridgeCommand::Close).is_ok());
2570        let (cmd, responder) = receiver.recv().unwrap();
2571        assert_eq!(cmd, super::BridgeCommand::Close);
2572        assert!(responder.is_none());
2573    }
2574
2575    // ═══════════════════════════════════════════════════════════════════════
2576    // Extended unit tests for bridge types and polyfills
2577    // @trace REQ-BRW-003 [req:REQ-BRW-003] [level:unit]
2578    // ═══════════════════════════════════════════════════════════════════════
2579
2580    // ─── BridgeCommand Debug format tests ──────────────────────────────────
2581
2582    #[test]
2583    fn bridge_command_debug_format_navigate() {
2584        let cmd = super::BridgeCommand::Navigate("https://example.com".into());
2585        let debug_str = format!("{:?}", cmd);
2586        assert!(debug_str.contains("Navigate"));
2587        assert!(debug_str.contains("https://example.com"));
2588    }
2589
2590    #[test]
2591    fn bridge_command_debug_format_evaluate() {
2592        let cmd = super::BridgeCommand::Evaluate("return 42".into());
2593        let debug_str = format!("{:?}", cmd);
2594        assert!(debug_str.contains("Evaluate"));
2595        assert!(debug_str.contains("return 42"));
2596    }
2597
2598    #[test]
2599    fn bridge_command_debug_format_screenshot() {
2600        let cmd = super::BridgeCommand::Screenshot;
2601        let debug_str = format!("{:?}", cmd);
2602        assert!(debug_str.contains("Screenshot"));
2603    }
2604
2605    #[test]
2606    fn bridge_command_debug_format_close() {
2607        let cmd = super::BridgeCommand::Close;
2608        let debug_str = format!("{:?}", cmd);
2609        assert!(debug_str.contains("Close"));
2610    }
2611
2612    #[test]
2613    fn bridge_command_debug_format_resize() {
2614        let cmd = super::BridgeCommand::Resize(1920, 1080);
2615        let debug_str = format!("{:?}", cmd);
2616        assert!(debug_str.contains("Resize"));
2617        assert!(debug_str.contains("1920"));
2618        assert!(debug_str.contains("1080"));
2619    }
2620
2621    #[test]
2622    fn bridge_command_debug_format_get_title() {
2623        let cmd = super::BridgeCommand::GetTitle;
2624        let debug_str = format!("{:?}", cmd);
2625        assert!(debug_str.contains("GetTitle"));
2626    }
2627
2628    #[test]
2629    fn bridge_command_debug_format_get_url() {
2630        let cmd = super::BridgeCommand::GetUrl;
2631        let debug_str = format!("{:?}", cmd);
2632        assert!(debug_str.contains("GetUrl"));
2633    }
2634
2635    // ─── BridgeCommand Clone tests ────────────────────────────────────────
2636
2637    #[test]
2638    fn bridge_command_clone_navigate() {
2639        let cmd = super::BridgeCommand::Navigate("https://test.com".into());
2640        let cloned = cmd.clone();
2641        assert_eq!(cmd, cloned);
2642    }
2643
2644    #[test]
2645    fn bridge_command_clone_evaluate() {
2646        let cmd = super::BridgeCommand::Evaluate("x + y".into());
2647        let cloned = cmd.clone();
2648        assert_eq!(cmd, cloned);
2649    }
2650
2651    #[test]
2652    fn bridge_command_clone_resize() {
2653        let cmd = super::BridgeCommand::Resize(1024, 768);
2654        let cloned = cmd.clone();
2655        assert_eq!(cmd, cloned);
2656    }
2657
2658    // ─── BridgeResponse Debug/Clone/Equality tests ────────────────────────
2659
2660    #[test]
2661    fn bridge_response_debug_format_ok() {
2662        let resp = super::BridgeResponse::Ok;
2663        let debug_str = format!("{:?}", resp);
2664        assert!(debug_str.contains("Ok"));
2665    }
2666
2667    #[test]
2668    fn bridge_response_debug_format_err() {
2669        let resp = super::BridgeResponse::Err("something went wrong".into());
2670        let debug_str = format!("{:?}", resp);
2671        assert!(debug_str.contains("Err"));
2672        assert!(debug_str.contains("something went wrong"));
2673    }
2674
2675    #[test]
2676    fn bridge_response_debug_format_null() {
2677        let resp = super::BridgeResponse::Null;
2678        let debug_str = format!("{:?}", resp);
2679        assert!(debug_str.contains("Null"));
2680    }
2681
2682    #[test]
2683    fn bridge_response_debug_format_value() {
2684        let resp = super::BridgeResponse::Value("result string".into());
2685        let debug_str = format!("{:?}", resp);
2686        assert!(debug_str.contains("Value"));
2687        assert!(debug_str.contains("result string"));
2688    }
2689
2690    #[test]
2691    fn bridge_response_debug_format_binary() {
2692        let resp = super::BridgeResponse::Binary(vec![0xDE, 0xAD, 0xBE, 0xEF]);
2693        let debug_str = format!("{:?}", resp);
2694        assert!(debug_str.contains("Binary"));
2695    }
2696
2697    #[test]
2698    fn bridge_response_clone_ok() {
2699        let resp = super::BridgeResponse::Ok;
2700        let cloned = resp.clone();
2701        assert_eq!(resp, cloned);
2702    }
2703
2704    #[test]
2705    fn bridge_response_clone_err() {
2706        let resp = super::BridgeResponse::Err("error".into());
2707        let cloned = resp.clone();
2708        assert_eq!(resp, cloned);
2709    }
2710
2711    #[test]
2712    fn bridge_response_clone_value() {
2713        let resp = super::BridgeResponse::Value("value".into());
2714        let cloned = resp.clone();
2715        assert_eq!(resp, cloned);
2716    }
2717
2718    #[test]
2719    fn bridge_response_clone_binary() {
2720        let resp = super::BridgeResponse::Binary(vec![1, 2, 3, 4]);
2721        let cloned = resp.clone();
2722        assert_eq!(resp, cloned);
2723    }
2724
2725    #[test]
2726    fn bridge_response_equality_ok() {
2727        assert_eq!(super::BridgeResponse::Ok, super::BridgeResponse::Ok);
2728    }
2729
2730    #[test]
2731    fn bridge_response_equality_err() {
2732        assert_eq!(
2733            super::BridgeResponse::Err("same error".into()),
2734            super::BridgeResponse::Err("same error".into())
2735        );
2736        assert_ne!(
2737            super::BridgeResponse::Err("error a".into()),
2738            super::BridgeResponse::Err("error b".into())
2739        );
2740    }
2741
2742    #[test]
2743    fn bridge_response_equality_value() {
2744        assert_eq!(
2745            super::BridgeResponse::Value("same".into()),
2746            super::BridgeResponse::Value("same".into())
2747        );
2748        assert_ne!(
2749            super::BridgeResponse::Value("a".into()),
2750            super::BridgeResponse::Value("b".into())
2751        );
2752    }
2753
2754    #[test]
2755    fn bridge_response_equality_binary() {
2756        assert_eq!(
2757            super::BridgeResponse::Binary(vec![1, 2, 3]),
2758            super::BridgeResponse::Binary(vec![1, 2, 3])
2759        );
2760        assert_ne!(
2761            super::BridgeResponse::Binary(vec![1, 2, 3]),
2762            super::BridgeResponse::Binary(vec![1, 2, 4])
2763        );
2764    }
2765
2766    #[test]
2767    fn bridge_response_variants_distinct() {
2768        let responses = [
2769            super::BridgeResponse::Ok,
2770            super::BridgeResponse::Err("e".into()),
2771            super::BridgeResponse::Null,
2772            super::BridgeResponse::Value("v".into()),
2773            super::BridgeResponse::Binary(vec![1]),
2774        ];
2775        for i in 0..responses.len() {
2776            for j in 0..responses.len() {
2777                if i != j {
2778                    assert_ne!(responses[i], responses[j]);
2779                }
2780            }
2781        }
2782    }
2783
2784    // ─── BridgeChannel edge case tests ────────────────────────────────────
2785
2786    #[test]
2787    fn bridge_channel_send_timeout_zero_timeout_returns_err() {
2788        // send_timeout with Duration::ZERO: command is sent to channel,
2789        // but no worker responds within 0ms → timeout error.
2790        let (channel, receiver) = super::BridgeChannel::new();
2791        // Drain the receiver in a separate thread so the send doesn't block
2792        let _drainer = std::thread::spawn(move || {
2793            // Just drain the command, don't respond
2794            let _ = receiver.recv();
2795        });
2796        let result = channel.send_timeout(
2797            super::BridgeCommand::GetTitle,
2798            std::time::Duration::from_secs(0),
2799        );
2800        assert!(result.is_err());
2801    }
2802
2803    #[test]
2804    fn bridge_channel_send_timeout_short_timeout() {
2805        let (channel, _receiver) = super::BridgeChannel::new();
2806        // No worker to respond — should timeout
2807        let result = channel.send_timeout(
2808            super::BridgeCommand::GetTitle,
2809            std::time::Duration::from_millis(1),
2810        );
2811        assert!(result.is_err());
2812    }
2813
2814    #[test]
2815    fn bridge_channel_fire_and_forget_multiple() {
2816        let (channel, receiver) = super::BridgeChannel::new();
2817        assert!(channel
2818            .fire_and_forget(super::BridgeCommand::GetTitle)
2819            .is_ok());
2820        assert!(channel
2821            .fire_and_forget(super::BridgeCommand::GetUrl)
2822            .is_ok());
2823        assert!(channel
2824            .fire_and_forget(super::BridgeCommand::Screenshot)
2825            .is_ok());
2826
2827        let (cmd1, _) = receiver.recv().unwrap();
2828        let (cmd2, _) = receiver.recv().unwrap();
2829        let (cmd3, _) = receiver.recv().unwrap();
2830
2831        assert_eq!(cmd1, super::BridgeCommand::GetTitle);
2832        assert_eq!(cmd2, super::BridgeCommand::GetUrl);
2833        assert_eq!(cmd3, super::BridgeCommand::Screenshot);
2834    }
2835
2836    #[test]
2837    fn bridge_channel_close_then_send_fails() {
2838        let (channel, receiver) = super::BridgeChannel::new();
2839        channel.close();
2840        // Channel is marked closed but underlying mpsc still works
2841        // The alive flag is just a marker, not a hard barrier
2842        // Verify the alive flag is set
2843        assert!(!channel.is_alive());
2844        // Drop receiver to actually close the channel
2845        drop(receiver);
2846        // Now send should fail
2847        let result = channel.send(super::BridgeCommand::GetTitle);
2848        assert!(result.is_err());
2849    }
2850
2851    #[test]
2852    fn bridge_channel_close_then_fire_and_forget_fails() {
2853        let (channel, receiver) = super::BridgeChannel::new();
2854        channel.close();
2855        // Drop receiver to actually close the channel
2856        drop(receiver);
2857        let result = channel.fire_and_forget(super::BridgeCommand::Close);
2858        assert!(result.is_err());
2859    }
2860
2861    #[test]
2862    fn bridge_channel_receiver_sees_close_flag() {
2863        let (channel, receiver) = super::BridgeChannel::new();
2864        assert!(receiver.is_alive());
2865        channel.close();
2866        assert!(!receiver.is_alive());
2867    }
2868
2869    #[test]
2870    fn bridge_channel_multiple_send_response_pairs() {
2871        let (channel, receiver) = super::BridgeChannel::new();
2872
2873        let worker = std::thread::spawn(move || {
2874            for _ in 0..3 {
2875                let (cmd, responder) = receiver.recv().unwrap();
2876                if let Some(resp_tx) = responder {
2877                    let resp = match cmd {
2878                        super::BridgeCommand::GetTitle => {
2879                            super::BridgeResponse::Value("Title".into())
2880                        }
2881                        super::BridgeCommand::GetUrl => {
2882                            super::BridgeResponse::Value("https://url.com".into())
2883                        }
2884                        _ => super::BridgeResponse::Ok,
2885                    };
2886                    resp_tx.send(resp).unwrap();
2887                }
2888            }
2889        });
2890
2891        let r1 = channel.send(super::BridgeCommand::GetTitle).unwrap();
2892        let r2 = channel.send(super::BridgeCommand::GetUrl).unwrap();
2893        let r3 = channel.send(super::BridgeCommand::Screenshot).unwrap();
2894
2895        assert_eq!(r1, super::BridgeResponse::Value("Title".into()));
2896        assert_eq!(r2, super::BridgeResponse::Value("https://url.com".into()));
2897        assert_eq!(r3, super::BridgeResponse::Ok);
2898
2899        worker.join().unwrap();
2900    }
2901
2902    // ─── BridgeReceiver edge case tests ───────────────────────────────────
2903
2904    #[test]
2905    fn bridge_receiver_recv_timeout_short() {
2906        let (_channel, receiver) = super::BridgeChannel::new();
2907        // No command sent — should timeout
2908        let result = receiver.recv_timeout(std::time::Duration::from_millis(1));
2909        assert!(result.is_err());
2910    }
2911
2912    #[test]
2913    fn bridge_receiver_recv_after_channel_dropped() {
2914        let (channel, receiver) = super::BridgeChannel::new();
2915        drop(channel);
2916        // recv should return error when sender is dropped
2917        let result = receiver.recv();
2918        assert!(result.is_err());
2919        assert_eq!(result.unwrap_err(), "channel closed");
2920    }
2921
2922    #[test]
2923    fn bridge_receiver_debug_format() {
2924        let (_channel, receiver) = super::BridgeChannel::new();
2925        let debug_str = format!("{:?}", receiver);
2926        assert!(debug_str.contains("BridgeReceiver"));
2927        assert!(debug_str.contains("alive"));
2928    }
2929
2930    // ─── RuntimeBridge edge case tests ────────────────────────────────────
2931
2932    #[test]
2933    fn runtime_bridge_send_timeout() {
2934        let (bridge, receiver) = super::RuntimeBridge::new();
2935
2936        let worker = std::thread::spawn(move || {
2937            let (cmd, responder) = receiver.recv().unwrap();
2938            if let Some(resp_tx) = responder {
2939                let resp = match cmd {
2940                    super::BridgeCommand::Evaluate(ref code) => {
2941                        super::BridgeResponse::Value(format!("evaluated: {}", code))
2942                    }
2943                    _ => super::BridgeResponse::Ok,
2944                };
2945                resp_tx.send(resp).unwrap();
2946            }
2947        });
2948
2949        let result = bridge
2950            .send_timeout(
2951                super::BridgeCommand::Evaluate("1+1".into()),
2952                std::time::Duration::from_secs(5),
2953            )
2954            .unwrap();
2955        assert_eq!(
2956            result,
2957            super::BridgeResponse::Value("evaluated: 1+1".into())
2958        );
2959
2960        worker.join().unwrap();
2961    }
2962
2963    #[test]
2964    fn runtime_bridge_close_propagates() {
2965        let (bridge, receiver) = super::RuntimeBridge::new();
2966        assert!(bridge.is_alive());
2967        assert!(receiver.is_alive());
2968        bridge.close();
2969        assert!(!bridge.is_alive());
2970        assert!(!receiver.is_alive());
2971    }
2972
2973    #[test]
2974    fn runtime_bridge_fire_and_forget_after_close_still_works() {
2975        let (bridge, receiver) = super::RuntimeBridge::new();
2976        bridge.close();
2977        // close() only sets the alive flag, doesn't close the channel
2978        // fire_and_forget should still work until receiver is dropped
2979        assert!(bridge.fire_and_forget(super::BridgeCommand::Close).is_ok());
2980        let (cmd, responder) = receiver.recv().unwrap();
2981        assert_eq!(cmd, super::BridgeCommand::Close);
2982        assert!(responder.is_none());
2983    }
2984
2985    #[test]
2986    fn runtime_bridge_send_after_receiver_dropped() {
2987        let (bridge, receiver) = super::RuntimeBridge::new();
2988        drop(receiver);
2989        let result = bridge.send(super::BridgeCommand::GetTitle);
2990        assert!(result.is_err());
2991    }
2992
2993    #[test]
2994    fn runtime_bridge_debug_format() {
2995        let (bridge, _receiver) = super::RuntimeBridge::new();
2996        let debug_str = format!("{:?}", bridge);
2997        assert!(debug_str.contains("RuntimeBridge"));
2998    }
2999
3000    // ─── NODE_POLYFILLS content tests ─────────────────────────────────────
3001
3002    #[test]
3003    fn node_polyfills_process_version() {
3004        let poly = super::NODE_POLYFILLS;
3005        assert!(poly.contains("version: 'v20.11.0'"));
3006    }
3007
3008    #[test]
3009    fn node_polyfills_process_versions_structure() {
3010        let poly = super::NODE_POLYFILLS;
3011        // Check key version fields exist
3012        assert!(poly.contains("node: '20.11.0'"));
3013        assert!(poly.contains("v8: '12.4.254.14'"));
3014        assert!(poly.contains("uv: '1.27.0'"));
3015        assert!(poly.contains("zlib: '1.2.13'"));
3016        assert!(poly.contains("brotli: '1.0.9'"));
3017        assert!(poly.contains("ares: '1.19.1'"));
3018        assert!(poly.contains("modules: '115'"));
3019        assert!(poly.contains("openssl: '3.0.12'"));
3020        assert!(poly.contains("icu: '74.2'"));
3021        assert!(poly.contains("bun: '1.0.25'"));
3022        assert!(poly.contains("bao: '0.1.0'"));
3023    }
3024
3025    #[test]
3026    fn node_polyfills_process_env() {
3027        let poly = super::NODE_POLYFILLS;
3028        assert!(poly.contains("env:"));
3029        assert!(poly.contains("e.HOME = '/'"));
3030        assert!(poly.contains("e.PATH = '/usr/local/bin:/usr/bin:/bin'"));
3031        assert!(poly.contains("e.TERM = 'xterm-256color'"));
3032        assert!(poly.contains("e.NODE_VERSION = '20.11.0'"));
3033        assert!(poly.contains("e.BAO_VERSION = '0.1.0'"));
3034    }
3035
3036    #[test]
3037    fn node_polyfills_process_argv() {
3038        let poly = super::NODE_POLYFILLS;
3039        assert!(poly.contains("argv:"));
3040        assert!(poly.contains("argv0: 'bao'"));
3041    }
3042
3043    #[test]
3044    fn node_polyfills_buffer_from() {
3045        let poly = super::NODE_POLYFILLS;
3046        assert!(poly.contains("B.from = function"));
3047        assert!(poly.contains("if (data instanceof B)"));
3048        assert!(poly.contains("if (encoding === 'hex')"));
3049        assert!(poly.contains("if (encoding === 'base64')"));
3050    }
3051
3052    #[test]
3053    fn node_polyfills_buffer_alloc() {
3054        let poly = super::NODE_POLYFILLS;
3055        assert!(poly.contains("B.alloc = function"));
3056        assert!(poly.contains("B.allocUnsafe = function"));
3057        assert!(poly.contains("B.allocUnsafeSlow = function"));
3058    }
3059
3060    #[test]
3061    fn node_polyfills_buffer_static_methods() {
3062        let poly = super::NODE_POLYFILLS;
3063        assert!(poly.contains("B.isBuffer = function"));
3064        assert!(poly.contains("B.concat = function"));
3065        assert!(poly.contains("B.byteLength = function"));
3066        assert!(poly.contains("B.compare = function"));
3067    }
3068
3069    #[test]
3070    fn node_polyfills_buffer_instance_methods() {
3071        let poly = super::NODE_POLYFILLS;
3072        assert!(poly.contains("B.prototype.slice = function"));
3073        assert!(poly.contains("B.prototype.toString = function"));
3074        assert!(poly.contains("B.prototype.toJSON = function"));
3075        assert!(poly.contains("B.prototype.equals = function"));
3076        assert!(poly.contains("B.prototype.compare = function"));
3077        assert!(poly.contains("B.prototype.copy = function"));
3078        assert!(poly.contains("B.prototype.fill = function"));
3079        assert!(poly.contains("B.prototype.write = function"));
3080        assert!(poly.contains("B.prototype.indexOf = function"));
3081    }
3082
3083    #[test]
3084    fn node_polyfills_buffer_read_methods() {
3085        let poly = super::NODE_POLYFILLS;
3086        assert!(poly.contains("B.prototype.readUInt8 = function"));
3087        assert!(poly.contains("B.prototype.readUInt16LE = function"));
3088        assert!(poly.contains("B.prototype.readUInt16BE = function"));
3089        assert!(poly.contains("B.prototype.readUInt32LE = function"));
3090        assert!(poly.contains("B.prototype.readInt8 = function"));
3091        assert!(poly.contains("B.prototype.readInt16LE = function"));
3092        assert!(poly.contains("B.prototype.readInt32LE = function"));
3093        assert!(poly.contains("B.prototype.readFloatLE = function"));
3094        assert!(poly.contains("B.prototype.readDoubleLE = function"));
3095    }
3096
3097    #[test]
3098    fn node_polyfills_buffer_write_methods() {
3099        let poly = super::NODE_POLYFILLS;
3100        assert!(poly.contains("B.prototype.writeUInt8 = function"));
3101        assert!(poly.contains("B.prototype.writeUInt16LE = function"));
3102        assert!(poly.contains("B.prototype.writeUInt32LE = function"));
3103        assert!(poly.contains("B.prototype.writeInt8 = function"));
3104        assert!(poly.contains("B.prototype.writeInt16LE = function"));
3105        assert!(poly.contains("B.prototype.writeInt32LE = function"));
3106        assert!(poly.contains("B.prototype.writeFloatLE = function"));
3107        assert!(poly.contains("B.prototype.writeDoubleLE = function"));
3108    }
3109
3110    #[test]
3111    fn node_polyfills_require_cache() {
3112        let poly = super::NODE_POLYFILLS;
3113        assert!(poly.contains("require.cache = _module_cache"));
3114        assert!(poly.contains("_module_cache = {}"));
3115    }
3116
3117    #[test]
3118    fn node_polyfills_require_builtin_modules() {
3119        let poly = super::NODE_POLYFILLS;
3120        // Check key built-in modules are defined
3121        assert!(poly.contains("'fs':"));
3122        assert!(poly.contains("'path':"));
3123        assert!(poly.contains("'url':"));
3124        assert!(poly.contains("'querystring':"));
3125        assert!(poly.contains("'events':"));
3126        assert!(poly.contains("'util':"));
3127        assert!(poly.contains("'stream':"));
3128        assert!(poly.contains("'buffer':"));
3129        assert!(poly.contains("'crypto':"));
3130        assert!(poly.contains("'os':"));
3131        assert!(poly.contains("'assert':"));
3132        assert!(poly.contains("'timers':"));
3133    }
3134
3135    #[test]
3136    fn node_polyfills_path_module() {
3137        let poly = super::NODE_POLYFILLS;
3138        assert!(poly.contains("join: function"));
3139        assert!(poly.contains("resolve: function"));
3140        assert!(poly.contains("dirname: function"));
3141        assert!(poly.contains("basename: function"));
3142        assert!(poly.contains("extname: function"));
3143        assert!(poly.contains("sep: '/'"));
3144        assert!(poly.contains("posix:"));
3145        assert!(poly.contains("win32:"));
3146    }
3147
3148    #[test]
3149    fn node_polyfills_global_alias() {
3150        let poly = super::NODE_POLYFILLS;
3151        assert!(poly.contains("global = globalThis"));
3152    }
3153
3154    #[test]
3155    fn node_polyfills_text_encoder_decoder() {
3156        let poly = super::NODE_POLYFILLS;
3157        assert!(poly.contains("TextEncoder"));
3158        assert!(poly.contains("TextDecoder"));
3159    }
3160
3161    #[test]
3162    fn node_polyfills_btoa_atob() {
3163        let poly = super::NODE_POLYFILLS;
3164        assert!(poly.contains("btoa = function"));
3165        assert!(poly.contains("atob = function"));
3166        assert!(poly.contains("_b64chars"));
3167    }
3168
3169    // ─── Edge case tests ──────────────────────────────────────────────────
3170
3171    #[test]
3172    fn bridge_command_empty_navigate_url() {
3173        let cmd = super::BridgeCommand::Navigate("".into());
3174        let cloned = cmd.clone();
3175        assert_eq!(cmd, cloned);
3176        let debug_str = format!("{:?}", cmd);
3177        assert!(debug_str.contains("Navigate"));
3178    }
3179
3180    #[test]
3181    fn bridge_command_empty_evaluate_string() {
3182        let cmd = super::BridgeCommand::Evaluate("".into());
3183        let cloned = cmd.clone();
3184        assert_eq!(cmd, cloned);
3185        let debug_str = format!("{:?}", cmd);
3186        assert!(debug_str.contains("Evaluate"));
3187    }
3188
3189    #[test]
3190    fn bridge_response_empty_value() {
3191        let resp = super::BridgeResponse::Value("".into());
3192        assert!(!resp.is_ok());
3193        assert!(!resp.is_err());
3194        let result = resp.ok();
3195        assert!(result.is_ok());
3196        assert_eq!(result.unwrap(), super::BridgeResponse::Value("".into()));
3197    }
3198
3199    #[test]
3200    fn bridge_response_empty_binary() {
3201        let resp = super::BridgeResponse::Binary(vec![]);
3202        assert!(!resp.is_ok());
3203        assert!(!resp.is_err());
3204        let cloned = resp.clone();
3205        assert_eq!(resp, cloned);
3206    }
3207
3208    #[test]
3209    fn bridge_response_large_binary_payload() {
3210        // Create a large binary payload (1MB)
3211        let large_data: Vec<u8> = (0..=255).cycle().take(1024 * 1024).collect();
3212        let resp = super::BridgeResponse::Binary(large_data.clone());
3213        assert!(!resp.is_ok());
3214        assert!(!resp.is_err());
3215        let cloned = resp.clone();
3216        assert_eq!(resp, cloned);
3217        // Verify the data is intact
3218        if let super::BridgeResponse::Binary(data) = cloned {
3219            assert_eq!(data.len(), 1024 * 1024);
3220            assert_eq!(data[0], 0);
3221            assert_eq!(data[255], 255);
3222            assert_eq!(data[256], 0); // cycles back
3223        } else {
3224            panic!("Expected Binary variant");
3225        }
3226    }
3227
3228    #[test]
3229    fn bridge_command_unicode_navigate_url() {
3230        let unicode_url = "https://例子.测试/路径?查询=值#片段";
3231        let cmd = super::BridgeCommand::Navigate(unicode_url.into());
3232        let cloned = cmd.clone();
3233        assert_eq!(cmd, cloned);
3234        let debug_str = format!("{:?}", cmd);
3235        assert!(debug_str.contains(unicode_url));
3236    }
3237
3238    #[test]
3239    fn bridge_command_unicode_evaluate_string() {
3240        let unicode_code = "console.log('你好世界 🎉')";
3241        let cmd = super::BridgeCommand::Evaluate(unicode_code.into());
3242        let cloned = cmd.clone();
3243        assert_eq!(cmd, cloned);
3244        let debug_str = format!("{:?}", cmd);
3245        assert!(debug_str.contains(unicode_code));
3246    }
3247
3248    #[test]
3249    fn bridge_response_unicode_value() {
3250        let unicode_value = "结果: 成功 ✅ 日本語 한국어 العربية";
3251        let resp = super::BridgeResponse::Value(unicode_value.into());
3252        let cloned = resp.clone();
3253        assert_eq!(resp, cloned);
3254        let debug_str = format!("{:?}", resp);
3255        assert!(debug_str.contains(unicode_value));
3256    }
3257
3258    #[test]
3259    fn bridge_response_unicode_error() {
3260        let unicode_error = "错误: 文件未找到 📁❌";
3261        let resp = super::BridgeResponse::Err(unicode_error.into());
3262        assert!(resp.is_err());
3263        let result = resp.ok();
3264        assert!(result.is_err());
3265        assert_eq!(result.unwrap_err(), unicode_error);
3266    }
3267
3268    #[test]
3269    fn bridge_channel_debug_format() {
3270        let (channel, _receiver) = super::BridgeChannel::new();
3271        let debug_str = format!("{:?}", channel);
3272        assert!(debug_str.contains("BridgeChannel"));
3273        assert!(debug_str.contains("alive"));
3274    }
3275
3276    // ── REQ-SEC-002/003: Runtime bridge security structural verification ──
3277    // @trace TEST-SEC-003 [req:REQ-SEC-001,REQ-SEC-002,REQ-SEC-003] [level:unit]
3278
3279    /// Verify install_all_native calls install_web_apis (NOT install_all or install_node_apis).
3280    /// REQ-SEC-003: The bridge must NOT inject Node APIs on page global.
3281    #[test]
3282    fn runtime_bridge_calls_web_apis_not_install_all() {
3283        let source = include_str!("runtime_bridge.rs");
3284        let func_start = source
3285            .find("unsafe fn install_all_native")
3286            .expect("install_all_native function not found");
3287        // Extract just the function body — 5000 chars max to avoid test code.
3288        let func_body = &source[func_start..func_start + 5000.min(source.len() - func_start)];
3289
3290        assert!(
3291            func_body.contains("bun_runtime::fetch_api::install_fetch_global"),
3292            "REQ-SEC-003 REGRESSION: install_all_native must install Web APIs (fetch)"
3293        );
3294        assert!(
3295            func_body.contains("bun_runtime::timers::install_timer_globals"),
3296            "REQ-SEC-003 REGRESSION: install_all_native must install Web APIs (timers)"
3297        );
3298        assert!(
3299            !func_body.contains("globals::install_all("),
3300            "REQ-SEC-003 REGRESSION: install_all_native must NOT call install_all()"
3301        );
3302        assert!(
3303            !func_body.contains("globals::install_node_apis("),
3304            "REQ-SEC-003 REGRESSION: install_all_native must NOT call install_node_apis()"
3305        );
3306    }
3307
3308    /// Verify worker_scope_init_native installs ONLY stealth properties, NOT
3309    /// any bun_runtime Web APIs (fetch/timers/crypto/performance/etc.).
3310    /// BCE-20260627-009: servo's DedicatedWorkerGlobalScope provides Web APIs
3311    /// natively via its resource thread; Bao's duplicate installation caused
3312    /// SIGSEGV in js::Atomize (Compartment not entered) and promise execution
3313    /// model conflicts. stealth getter install (DEC-WK-007) is the only allowed
3314    /// installation in worker scope_init.
3315    #[test]
3316    fn worker_scope_init_native_is_stealth_only() {
3317        let source = include_str!("runtime_bridge.rs");
3318        let func_start = source
3319            .find("unsafe fn worker_scope_init_native")
3320            .expect("worker_scope_init_native function not found");
3321        // Extract just the function body — bounded to next fn/doc to avoid overflow.
3322        let search_end = source[func_start..]
3323            .find("\n// @trace REQ-SEC-003 [entity:WebPolyfills]")
3324            .or_else(|| source[func_start..].find("\nconst WEB_POLYFILLS"))
3325            .unwrap_or(5000)
3326            .min(5000);
3327        let func_body = &source[func_start..func_start + search_end];
3328
3329        // stealth getter install MUST be present (DEC-WK-007)
3330        assert!(
3331            func_body.contains("install_stealth_props"),
3332            "BCE-20260627-009 REGRESSION: worker_scope_init_native must install stealth props (DEC-WK-007)"
3333        );
3334
3335        // Web APIs MUST NOT be present — servo DedicatedWorkerGlobalScope provides them natively
3336        assert!(
3337            !func_body.contains("bun_runtime::fetch_api::install_fetch_global"),
3338            "BCE-20260627-009 REGRESSION: worker_scope_init_native must NOT install fetch (servo native)"
3339        );
3340        assert!(
3341            !func_body.contains("bun_runtime::timers::install_timer_globals"),
3342            "BCE-20260627-009 REGRESSION: worker_scope_init_native must NOT install timers (servo native)"
3343        );
3344        assert!(
3345            !func_body.contains("bun_runtime::web_api::install_performance"),
3346            "BCE-20260627-009 REGRESSION: worker_scope_init_native must NOT install performance (servo native)"
3347        );
3348        assert!(
3349            !func_body.contains("bun_runtime::globals::install_crypto_global"),
3350            "BCE-20260627-009 REGRESSION: worker_scope_init_native must NOT install crypto (servo native)"
3351        );
3352        assert!(
3353            !func_body.contains("bun_runtime::globals::install_structured_clone"),
3354            "BCE-20260627-009 REGRESSION: worker_scope_init_native must NOT install structuredClone (servo native)"
3355        );
3356    }
3357
3358    /// Verify create_node_realm_native creates Node Realm in NewCompartmentAndZone.
3359    /// REQ-SEC-002: Node Realm must be in its own Compartment — physically isolated.
3360    #[test]
3361    fn runtime_bridge_node_realm_uses_new_compartment() {
3362        let source = include_str!("runtime_bridge.rs");
3363
3364        let func_start = source
3365            .find("unsafe fn create_node_realm_native")
3366            .expect("create_node_realm_native function not found");
3367        let func_body_start = source[func_start..]
3368            .find("{")
3369            .expect("function body start not found");
3370        let search_limit = source[func_start + func_body_start..]
3371            .find("pub fn inject_node_apis")
3372            .or_else(|| {
3373                source[func_start + func_body_start..].find("/// Inject Node.js APIs as native")
3374            })
3375            .unwrap_or(3000)
3376            .min(3000);
3377        let func_body =
3378            &source[func_start + func_body_start..func_start + func_body_start + search_limit];
3379
3380        assert!(
3381            func_body.contains("NewCompartmentAndZone"),
3382            "REQ-SEC-002 REGRESSION: create_node_realm_native must use NewCompartmentAndZone"
3383        );
3384        assert!(
3385            func_body.contains("SIMPLE_GLOBAL_CLASS"),
3386            "REQ-SEC-002 REGRESSION: create_node_realm_native must use SIMPLE_GLOBAL_CLASS"
3387        );
3388        assert!(
3389            func_body.contains("AutoRealm::new_from_handle"),
3390            "REQ-SEC-002 REGRESSION: create_node_realm_native must use AutoRealm"
3391        );
3392        assert!(
3393            func_body.contains("bun_runtime::globals::install_node_apis"),
3394            "REQ-SEC-002 REGRESSION: Node APIs must be installed on Node Realm global"
3395        );
3396    }
3397
3398    /// Verify evaluate_in_node_realm uses AutoRealm for Compartment isolation.
3399    /// REQ-SEC-002: Scripts must execute in Node Realm, not Page Realm.
3400    #[test]
3401    fn runtime_bridge_evaluate_in_node_realm_uses_auto_realm() {
3402        let source = include_str!("runtime_bridge.rs");
3403
3404        let func_start = source
3405            .find("pub unsafe fn evaluate_in_node_realm")
3406            .expect("evaluate_in_node_realm function not found");
3407        let func_body_start = source[func_start..]
3408            .find("{")
3409            .expect("function body start not found");
3410        let search_limit = source[func_start + func_body_start..]
3411            .find("unsafe fn create_node_realm_native")
3412            .unwrap_or(3000)
3413            .min(3000);
3414        let func_body =
3415            &source[func_start + func_body_start..func_start + func_body_start + search_limit];
3416
3417        assert!(
3418            func_body.contains("AutoRealm::new"),
3419            "REQ-SEC-002 REGRESSION: evaluate_in_node_realm must use AutoRealm"
3420        );
3421        assert!(
3422            func_body.contains("evaluate_script"),
3423            "REQ-SEC-002: evaluate_in_node_realm must call evaluate_script"
3424        );
3425    }
3426
3427    /// Verify per-page Node Realm storage exists (REQ-SEC-002).
3428    /// Node Realm globals are stored keyed by WebViewId (NOT *mut JSObject).
3429    /// BCE-20260621-001: WebViewId-keyed storage eliminates cross-thread
3430    /// *mut JSObject dereferences. servo routes callbacks by WebViewId, so
3431    /// pointers stored under WebViewId are always accessed on the owning
3432    /// ScriptThread — no activation-stack corruption.
3433    #[test]
3434    fn runtime_bridge_has_per_page_node_realm_storage() {
3435        let source = include_str!("runtime_bridge.rs");
3436        assert!(
3437            source.contains("NODE_REALM_BY_WEBVIEW"),
3438            "REQ-SEC-002 REGRESSION: must have NODE_REALM_BY_WEBVIEW per-page storage"
3439        );
3440        assert!(
3441            source.contains("store_node_realm"),
3442            "REQ-SEC-002 REGRESSION: must have store_node_realm accessor"
3443        );
3444        assert!(
3445            source.contains("get_node_realm_by_id"),
3446            "REQ-SEC-002 REGRESSION: must have get_node_realm_by_id accessor (WebViewId-keyed)"
3447        );
3448        assert!(
3449            source.contains("get_node_realm_global"),
3450            "REQ-SEC-002 REGRESSION: must have get_node_realm_global accessor"
3451        );
3452        // BCE-20260621-001: enforce NO cross-thread *mut JSObject globals.
3453        // Use compile-time symbol references (positive) + word-boundary source
3454        // scan for stale globals (the source-grep must avoid matching its own
3455        // assertion text, so we use the `static NAME:` declaration prefix
3456        // combined with line-start anchoring via split).
3457        for line in source.lines() {
3458            let trimmed = line.trim_start();
3459            // Only flag actual top-level/static declarations of the BUG globals.
3460            // Comments and prose mentions are excluded by requiring leading `static`.
3461            assert!(
3462                !(trimmed.starts_with("static NODE_REALMS:")
3463                    && !trimmed.starts_with("static NODE_REALM_BY_WEBVIEW")),
3464                "BCE-20260621-001 REGRESSION: cross-thread *mut JSObject-keyed NODE_REALMS must not exist"
3465            );
3466            assert!(
3467                !(trimmed.starts_with("static PAGE_GLOBALS:")
3468                    && !trimmed.starts_with("static PAGE_GLOBAL_BY_WEBVIEW")),
3469                "BCE-20260621-001 REGRESSION: cross-thread *mut JSObject-keyed PAGE_GLOBALS must not exist"
3470            );
3471            assert!(
3472                !trimmed.starts_with("static LAST_PAGE_GLOBAL:"),
3473                "BCE-20260621-001 REGRESSION: process-wide LAST_PAGE_GLOBAL must not exist"
3474            );
3475        }
3476    }
3477
3478    /// Verify inject_node_apis_with_stealth uses drain_callbacks (not evaluate_js).
3479    /// REQ-SEC-002: Internal drain must NOT trigger Node API injection (avoid recursion).
3480    /// drain_callbacks handles InternalError from pending pipeline gracefully.
3481    #[test]
3482    fn runtime_bridge_drain_uses_callbacks_method() {
3483        let source = include_str!("runtime_bridge.rs");
3484
3485        let func_start = source
3486            .find("pub fn inject_node_apis_with_stealth")
3487            .expect("inject_node_apis_with_stealth function not found");
3488        let func_end = source[func_start..]
3489            .find("fn register_native_host_functions")
3490            .expect("end boundary not found");
3491        let func_body = &source[func_start..func_start + func_end];
3492
3493        assert!(
3494            func_body.contains("drain_callbacks"),
3495            "REQ-SEC-002 REGRESSION: inject_node_apis_with_stealth must use drain_callbacks (not evaluate_js)"
3496        );
3497        assert!(
3498            !func_body.contains("page.evaluate_js(\""),
3499            "REQ-SEC-002 REGRESSION: inject_node_apis_with_stealth must NOT call evaluate_js with string arg (would cause recursion)"
3500        );
3501        assert!(
3502            !func_body.contains("let _"),
3503            "REQ-SEC-003 REGRESSION: inject_node_apis_with_stealth must NOT swallow errors with let _"
3504        );
3505    }
3506
3507    /// Verify NODE_POLYFILLS contains Node API names (for fallback mode).
3508    #[test]
3509    fn node_polyfills_contains_security_sensitive_names() {
3510        let poly = super::NODE_POLYFILLS;
3511        assert!(
3512            poly.contains("require"),
3513            "NODE_POLYFILLS must contain 'require'"
3514        );
3515        assert!(
3516            poly.contains("Buffer"),
3517            "NODE_POLYFILLS must contain 'Buffer'"
3518        );
3519        assert!(
3520            poly.contains("process"),
3521            "NODE_POLYFILLS must contain 'process'"
3522        );
3523    }
3524
3525    // ── TEST-SEC-003: Node API Sandbox Isolation ────────────────────────
3526
3527    /// Verify WEB_POLYFILLS exists and does NOT contain Node.js API names.
3528    /// REQ-SEC-003: Page Realm fallback polyfills must NOT include Node APIs.
3529    #[test]
3530    fn web_polyfills_excludes_node_apis() {
3531        let poly = super::WEB_POLYFILLS;
3532        assert!(
3533            !poly.contains("require"),
3534            "REQ-SEC-003 REGRESSION: WEB_POLYFILLS must NOT contain 'require'"
3535        );
3536        assert!(
3537            !poly.contains("Buffer"),
3538            "REQ-SEC-003 REGRESSION: WEB_POLYFILLS must NOT contain 'Buffer'"
3539        );
3540        assert!(
3541            !poly.contains("process"),
3542            "REQ-SEC-003 REGRESSION: WEB_POLYFILLS must NOT contain 'process'"
3543        );
3544        assert!(
3545            !poly.contains("Bun"),
3546            "REQ-SEC-003 REGRESSION: WEB_POLYFILLS must NOT contain 'Bun'"
3547        );
3548        assert!(
3549            !poly.contains("module"),
3550            "REQ-SEC-003 REGRESSION: WEB_POLYFILLS must NOT contain 'module'"
3551        );
3552        assert!(
3553            !poly.contains("__dirname"),
3554            "REQ-SEC-003 REGRESSION: WEB_POLYFILLS must NOT contain '__dirname'"
3555        );
3556        assert!(
3557            !poly.contains("__filename"),
3558            "REQ-SEC-003 REGRESSION: WEB_POLYFILLS must NOT contain '__filename'"
3559        );
3560    }
3561
3562    /// Verify WEB_POLYFILLS includes essential Web APIs.
3563    /// REQ-SEC-003 criterion 6-8: console/fetch/URL/URLSearchParams must work.
3564    #[test]
3565    fn web_polyfills_includes_web_apis() {
3566        let poly = super::WEB_POLYFILLS;
3567        assert!(
3568            poly.contains("TextEncoder"),
3569            "WEB_POLYFILLS must contain TextEncoder"
3570        );
3571        assert!(
3572            poly.contains("TextDecoder"),
3573            "WEB_POLYFILLS must contain TextDecoder"
3574        );
3575        assert!(poly.contains("URL"), "WEB_POLYFILLS must contain URL");
3576        assert!(
3577            poly.contains("URLSearchParams"),
3578            "WEB_POLYFILLS must contain URLSearchParams"
3579        );
3580        assert!(poly.contains("btoa"), "WEB_POLYFILLS must contain btoa");
3581        assert!(poly.contains("atob"), "WEB_POLYFILLS must contain atob");
3582    }
3583
3584    /// Verify fallback path uses WEB_POLYFILLS (not NODE_POLYFILLS).
3585    /// REQ-SEC-003: inject_node_apis_with_stealth fallback must not inject Node APIs.
3586    #[test]
3587    fn fallback_uses_web_polyfills_not_node_polyfills() {
3588        let source = include_str!("runtime_bridge.rs");
3589
3590        let func_start = source
3591            .find("pub fn inject_node_apis_with_stealth")
3592            .expect("inject_node_apis_with_stealth function not found");
3593        let func_end = source[func_start..]
3594            .find("fn register_native_host_functions")
3595            .expect("end boundary not found");
3596        let func_body = &source[func_start..func_start + func_end];
3597
3598        assert!(
3599            func_body.contains("WEB_POLYFILLS"),
3600            "REQ-SEC-003 REGRESSION: fallback must use WEB_POLYFILLS (not NODE_POLYFILLS)"
3601        );
3602        // The fallback path should NOT reference NODE_POLYFILLS
3603        let fallback_section = func_body
3604            .find("if !registered")
3605            .map(|i| &func_body[i..])
3606            .unwrap_or("");
3607        assert!(
3608            !fallback_section.contains("NODE_POLYFILLS"),
3609            "REQ-SEC-003 REGRESSION: fallback path must NOT reference NODE_POLYFILLS"
3610        );
3611    }
3612
3613    /// Verify install_all_native does NOT call install_node_apis or install_all.
3614    /// REQ-SEC-003 criterion 1+10: Page Realm must only get Web APIs.
3615    #[test]
3616    fn install_all_native_web_apis_only() {
3617        let source = include_str!("runtime_bridge.rs");
3618
3619        let func_start = source
3620            .find("unsafe fn install_all_native")
3621            .expect("install_all_native function not found");
3622        let func_body_start = source[func_start..]
3623            .find("{")
3624            .expect("function body start not found");
3625        let search_limit = source[func_start + func_body_start..]
3626            .find("const NODE_POLYFILLS")
3627            .or_else(|| {
3628                source[func_start + func_body_start..].find("/// Inject Node.js APIs as native")
3629            })
3630            .unwrap_or(5000)
3631            .min(5000);
3632        let func_body =
3633            &source[func_start + func_body_start..func_start + func_body_start + search_limit];
3634
3635        assert!(
3636            func_body.contains("bun_runtime::fetch_api::install_fetch_global"),
3637            "REQ-SEC-003 REGRESSION: install_all_native must install Web APIs (fetch)"
3638        );
3639        assert!(
3640            func_body.contains("bun_runtime::timers::install_timer_globals"),
3641            "REQ-SEC-003 REGRESSION: install_all_native must install Web APIs (timers)"
3642        );
3643        assert!(
3644            !func_body.contains("globals::install_all("),
3645            "REQ-SEC-003 REGRESSION: install_all_native must NOT call install_all()"
3646        );
3647        assert!(
3648            !func_body.contains("globals::install_node_apis("),
3649            "REQ-SEC-003 REGRESSION: install_all_native must NOT call install_node_apis()"
3650        );
3651    }
3652
3653    /// Verify Node APIs are installed in Node Realm (create_node_realm_native).
3654    /// REQ-SEC-003 criterion 9: Node APIs must exist ONLY in Node Realm.
3655    #[test]
3656    fn node_apis_installed_in_node_realm_only() {
3657        let source = include_str!("runtime_bridge.rs");
3658
3659        let func_start = source
3660            .find("unsafe fn create_node_realm_native")
3661            .expect("create_node_realm_native function not found");
3662        let func_body_start = source[func_start..]
3663            .find("{")
3664            .expect("function body start not found");
3665        let search_limit = source[func_start + func_body_start..]
3666            .find("unsafe fn wrap_and_install_dom_proxy")
3667            .or_else(|| source[func_start + func_body_start..].find("/// Wrap a DOM property"))
3668            .unwrap_or(3000)
3669            .min(3000);
3670        let func_body =
3671            &source[func_start + func_body_start..func_start + func_body_start + search_limit];
3672
3673        assert!(
3674            func_body.contains("bun_runtime::globals::install_node_apis"),
3675            "REQ-SEC-003 REGRESSION: Node Realm must install Node APIs (install_node_apis)"
3676        );
3677        assert!(
3678            func_body.contains("NewCompartmentAndZone"),
3679            "REQ-SEC-003 REGRESSION: Node Realm must be isolated via NewCompartmentAndZone"
3680        );
3681    }
3682
3683    /// Verify WEB_POLYFILLS is valid JS (self-executing function).
3684    #[test]
3685    fn web_polyfills_is_valid_js() {
3686        let poly = super::WEB_POLYFILLS;
3687        assert!(
3688            poly.starts_with("(function()"),
3689            "WEB_POLYFILLS must be an IIFE"
3690        );
3691        assert!(poly.ends_with("})();"), "WEB_POLYFILLS must close IIFE");
3692    }
3693
3694    /// REQ-SEC-002: remove_node_realm_by_id is pub and is a no-op for unknown WebViewId.
3695    /// BCE-20260621-001: by-WebViewId API; null/raw-pointer API removed.
3696    #[test]
3697    fn remove_node_realm_by_id_is_safe_no_op() {
3698        // Synthesize a WebViewId via servo's mock helper. We do not exercise
3699        // real servo script-thread routing here — we only assert the API does
3700        // not panic when called with an unknown WebViewId.
3701        // Using a sentinel-style test: call remove on a freshly-cleared map.
3702        let _guard = super::test_serial_lock().lock().unwrap();
3703        super::clear_all_node_realms();
3704        // Constructing a WebViewId requires PainterId. servo exposes
3705        // WebViewId::new(PainterId::next()) but PainterId is not re-exported
3706        // from the `servo` crate root. We rely on the fact that remove is
3707        // a no-op for unknown keys — we simply assert the function exists
3708        // and is callable. Compile-time check.
3709        let _f: fn(servo::WebViewId) = super::remove_node_realm_by_id;
3710    }
3711
3712    /// REQ-SEC-002: WebViewId-keyed storage API exists and is structurally sound.
3713    /// BCE-20260621-001: all accessor signatures are WebViewId-based.
3714    #[test]
3715    fn webview_id_keyed_storage_api_exists() {
3716        // Compile-time check that the WebViewId-keyed API exists.
3717        let _store: fn(servo::WebViewId, *mut mozjs::jsapi::JSObject, *mut mozjs::jsapi::JSObject) =
3718            super::store_node_realm;
3719        let _get_node: fn(servo::WebViewId) -> *mut mozjs::jsapi::JSObject =
3720            super::get_node_realm_by_id;
3721        let _get_page: fn(servo::WebViewId) -> *mut mozjs::jsapi::JSObject =
3722            super::get_page_global_by_id;
3723        let _get_node_global: fn(servo::WebViewId) -> *mut mozjs::jsapi::JSObject =
3724            super::get_node_realm_global;
3725        let _get_page_global: fn(servo::WebViewId) -> *mut mozjs::jsapi::JSObject =
3726            super::get_page_global;
3727        let _remove: fn(servo::WebViewId) = super::remove_node_realm_by_id;
3728    }
3729
3730    /// REQ-SEC-002: clear_all_node_realms still works (empties both maps).
3731    /// BCE-20260621-001: clears WebViewId-keyed maps; no raw-pointer cleanup needed.
3732    #[test]
3733    fn clear_all_removes_all_entries() {
3734        let _guard = super::test_serial_lock().lock().unwrap();
3735        // Clear twice — must be idempotent.
3736        super::clear_all_node_realms();
3737        super::clear_all_node_realms();
3738    }
3739
3740    /// REQ-SEC-002: lazy getter functions exist and have correct ABI.
3741    #[test]
3742    fn lazy_dom_getters_are_valid_jsnative() {
3743        // Verify the functions can be cast to JSNative (Option<extern "C" fn>).
3744        let _: mozjs::jsapi::JSNative = Some(super::lazy_dom_getter_window);
3745        let _: mozjs::jsapi::JSNative = Some(super::lazy_dom_getter_document);
3746        let _: mozjs::jsapi::JSNative = Some(super::lazy_dom_getter_navigator);
3747        // @trace REQ-BRW-004: Worker/SharedWorker/ServiceWorker lazy getters
3748        let _: mozjs::jsapi::JSNative = Some(super::lazy_dom_getter_worker);
3749        let _: mozjs::jsapi::JSNative = Some(super::lazy_dom_getter_shared_worker);
3750        let _: mozjs::jsapi::JSNative = Some(super::lazy_dom_getter_service_worker);
3751    }
3752
3753    /// @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:Worker] DF-WK-11
3754    /// Structural assertion: Worker/SharedWorker/ServiceWorker constructor lazy
3755    /// getters use JSPROP_PERMANENT (non-configurable), matching Web IDL semantics.
3756    /// Ordinary DOM object getters (window/document/navigator) use JSPROP_READONLY
3757    /// without JSPROP_PERMANENT.
3758    #[test]
3759    fn worker_constructor_getters_use_permanent_attribute() {
3760        let source = include_str!("runtime_bridge.rs");
3761        // Constructor getters must include JSPROP_PERMANENT
3762        assert!(
3763            source.contains("ctor_attrs = (mozjs::jsapi::JSPROP_ENUMERATE | mozjs::jsapi::JSPROP_READONLY | mozjs::jsapi::JSPROP_PERMANENT)"),
3764            "REQ-BRW-004 REGRESSION: Worker/SharedWorker/ServiceWorker constructors must use JSPROP_PERMANENT"
3765        );
3766        // Object getters must NOT include JSPROP_PERMANENT
3767        assert!(
3768            source.contains("obj_attrs = (mozjs::jsapi::JSPROP_ENUMERATE | mozjs::jsapi::JSPROP_READONLY)"),
3769            "REQ-BRW-004 REGRESSION: window/document/navigator getters should use obj_attrs without JSPROP_PERMANENT"
3770        );
3771        // Verify Worker/SharedWorker/ServiceWorker are in ctor_getters
3772        assert!(
3773            source.contains("(c\"Worker\", Some(lazy_dom_getter_worker))"),
3774            "REQ-BRW-004 REGRESSION: Worker must be in ctor_getters"
3775        );
3776        assert!(
3777            source.contains("(c\"SharedWorker\", Some(lazy_dom_getter_shared_worker))"),
3778            "REQ-BRW-004 REGRESSION: SharedWorker must be in ctor_getters"
3779        );
3780        assert!(
3781            source.contains("(c\"ServiceWorker\", Some(lazy_dom_getter_service_worker))"),
3782            "REQ-BRW-004 REGRESSION: ServiceWorker must be in ctor_getters"
3783        );
3784    }
3785
3786    // ── DF-WK-11: Cross-Compartment Worker Constructor Proxy Behavioral Tests ──
3787    // @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:Worker] [DF-WK-11]
3788
3789    /// DF-WK-11: lazy_constructor_getter_impl reads from Page Realm (not Node Realm).
3790    ///
3791    /// Behavioral assertion: the getter fetches the constructor from the
3792    /// PER_THREAD_PAGE_GLOBAL (Page Realm's Window global), NOT from
3793    /// CurrentGlobalOrNull (which would be the Node Realm global in the
3794    /// getter's execution context). This is the core of cross-Compartment
3795    /// proxying — the constructor physically lives in Page Realm's Compartment,
3796    /// and JS_WrapObject creates the proxy for Node Realm access.
3797    ///
3798    /// @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:Worker] [DF-WK-11]
3799    #[test]
3800    fn constructor_getter_reads_from_page_realm_not_node_realm() {
3801        let source = include_str!("runtime_bridge.rs");
3802        let func_start = source
3803            .find("unsafe fn lazy_constructor_getter_impl")
3804            .expect("lazy_constructor_getter_impl not found");
3805        let func_body = &source[func_start..func_start + 3000.min(source.len() - func_start)];
3806
3807        // Must read page_global from PER_THREAD_PAGE_GLOBAL
3808        assert!(
3809            func_body.contains("PER_THREAD_PAGE_GLOBAL.with"),
3810            "DF-WK-11 REGRESSION: lazy_constructor_getter_impl must read Page Realm global from PER_THREAD_PAGE_GLOBAL"
3811        );
3812        // Must get the property from page_global_root (Page Realm), not node_global
3813        assert!(
3814            func_body.contains("page_global_root.handle()"),
3815            "DF-WK-11 REGRESSION: lazy_constructor_getter_impl must JS_GetProperty from page_global_root (Page Realm)"
3816        );
3817    }
3818
3819    /// DF-WK-11: lazy_constructor_getter_impl uses JS_WrapObject for cross-Compartment proxy.
3820    ///
3821    /// Behavioral assertion: the getter wraps the fetched Page Realm constructor
3822    /// with JS_WrapObject, creating a cross-Compartment proxy. This is what
3823    /// enables Node Realm scripts to call `new Worker(url)` — SpiderMonkey
3824    /// transparently enters Page Realm's Compartment when [[Construct]] is
3825    /// invoked on the proxy.
3826    ///
3827    /// @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:Worker] [DF-WK-11]
3828    #[test]
3829    fn constructor_getter_uses_js_wrap_object_for_cross_compartment_proxy() {
3830        let source = include_str!("runtime_bridge.rs");
3831        let func_start = source
3832            .find("unsafe fn lazy_constructor_getter_impl")
3833            .expect("lazy_constructor_getter_impl not found");
3834        let func_body = &source[func_start..func_start + 3000.min(source.len() - func_start)];
3835
3836        assert!(
3837            func_body.contains("JS_WrapObject"),
3838            "DF-WK-11 REGRESSION: lazy_constructor_getter_impl must call JS_WrapObject for cross-Compartment proxy"
3839        );
3840    }
3841
3842    /// DF-WK-11: lazy_constructor_getter_impl validates IsConstructor before returning.
3843    ///
3844    /// Behavioral assertion: the getter checks IsConstructor on the fetched
3845    /// object before wrapping it. If the Page Realm property is not a constructor
3846    /// (e.g., it's a plain object or undefined), a ReferenceError is thrown
3847    /// instead of returning an unusable value. This prevents cryptic
3848    /// "X is not a constructor" TypeErrors at `new Worker()` call sites.
3849    ///
3850    /// @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:Worker] [DF-WK-11]
3851    #[test]
3852    fn constructor_getter_validates_is_constructor_before_wrapping() {
3853        let source = include_str!("runtime_bridge.rs");
3854        let func_start = source
3855            .find("unsafe fn lazy_constructor_getter_impl")
3856            .expect("lazy_constructor_getter_impl not found");
3857        let func_body = &source[func_start..func_start + 3000.min(source.len() - func_start)];
3858
3859        assert!(
3860            func_body.contains("mozjs::jsapi::IsConstructor"),
3861            "DF-WK-11 REGRESSION: lazy_constructor_getter_impl must call IsConstructor to validate the constructor"
3862        );
3863    }
3864
3865    /// DF-WK-11: constructor getter throws ReferenceError (not TypeError) on failure.
3866    ///
3867    /// Behavioral assertion: when the constructor is unavailable (no page loaded,
3868    /// property not an object, or not a constructor), the getter throws a
3869    /// ReferenceError via report_reference_error. This matches Web IDL semantics
3870    /// where accessing an unsupported interface constructor should produce
3871    /// ReferenceError, not a confusing TypeError at the call site.
3872    ///
3873    /// @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:Worker] [DF-WK-11]
3874    #[test]
3875    fn constructor_getter_throws_reference_error_on_failure() {
3876        let source = include_str!("runtime_bridge.rs");
3877        let func_start = source
3878            .find("unsafe fn lazy_constructor_getter_impl")
3879            .expect("lazy_constructor_getter_impl not found");
3880        let func_body = &source[func_start..func_start + 3000.min(source.len() - func_start)];
3881
3882        // Must call report_reference_error for each failure case
3883        assert!(
3884            func_body.contains("report_reference_error"),
3885            "DF-WK-11 REGRESSION: lazy_constructor_getter_impl must call report_reference_error for error reporting"
3886        );
3887        // Error messages must contain the property name for diagnosability
3888        assert!(
3889            func_body.contains("property_name"),
3890            "DF-WK-11 REGRESSION: lazy_constructor_getter_impl error messages must reference property_name"
3891        );
3892    }
3893
3894    /// DF-WK-11: null page_global triggers ReferenceError (not silent undefined).
3895    ///
3896    /// Behavioral assertion: when PER_THREAD_PAGE_GLOBAL returns null (no page
3897    /// loaded yet), the getter throws a ReferenceError instead of silently
3898    /// returning undefined. This is critical for Node Realm scripts that use
3899    /// `new Worker()` — they need a clear diagnostic that the browser context
3900    /// isn't ready, not a mysterious "Worker is not a constructor" TypeError.
3901    ///
3902    /// @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:Worker] [DF-WK-11]
3903    #[test]
3904    fn constructor_getter_throws_on_null_page_global() {
3905        let source = include_str!("runtime_bridge.rs");
3906        let func_start = source
3907            .find("unsafe fn lazy_constructor_getter_impl")
3908            .expect("lazy_constructor_getter_impl not found");
3909        let func_body = &source[func_start..func_start + 3000.min(source.len() - func_start)];
3910
3911        // Must check for null page_global and throw
3912        assert!(
3913            func_body.contains("page_global.is_null()"),
3914            "DF-WK-11 REGRESSION: lazy_constructor_getter_impl must check page_global.is_null()"
3915        );
3916        // Must throw ReferenceError (return false) on null page_global
3917        // Search specifically for "page_global.is_null()" context — there's also
3918        // node_global.is_null() earlier in the function which returns true silently.
3919        let page_global_null_pos = func_body
3920            .find("page_global.is_null()")
3921            .expect("DF-WK-11: page_global.is_null() check not found");
3922        let after_null_check = &func_body[page_global_null_pos..page_global_null_pos + 300];
3923        assert!(
3924            after_null_check.contains("report_reference_error"),
3925            "DF-WK-11 REGRESSION: null page_global must trigger report_reference_error, not silent return"
3926        );
3927    }
3928
3929    /// DF-WK-11: install_lazy_dom_getters registers constructors with PERMANENT attribute.
3930    ///
3931    /// Behavioral assertion: the ctor_getters are registered with
3932    /// JSPROP_PERMANENT (non-deletable), matching Web IDL semantics where
3933    /// interface constructors on the global must not be configurable.
3934    /// This prevents page JS (or Node Realm scripts) from accidentally
3935    /// deleting Worker/SharedWorker/ServiceWorker from the global.
3936    ///
3937    /// @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:Worker] [DF-WK-11]
3938    #[test]
3939    fn install_lazy_dom_getters_uses_js_define_property1_for_constructors() {
3940        // Structural DF-WK-11 regression guard. Search the whole file (not a
3941        // fixed byte window) so the assertions survive rustfmt reflowing the
3942        // function's layout — the ctor_getters loop can land past 2000 bytes.
3943        let source = include_str!("runtime_bridge.rs");
3944        assert!(
3945            source.find("unsafe fn install_lazy_dom_getters").is_some(),
3946            "install_lazy_dom_getters not found"
3947        );
3948
3949        // Must use JS_DefineProperty1 (not JS_SetProperty) for initial registration
3950        assert!(
3951            source.contains("JS_DefineProperty1"),
3952            "DF-WK-11 REGRESSION: install_lazy_dom_getters must use JS_DefineProperty1 for property registration"
3953        );
3954        // Must iterate ctor_getters separately from obj_getters
3955        assert!(
3956            source.contains("for &(name, getter) in ctor_getters"),
3957            "DF-WK-11 REGRESSION: ctor_getters must be registered with their own (PERMANENT) attributes"
3958        );
3959    }
3960
3961    /// DF-WK-11: constructor getters are called from Node Realm (not Page Realm).
3962    ///
3963    /// Structural assertion: install_lazy_dom_getters is called inside
3964    /// create_node_realm_native, which creates the Node Realm's global.
3965    /// The lazy getters are installed on the Node Realm global, meaning
3966    /// they execute in the Node Realm context (CurrentGlobalOrNull returns
3967    /// the Node Realm global). The getter then reads from Page Realm via
3968    /// PER_THREAD_PAGE_GLOBAL and wraps via JS_WrapObject.
3969    ///
3970    /// @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:Worker] [DF-WK-11]
3971    #[test]
3972    fn lazy_dom_getters_installed_on_node_realm_not_page_realm() {
3973        let source = include_str!("runtime_bridge.rs");
3974        let func_start = source
3975            .find("unsafe fn create_node_realm_native")
3976            .expect("create_node_realm_native not found");
3977        // Search the function body for install_lazy_dom_getters call
3978        let func_body = &source[func_start..func_start + 5000.min(source.len() - func_start)];
3979
3980        assert!(
3981            func_body.contains("install_lazy_dom_getters(realm_cx, global.handle())"),
3982            "DF-WK-11 REGRESSION: install_lazy_dom_getters must be called from create_node_realm_native on the Node Realm global"
3983        );
3984        // The 'global' in that call is the Node Realm global (created via JS_NewGlobalObject
3985        // with NewCompartmentAndZone), NOT the servo Window global
3986        assert!(
3987            func_body.contains("JS_NewGlobalObject") && func_body.contains("NewCompartmentAndZone"),
3988            "DF-WK-11 REGRESSION: create_node_realm_native must create Node Realm with NewCompartmentAndZone before installing lazy getters"
3989        );
3990    }
3991
3992    /// DF-WK-11: report_reference_error produces JSEXN_REFERENCEERR.
3993    ///
3994    /// Behavioral assertion: the error reporting function uses
3995    /// JSEXN_REFERENCEERR (not JSEXN_TYPEERR or generic error) when
3996    /// constructor access fails. This matches the Web IDL convention
3997    /// where accessing an undefined interface produces ReferenceError.
3998    ///
3999    /// @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:Worker] [DF-WK-11]
4000    #[test]
4001    fn report_reference_error_uses_reference_err_type() {
4002        let source = include_str!("runtime_bridge.rs");
4003        let func_start = source
4004            .find("unsafe fn report_reference_error")
4005            .expect("report_reference_error not found");
4006        let func_body = &source[func_start..func_start + 2000.min(source.len() - func_start)];
4007
4008        assert!(
4009            func_body.contains("JSEXN_REFERENCEERR"),
4010            "DF-WK-11 REGRESSION: report_reference_error must use JSEXN_REFERENCEERR error type"
4011        );
4012        assert!(
4013            func_body.contains("JS_ReportErrorNumberUTF8"),
4014            "DF-WK-11 REGRESSION: report_reference_error must use JS_ReportErrorNumberUTF8 for proper error reporting"
4015        );
4016    }
4017
4018    // ── DashMap + OnceLock refactoring tests ──────────────────────────────
4019    // @trace REQ-PURE-002 [req:REQ-PURE-002] [level:unit]
4020
4021    /// BCE-20260621-001: storage is WebViewId-keyed DashMap (not raw-pointer).
4022    /// Structural assertion: source contains the WebViewId-keyed static.
4023    #[test]
4024    fn storage_is_webview_id_keyed_dashmap() {
4025        let source = include_str!("runtime_bridge.rs");
4026        assert!(
4027            source.contains(
4028                "static NODE_REALM_BY_WEBVIEW: OnceLock<DashMap<servo::WebViewId, usize>>"
4029            ),
4030            "BCE-20260621-001 REGRESSION: NODE_REALM_BY_WEBVIEW must be WebViewId-keyed"
4031        );
4032        assert!(
4033            source.contains(
4034                "static PAGE_GLOBAL_BY_WEBVIEW: OnceLock<DashMap<servo::WebViewId, usize>>"
4035            ),
4036            "BCE-20260621-001 REGRESSION: PAGE_GLOBAL_BY_WEBVIEW must be WebViewId-keyed"
4037        );
4038        assert!(
4039            source.contains("thread_local! {\n    static PER_THREAD_PAGE_GLOBAL"),
4040            "BCE-20260621-001 REGRESSION: PER_THREAD_PAGE_GLOBAL thread_local must exist for lazy getters"
4041        );
4042    }
4043
4044    /// BCE-20260621-001: no process-wide cross-thread *mut JSObject storage remains.
4045    /// Structural sweep — confirms the BUG pattern signature has zero residual.
4046    /// Uses line-start scan to avoid matching assertion strings inside tests.
4047    #[test]
4048    fn no_cross_thread_raw_jsobject_storage_residual() {
4049        let source = include_str!("runtime_bridge.rs");
4050        for line in source.lines() {
4051            let trimmed = line.trim_start();
4052            assert!(
4053                !(trimmed.starts_with("static NODE_REALMS:")
4054                    && !trimmed.starts_with("static NODE_REALM_BY_WEBVIEW")),
4055                "BCE-20260621-001 RESIDUAL: NODE_REALMS usize-keyed DashMap still present"
4056            );
4057            assert!(
4058                !(trimmed.starts_with("static PAGE_GLOBALS:")
4059                    && !trimmed.starts_with("static PAGE_GLOBAL_BY_WEBVIEW")),
4060                "BCE-20260621-001 RESIDUAL: PAGE_GLOBALS usize-keyed DashMap still present"
4061            );
4062            assert!(
4063                !trimmed.starts_with("static LAST_PAGE_GLOBAL:"),
4064                "BCE-20260621-001 RESIDUAL: LAST_PAGE_GLOBAL AtomicUsize still present"
4065            );
4066            assert!(
4067                !trimmed.starts_with("fn get_last_page_global"),
4068                "BCE-20260621-001 RESIDUAL: get_last_page_global accessor still present"
4069            );
4070            assert!(
4071                !trimmed.starts_with("fn set_last_page_global"),
4072                "BCE-20260621-001 RESIDUAL: set_last_page_global accessor still present"
4073            );
4074            assert!(
4075                !trimmed.starts_with("pub fn get_last_page_global"),
4076                "BCE-20260621-001 RESIDUAL: pub get_last_page_global accessor still present"
4077            );
4078        }
4079    }
4080
4081    /// OnceLock EvaluateResult: set + get works.
4082    #[test]
4083    fn oncelock_evaluate_result_set_and_get() {
4084        use std::sync::Arc;
4085        let lock: Arc<OnceLock<super::EvaluateResult>> = Arc::new(OnceLock::new());
4086        assert!(lock.get().is_none(), "OnceLock should be unset initially");
4087
4088        let result = super::EvaluateResult::ok("42".into());
4089        assert!(lock.set(result).is_ok(), "First set should succeed");
4090
4091        let got = lock.get().unwrap();
4092        assert_eq!(got.value, Some("42".into()));
4093        assert!(got.error.is_none());
4094    }
4095
4096    /// OnceLock EvaluateResult: second set() fails gracefully (returns Err).
4097    #[test]
4098    fn oncelock_evaluate_result_second_set_fails() {
4099        use std::sync::Arc;
4100        let lock: Arc<OnceLock<super::EvaluateResult>> = Arc::new(OnceLock::new());
4101
4102        let first = super::EvaluateResult::ok("first".into());
4103        assert!(lock.set(first).is_ok());
4104
4105        let second = super::EvaluateResult::err("second".into());
4106        let set_result = lock.set(second);
4107        assert!(set_result.is_err(), "Second set should return Err");
4108        // Original value is preserved
4109        assert_eq!(lock.get().unwrap().value, Some("first".into()));
4110    }
4111}