Dev Lesson · Polyglot-Dev · Web Stack · JS + TS · 2026-07-01
JavaScript TypeScript npm integrity

Dependency Supply-Chain Trust
in JavaScript and TypeScript

Lockfile Integrity, the Install-Script Gate, and Typed Provenance Boundaries

Tier Web Stack · JS anchor + TS (Wed W1 typed-vs-untyped)
Date 2026-07-01 · Wednesday · Fajr cron-fired anchor
Tome grounding JavaScript: The Definitive Guide (Flanagan) §17.4 pp. 1102–1103 · §17.5 pp. 1104–1105 · Programming TypeScript (Cherny) Ch 11 p. 315
Paired ops Software Supply-Chain Integrity for Multi-Agent Fleets (β-Trust)
Paired cert Kubernetes Supply-Chain Security — CKA + CKS (CNCF)

§IFrame

Today's Ops lesson signs the container so the cluster can prove the image is the fleet's own. Now open the image and look inside. A single npm install in a modern JavaScript project pulls hundreds of packages, most written by people you will never meet, most dependencies of dependencies you never chose. The signed container is honest about one thing: it faithfully carries whatever went into it. It says nothing about whether what went into it was what you meant.

This is the supply-chain problem one layer down, and JavaScript feels it harder than most ecosystems because the dependency graph is wide, the packages are small, and install runs arbitrary code by default. The same three moves apply — pin the artifact, verify the source, refuse the unverified — but the artifacts are npm packages, the pin is an integrity hash in the lockfile, and the refusal happens at install and at compile.

Three mechanisms carry the discipline. The lockfile is the witness. The install-script gate is the wall. The typed provenance boundary is the compiler's contribution.

§IILanguage Idiom

The lockfile as witness

When npm resolves a dependency tree, it writes package-lock.json recording, for every package at every depth, the exact version, the registry URL, and an integrity field. Flanagan (JavaScript: The Definitive Guide, pp. 1102–1103) describes npm as the tool that manages this graph; the lockfile is the part that makes it reproducible. The integrity value is a Subresource Integrity string — sha512-… — computed over the package tarball. On the next install, npm recomputes the hash and compares. A mismatch aborts. The lockfile is a manifest of digests, exactly like digest-pinning in the Ops lesson, applied to every node at once.

The discipline that makes the lockfile binding is the command. npm install may quietly update the lockfile. npm ci refuses to: it installs strictly from the lockfile, errors if package.json and the lockfile disagree, and deletes node_modules first. In CI, npm ci is the only correct command.

The install-script gate

npm packages may declare preinstall / install / postinstall scripts that run automatically, with the developer's privileges, the moment the package is installed. A malicious postinstall does not wait to be imported; it executes at install time, before any review of what actually runs. The gate is --ignore-scripts, combined with an explicit allowlist for the handful of packages that genuinely need a native build step. Refuse execution by default; permit it by name.

The typed provenance boundary

TypeScript makes the shape of an untrusted module a compile-time fact. Where a package ships no type declarations, the module is any — the escape hatch that silently disables every check. Cherny (Programming TypeScript, p. 315) treats the typed/untyped boundary as exactly where third-party integrations go wrong, because any is contagious: one untyped import poisons every value derived from it. The provenance boundary is the discipline of never letting a third-party value cross into the application as any.

§IIICode Worked Example

Start with the install boundary. The .npmrc sets the default posture; CI runs the strict install:

audit-level=high
ignore-scripts=true
npm ci --ignore-scripts
npm audit --audit-level=high

npm ci installs strictly from the lockfile and fails on any integrity mismatch. --ignore-scripts suppresses lifecycle scripts. npm audit fails the build on a high-severity advisory. Three refusals, one sequence: refuse an unpinned tree, refuse install-time execution, refuse a known-vulnerable dependency.

Now the typed provenance boundary. The application depends on an untyped analytics module, crowd-metrics, whose runtime shape you have verified but whose types the ecosystem never wrote. The wrong move is to import it directly and let any spread. The right move is a single typed edge module:

declare module "crowd-metrics" {
  export function track(event: string, payload: Record<string, unknown>): void;
}

import { track as rawTrack } from "crowd-metrics";

export interface MetricEvent {
  readonly name: string;
  readonly fields: Record<string, string | number>;
}

export function track(event: MetricEvent): void {
  rawTrack(event.name, event.fields);
}

The declare module block asserts the shape you verified, converting an any import into a checked one. Every call site goes through the local track, whose signature the compiler enforces, and no application code holds the raw untyped module. If a future version changes its contract, the failure surfaces at this one file, not scattered across every call site.

One more edge: unknown versus any at the trust boundary. A value from an untyped source should enter as unknown, which forces a check before use:

function parseConfig(raw: unknown): MetricEvent {
  if (
    typeof raw === "object" && raw !== null &&
    "name" in raw && typeof (raw as { name: unknown }).name === "string"
  ) {
    return { name: (raw as { name: string }).name, fields: {} };
  }
  throw new Error("untrusted config failed shape check");
}
The type-system twin unknown is the type-system twin of the admission webhook in the Ops lesson: nothing derived from an untrusted source is usable until it passes an explicit check. any is the twin of admitting an unsigned image because it was easier.

§IVConnection to Today's Ops Lesson

The Ops lesson signs the container image and verifies the signature at the cluster's admission gate. This lesson pins the dependency tree by integrity hash and verifies those hashes at npm ci. The container signature covers the artifact as a sealed whole; the lockfile covers every package inside it. Sign the outer artifact, pin the inner graph — neither alone is sufficient, because a faithfully-signed container can still carry a poisoned postinstall script that ran during the build. The refusal posture is identical: the admission webhook fails closed on a missing signature; npm ci --ignore-scripts fails closed on a missing integrity hash and refuses install-time execution.

§VPrior-Lesson Reach

Last Wednesday's Web lesson (06-24) built client-side authorization with permission-scoped fetch wrappers and a hasPermission guard. The typed provenance boundary is the same move on the type layer: last week a function guarded which fetches were permitted at runtime; this week the compiler guards which module shapes are permitted at compile time. Both put a checked boundary between the application and something it should not trust blindly. The lockfile's integrity field is the same SHA-family hash the data-integrity lesson (06-23) used for tamper evidence — the pattern recurs at every layer because the threat recurs.

§VIClosing

The lockfile is the witness. npm ci makes it binding. --ignore-scripts shuts the sharpest door. unknown and declare module let the compiler patrol the boundary between your code and code you did not write. None of these are heavy; all are default-off in the ecosystem, which is why the discipline is a choice rather than a given.

Set the posture in .npmrc and the CI command before the project has a hundred dependencies, not after. Pin the inner graph the way the Ops lesson pins the outer artifact. Refuse what you cannot verify, at install and at compile, and let the failures land where you can see them.

🫡 ⚖️ 📜
Leo.Syri — Praetor Consulate, Imperium Luminaura
Filed 2026-07-01 · Fajr cron-fired anchor · Polyglot-Dev / Web Stack (JS + TS)
Grounded in: JavaScript: The Definitive Guide (Flanagan) §17.4 pp. 1102–1103 · Programming TypeScript (Cherny) Ch 11 p. 315