All Docs
FeaturesCalmony PayUpdated March 15, 2026

Supply Chain Hardening: Enforcing Reproducible Builds on a Payment Platform

Supply Chain Hardening: Enforcing Reproducible Builds on a Payment Platform

Release: v1.0.58 · Security ticket: SEC-26 (High)

Calmony Pay processes card payments and settles funds to a regulated UK bank account. A compromised npm dependency in this environment is not an abstract risk — it could silently exfiltrate payment credentials or tamper with transaction processing. This post explains what changed in v1.0.58, why it matters, and what you need to do.


The Problem

Prior to this release, the repository had no committed package-lock.json, and CI ran npm install on every build. This created four concrete security and reliability gaps:

1. Non-Deterministic Builds

npm install resolves the latest compatible version of every transitive dependency at the time it runs. A package that was safe on Monday can include a malicious patch release by Friday — and your CI, staging, and production environments may each install a different version without any indication that anything changed.

2. Supply Chain Attack Surface

Without a lock file pinning every package in the dependency tree, a hijacked or typosquatted package on the npm registry can silently enter the build between runs. On a payment platform, this is a critical exposure.

3. Dependabot Blocked

GitHub Dependabot cannot open version-bump PRs without a lock file to update. Automated dependency maintenance was effectively disabled.

4. Misleading Audit Results

npm audit run against an unpinned tree may flag — or miss — different vulnerabilities than what is actually running in production.


What Changed

New CI Gate: lock-file-check

A dedicated parallel CI job was added to .github/workflows/ci.yml. It runs alongside the existing build job on every PR and fails with a detailed, actionable message if package-lock.json is not committed:

WHY THIS MATTERS:
  1. Non-deterministic builds — npm install may resolve different versions
     across developer machines, CI, and production.
  2. Supply chain risk — without pinned versions, a hijacked package on
     the npm registry can silently compromise the build.
  3. Dependabot cannot open version-bump PRs without a lock file.
  4. npm audit results may differ from the actual production dependency tree.

FIX:
  Run 'npm install' locally, then commit the generated package-lock.json:
    npm install
    git add package-lock.json
    git commit -m 'chore: add package-lock.json for reproducible builds'
    git push

This gate blocks merges to main from any branch that omits the lock file.

npm ci for Reproducible Installs

The build job's install step was updated to use npm ci whenever package-lock.json is present. Unlike npm install, npm ci:

  • Installs exactly the versions recorded in the lock file — no version resolution
  • Fails loudly if the lock file is out of sync with package.json
  • Never writes back to the lock file, making it safe to cache
  • Is significantly faster in CI because dependency resolution is skipped

A fallback to npm install is retained only for the initial bootstrap run where no lock file exists yet, and it prints a clear warning prompting the developer to commit the generated file.

New .npmrc Configuration

A new .npmrc file was added to the repository root with three settings:

# Always generate and maintain a package-lock.json
package-lock=true

# Enforce SSL certificate validation for all registry requests
strict-ssl=true

# Pin to the public registry — prevents dependency confusion attacks
registry=https://registry.npmjs.org/

package-lock=true prevents any developer or script from accidentally bypassing lock file generation with --no-package-lock.

strict-ssl=true ensures all communication with the npm registry is over verified TLS. This prevents registry responses from being tampered with in transit.

registry=https://registry.npmjs.org/ explicitly pins the registry. This mitigates dependency confusion attacks — a class of supply chain attack where a private package name is registered on the public registry and served instead of the intended internal package.


One-Time Bootstrap Step Required

Because package-lock.json does not yet exist in the repository, the lock-file-check job currently runs with continue-on-error: true. To fully activate this protection, a developer must run the following once:

git checkout fix/sec-26-lock-file-npm-ci   # or main, after merge
npm install                                 # generates package-lock.json
git add package-lock.json
git commit -m "chore: add package-lock.json for reproducible builds (SEC-26)"
git push

Once package-lock.json is committed:

  • All CI runs will use npm ci exclusively
  • continue-on-error: true can be removed from lock-file-check to make the gate strictly enforcing
  • Dependabot will activate and begin opening version-bump PRs automatically
  • npm audit results will reflect the actual production dependency tree

Summary

Before v1.0.58After v1.0.58
npm install (non-deterministic)npm ci (exact-version, reproducible)
No lock file enforcedlock-file-check CI gate blocks merges without lock file
No .npmrc.npmrc enforces package-lock=true, strict-ssl=true, pinned registry
Dependabot inactiveDependabot activates once lock file is committed
Audit results may differ from productionAudit reflects the actual pinned dependency tree

This change closes SEC-26 and significantly reduces the supply chain attack surface of Calmony Pay's build pipeline.