npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mojaloop/license-scanner-tool

v1.1.1

Published

SBOM-first license compliance gate for Mojaloop (Syft CycloneDX + curated SPDX allowlist).

Readme

@mojaloop/license-scanner-tool

SBOM-first license-compliance gate for Mojaloop. It evaluates a CycloneDX SBOM, normalises component licences to canonical SPDX, and fails if any npm dependency's licence is not on a curated allowlist.

This replaces the legacy bash tool (Syft-less, eval/docker cp based, depended on an unmaintained license-checker fork), which is removed in this release. Consumers must move to mojaloop/build@>=2.0.0 (which runs this package) — CI on the old orb path (git clone … && make build) will fail by design. See Legacy tool.

Install / run

# one-time: install Syft (only needed when scanning a directory; see "Where the SBOM comes from")
brew install syft

# scan the current repo (the tool builds the SBOM with Syft, then gates it)
npx @mojaloop/license-scanner-tool .

# gate a pre-generated CycloneDX SBOM (no Syft needed)
npx @mojaloop/license-scanner-tool sbom.cdx.json

# non-blocking: report violations but exit 0 (used during migration windows)
npx @mojaloop/license-scanner-tool --warn .

Exit codes: 0 pass (or --warn), 1 violations, 2 usage/tool error.

Using it in another repo

Four ways, best-first. (Prereqs: the package must be published; Syft on PATH for directory scans — not needed when you pass a CycloneDX SBOM; Node ≥ 20.)

1. Through the orb — automatic (recommended for CI)

If the repo's CI uses mojaloop/build@>=2.0.0, the license gate runs automatically — the orb's license_scan / audit_licenses jobs already do generate_sbom (Syft) → license_gate (npx @mojaloop/license-scanner-tool). Nothing to wire per repo — just be on orb 2.0.0.

2. CLI / npm script (local or custom CI)

npx @mojaloop/license-scanner-tool .            # scan source (Syft builds the SBOM)
npx @mojaloop/license-scanner-tool sbom.cdx.json # gate a pre-built SBOM (no Syft)
// package.json
"scripts": { "license:check": "npx @mojaloop/license-scanner-tool ." }

3. Git pre-push hook (local enforcement)

With husky:

printf 'npx --yes @mojaloop/license-scanner-tool@^0 .\n' > .husky/pre-push

(or copy hooks/pre-push). Bypass once with LICENSE_CHECK_SKIP=1 git push.

4. Programmatically (library)

const { evaluate } = require('@mojaloop/license-scanner-tool') // src/gate.js
const sbom = require('./sbom.cdx.json')                        // a CycloneDX SBOM

const { npm, violations } = evaluate(sbom)   // optional 2nd arg: custom policy
if (violations.length) { console.error(violations.join('\n')); process.exit(1) }
console.log(`OK — ${npm} npm components clean`)

evaluate(sbom, policy?, now?){ npm, violations }.

Where the SBOM comes from

The tool accepts either a CycloneDX SBOM file or a path to scan, so it works the same in CI and locally — but who builds the SBOM differs:

| Context | Who runs Syft | What the tool receives | |---|---|---| | CI (mojaloop/build orb) | the orb (generate_sbom) | a pre-built sbom.cdx.json — the tool consumes it, does not re-run Syft | | Local / git hook / standalone | the tool (cli.js) | a directory (e.g. .) — the tool runs syft dir:. itself, then gates |

Why the orb builds it in CI (deliberate, "one SBOM, many gates"):

  1. The orb generates the SBOM once and feeds the same artifact to both the vulnerability scan (grype_scan) and this license gate — no double Syft runs.
  2. license_scan gates a Docker image, not a source tree; the orb's generate_sbom handles images and directories uniformly. The tool's own generation path only scans source directories.

So in CI: orb creates the SBOM → tool gates it. Locally: tool creates and gates in one step. Either way the verdict is identical because both feed the same policy (data.json).

How a scan is evaluated

  1. Scope — only pkg:npm/ components are gated. Syft also catalogues workflow YAMLs, lockfiles and binaries; those are ignored.
  2. Normalise — a component's licence is resolved to an SPDX id/expression only via the curated alias map (data.json → aliases). Proprietary markers like UNLICENSED are never "corrected". Anything unresolved is undetermined. (src/normalize.js)
  3. Gate — the SPDX id/expression is checked against the allowlist; per-package exceptions (with expiry) waive findings. (src/gate.js)

Handling a license-scan failure

When the gate fails you get one or both of these, listed per package (all at once — the scan does not stop at the first):

DISALLOWED: npm package [email protected] uses licence "GPL-3.0-only", which is not on the allowlist (data.json -> allowed).
UNDETERMINED: npm package [email protected] declares licence "Free for all", which is not a recognised SPDX id. Verify the real licence, then add a vetted alias or exception.

Do not "make it pass" blindly. A failure is a question, not a bug. Resolve each finding deliberately; every policy change is reviewed via PR and affects all Mojaloop repos that use this tool.

Step 1 — verify the real licence

Before changing anything, find out what the package is actually licensed under:

  • the package's repository LICENSE file (most authoritative),
  • its package.json license field,
  • its npm page (https://www.npmjs.com/package/<name>),
  • the SBOM entry — the message quotes the declared string (e.g. "Free for all") so you can see exactly what the package put in its metadata.

Step 2 — pick the right resolution

| Finding | What you confirmed | Action | |---|---|---| | DISALLOWED | A valid, permissive SPDX licence missing from the allowlist (e.g. MIT-0, 0BSD) | Add the SPDX id to data.json → allowed. | | DISALLOWED | A copyleft / incompatible licence (e.g. GPL-3.0-only, AGPL) | The gate is correct — do not allowlist it. Remove or replace the dependency. | | UNDETERMINED | The package declares a valid licence as free text (e.g. "Apache 2.0", "BSD License") | Add a vetted entry to data.json → aliases: "<declared string>": "<SPDX id>". | | UNDETERMINED | The package has no machine-readable licence, but you confirmed a real, acceptable licence elsewhere (e.g. MIT on GitHub, missing from package.json) | Add a dated exception to data.json → exceptions (org-wide), or — if it's specific to your repo's dependency graph — a project-local exception (see below), no central release needed. | | UNDETERMINED | You cannot determine or trust the licence | Treat as a blocker — replace the dependency. |

Step 3 — make the change (examples)

Allow a permissive licence — add the SPDX id:

// data.json → licenses.allowed
"allowed": [ "...", "MIT-0" ]

Add a vetted alias — map a verified free-text string to an SPDX id:

// data.json → licenses.aliases
"aliases": {
  "Apache 2.0": "Apache-2.0"   // declared string -> canonical SPDX id
}

Add an exception — waive a specific package@version, with a reason and an expiry so it is revisited:

// data.json → licenses.exceptions
"exceptions": {
  "[email protected]": { "reason": "MIT on GitHub, not in package.json", "expires": "2027-01-01" }
}

Add a project-local exception — for an UNDETERMINED finding that's specific to your repo's dependency graph (e.g. a transitive dep with empty license metadata), waive it in your own repo, reviewed in your own PR, without a central tool/orb release. Create .license-scanner.json at the repo root:

// .license-scanner.json (repo root) — auto-discovered by the CLI/orb
{
  "exceptions": {
    "[email protected]": { "reason": "MIT per LICENSE file; empty license field in package.json (via mysql2)", "expires": "2027-01-01" }
  }
}

The gate auto-discovers this file (or pass --exceptions <path>). To preserve the central gate's integrity, project-local exceptions:

  • waive UNDETERMINED findings only — a DISALLOWED (known copyleft/proprietary) licence still fails and requires a central decision;
  • may define exceptions only — never allowed/aliases (those stay central; the tool errors if present);
  • must carry a non-empty reason and a YYYY-MM-DD expires;
  • are overridden by a central data.json exception on the same name@version (bundled wins).

Only the repo-root file (or an explicit --exceptions path) is read — nested/dependency-provided files are ignored, so a transitive dep cannot ship its own waiver.

Hard rules

  • Never add UNLICENSED (or any proprietary marker) to allowed or aliases — npm's UNLICENSED means no rights granted; the validator rejects it.
  • Never blanket-except an undetermined finding — verify the actual licence first.
  • Exceptions must carry an expires date (the validator warns on expired ones).
  • Prefer fixing the dependency or adding a precise alias over a broad exception.

Step 4 — validate and re-run locally

npm run validate                          # SPDX-lint data.json (fails on a bad id/alias/expiry)
npx @mojaloop/license-scanner-tool .      # re-run the gate; should now pass

Open a PR with the data.json change. Once released, every consuming repo picks it up via the package version.

Unblocking a build while you reconcile

During a migration window (or while a fix is in review) a repo can run the gate non-blocking so it reports without failing:

npx @mojaloop/license-scanner-tool --warn .

In the orb, this is the license_gate warn: true parameter. Use sparingly — a warn-mode gate does not actually enforce compliance.


Editing the policy (data.json)

| Field | Purpose | |---|---| | allowed | Permitted canonical SPDX ids / expressions. | | aliases | Reviewed free-text → SPDX mappings (reject-by-default). | | exceptions | Per-package waivers (name@version → reason + expires). |

All entries are SPDX-validated at build time (npm run validate).

Local pre-flight & git hook

Enforce before push (repo with husky):

printf 'npx --yes @mojaloop/license-scanner-tool@^1 .\n' > .husky/pre-push

Or copy hooks/pre-push to .git/hooks/pre-push. Bypass once with LICENSE_CHECK_SKIP=1 git push.

Development

The Node version is pinned in .nvmrc (and used by CI via nvm, matching the orb convention).

nvm use            # node version from .nvmrc
npm ci
npm run validate   # SPDX-lint data.json
npm test           # gate unit tests (node --test)
node src/cli.js .  # run the gate locally

CI / publishing

This repo uses a standalone CircleCI pipeline (.circleci/config.yml) that publishes to npm on a vX.Y.Z tag. It deliberately does not use the mojaloop/build orb, because this package is a dependency of that orb's license gate — using the orb here would be circular.

Legacy tool (removed)

The previous make build / config.toml bash tool has been removed. It relied on an unmaintained license-checker fork and used eval / docker cp — the supply-chain and command-injection risks this package was created to eliminate.

The Makefile is now a stub that exits non-zero with upgrade guidance. If your CI still uses the old mojaloop/build license path (git clone … && make build), it will fail by design — upgrade to mojaloop/build@>=2.0.0, which runs npx @mojaloop/license-scanner-tool.