While writing the slides for my Trusted Publishing & Digital Attestations talk, I was looking for a resource similar to Are we PEP 740 yet? – which shows the top 360 most-downloaded packages on PyPI and which ones have been uploaded with attestations.
I couldn’t find one, so obviously the only logical next step is to build my own. Check out the source code at https://github.com/j4ckofalltrades/npm-package-provenance-stats.
Using the Are we PEP 740 yet? site as the reference implementation, I needed a way to fetch the n top most-downloaded packages from npm, extract the relevant details for each package from the npmjs registry API, write the results to a file and display the data in a static site, and configure all this to run on a scheduled interval i.e., every x hours.
The download-counts package, updated with a new version twice per month, just exports a single giant static object whose keys are package names and whose values are monthly download counts.
Using this package we can easily get the top 500 most-downloaded packages (by monthly download count) from npm.
const downloadCounts = require('download-counts');
/** Get top 500 most downloaded packages */
const mostDownloadedPackages = Object.entries(downloadCounts)
.sort(([_, cnt1], [__, cnt2]) => cnt2 - cnt1)
.slice(0, 500)
.map(([name, _]) => name);
Given the package names, the next step is parsing the relevant details from the npm registry at https://registry.npmjs.org.
The endpoint to get details for a specific package is https://registry.npmjs.org/<package>
. The following is an abridged version of the JSON data for the @j4ckofalltrades/steam-webapi-ts
package.
{
"name": "@j4ckofalltrades/steam-webapi-ts",
"dist-tags": {
"latest": "1.2.2"
},
"versions": {
"1.2.2": {
"name": "@j4ckofalltrades/steam-webapi-ts",
"description": "Isomorphic Steam WebAPI wrapper in TypeScript",
"version": "1.2.2",
"repository": {
"type": "git",
"url": "git+https://github.com/j4ckofalltrades/steam-webapi-ts.git"
},
"dist": {
"attestations": {
"url": "https://registry.npmjs.org/-/npm/v1/attestations/@j4ckofalltrades%[email protected]",
"provenance": {
"predicateType": "https://slsa.dev/provenance/v1"
}
}
},
"_npmUser": {
"name": "GitHub Actions",
"email": "[email protected]",
"trustedPublisher": {
"id": "github",
"oidcConfigId": "oidc:4bcac187-e959-43d6-a0b2-995f804750a1"
}
}
}
},
"time": {
"1.2.2": "2025-08-02T16:03:41.470Z"
},
"repository": {
"type": "git",
"url": "git+https://github.com/j4ckofalltrades/steam-webapi-ts.git"
}
}
The data can then be structured and pared down to only the relevant details and written out to a JSON file.
const version = data['dist-tags']?.latest;
const versionData = data.versions?.[version];
const data = {
package: data.name,
version: data['dist-tags']?.latest,
lastUploaded: data.time?.[version] || "",
repositoryUrl: data.repository.url,
// Non-empty if the package was published with attestations
attestationsUrl: versionData.dist?.attestations?.url || "",
// Non-empty if the package was published with a Trusted Publisher
trustedPublisher: versionData._npmUser?.trustedPublisher?.id || "",
};
The static site basically just reads the contents of the resulting JSON file and displays them in a searchable columnar layout. The packages are color coded and have different icons to differentiate package status.
- Green packages with a 🔏 have attestations for their latest release
- Grey packages with a ⏰ come from a supported CI/CD provider but were uploaded before attestations were available
- Yellow packages with a ➖ come from a supported CI/CD provider but have no attestations (yet!)
- Pink packages with a 🚫 come from an unsupported CI/CD provider
- Packages with a 📄 use Trusted Publishing instead of long-lived API tokens
The last step is just a matter of configuring a scheduled workflow that fetches the package names and attestations, then deploys the static site.
---
name: Deploy site
on:
push:
branches:
- main
# Scheduled tasks only run on the main branch.
schedule:
- cron: '0 */4 * * *' # Every 4 hours.
workflow_dispatch:
permissions: {}
jobs:
deploy:
permissions:
contents: read
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Fetch attestations data
run: node fetch-attestations.js
- name: Prepare deployment files
run: |
mkdir site
cp index.html site/
cp results.json site/
- name: Setup Pages
uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0
- name: Upload artifact
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0
with:
path: 'site'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5