Blog··9 min read

How I took my npm package's supply-chain score from 78 to zero alerts

Godfrey Lebo
Godfrey Lebo
Founder, Sether · Last updated 2026-07-09
Direct answer

In June 2026, Socket.dev scored my npm package 78 because of 7 alerts, all from devDependencies and none from runtime code. I fixed everything anyway: upgraded dev tooling to reach 0 npm-audit vulnerabilities, dropped both SDK peer dependencies via structural typing, and shipped ASCII-only bundles. Result: zero open alerts, 1 runtime dependency.

TL;DR
  • Someone evaluating a security library checks its supply-chain score before its features. That is the right order, and mine said 78.
  • All 7 alerts came from devDependencies: a Critical CVE in vitest plus transitive esbuild, vite, and brace-expansion advisories. Zero were in runtime code.
  • "It's just devDeps" is technically true and a bad answer for a security tool. I fixed everything: dev tooling upgraded, npm audit now reports 0.
  • Then I went further: dropped the openai and @anthropic-ai/sdk peer dependencies via structural typing (1 runtime dependency left), shipped ASCII-only bundles, ReDoS-scanned all 161 regex patterns (0 unsafe).
  • Lessons for maintainers: your score is usually your devDeps, check the repo-specific alerts view, and a small dependency surface is a feature you can design for.

Why does a supply-chain score matter for a security library?

Put yourself in the shoes of an engineer evaluating Sether. I am asking them to pipe their most sensitive traffic, the prompts containing their customers' data, through my code. The first sane thing to do is check whether my package is itself a risk.

Tools like Socket.dev exist for exactly this. Socket is a supply-chain watchdog: it scans npm packages for known vulnerabilities, risky install scripts, obfuscated code, and dependency problems, and rolls it up into a score. Security teams check it the way they check a company's SOC 2.

So in June 2026, during a hardening pass across releases 0.5.2 to 0.5.4, I looked Sether up the way an evaluator would. The score was 78. For a library whose whole pitch is trust, that number reads like a warning label.

What actually caused the score of 78?

The first few minutes were honest panic. Had I shipped something vulnerable? I opened the alerts view and started reading.

Seven alerts. The headline was a Critical CVE in vitest (CVE-2026-47429), my test runner. The rest were transitive advisories in esbuild, vite, and brace-expansion, all pulled in through the same dev toolchain. I traced each one to its source.

Every single alert came from devDependencies. Not one was in code that ships to users. The published bundle, the thing npm install @raeven-co/sether actually puts on your machine, was untouched by all seven.

Alert sourceWhere it livesShips to users?Fix
vitest Critical CVE (CVE-2026-47429)devDependencies (test runner)NoUpgraded vitest to 4.x
esbuild advisories (transitive)devDependencies (build tooling)NoResolved by the tooling upgrade
vite advisories (transitive)devDependencies (build tooling)NoResolved by the tooling upgrade
brace-expansion advisory (transitive)devDependenciesNoResolved by the tooling upgrade

One practical detail that cost me time: check the repo-specific alerts view, not the org-wide one. The org-wide dashboard mixed in signals from other repositories and made the picture look worse and vaguer than it was. The per-repo view showed exactly seven alerts with exact provenance.

So why not just say "it's only devDeps"?

Because nobody evaluating a security library owes me that level of reading. The score says 78. Explaining that the alerts are "only" in my test runner is technically correct and rhetorically dead. If my pitch is "trust me with your PII", my answer to a supply-chain flag cannot start with "well, actually".

There is also a real, if smaller, risk story. devDependencies run on maintainer machines and in CI, where releases are built and signed. A compromised dev toolchain is a classic path to a poisoned release. "It can't reach users" assumes the build pipeline stays honest.

So the standard I settled on: for a security tool, the only acceptable number of open alerts is zero, regardless of where they live.

What did the fix involve?

The direct fixes were unglamorous. I upgraded the dev toolchain, vitest to 4.x, which pulled the transitive esbuild, vite, and brace-expansion advisories along with it. After the upgrades, npm audit reports 0 vulnerabilities, and all 134 tests still pass.

Then I used the momentum to fix things no scanner had flagged yet, starting with peer dependencies. Sether ships wrapOpenAI and wrapAnthropic middlewares, and the lazy way to type those is to depend on the openai and @anthropic-ai/sdk packages. That put two heavyweight SDKs in my dependency story for what is, structurally, a function that wraps another function.

The fix is structural typing. TypeScript does not need the SDK package to describe the shape of a client; it just needs the shape. I declared minimal structural interfaces for the parts the middlewares actually touch, and deleted both peer dependencies. The SDKs are never imported. Anything with the right shape works, including future SDK versions I have never seen.

Structural typing instead of a peer dependency (sketch)
// Before: peerDependencies: { "openai": "^..." } just for a type.

// After: describe only the shape we touch. No import, no dependency.
interface ChatCompletionsLike {
  chat: {
    completions: {
      create: (body: unknown, options?: unknown) => Promise<unknown>;
    };
  };
}

export function wrapOpenAI<T extends ChatCompletionsLike>(client: T): T {
  // intercept create(), redact the messages, restore the reply
  // ...
  return client;
}

That left exactly one declared runtime dependency: libphonenumber-js, which does real work (phone parsing against country metadata) that I should not hand-roll. One dependency is not an accident of the codebase, it is now a design constraint.

What did going further look like?

  • ASCII-only bundles. The identity pack carries multilingual labels (CJK, Cyrillic, Arabic), and non-ASCII characters in a published bundle can trip Trojan-Source-style scanners, which look for invisible or bidirectional characters hiding malicious code. I escaped every non-ASCII label so the published bundles are ASCII-only and the flag cannot fire.
  • ReDoS scan. A redaction library is regex-heavy, and a catastrophically backtracking pattern is a denial-of-service bug waiting for hostile input. I scanned all 161 regex patterns in the codebase: 0 unsafe.
  • Byte-identical verification. Where a release contained no runtime change, I verified the published bundles were byte-identical to the previous version. If only devDependencies moved, users should be able to confirm the artifact did not.

None of this was demanded by the original seven alerts. But an evaluator does not stop at the first screen, and a security tool should survive the second and third look too. The library's trust surface, source on GitHub, MIT license, ~35 KB, one runtime dependency, is the product as much as the detectors are.

What should other maintainers take from this?

  1. Your score is usually your devDeps. Before assuming your shipped code is the problem, trace each alert to its source. Mine were 7 for 7 in dev tooling.
  2. Check the repo-specific alerts view. Org-wide dashboards blur provenance across repositories. The per-repo view tells you what is actually attributed to the package people install.
  3. Runtime and dev alerts communicate differently. A runtime alert says "installing this exposes you". A dev alert says "the maintainer's toolchain has a known issue". Users cannot tell them apart from the score, so fix both.
  4. Structural typing kills peer dependencies. If you depend on an SDK only for its types, describe the shape you touch instead. Your users lose an entire dependency subtree.
  5. A small dependency surface is a feature. "One runtime dependency" fits in a README line and an evaluator's head. It is also simply less code you have to vouch for.
  6. Hold your own tool to its own standard. If your library exists because you do not trust data paths, your package should be the easiest data path in the stack to audit.

This post is part of a series on building a redaction layer you can actually trust. The big picture is in the complete guide to PII redaction for LLM apps, the comparison with heavier tooling is in lightweight Presidio alternatives for TypeScript, and the hardest bug the test suite caught is in the chunk-boundary story.

Frequently asked questions

Do devDependency CVEs affect people who install my package?
Not directly: devDependencies are not installed by your users. The risk is indirect, through your build and release pipeline, and reputational, because scanners and scores do not always separate the two clearly. Fixing them is cheap; explaining them forever is not.
How does dropping a peer dependency help users?
Peer dependencies push an install and version-compatibility burden onto every consumer, and they widen the audit surface. Structural typing gives the same type safety with zero packages: the middleware accepts anything with the right shape, so SDK version bumps stop being your problem.
What is a Trojan-Source flag?
Trojan Source is a class of attack that hides malicious logic using invisible or bidirectional Unicode characters that render differently than they parse. Scanners flag non-ASCII characters in published code as a precaution. Escaping legitimate non-ASCII content (like multilingual labels) keeps the bundle ASCII-only so the flag cannot fire.
How do I check my own package?
Run npm audit locally, then look your package up on socket.dev and open the repo-specific alerts view. Trace every alert to runtime versus dev provenance before deciding anything. Then look at your own dependency tree the way a stranger would: every entry is something you are asking users to trust.
Try it

Sether is free and MIT-licensed: npm i @raeven-co/sether · pip install sether. Or paste some text into the live sandbox and watch the redaction happen.

Keep reading