Supply-chain / lockfile discipline
Commit the lockfile. Install exactly. Type third-party code at the edge.
<!-- hal:authoritative:yaml -->
*Commit the lockfile. Install exactly what it pins. Override when you must. Type third-party code at the edge, never as any.*
§I - Frame
Duha Primary TypeScript session 8. Topics #15: supply-chain / lockfile discipline. Topics #14 typed env shipped 2026-09-22. Bun-centric rows 5-8 and syllabus #11-13 stay deferred. This is tooling and trust depth, not a Bun survey and not a re-ship of typed env.
Done-criteria: Can explain why a committed lockfile is the install contract, name one override/resolution mechanism, and sketch a typed boundary for a third-party dependency.
Primary cite: Cherny Programming TypeScript Ch.11 on the typed/untyped edge. Secondary: prior Web lesson (2026-07-01 lockfile integrity) + Handbook modules / unknown. Bun lockfile docs only as a runtime cite when the project uses Bun.
Home: .
§II - Lockfile as install contract
package.json names ranges. The lockfile names exact trees: resolved versions, registry URLs, and integrity hashes for every node. Commit it. Without that file in source control, every machine and every CI run may resolve a different graph from the same ranges.
The install command makes the contract binding:
# npm: install exactly from the lockfile, or fail
npm ci
# Bun: frozen install from bun.lock (runtime cite)
bun install --frozen-lockfile
# or: bun ci
npm install may rewrite the lockfile when it resolves something new. npm ci refuses that: it deletes node_modules, installs from the lockfile, and errors if package.json and the lockfile disagree. In CI, the strict command is the contract. The lockfile is not a hint; it is the pin.
Integrity fields (for example sha512-… in npm) mean the next install recomputes the tarball hash and aborts on mismatch. That is reproducibility: same inputs, same tree, or a hard fail.
§III - Overrides and resolutions
Sometimes a transitive dependency must be forced to a known-safe version while you wait for upstream. Package managers expose a pin for that:
{
"name": "app",
"dependencies": {
"left-pad-wrapper": "^2.0.0"
},
"overrides": {
"vulnerable-lib": "1.2.3"
}
}
npm uses overrides. Yarn Classic uses resolutions. Bun accepts an overrides field in package.json with the same intent: force one version (or a nested path) across the tree. After you change an override, regenerate and commit the lockfile, then re-run the frozen install. An override without a committed lockfile update is unfinished work.
Use overrides sparingly. Prefer upstream fixes. When you do override, treat the entry as temporary debt with a tracked reason.
§IV - Typed boundary for a third-party dependency
Cherny treats the seam between typed and untyped JavaScript as the place third-party imports go wrong: an untyped module enters as any, and any spreads into every value derived from it. The fix is a single typed edge the rest of the app imports instead of the raw package.
// types/crowd-metrics.d.ts - ambient shape you verified
declare module "crowd-metrics" {
export function track(
event: string,
payload: Record<string, unknown>,
): void
}
// src/metrics.ts - local boundary
import { track as rawTrack } from "crowd-metrics"
export type MetricEvent = {
readonly name: string
readonly fields: Record<string, string | number>
}
export function track(event: MetricEvent): void {
rawTrack(event.name, event.fields)
}
Application code imports track from src/metrics.ts, never from "crowd-metrics" directly. The compiler patrols the shape. If the runtime package later changes, the break surfaces at this one file.
Prefer unknown over any when a value arrives from an untrusted parse (JSON, config blob). Narrow before use:
function asMetricName(raw: unknown): string {
if (typeof raw === "string" && raw.length > 0) return raw
throw new Error("metric name must be a non-empty string")
}
That pairs with session 7's fail-fast config habit: refuse bad input at the edge.
§V - Audit and install reproducibility
After a clean frozen install, check the resolved tree against known advisories:
npm audit --audit-level=high
# or the equivalent Bun/npm audit path your CI already uses
Fail the build on high severity when policy says so. Combine with install-script posture from the prior Web lesson: prefer ignoring dependency lifecycle scripts by default (ignore-scripts / Bun trustedDependencies allowlist) so a new package cannot run code at install time unless you named it.
Checklist for a Primary-depth proof:
- Lockfile committed; CI uses
npm ciorbun install --frozen-lockfile. - One
overrides(or Yarnresolutions) example explained. - One
declare module+ wrapper for an untyped dependency. - Audit runs in CI; install scripts gated.
§VI - Closing
The lockfile is the install contract. Strict CI install makes it binding. Overrides pin a transitive when you must. Cherny's typed/untyped edge becomes a declare module wrapper so third-party code never crosses as any. Bun rows 5-8 and #11-13 stay deferred. Next Primary TypeScript topic follows the syllabus after #15 once a teammate seats it.
Done-criteria: committed lockfile as contract + one override/resolution mechanism + typed third-party boundary sketch.