Skip to content
academia.sh

Lesson 09 / 14

Dependency Risk

Measuring the transitive dependency mass, what code running at install time means, integrity verification, locking discipline, package selection criteria, and managing abandoned packages.

Contents

The previous lessons treated dependencies as a technical resolution problem: which version gets chosen, where it gets installed, who sees it. This lesson asks a different question — how much is the installed code as a whole trusted, and how is that trust narrowed?

The question is a management one, but the answer is entirely technical, starting with a measurable quantity: how many packages a project actually runs.

The Transitive Mass

The packages written in the manifest are a small fraction of the installed packages. Every dependency brings its own dependencies; this relationship continues until it closes. The entire set is called the dependency closure.

The closure is computed with a breadth-first search over the manifests:

// file: closure.mjs -- reads registry/<name>/package.json files
import { readFile } from 'node:fs/promises';

async function manifest(name) {
  return JSON.parse(await readFile(`registry/${name}/package.json`, 'utf8'));
}

const root = await manifest('text-measurer');
const direct = Object.keys(root.dependencies);
const closure = new Map();
const queue = [...direct];

while (queue.length > 0) {
  const name = queue.shift();
  if (closure.has(name)) continue;
  const pkg = await manifest(name);
  closure.set(name, pkg.version);
  queue.push(...Object.keys(pkg.dependencies ?? {}));
}

console.log('direct dependencies:', direct.length, '->', direct.join(', '));
console.log('transitive closure:', closure.size);
for (const [name, version] of closure) console.log(`  ${name}@${version}`);

In a small sample registry — a six-package directory — the result is this:

$ node closure.mjs
direct dependencies: 2 -> format-helper, string-tool
transitive closure: 5
  [email protected]
  [email protected]
  [email protected]
  [email protected]
  [email protected]

Two deliberate choices brought in five packages. The ratio is not due to the example’s small size: there is a multiplier effect at every level, and in real projects a double-digit count of direct dependencies can produce four-digit closures.

What this number means: the code of every package in the closure runs inside your program, with your program’s privileges. You made the decision for the packages you chose directly; for the rest, you trusted indirectly through the packages that chose them. Closure size is the measure of that indirect trust.

Code That Runs at Install Time

The trust boundary does not begin the moment you call the library. The manifest file can define scripts to be run during installation:

{
  "name": "sample-package",
  "version": "1.0.0",
  "scripts": {
    "postinstall": "node setup/prepare.mjs"
  }
}

This field exists for legitimate work: local compilation, cache preparation, compatibility checks. The consequence, though — installing a package is equivalent to running that package’s code. Without ever importing the package, just by running the install command, execution rights go to the install script of every package in the closure. Scripts run with the user’s privileges and can reach the filesystem and the network.

The defense is three steps:

Turn off install scripts by default. Installation tools offer this as an option. With them off, packages whose installation fails are examined one by one, and an exception is granted only if genuinely necessary.

Install in an isolated environment. Installing not on the developer’s machine but in a limited-privilege, discard-afterward environment narrows the area a script can reach — the principle of least privilege from the Introduction to Linux course applies directly here.

Review changes that update the lock file. A changed lock line means the archive to be installed has changed, and should be reviewed as carefully as source code.

Integrity Verification

The third piece of information the lock file records — the integrity digest — answers a specific question: is the downloaded archive identical to the archive at the moment it was locked?

The digest is a cryptographic checksum computed over the archive’s bytes. Changing a single byte changes the digest entirely:

// file: digest.mjs
import { createHash } from 'node:crypto';
import { readFile } from 'node:fs/promises';

const content = await readFile(process.argv[2]);
const digest = createHash('sha512').update(content).digest('base64');
console.log(`bytes: ${content.length}`);
console.log(`sha512-${digest.slice(0, 44)}...`);
// file: package.mjs
export function decimal(value, digits = 2) {
  return value.toFixed(digits);
}
$ node digest.mjs package.mjs
bytes: 100
sha512-+J2oBbbq0tFWSv0c1SoceVACG6lMhXEwdJ/46hX89Uru...

With a single character in the file — the default digit count — changed to 3 and the same command run again:

$ node digest.mjs package.mjs
bytes: 100
sha512-AuXc8NVQcn4vfnfQvKDNnEHicRcsEPDkRoSygM0ma2DA...

The size stayed the same, the digest changed completely. The script prints the digest truncated; the real digest is longer and is stored in full in the lock file.

The surface it closes is clear: the same name and version served with different content. The surface it does not close is equally clear: publishing a new version changes the digest anyway. The integrity digest proves the content has not changed; it does not prove the content is safe.

Upgrade Discipline

In a project with a lock file, installation installs exactly the tree written in the lock. This turns upgrading from an accident into a deliberate process. A working routine includes these steps:

  1. An upgrade is made as a separate change; it is not merged into the same commit as another edit.
  2. The changed lock lines are reviewed: which packages, from which version to which.
  3. The tests are run.
  4. The reason for the upgrade is recorded.

Delaying the upgrade is a risk too: staying on an old version means continuing to carry fixed defects. The balance is regular, small upgrades; accumulated upgrades done all at once make review impractical and, on failure, make it hard to tell which package is responsible.

Package Selection Criteria

Adding a dependency is a decision, and it can be evaluated with criteria that do not depend on time:

Closure size. How many packages are in the package’s own dependency closure? One that brings a large closure for a small function is expensive.

Interface surface. How many of the package’s functions will actually be used? A large library pulled in for a single function makes both tree shaking and review harder.

Replaceability. What replaces the package if it is dropped? One with a narrow, standard interface is replaceable; one that imposes its own concepts on the project is not.

Maintenance signals. Are reported defects answered, are there tests, how many contributors? Single-person maintenance ends when that person loses interest.

License. Do the usage and distribution terms fit the project?

The cost of writing it yourself. If the function can be written in a few lines, the dependency’s installation, update, and trust cost may not be worth it. The same measure applies in reverse too: in high-detail domains like cryptography, date computation, or text encoding, writing your own implementation is riskier than using a mature package.

Name Verification and Abandoned Packages

Package names are unique within a registry, but similarity is unrestricted. A name with a letter added to a popular package, or a hyphen moved, can be picked unnoticed when typed by hand. The defense is spelling discipline: the name is copied from documentation or the manifest, never typed from memory; after adding it, the manifest line and the package’s source repository link are checked.

An abandoned package is a separate problem, diagnosed by observation: defects have piled up, no releases are being made, its own transitive dependencies are unmaintained too. The options:

Replace it. Swap in a maintained package doing the same job. Workable if the interface surface is narrow.

Vendor it. Copy the source into the project and take over maintenance. Suitable if the code is small and can be considered frozen; its closure has to be carried along too.

Take over maintenance. Fork it and publish under your own name. Responsibility increases; in exchange, control is entirely yours.

In all three, the first step is the same: asking again whether the package is really needed. The safest dependency is the one removed.

Summary

  • The dependency closure is far larger than the packages chosen directly; every package in the closure runs with the program’s privileges.
  • Install scripts run code without the package ever being imported; the defenses are turning scripts off, installing in an isolated environment, and reviewing lock changes.
  • The integrity digest catches the same name and version being served with different content; it does not prove the content is safe.
  • An upgrade should be a separate, regular action; delaying it is a risk too.
  • Package selection is evaluated by closure size, interface surface, replaceability, maintenance signals, and license.
  • There are three paths for an abandoned package — replace it, vendor it, take over maintenance — and before all three, the package’s necessity is questioned.

Next Step

Dependencies have been resolved, locked, and audited. What remains is how these packages and your own modules get delivered to the target environment. The next topic covers the class of tools that take the module graph as input and produce a runnable output; its first lesson shows why this production is needed and how it works with a running example.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close