Back to publishing & release Automate semantic versioning Publish to the npm registry Harden the supply chain

Package Publishing & Release Engineering

Take a built package from a working dist/ folder to a versioned, signed, reproducible release on the npm registry. This guide is for library authors and platform teams who need publishing to be deterministic, auditable, and safe enough to run unattended in CI — not a manual npm publish from a laptop.

Publishing is the last mile where a healthy package becomes a liability if done carelessly. A leaked token, a missing files allowlist, an unsigned tarball, or a botched version bump all ship to thousands of installs before anyone notices. Release engineering treats that last mile as code: every step gated, versioned, and traceable back to a Git commit.

How This Section Is Organized

Three connected topics carry the full publishing lifecycle. Semantic Versioning and Release Automation governs how you choose the next version number and bump it automatically from commit history, including dist-tags and prerelease channels. npm Registry Publishing Workflows covers the mechanics of the publish itself — scoped packages, authentication, the 403 errors that block first-time publishes, and provenance attestation. Supply-Chain Security Hardening closes the loop with audit thresholds, lockfile linting, and SLSA build provenance so that what you publish is exactly what you built. Read them in that order if you are standing up a release pipeline from scratch; jump straight to the relevant one if you are debugging a single broken stage. When those packages are internal rather than public, Private Registries and Access Control covers hosting them safely with scoped authentication and provenance.

Release pipeline A five-stage pipeline flows from build to version to pack to publish to provenance attestation. build emit dist/ version semver bump pack files allowlist publish token + OTP provenance attest From artifact to attested release each stage gated in CI, traceable to a Git commit version, pack and publish run from one CI job — no laptop
The release pipeline: a built artifact is versioned, packed against an allowlist, published with a scoped token, and attested with provenance.

Version Strategy: Semver, Prerelease Channels, and Dist-Tags

A published version number is a contract. Consumers pin ranges like ^2.3.0 against the promises encoded in semantic versioning: patch releases fix bugs, minor releases add backward-compatible features, and major releases may break the public API. Violate that contract once — ship a breaking change in a minor bump — and you break every downstream npm ci that resolves your package.

Version + channel strategy Semver bump feeds a dist-tag channel for consumers. classify change major / minor / patch bump version semver rules dist-tag latest / next / beta
Version choice and dist-tag decide who receives a release.

Decide the bump from the change, not from how big it feels. A renamed export, a removed function, a stricter type signature, or a raised engines floor are all breaking. New optional parameters and new exports are minor. Internal refactors and bug fixes are patch.

# Inspect what npm thinks the next versions would be
npm version --help
# Manual bumps create a commit + git tag by default
npm version patch   # 2.3.0 -> 2.3.1
npm version minor   # 2.3.1 -> 2.4.0
npm version major   # 2.4.0 -> 3.0.0

Prerelease versions let you ship a release candidate without disturbing the stable line. A version like 3.0.0-beta.2 sorts below 3.0.0, so range matchers such as ^2.0.0 never resolve to it, and a fresh npm install your-pkg never picks it up. Pair every prerelease with a dist-tag so consumers opt in explicitly:

# Publish a prerelease under the "next" channel, not "latest"
npm version 3.0.0-beta.0
npm publish --tag next

# Consumers opt in deliberately
npm install your-pkg@next
npm install your-pkg@beta

The latest dist-tag is what a bare npm install your-pkg installs. Never publish a prerelease without --tag, or it silently becomes latest and every consumer pulls an unstable build. The full set of bumping rules, ranges, and automated changelog generation is covered in Semantic Versioning and Release Automation.

The version number is a contract with consumers, and the discipline is to derive the bump from the change, not from how significant it feels. A removed export, a renamed function, a stricter type signature, or a raised engines floor are all breaking and demand a major, no matter how small the diff; a new optional parameter or a new export is a minor; a bug fix with no API change is a patch. Violate the contract once — ship a breaking change in a minor — and every downstream npm ci that resolves your package under a caret range breaks without warning.

Dist-tags and prerelease channels let you decouple publishing a version from promoting it to the default latest. Publishing under next or beta makes a version installable only by consumers who opt in (npm install pkg@next), so you can validate a release in the wild before moving the latest tag. Prerelease identifiers (2.0.0-rc.1) sort below their stable counterpart, so a caret range never accidentally resolves to a release candidate — which is exactly what you want when shipping a risky major behind a channel.

Ranges are the other half of the contract, expressing how much drift a consumer accepts. A caret (^2.3.0) admits any compatible 2.x, a tilde (~2.3.0) admits only patch updates, and an exact pin accepts nothing new. Publishing libraries should keep their own dependency ranges reasonably wide so consumers can deduplicate shared packages, while applications tend to pin more tightly and rely on the lockfile for exactness.

Prerelease channels are the safety valve for risky releases, letting a version reach real users without touching the default latest tag that most installs resolve. Publishing 3.0.0-beta.1 under a beta dist-tag means only consumers who explicitly opt in with npm install pkg@beta receive it, so you gather real-world feedback on a breaking major while every existing consumer stays on the stable line. Once the release proves out, promoting it is a single npm dist-tag add pkg@3.0.0 latest — the version already exists and is verified, you are only moving which one is the default.

Coordinating versions across a set of related packages is its own discipline. Independently-versioned packages each move on their own semver cadence, which is precise but produces many small releases; fixed or lockstep versioning moves them together, which is simpler to reason about but bumps packages that did not change. Most monorepos land on independent versioning with a tool that computes each package's next version from the changes that touched it, so a consumer of one package is not forced to upgrade because an unrelated sibling shipped a major.

The Publish Lifecycle: Hooks, the Files Allowlist, and Exports

npm publish runs a fixed sequence of lifecycle scripts. Understanding the order prevents the classic mistake of shipping uncompiled source or, worse, never running the build at all.

Publish lifecycle prepublishOnly through pack, exports check and registry upload. prepublishOnly build + validate pack files allowlist only dist ships exports check publint / attw npm publish registry upload
Each publish hook is a gate before the tarball leaves your machine.
{
  "name": "@scope/widget",
  "version": "2.3.1",
  "scripts": {
    "build": "tsup",
    "test": "vitest run",
    "prepublishOnly": "npm run test && npm run build"
  }
}

prepublishOnly runs only on npm publish (not on local npm install), which makes it the correct gate for tests and the production build. The older prepare script runs on both publish and on npm install from a Git URL, so reserve it for steps that must also run for Git-dependency consumers. If your dist/ is committed, you can skip the build hook — but committing build output is a maintenance trap; prefer building in prepublishOnly or in CI.

What actually ends up in the tarball is the highest-leverage decision in the whole lifecycle. Use the files allowlist — an explicit include list — rather than .npmignore, which is an error-prone denylist. With files, anything you forget to mention is simply excluded; with .npmignore, anything you forget to exclude leaks into the published package, including .env files, test fixtures, and internal scripts.

{
  "files": ["dist", "README.md"],
  "main": "./dist/index.cjs",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    },
    "./package.json": "./package.json"
  }
}

Note that package.json, README, LICENSE, and the file pointed to by main are always included regardless of files. The exports map is the public surface of the package — it both routes consumers to the right artifact and blocks access to anything not listed, so deep imports into your internals fail loudly. Getting exports, main, and files consistent is the same problem as configuring dual modules; the field-level details live in Understanding package.json Fields, and the routing rules for shipping both formats are in ESM and CJS Interoperability.

Always dry-run the pack before publishing to see the exact file list and tarball size:

# Show every file that would ship, plus integrity hash and size
npm pack --dry-run
# Produces a real tarball you can inspect with `tar -tf`
npm pack

Publishing is a sequence of gates, each an opportunity to stop a bad release before it becomes immutable. prepublishOnly runs the full build and validation so a broken artifact cannot be published from a laptop; the pack step assembles the tarball from the files allowlist so only intended output ships; an exports check with publint and @arethetypeswrong/cli confirms the map resolves as consumers will; and only then does the tarball upload. Running npm pack --dry-run in review makes the tarball contents visible in the pull request, so a leaked source directory or a missing dist is caught by a human before publish.

The immutability of published versions is the reason these gates matter. Once a version is on the registry it can never be overwritten — consumers and lockfiles pin it, and integrity hashes verify it never changes — so a mistake ships to everyone who installs before you notice. Recovery is always forward: publish a fixed higher version and, if the bad one is dangerous, deprecate it with a message pointing at the fix. There is no edit, only a new release, which is why the pre-publish gates are worth the friction.

The files allowlist deserves special attention because it is the difference between a lean, professional package and a bloated one that leaks internals. Without it, the tarball ships source, tests, configs, and CI files — larger for every consumer to download and a disclosure of implementation detail. An allowlist naming only the dist directory (npm always adds the README and LICENSE) keeps the published surface to exactly the built output, and npm pack --dry-run in review makes that surface visible so a reviewer catches a leak before it is immutable.

Registry Authentication: Tokens, Granular Access, and 2FA

Authentication is where most first publishes fail with a 403 or 404. The registry needs to know who you are and that you are allowed to write to that package name.

Auth layers The layered controls guarding a publish. granular token scoped to package 2FA / OIDC human or CI identity provenance signed build origin access level public / restricted
Publishing safely means stacking token scope, 2FA and provenance.

For automation, never use a classic publish token that grants account-wide write access. Use a granular access token scoped to the specific packages or scope it may publish, with a short expiry and an IP allowlist where possible. Store it as a CI secret and expose it through .npmrc at build time only:

# .npmrc — written in CI, never committed with a real token
//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}
@scope:registry=https://registry.npmjs.org/
# In CI, the token comes from a secret, not the file
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > ~/.npmrc
npm whoami   # confirms the token authenticates before you publish

Two-factor authentication adds a one-time password (OTP) requirement on publish. Interactive publishes prompt for it; automation cannot answer a prompt, so you either set the package's 2FA mode to "authorization only" (2FA for login and settings, but automation tokens may publish without an OTP) or pass --otp from a provisioning step. The cleanest path is a granular automation token plus the registry's "automation" 2FA exemption, which keeps humans on full 2FA while letting CI publish unattended. The specific failure modes — wrong scope, missing access, expired token — are diagnosed in Fixing npm publish 403 Forbidden Errors.

Rotating a publish token is a two-sided operation that breaks pipelines when the ordering is wrong: revoke the registry side before the secret store is updated and every release in that window fails for lack of a valid credential. The safe order is always create, deploy, verify, then revoke — mint the new token, update the CI secret without revoking the old one, prove it with a dry-run publish on the real pipeline, and only then revoke the old token. Alerting on token expiry ahead of time turns rotation from a 3 a.m. incident into scheduled maintenance.

Scoping is what limits the damage when a credential does leak, and leaks happen. A classic token with organization-wide publish rights can republish or deprecate everything you own; a granular token scoped to a single package can touch only that package. Prefer the narrowest scope the job needs, set an expiry so a forgotten token does not live forever, and read it from a secret store rather than embedding it — so a leaked pipeline log or a compromised laptop exposes a short-lived, single-package credential instead of the keys to the whole namespace.

Provenance and Supply-Chain Integrity

A published tarball, by default, is an unsigned blob with no verifiable link back to the source that produced it. Provenance closes that gap. When you publish with --provenance from a supported CI environment, npm generates a signed attestation binding the tarball to the exact Git commit, repository, and workflow run that built it, recorded in a public transparency log.

Provenance chain CI OIDC signs a provenance attestation the registry verifies. CI OIDC identity trusted runner build attestation source + commit registry verifies provenance badge
Provenance ties a published tarball back to the exact CI build.
# Requires OIDC-capable CI (e.g. GitHub Actions) and a public package
npm publish --provenance --access public

Provenance is meaningful only when the build is itself trustworthy: the published bytes must come from the attested commit and nothing else. That depends on a frozen, verified dependency graph at build time — installs that reproduce exactly from the committed lockfile, as covered in Lockfile Management Strategies, so a tampered transitive dependency cannot slip into the artifact. The deeper hardening — audit gates, lockfile linting, and SLSA build levels — is the subject of Supply-Chain Security Hardening, and the CI wiring specifically for npm attestations is in Setting Up npm Provenance with GitHub Actions.

Verifying provenance is the consumer side of the same guarantee, and it scales through automation rather than manual checks. Running npm audit signatures in CI walks the installed tree and verifies registry signatures and provenance attestations, failing the build when a package loses its expected signature or its attestation does not verify. For security-critical dependencies, prefer ones that publish provenance and treat a missing attestation as a reason to pin and review — participating in the chain of trust as a consumer, not only as a publisher.

Release Automation in CI

Manual releases drift: someone forgets to bump, forgets to tag, publishes from a dirty tree, or ships from a stale dist/. Automating the release into CI makes every publish identical and removes the laptop — and its long-lived token — from the loop entirely.

Automated release Merged commits drive versioning, changelog and publish. conventional commits intent encoded version + changelog computed publish + tag hands-off
Conventional commits let CI version, log and publish without hand steps.

The canonical flow ties version, pack, and publish into a single job triggered on a merge to main:

# .github/workflows/release.yml
name: release
on:
  push:
    branches: [main]
permissions:
  contents: write      # create tags / commits
  id-token: write      # OIDC for provenance attestation
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0          # full history for version inference
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          registry-url: 'https://registry.npmjs.org'  # injects auth into .npmrc
          cache: 'npm'
      - run: npm ci                # frozen install from the lockfile
      - run: npm run build         # produce dist/ deterministically
      - run: npm test              # gate the release on green tests
      - run: npm publish --provenance --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Two automation styles dominate, and both layer on top of this skeleton: changeset-driven releases, where contributors declare intent in changeset files and a bot batches the version bump into a release PR, and commit-driven releases, where the version is inferred from Conventional Commit messages. Both are detailed under Semantic Versioning and Release Automation.

Automating the release removes the last manual step where a mistake can enter: a human choosing and typing a version. In an automated pipeline, the version is derived from the accumulated changes — from conventional commit messages with semantic-release, or from intent files with Changesets — so a publish can never reuse a version or ship a bump that does not match the change. The pipeline reads the changes, computes the next version, updates the manifest, generates a changelog, publishes, and tags the commit, all without a laptop in the loop.

The two automation models trade off differently. Commit-driven tools infer the bump from message conventions, so nothing is forgotten but correctness depends on commit discipline; intent-driven tools ask contributors to write an explicit changeset, which is more work per change but produces curated changelogs and handles independently-versioned monorepo packages naturally. Enforce whichever you choose in CI — a required changeset status check, or a commit-message linter — so the model is self-policing rather than reliant on everyone remembering the extra step.

Common Pitfalls & Remediation

Mistake Impact Resolution
Publishing a prerelease without --tag The unstable build becomes latest; every npm install pulls it. Always pass --tag next (or beta) for prereleases; reserve latest for stable.
Using .npmignore instead of files Forgotten excludes leak .env, tests, and source into the tarball. Switch to a files allowlist; verify with npm pack --dry-run.
Build only in prepare or not at all Stale or uncompiled dist/ ships to consumers. Run tests and build in prepublishOnly; never commit dist/.
Long-lived account-wide publish token in CI A single leaked secret can hijack every package on the account. Use a granular, scoped, short-lived automation token; store it as a CI secret.
Breaking change shipped as a minor bump Downstream ^ ranges silently break on npm ci. Classify by public-API impact; major-bump any removal, rename, or signature change.
First scoped publish without --access public Scoped packages default to private and fail or stay hidden. Pass --access public (or set publishConfig.access) on the first publish.
Common Pitfalls & Remediation Common Pitfalls & Remediation in production JavaScript package workflows. Common Pitfalls & Remediation Common Pitfalls & Remediation in production JavaScript package workflows.
Common Pitfalls & Remediation — the core idea of this section at a glance.

The costliest publishing mistakes share a root cause: treating the last mile as a manual chore rather than as code. A token committed to a .npmrc, a missing files allowlist that leaks source, a version bumped by hand that collides with an existing one, an unsigned tarball with no provenance — each is prevented by moving the step into an automated, reviewed pipeline. The pattern throughout is the same: make the safe path the default and the unsafe path impossible, so a tired engineer at the end of a release cannot accidentally do the wrong thing.

Registry Authentication: Tokens, Granular Access, and 2FA

Every publish is an authenticated write to a shared namespace, so the credential that performs it is a high-value secret. The modern posture is least-privilege and short-lived: prefer a granular access token scoped to the single package it publishes over a classic token with organization-wide publish rights, require two-factor authentication for human publishes, and mint automation tokens that CI reads from a secret store rather than embedding a personal credential in a pipeline. A leaked broad token can republish or deprecate everything you own; a leaked granular token can touch one package.

Registry Authentication: Tokens, Granular Access, and 2FA Every publish is an authenticated write to a shared namespace, so the credential that performs it is a high-value secret Registry Authentication: Tokens, Granular Access, and 2FA Every publish is an authenticated write to a shared namespace, so the credential that performs it is a high-value secret.
Registry Authentication: Tokens, Granular Access, and 2FA — the core idea of this section at a glance.

The strongest option removes the standing secret entirely. OIDC-based publishing lets a CI job exchange its verified identity for short-lived publish rights per run, so there is no long-lived token to leak, expire, or rotate — and it doubles as the foundation for provenance, because the registry knows the publish came from a trusted, identifiable build. Whichever mechanism you use, verify authentication early in the pipeline with a whoami check so a broken credential fails fast and names the registry, rather than surfacing as a confusing error mid-publish.

Rotating a publish token is a two-sided operation that breaks pipelines when the ordering is wrong: revoke the registry side before the secret store is updated and every release in that window fails for lack of a valid credential. The safe order is always create, deploy, verify, then revoke — mint the new token, update the CI secret without revoking the old one, prove it with a dry-run publish on the real pipeline, and only then revoke the old token. Alerting on token expiry ahead of time turns rotation from a 3 a.m. incident into scheduled maintenance.

Scoping is what limits the damage when a credential does leak, and leaks happen. A classic token with organization-wide publish rights can republish or deprecate everything you own; a granular token scoped to a single package can touch only that package. Prefer the narrowest scope the job needs, set an expiry so a forgotten token does not live forever, and read it from a secret store rather than embedding it — so a leaked pipeline log or a compromised laptop exposes a short-lived, single-package credential instead of the keys to the whole namespace.

Provenance and Supply-Chain Integrity

Provenance closes the gap between the code a consumer can read on your source host and the tarball the registry serves them. Generated by CI via OIDC, a provenance attestation cryptographically binds a published version to the exact source commit and build workflow that produced it, so a consumer running npm audit signatures can verify that what they installed really came from your repository and was not tampered with or published by a hijacked account. Publishing with --provenance from a workflow granted id-token: write is usually the entire change.

Provenance and Supply-Chain Integrity Provenance closes the gap between the code a consumer can read on your source host and the tarball the registry serves t Provenance and Supply-Chain Integrity Provenance closes the gap between the code a consumer can read on your source host and the tarball the registry serves them.
Provenance and Supply-Chain Integrity — the core idea of this section at a glance.

Provenance is one layer of a larger supply-chain posture, not a guarantee of safety on its own — it proves origin and integrity, not that the code is benign. Combine it with the defenses that harden the rest of the chain: an npm audit threshold that fails the build on known-vulnerable dependencies, lockfile-lint to restrict resolution to allowed hosts, and --ignore-scripts to neutralize arbitrary install-time code. Layered, these mean an attacker must defeat every gate rather than any single one, which is what turns publishing from a trust exercise into a verifiable one.

Verifying provenance is the consumer side of the same guarantee, and it scales through automation rather than manual checks. Running npm audit signatures in CI walks the installed tree and verifies registry signatures and provenance attestations, failing the build when a package loses its expected signature or its attestation does not verify. For security-critical dependencies, prefer ones that publish provenance and treat a missing attestation as a reason to pin and review — participating in the chain of trust as a consumer, not only as a publisher.

Publishing internal versus public packages

Not every package belongs on the public registry, and conflating the two is how proprietary code leaks. Internal packages should default to invisible: mark them private: true during development, set publishConfig.access to restricted, and route them to a private registry scoped to your organization so a stray npm publish cannot push them to the public index. Pinning publishConfig.registry in the manifest means the publish target is fixed regardless of a developer's default registry — a small guard that prevents an embarrassing and often irreversible disclosure.

Internal vs public How internal and public packages differ across the publish. Aspect Internal Public Visibility private + restricted public Registry scoped private npm registry Verification org access control provenance
Internal packages default to invisible; public packages default to verifiable.

Public packages invert the defaults: they are access: public, published with provenance so consumers can verify them, and versioned for a wide audience that pins ranges against your releases. The two paths share the same lifecycle — build, validate, pack, publish — but differ in destination, visibility, and access control. Deciding which a package is at creation time, and encoding that decision in the manifest and pipeline rather than in a person's memory, is what keeps internal code internal and public code verifiable.

Deprecation and the end of a package's life

Publishing well includes retiring versions and packages gracefully, because a package's lifecycle does not end at its last release. npm deprecate attaches a warning that installers see without removing the version — the right tool for steering consumers off a broken release, an insecure old major, or a package superseded by a successor. A clear deprecation message that names the replacement — for example, pointing at v3 and a migration guide — turns a dead end into a signpost, which is far more useful to a consumer than silence.

Package lifecycle Release, supersede, deprecate with a signpost, retire. active release supported superseded successor ships deprecate warning + pointer retired consumers migrated
A graceful lifecycle is additive: ship forward and deprecate the old with a clear pointer.

Unpublishing is the blunt, dangerous alternative, and the registry deliberately restricts it: only within a short window after publish, and never for a version others may already depend on, because removing a version breaks every lockfile that pinned it — the incident that motivated the modern immutability rules. The disciplined lifecycle is therefore additive: ship forward, deprecate the old, and reserve unpublish for genuine mistakes caught within minutes. Treating deprecation as a normal, communicated step rather than an admission of failure keeps consumers moving with you across major versions instead of stranded on an unsupported one.

Release automation end to end

A fully automated release ties the earlier pieces into one hands-off pipeline: a merge to the release branch triggers a job that computes the next version from the accumulated changes, generates the changelog, builds and validates the artifact, publishes with provenance, and tags the commit — with no human choosing a version or running npm publish from a laptop. Each stage is a gate that can stop the release, so a failed build, a broken exports map, or an audit threshold breach blocks the publish rather than shipping a bad version to thousands of installs.

Hands-off release Merge triggers version, build, publish, tag — every input derived. merge to release trigger version + changelog from changes build + validate gated publish + provenance tag commit
Deriving or verifying every input turns the last mile into the most reproducible stage.

The discipline that makes this safe is that every input is derived or verified rather than typed. The version comes from conventional commits or changeset intent files, so it can never collide with an existing one; the credential is a short-lived OIDC identity, so there is nothing to leak or rotate; the tarball contents come from the files allowlist, so nothing leaks; and the provenance attestation ties the result back to the exact commit and build. Automating the last mile this way turns publishing from the riskiest manual step in the lifecycle into its most reproducible one — the same code-not-chores principle that governs every other stage of a mature release process.

Frequently Asked Questions

What is the difference between a dist-tag and a Git tag? A Git tag marks a commit in your repository; a dist-tag is a named pointer on the npm registry (like latest, next, or beta) that maps to a published version. Installing pkg@next resolves the dist-tag, not a Git ref. They are independent, though release automation usually creates a matching Git tag for each published version.

Should I commit my dist/ directory so consumers always have built output? No. Committed build output drifts from source, bloats the repository, and produces noisy diffs. Build in prepublishOnly or in CI so the published tarball is always freshly compiled from the tagged commit, and keep dist/ in .gitignore.

Do I need --access public every time I publish a scoped package? Only the first publish of a new scoped package needs it, because scoped packages default to restricted. After the package exists as public, subsequent publishes inherit that access. Setting "publishConfig": { "access": "public" } in package.json makes it automatic and removes the flag from your command.

Can I unpublish a version if I made a mistake? Unpublishing is heavily restricted: within 72 hours for versions with no dependents, and effectively blocked afterward to protect the ecosystem from broken installs. Treat publishes as permanent — prefer publishing a corrected patch version and, if needed, deprecating the bad one with npm deprecate.

How does provenance help if my npm token is stolen? A stolen token still lets an attacker publish, but provenance makes the tampering visible: a legitimately published version carries a signed attestation tying it to your repository and CI run, while a malicious publish from a stolen token cannot forge that link from your real source. Consumers and scanners can verify provenance and flag the discrepancy.

Why can't I overwrite a published version to fix a mistake?

Published versions are immutable so that lockfiles and integrity hashes stay valid — consumers who already resolved a version must keep getting identical code. Recovery is forward: publish a fixed higher version and deprecate the broken one with a message pointing at the fix.

What's the safest credential for publishing from CI?

OIDC-based publishing, which exchanges the CI job's verified identity for short-lived publish rights per run, so there is no standing token to leak or rotate — and it enables provenance. Failing that, a granular, package-scoped automation token read from a secret store.

How do consumers verify my package came from my repo?

Publish provenance: with id-token: write permission and npm publish --provenance, the registry attaches an attestation binding the tarball to its source commit and build. Consumers run npm audit signatures to verify it.

Should a library pin its dependencies as tightly as an application?

No. Libraries should keep reasonably wide ranges so consumers can deduplicate shared packages into a single copy. Applications pin more tightly and rely on the lockfile for exactness, since nothing depends on an application's ranges.

Related

Home