-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Adding a CDP method or Web API
SGavrl edited this page May 30, 2026
·
1 revision
Two recipes for the two most common extensions: a new CDP method, and a new JS Web API.
Worked example: MyDomain.doThing that takes { name } and returns { ok }.
Create or edit a file under crates/obscura-cdp/src/domains/:
// crates/obscura-cdp/src/domains/my_domain.rs
use serde_json::{json, Value};
use crate::dispatch::CdpContext;
pub async fn do_thing(
params: &Value,
_ctx: &mut CdpContext,
_session_id: &Option<String>,
) -> Result<Value, String> {
let name = params.get("name")
.and_then(|v| v.as_str())
.ok_or("missing name")?;
// do the work
Ok(json!({ "ok": true, "name": name }))
}In crates/obscura-cdp/src/dispatch.rs, add a match arm:
"MyDomain.doThing" => domains::my_domain::do_thing(&req.params, ctx, &req.session_id).await,crates/obscura-cdp/tests/cdp_my_domain.rs:
use obscura_cdp::dispatch::{dispatch, CdpContext};
use obscura_cdp::types::CdpRequest;
use serde_json::json;
#[tokio::test(flavor = "current_thread")]
async fn my_domain_do_thing_returns_ok() {
let mut ctx = CdpContext::new();
let resp = dispatch(&CdpRequest {
id: 1,
method: "MyDomain.doThing".into(),
params: json!({ "name": "test" }),
session_id: None,
}, &mut ctx).await;
assert!(resp.error.is_none());
assert_eq!(resp.result.unwrap()["ok"], true);
}Run:
cargo test -p obscura-cdp my_domainWorked example: crypto.subtle.digest, real implementation backed by a Rust hash op.
In crates/obscura-js/src/ops.rs:
#[op2]
#[buffer]
fn op_subtle_digest(#[string] algorithm: &str, #[buffer] data: &[u8]) -> Vec<u8> {
use sha1::Digest as _;
match algorithm.to_ascii_uppercase().as_str() {
"SHA-1" => sha1::Sha1::digest(data).to_vec(),
"SHA-256" => sha2::Sha256::digest(data).to_vec(),
"SHA-384" => sha2::Sha384::digest(data).to_vec(),
"SHA-512" => sha2::Sha512::digest(data).to_vec(),
_ => sha2::Sha256::digest(data).to_vec(),
}
}In the same file, build_extension():
ops: std::borrow::Cow::Owned(vec![
op_dom(),
op_console_msg(),
// ...
op_subtle_digest(),
]),In crates/obscura-js/js/bootstrap.js:
globalThis.crypto = globalThis.crypto || {};
globalThis.crypto.subtle = globalThis.crypto.subtle || {};
globalThis.crypto.subtle.digest = function digest(algorithm, data) {
const algName = typeof algorithm === 'string' ? algorithm : algorithm.name;
const bytes = data instanceof ArrayBuffer
? new Uint8Array(data)
: new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
const out = Deno.core.ops.op_subtle_digest(algName, bytes);
return Promise.resolve(out.buffer);
};crates/obscura-js/Cargo.toml:
sha1 = "0.10"
sha2 = "0.10"cargo build --release
./target/release/obscura fetch https://example.com --eval "
crypto.subtle.digest('SHA-256', new TextEncoder().encode('hi'))
.then(buf => Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join(''))
"- Keep the JS shim thin. All side effects go through ops.
- Use
Promise.resolveto keep async-shaped APIs callable from sync ops. - Match the spec: Web API names and shapes are checked by Puppeteer / Playwright wrappers.
- DOM mutations go through
op_dom, not new ops. - For events that need to fire across handlers, use the existing
_makeListenerBoxhelper inbootstrap.js.
- CDP method with intercept:
crates/obscura-cdp/src/domains/page.rsdo_navigate. - Web API with op + JS shim:
crypto.subtle.digest(above). - Web API in pure JS (no op):
DOMParserinbootstrap.js. - Web API with async event firing:
WebSocket,IntersectionObserverinbootstrap.js.