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

@sophonz/redaction

v0.0.4

Published

Sophonz Redaction

Readme

@sophonz/redaction

한국어

Redaction primitives for the Sophonz browser SDK, in two halves: URL sanitizing and attribute scrubbing.

The URL half strips user:password@ credentials and the values of well-known sensitive query parameters before a URL is written to a span, a log record or an exported attribute.

Ported from the OpenTelemetry browser navigation instrumentation's defaultSanitizeUrl, with Embrace's additionalQueryParamsToScrub ergonomic layered on top.

No runtime dependencies.

Part of the Sophonz OpenTelemetry suite.

Install

bun add @sophonz/redaction
# or
pnpm add @sophonz/redaction
# or
npm install @sophonz/redaction

Usage

import { defaultSanitizeUrl } from '@sophonz/redaction';

defaultSanitizeUrl('https://api.example.com/v1/me?token=abc&page=2');
// 'https://api.example.com/v1/me?token=REDACTED&page=2'

defaultSanitizeUrl('https://alice:[email protected]/v1/me');
// 'https://REDACTED:[email protected]/v1/me'

To redact your own parameters on top of the defaults:

import { createSanitizeUrl } from '@sophonz/redaction';

const sanitizeUrl = createSanitizeUrl({
  additionalQueryParamsToScrub: ['x-api-key', 'sig'],
});

sanitizeUrl('https://api.example.com/v1?x-api-key=abc&password=p&page=2');
// 'https://api.example.com/v1?x-api-key=REDACTED&password=REDACTED&page=2'

API

defaultSanitizeUrl(url: string): string

Redacts credentials and the 19 default parameters. Never throws.

createSanitizeUrl(options?: SanitizeUrlOptions): SanitizeUrl

Builds a sanitizer. createSanitizeUrl() with no options is equivalent to defaultSanitizeUrl.

| Option | Type | Default | Meaning | |---|---|---|---| | additionalQueryParamsToScrub | readonly string[] | [] | Names redacted in addition to the defaults | | queryParamsToScrub | readonly string[] | the 19 defaults | Replaces the default list outright. Prefer the option above | | redactCredentials | boolean | true | Redact user:password@ in the authority | | scrubFragment | boolean | true | Also scan the fragment for parameters |

DEFAULT_QUERY_PARAMS_TO_SCRUB: readonly string[]

The 19 default names, frozen: password, passwd, secret, api_key, apikey, auth, authorization, token, access_token, refresh_token, jwt, session, sessionid, key, private_key, client_secret, client_id, signature, hash.

REDACTED: 'REDACTED'

The marker substituted for every redacted value.

type SanitizeUrl = (url: string) => string

The hook signature instrumentations accept.

Behaviour

The value is replaced, never the key. ?token=abc becomes ?token=REDACTED. Deleting the key would hide that a secret was ever present, which is the thing an operator reading the telemetry most needs to know. This follows the OpenTelemetry url.query guidance that a redacted key SHOULD be preserved.

Parameter names match exactly, case-insensitively. ?Token= and ?TOKEN= are redacted; ?tokenizer=, ?keyword= and ?monkey= are not. Substring matching would be catastrophic here — the default list contains short generic words (key, auth, hash, session) that appear inside a great many innocent parameter names, and over-redaction has no off switch. additionalQueryParamsToScrub is the supported way to cover variants such as api-key.

Names are compared after decoding + to a space and resolving percent escapes, and after trimming surrounding whitespace, so ?%74oken= and ?%20token= are both redacted.

Every occurrence is redacted independently. ?token=a&token=b becomes ?token=REDACTED&token=REDACTED rather than collapsing to one pair; losing the repetition would misrepresent the request.

The fragment is scanned. The OAuth 2.0 implicit grant delivers access_token in the fragment precisely so it never reaches a server, which makes the fragment a place real secrets live. Both #access_token=… and hash routes carrying #/checkout?token=… are covered. A fragment that is not a parameter list (#installation, #/orders/42) contains no name=value pair and passes through untouched. Set scrubFragment: false to opt out.

Nothing else changes. The implementation is string-based and does not round-trip through URL or URLSearchParams, so the path, encoding, parameter order, host case, default port and trailing-slash-or-not all come back byte-for-byte identical. A URL round-trip would rewrite http://x.test to http://x.test/, drop :80, lower-case the host, and re-encode the whole query string (%20 to +) whether or not anything was redacted — differences that make a URL harder to match against what the application actually requested.

It never throws, and there is no unparseable input. Nothing is parsed, so nothing can fail to parse: relative URLs (/api?token=…), protocol-relative URLs, data: and blob: URIs, and malformed strings are all handled by the same code path and are still redacted. Two edge cases are defined explicitly:

  • A non-string argument returns ''. This is a caller type error, not a redaction failure; there is provably no secret in undefined, and coercing an arbitrary object would mean invoking a toString we have not inspected.
  • An internal failure — unreachable by construction, but guarded — returns the bare string REDACTED. Returning the input unchanged would be unsafe, because the input is exactly what we failed to redact. A url.full attribute equal to exactly REDACTED means the sanitizer bailed on that URL.

Differences from the upstream OpenTelemetry implementation

Three deliberate corrections, all covered by tests:

| Upstream | Here | |---|---| | searchParams.has(param) is case-sensitive, so ?Token= survives on the URL path (the regex fallback is case-insensitive, so behaviour differs by input) | Case-insensitive on every input | | searchParams.set(param, …) collapses repeated parameters, so ?token=a&token=b becomes ?token=REDACTED | Each occurrence redacted in place | | new URL(url) throws on relative URLs, falling back to building 19 RegExp objects per call; the URL path also re-encodes and normalises the whole URL | One string pass, no RegExp construction per call, no normalisation |

Attribute scrubbing

The URL sanitizer above covers URLs. createAttributeScrubber covers attributes — Embrace's attributeScrubbers, per-key redaction applied to every span and every log record, so a secret that lands in an attribute rather than a URL is caught too.

It is consumed by SophonzSpanAttributeScrubbingProcessor (@sophonz/span-processors) and SophonzLogAttributeScrubbingProcessor (@sophonz/log-processors), and configured through the SDK's attributeScrubbers option.

import { createAttributeScrubber } from '@sophonz/redaction';

const scrub = createAttributeScrubber([
  { keys: ['app.user.email'] },                    // -> 'REDACTED'
  { keyPattern: /^http\.request\.header\./ },       // -> 'REDACTED'
  {
    keys: ['app.query'],                           // transform, not blank
    scrub: (_key, value) =>
      typeof value === 'string' ? value.slice(0, 64) : value,
  },
  { keys: ['app.internal'], scrub: () => undefined }, // remove the attribute
]);

const attributes = { 'app.user.email': '[email protected]', 'app.span.type': 'route' };
scrub(attributes); // true
// attributes === { 'app.user.email': 'REDACTED', 'app.span.type': 'route' }

createAttributeScrubber(scrubbers?, options?): ScrubAttributes

Compiles a list of rules into one function that edits an attribute bag in place and returns whether anything changed. Never throws.

An empty, absent or entirely invalid list compiles to a shared no-op — literally the same function object every time, so an unconfigured SDK is not paying for a closure, an iteration or a copy.

options.onError(message, error?) receives misconfiguration and scrubber failures. This package has no dependency on @opentelemetry/api, so it cannot reach diag itself; the processors that wrap it pass one in. It defaults to a no-op — a redactor must never write to a customer's console uninvited.

The scrubber shape, and why it is not (key, value) => value | undefined

A scrubber is a matcher plus a transform:

| Field | Purpose | |---|---| | keys?: readonly string[] | Exact attribute keys, case-sensitive | | keyPattern?: RegExp \| readonly RegExp[] | Patterns tested against the key | | shouldScrub?: (key) => boolean | Arbitrary predicate; the escape hatch | | scrub?: (key, value) => value \| undefined | Replacement value. Omit for 'REDACTED'; return undefined to remove the attribute |

At least one matcher is required. A rule with none matches nothing and is dropped at construction with an onError report, rather than silently pretending to work.

The single-function alternative was rejected for two reasons.

It has no way to say "not mine". A (key, value) => value | undefined scrubber is called for every attribute, so it needs three outcomes — leave it, replace it, delete it — and undefined can only encode one. Whichever meaning it is given, the other becomes inexpressible; and the failure mode of getting it wrong is a scrubber that forgets to return the value it was handed, which silently empties every span. The split has no such state to get wrong.

It cannot be made cheap. This runs on every attribute of every span and every log record. With the matcher as data, all rules' exact keys compile into one shared Set and all their patterns into one shared, pre-compiled RegExp list, so an attribute nobody cares about costs one set lookup regardless of how many rules are configured — no closure, no allocation, no try/catch entered. With the matcher as a function, every rule must be invoked for every attribute. (Declaring a shouldScrub opts that rule out of the fast path, which is why the declarative matchers are the documented default.)

Patterns are compiled once. A g or y flag is stripped from a copy of the caller's RegExp, because RegExp.prototype.test advances lastIndex on a global or sticky pattern and would otherwise match only every other attribute. The caller's object is never mutated.

Every matching rule runs, in declaration order, threading the value through — so a truncating rule followed by a hashing rule composes the way it reads.

There are no default scrubbers, deliberately

The URL half ships secure-by-default with 19 parameter names. The attribute half ships empty. That asymmetry is intentional.

Attribute keys are a structured namespace; query parameter names are not. ?token= is free-form, author-chosen text where token really does mean a token. Attribute keys are http.request.header.authorization, app.screen.name, user.journey.id. Nothing in this SDK emits an attribute literally named key, hash, auth or session, so the same 19 names transplanted here would match almost nothing and buy almost no security.

Loosening the match to compensate is where it turns dangerous. key is a substring of service.key; session is a substring of session.id — the single most load-bearing attribute in this SDK, joined on by every dashboard and promoted to a named ClickHouse column. A substring default would silently zero them.

Over-redaction is silent and has no off switch. A blanked attribute leaves no trace at the collector, there is no un-redact, and nothing signals that it happened. Under-redaction is at least visible in the data. Given a default that cannot be audited from the outside, the safe direction is to redact nothing you were not asked to.

The dangerous default surface is already covered. URLs redact by default. The attributes that carry secrets in practice are ones the customer put there — globalAttributes, the console instrumentation's object spread, data-sophonz-* — and the customer is exactly who knows those key names. Embrace ships its attributeScrubbers empty for the same reason.

So: configure nothing and your telemetry is byte-identical to having never installed the processor.

What happens when a customer's scrubber throws

A scrubber is arbitrary code on the hot path of someone's page. Every invocation is wrapped, and the two failures are treated differently on purpose:

| Throws in | Outcome | Why | |---|---|---| | scrub(key, value) | Value becomes 'REDACTED'. The rule stays active. Reported once. | Fail closed. The matcher fired, so the customer has told us this key may carry a secret; the transform failing says nothing about whether the value is safe. 'REDACTED' is exactly as leak-proof as deleting the key, and unlike deleting it, leaves visible evidence that a value was there and was suppressed — a silently vanished key is indistinguishable from one that was never set, and can zero a dashboard that matches it by literal name. | | shouldScrub(key) | Attribute left alone. The rule is disabled for the rest of the page's life. Reported once. | A predicate that throws has told us nothing about this key. Treating the throw as a match would redact every attribute of every span with no way to switch it off; treating it as a non-match under-redacts, but only for a rule that is provably broken, and it is reported. Continuing to call it would burn CPU on a throw per attribute forever. |

Reports are emitted once per rule so a rule that fails on every attribute of every span cannot turn the console into a firehose. A broken rule never disables its siblings.

Scope

This package is transport- and SDK-agnostic: it has no dependency on @opentelemetry/api and knows nothing about spans or log records. Wiring it into the pipeline — where in the processor chain it runs, and what may be mutated at that point — lives in @sophonz/span-processors and @sophonz/log-processors.