Reproducible-build verification
July 19, 2026 · View on GitHub
Guarantee: the npm tarball published for any @quantakrypto/* version can be
re-created, byte for byte, from the tagged source tree. Anyone can therefore
audit that the registry serves exactly what is in git — no injected, altered, or
extra files between git and npm i.
This complements the Sigstore build provenance attached at publish time: provenance proves where the artifact was built; reproducibility proves that the source alone determines it.
How to verify
The workspaces must be built first (npm run build — its postbuild step pins
bin-file modes, see below).
npm run build
# Determinism (network-free): pack each workspace, perturb the known
# nondeterminism source, re-normalize, re-pack, and assert the SHA-512
# integrity is unchanged. This is the CI gate (`reproducible` job).
npm run repro:check
# Release verification (needs network + a published version): additionally
# diff each from-source pack integrity against the integrity npm recorded
# for that exact version.
npm run repro:npm
repro:npm output for the current release:
reproducible-build verification (vs npm)
package version deterministic vs-npm
--------------------------------------------------------------
@quantakrypto/agent 0.4.4 ✅ yes ✅ match
@quantakrypto/core 0.4.4 ✅ yes ✅ match
@quantakrypto/mcp 0.4.4 ✅ yes ✅ match
@quantakrypto/qprobe 0.4.4 ✅ yes ✅ match
@quantakrypto/qscan 0.4.4 ✅ yes ✅ match
@quantakrypto/sieve 0.4.4 ✅ yes ✅ match
All six published packages reproduce from source at v0.4.4.
Why it works — and the one trap we had to close
npm tarballs are already mostly deterministic: npm pack normalizes entry
order, sets a fixed mtime (1985-10-26), and drops owner/group. The build output
itself is deterministic because tsc emits the same JavaScript for the same
source, and the packages carry zero runtime dependencies (ADR-0001),
so there is no dependency tree to drift.
The one variance we found (and fixed) was bin file mode:
tsc -boverwrites a file's contents but preserves its existing mode.- A
dist/cli.jsthat once picked up an exec bit (0755) during local dev keeps it on every rebuild; a fresh CI checkout creates the same file at0644. npm packrecords the on-disk mode verbatim in the tar header, so identical source packed to two different tarballs — different integrity.
This affected exactly the three packages with a bin (qscan, mcp, sieve);
core, agent, and qprobe (no bin) always reproduced.
Fix: a root postbuild step, scripts/normalize-bin-modes.mjs,
pins every workspace bin target to 0644 after each build. 0644 is safe
because npm re-creates the exec bit on the installed bin shim from the bin
field at install time — the mode inside the tarball never affects whether the
CLI runs. Pinning explicitly also makes the result umask-independent.
The reproducible CI job runs repro:check on
every push and PR, so a new nondeterminism source (a generated file with an
embedded timestamp, a non-pinned mode, a stray artifact) fails the build instead
of silently shipping.
Scope
- Covered: the six published npm packages.
- Not covered here:
@quantakrypto/actionisprivateand ships its committeddist/as a GitHub Action (kept current by theaction dist is freshgate), not an npm tarball — so it is out of scope for tarball reproducibility. - Toolchain assumption: a matching major Node/npm (the releases are built on Node 20). A different npm major could in principle change tarball normalization; the CI gate pins Node 20, matching the release workflow.