npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@axumquant/cdp-network-interceptor

v0.1.0

Published

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

Readme

@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.