Skip to content

axumquant/cdp-network-interceptor

Repository files navigation

@axumquant/cdp-network-interceptor

Chrome DevTools Protocol network interceptor for MV3 extensions — with PII redaction, iframe auto-attach, and stale-debugger recovery.

What is this?

Building a Chrome extension that needs to observe XHR/fetch responses is harder than it looks. The chrome.webRequest API hides response bodies entirely. The chrome.debugger API gives you bodies but ships with a pile of footguns:

  • iframe traffic is invisible by default. Cross-origin iframes have their own CDP target. If you only attach to the top frame, you miss every API call inside an embedded checkout, payment, or analytics widget.
  • stale attachments freeze the browser. When your service worker reloads (or crashes), Chrome leaves the old debugger session dangling. The next attach() call rejects with "Another debugger is already attached" and the user has to manually close the tab to recover.
  • PII leaks into your logs. Raw response bodies contain emails, phone numbers, SSNs, and auth tokens. If you forward them to a backend or write them to disk, you've created a compliance liability.
  • DevTools collides with your extension. If a developer opens DevTools while your extension is capturing, both grab the debugger socket and your capture silently dies.

This library wraps chrome.debugger.* with all of these problems already solved. You pass a tab id, a session id, and a callback. The library attaches, enables Target.setAutoAttach for iframe piercing, scrubs PII out of every response, and hands you a clean NetworkEvent. When the tab closes or your service worker reloads, it cleans up after itself.

It's domain-agnostic — there's no Medicare, sales, or fintech-specific code. You bring the patterns you care about (or use the defaults: SSN, email, phone, MBI, common auth headers).

Install

npm install @axumquant/cdp-network-interceptor

This is an ES Module package. You need Node 18+ to consume it from a build pipeline. The runtime target is a Chrome MV3 extension service worker — chrome.* APIs are globals, not imports.

Quickstart

In your extension's background service worker:

import { startTrafficCapture, stopTrafficCapture } from "@axumquant/cdp-network-interceptor";

chrome.action.onClicked.addListener(async (tab) => {
  if (!tab.id) return;
  await startTrafficCapture(tab.id, crypto.randomUUID(), {
    onForward: (event) => {
      // event.body is the parsed JSON, already redacted
      console.log(event.method, event.url, event.statusCode, event.body);
    },
  });
});

chrome.tabs.onRemoved.addListener((tabId) => {
  // (the library handles this automatically too — this is just illustrative)
  stopTrafficCapture(tabId);
});

Add "debugger" to your manifest.json permissions. That's it.

API reference

startTrafficCapture(tabId, sessionId, options): Promise<StartTrafficCaptureResult>

Attach to a tab and begin capturing network traffic.

Parameter Type Description
tabId number Chrome tab id. Must be a positive integer.
sessionId string Caller-defined session id. Included on every forwarded event so you can route them downstream.
options StartTrafficCaptureOptions See below. onForward is required; everything else has sensible defaults.

StartTrafficCaptureOptions

Field Type Default Description
onForward (event: NetworkEvent) => void | Promise<void> required Called once per redacted event. Async callbacks are awaited but their rejections are swallowed.
mode string "default" Free-form label echoed back in getCaptureStats(). Useful for distinguishing "copilot" vs "autopilot" sessions.
iframeCapture boolean true When true, enables Target.setAutoAttach so cross-origin iframe traffic is also captured.
redactionPatterns RedactionPatterns bundled defaults Override the PII regexes. See PII redaction.
maxPendingRequests number 250 FIFO cap on tracked request ids — protects against memory bloat on chatty SPAs.
maxBodySize number 2 * 1024 * 1024 Drop response bodies larger than this many bytes.
forwardableContentTypes string[] JSON-ish list Content-type substrings to forward. Default: application/json, text/json, text/plain, application/javascript.
skipUrlPatterns RegExp[] static-asset list URLs matching any of these regexes are dropped before any body work.

Returns a StartTrafficCaptureResult:

{
  ok: true;
  tabId: number;
  sessionId: string;
  mode: string;
  recoveredStaleAttachment?: boolean;  // true if we had to detach a zombie session
  warnings?: string[];                  // non-fatal startup issues (e.g. setAutoAttach unsupported)
  alreadyCapturing?: boolean;           // set when called twice for the same tab
}

Throws a CaptureError (with a capture_phase of "validate", "attach", or "start") on failure.

stopTrafficCapture(tabId): Promise<void>

Detach the debugger and stop forwarding events. Safe to call on a tab that isn't capturing.

isCapturing(tabId): boolean

Returns true if the library currently holds an active capture session for this tab.

getCaptureStats(): CaptureStats[]

Returns one entry per active session with eventCount, redactedFieldCount, pendingRequests, and uptime (in seconds).

NetworkEvent

The shape passed to your onForward callback:

interface NetworkEvent {
  sessionId: string;
  requestId: string;
  url: string;                    // query values scrubbed
  method: string;
  statusCode: number;
  headers: Record<string, string>; // sensitive header values scrubbed
  body: unknown;                   // parsed JSON, redacted
  frameOrigin: string | null;
  targetId: string | null;
  redacted: boolean;
  redactedFieldCount: number;
}

PII redaction

By default the library scrubs three categories of data:

Headers — any header matching this regex has its value replaced with [REDACTED]:

^(set-cookie|cookie|authorization|proxy-authorization|x-api-key|x-auth-token|x-session|x-csrf|x-xsrf)

Object keys — any key in the JSON body whose name matches the sensitive-key regex has its entire value replaced. Default coverage:

ssn, social, dob, date_of_birth, mbi, medicare, member_id, subscriber_id,
policy_number, phone, email, address, first_name, last_name, full_name,
client_name, patient_name

Inline values — string values that look like an email, SSN (xxx-xx-xxxx), phone number, or Medicare Beneficiary Identifier are replaced inline, even when they appear under an innocent-looking key.

Adding custom patterns

Pass a redactionPatterns option built from the defaults:

import {
  startTrafficCapture,
  defaultRedactionPatterns,
} from "@axumquant/cdp-network-interceptor";

await startTrafficCapture(tabId, sessionId, {
  onForward: handler,
  redactionPatterns: {
    sensitiveKey: new RegExp(
      defaultRedactionPatterns.sensitiveKey.source + "|account_number|tax_id",
      "i",
    ),
    sensitiveHeader: defaultRedactionPatterns.sensitiveHeader,
    // Add UK National Insurance numbers (must include `g` flag for replace())
    sensitiveValue: new RegExp(
      defaultRedactionPatterns.sensitiveValue.source +
        "|\\b[A-Z]{2}\\d{6}[A-Z]\\b",
      "gi",
    ),
  },
});

Important: the sensitiveValue regex MUST have the g flag — the library uses String.prototype.replace to scrub all occurrences.

iframe capture

When iframeCapture: true (the default), the library calls Target.setAutoAttach with flatten: true. Chrome then routes events from every nested target (cross-origin iframes, OOPIFs, workers) through the parent debuggee — your onForward callback sees them all without extra wiring.

Disable this only if you're certain you don't need iframe traffic and want to minimise overhead. Some embedded checkout flows (Stripe, Plaid, etc.) live entirely inside a cross-origin iframe and will be invisible without it.

Permissions

Your manifest.json needs:

{
  "manifest_version": 3,
  "permissions": ["debugger", "tabs"],
  "host_permissions": ["<all_urls>"]
}

"debugger" is the only strictly required permission. "tabs" and a host permission are needed if you want to enumerate tabs or inject content scripts.

When you call chrome.debugger.attach(), Chrome shows a yellow infobar across the top of the tab: "Your Extension started debugging this browser." This is non-dismissible by users — it's Chrome's warning that an extension has elevated access. There's no way to suppress it.

Common pitfalls

Zombie debugger sessions on extension reload. If you reload your unpacked extension while a capture is active, Chrome leaves the old debugger attached. The library auto-recovers on the next startTrafficCapture call (and returns recoveredStaleAttachment: true), but you can also call chrome.debugger.getTargets() and detach manually on service worker startup.

Debugger collision with DevTools open. If a user opens DevTools on a tab you're capturing, Chrome refuses to attach a second debugger. The error message includes "Another debugger is already attached" — surface this to the user and ask them to close DevTools.

Performance on chatty SPAs. Each Network.getResponseBody call is a CDP round-trip. On a page firing 100 XHRs/sec, you'll feel it. Tune skipUrlPatterns and forwardableContentTypes aggressively to drop traffic you don't care about before the body fetch happens.

Service worker lifetime. MV3 service workers are killed after ~30s of idleness. The library's in-memory _sessions map dies with the worker. Persist session state to chrome.storage and re-establish capture on chrome.runtime.onStartup if you need long-running sessions.

Body size cap. Responses larger than maxBodySize (default 2 MiB) are silently dropped. If you're missing events, check whether you're trying to capture large payload responses (JSON dumps, file lists, etc.) and raise the cap.

JSON-only by default. Non-JSON responses (HTML, XML, binary) are skipped. Override forwardableContentTypes if you need them.

Comparison vs raw chrome.debugger

Concern Raw chrome.debugger This library
Attach + enable Network domain manual (4+ lines) one call
Stale attachment from extension reload manual recovery automatic, with recoveredStaleAttachment flag
Cross-origin iframe traffic requires manual Target.setAutoAttach + per-target listeners automatic with iframeCapture: true
PII scrubbing DIY bundled defaults + injectable patterns
Request/response correlation DIY bundled (FIFO-capped pending map)
Tab close cleanup DIY automatic
Stats / observability DIY getCaptureStats()

License

MIT — see LICENSE.

About

Chrome DevTools Protocol network interceptor for MV3 extensions with PII redaction, iframe auto-attach, and stale-debugger recovery

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors