Last week I shipped version 0.3.0 of SolAudit, a static analyzer for Solana programs. The publish step involved zero secrets. No npm token in GitHub Actions secrets. No token on my laptop. No token anywhere. And anyone can cryptographically verify that the tarball on npm came from a specific commit in my repo, built by a specific workflow run.
This is npm's trusted publishing (OIDC) plus Sigstore provenance, and it took me about 30 minutes to set up. This post is the full walkthrough, including the two gotchas that cost me a failed run each.
The problem with npm tokens
The classic way to publish from CI is:
-
npm token createon your machine - Paste it into GitHub repo secrets as
NPM_TOKEN - Reference it in a workflow
That token is a long-lived credential with full publish rights to your package. If it leaks — a compromised dependency, a malicious Action, a rogue postinstall script in your CI environment — someone else publishes as you, and every npm install your-package pulls their code.
This isn't hypothetical. Token theft and malicious version bumps are how several real npm supply-chain incidents have worked. The ecosystem's answer is to eliminate the standing credential entirely.
What trusted publishing actually does
Instead of a stored token, the GitHub Actions runner proves its identity to npm at publish time using OpenID Connect (OIDC):
- Your workflow runs. GitHub mints a short-lived OIDC token that says: "I am workflow
publish.ymlin repoyou/your-package, running on refmain." -
npm publishexchanges that token with the registry. - npm checks: does this package have a configured trust relationship matching that exact repo, workflow file, and (optionally) environment?
- If yes, npm issues a temporary, publish-scoped credential that expires almost immediately.
There's nothing to steal because there's nothing stored. An attacker who compromises your CI mid-run gets a credential that's scoped to one publish and dead within minutes.
What provenance adds on top
Trusted publishing controls who can publish. Provenance lets everyone else verify what was published.
When you publish from a trusted-publishing workflow, npm automatically generates a Sigstore attestation: a signed, publicly verifiable statement binding the package version to the source repository, the commit SHA, and the workflow run that built it. It shows up as the green "Provenance" badge on the package's npm page, and anyone can verify it locally:
npm audit signatures
That command checks the Sigstore signature on every installed package against the public transparency log. If someone tampered with the tarball or published it from anywhere other than your declared workflow, verification fails.
The setup, step by step
1. Link the package on npm
On npmjs.com, go to your package → Settings → Publishing access → Trusted publishing. Add:
- Repository owner: your GitHub username or org
- Repository name: your repo
-
Workflow filename: the exact filename, e.g.
publish.yml - Environment name: leave blank unless you use GitHub environments
Two things to know:
- The workflow filename must match exactly, including extension.
publish.yml≠publish.yaml. - For a brand-new package that doesn't exist on npm yet, you do one initial manual publish first (
npm publishfrom your machine with 2FA), then configure trusted publishing for all future releases.
2. The workflow
Here's the actual workflow SolAudit uses (.github/workflows/publish.yml):
name: Publish to npm
on:
workflow_dispatch:
permissions:
id-token: write # required for npm trusted publishing (OIDC)
contents: read
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
registry-url: https://registry.npmjs.org
- uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install
- name: Build self-contained CLI bundle
run: bun run build:cli
- name: Ensure npm supports trusted publishing
run: npm install -g npm@latest
- name: Publish solaudit to npm
working-directory: cli
run: npm publish
Three details matter:
-
permissions: id-token: write— without this, the runner can't mint the OIDC token and the exchange fails. -
No
NODE_AUTH_TOKEN, no secrets block at all. Theregistry-urlonsetup-nodeis just for registry config, not auth. -
npm install -g npm@latest— this is the gotcha that cost me a run. Trusted publishing requires a recent npm CLI (11.5.1+). The runner image ships an older one. Upgrade in the workflow or the OIDC exchange silently falls back to looking for a token you don't have.
3. Release
Bump the version, merge to main, then trigger the workflow manually from the Actions tab (that's what workflow_dispatch gives you — a big green "Run workflow" button). I keep publishing manual on purpose: merging code and shipping a public package are two different decisions, and I want a deliberate click between them.
4. Verify
After the run goes green:
npm view solaudit@0.3.0
npm audit signatures # in any project that installs it
And the package page on npmjs.com shows the provenance badge, linking to the public Sigstore transparency log entry with the source repo and commit.
What this looks like in practice
SolAudit 0.3.0 shipped this way this week. The full chain is inspectable:
- The commit that bumped the version
- The Actions run that published it
- The npm page with the provenance badge
If I ever lose my laptop, get phished, or a dependency in my CI turns malicious, the blast radius for "publish as me" is: nothing. There is no credential to take.
The honest limitations
- npm only, for now. Trusted publishing exists on PyPI and RubyGems too, but each registry implements it separately.
-
Your workflow file name is part of the trust config. Rename
publish.ymland publishing breaks until you update npm's side. - It protects the publish step, not your code. If a malicious dependency lands in your actual build, provenance will faithfully attest to compromised code. You still need lockfile hygiene and dependency review.
But as a 30-minute change that deletes an entire credential class from your threat model, it's the best security-per-minute trade I've made on this project.
SolAudit is a free, open-source static analyzer for Anchor/Solana programs — 94% precision on its public benchmark, with documented limits. GitHub · npm
Top comments (0)