上周有个同事跑来问我,说他们的发布流水线彻底坏了,报的错是 npm error code E404,PUT 请求打到 registry.npmjs.org,错误信息说包不存在。
问题是那个包他自己维护了两年多,npmjs.com 上能搜到,前几个版本都正常发布。
我第一反应也是去看权限、看 token、看包名拼写。全都对。最后发现是另外一个东西。
这篇文章讲这个坑本身,以及它背后那件更大的事:npm 正在把长生命周期的发布凭据慢慢取消掉,2027 年 1 月是终点。如果你还在用绕过了 2FA 的 granular access token 做自动发布,现在就该开始动了。
先说 404 这个错,它其实是在骗你
正常的 HTTP 语义里,403 是"你没权限",404 是"东西不存在"。npm registry 在有权限问题和包不存在这两种情况下,都会返回 404。
这是故意的。如果对未授权的请求返回 403,等于告诉对方"这个私有包确实存在",这是一种信息泄露。所以 registry 选择用 404 掩盖。
带来的后果就是:**一旦你的发布请求变成了未认证请求,你拿到的错误信息会指向一个根本不是原因的方向。**包不存在、包名拼错、scope 写错、甚至怀疑自己的账号是不是被移出维护者列表——这些排查方向全是死路。
真正的排查起点应该是反过来的:不要问"为什么包找不到",先问**“这个 PUT 请求带没带凭据”**。
什么情况下发布请求会不带凭据
Trusted Publishing 的认证过程和传统 token 完全不同。传统方式是.npmrc里写一行_authToken=xxx,npm 读这个变量发出去。
Trusted Publishing 走的是 OIDC:CI 平台给你一个签名的身份断言,npm 拿这个断言去 registry 换一个只对这一次 workflow run 有效的短期凭据。整个过程中没有任何长期凭据需要你保存。
关键是:这个功能由 npm 客户端里的lib/utils/oidc.js实现。这个文件不是所有版本的 npm 都有。
我对比过几个版本的 tarball:
| npm 版本 | 有无 oidc.js | Trusted Publishing |
|---|---|---|
| 10.8.2 | 无 | 不支持,会静默降级 |
| 11.5.0 | 有 | 支持 |
| 11.5.1 | 有 | 官方要求的最低版本 |
| 11.19.0 | 有 | 支持 |
如果你在 GitHub Actions 里这么写:
-uses:actions/setup-node@v4with:node-version:20那么 runner 上装的是 npm 10.8.2。这个版本的 npm 读到 setup-node 生成的.npmrc,里面写着_authToken=${NODE_AUTH_TOKEN},它老老实实把请求发出去了——但没有任何一步尝试做 OIDC 交换,因为实现这个的文件根本不在包里。
如果这时候NODE_AUTH_TOKEN是空的或者已经失效,请求就是未认证的。registry 一看,未授权写入已存在的包,返回 404。
于是你就得到了那个让人抓狂的错误。
为什么会去查这个方向
这个坑难查的地方在于,日志里最显眼的两个东西都在把你往错误方向带。
一个是provenance 签名成功了。发布日志里有 sigstore 的透明度日志记录,说明id-token: write权限是配好的,OIDC 交换本身没毛病。你会据此判断"认证部分没问题"。但 provenance 和 Trusted Publishing 是两个不同的功能——它们都用同一个 id-token,用途不一样。签名能成功不代表发布能认证通过。
另一个是那个看起来很可疑的 token 字符串。你会怀疑它过期了、权限不对、被 revoke 了,然后反复去 npmjs.com 上重建 token。但错误的根源根本不在 token 上,它是一个 npm 版本问题。
所以正确的排查顺序是:
**第一步,先确认 npm 版本。**在 workflow 里加一行npm --version,或者直接用npm -v打出来。低于 11.5.1 就不用往下查了。
-run:npm--version-run:node--version官方要求是npm CLI 11.5.1 以上,Node.js 22.14.0 以上。
**第二步,确认 runner 类型。**GitHub Actions 目前要求使用 GitHub 托管 Runner,自托管 Runner 不在支持范围内。如果你在自建 runner 上跑,即使版本对了也不行。
第三步,才是看 Trusted Publishing 配置。在 npmjs.com 上进包的 settings,确认 trusted publisher 里填的 repository、workflow 文件名、环境名和实际完全一致。注意 workflow 文件名是带 .yml 后缀的完整文件名。
还有一个容易忽略的:如果你之前配过 Trusted Publishing,现在想改配置,绕过了 2FA 的 granular access token 已经改不了了。这个下面说。
更大的背景:为什么会有这次迁移
2026 年 5 月,TeamPCP 这个组织搞了一次叫 Mini Shai-Hulud 的攻击。他们拿一个泄露的 npm token,在 22 分钟内推了 639 个恶意版本的包,覆盖 323 个包。这些包的周下载量加起来大约 1600 万。
攻击本身没有用什么新技术。一个静态的 npm token 就够了。
真正让这件事升级的是后续取证结果:恶意代码没有停在 npm token 上。它顺着同一个 runner 把 GitHub token、AWS key、Google Cloud 和 Azure 的凭据、SSH 私钥、Kubernetes service account、HashiCorp Vault 里的密钥、本地密码库的内容一并打包走了。
npm token 是入口,摆在旁边的那些长期凭据才是目标。
这件事动摇了一个长期以来的默认假设:**长生命周期凭据是可以被"妥善管理"的。**只要加密存储、定期轮换、缩小权限范围,风险就可控。
npm 的结论是:不行。一个凭据只要存在,就有可能被拷走。轮换只是缩短了暴露窗口,不能消除这个东西本身。所以它的做法是让凭据不再长期存在。
时间线:现在到明年 1 月
从 2026 年 7 月 31 日开始,配置为绕过 2FA 的 granular access token 已经不能再做账号、组织和包的管理操作。具体包括但不限于:
- 创建或删除 token
- 修改包的访问权限和维护者名单
- 调整 Trusted Publishing 配置
- 管理组织成员和包授权
- 涉及 recovery codes、密码、2FA 设置的敏感操作
这些操作现在要求交互式 2FA 挑战——脚本过不了,被偷走的 token 也过不了。
2027 年 1 月,这类 token 会失去直接发布的能力。到时候它们只剩两个功能:读取私有包,以及暂存一次发布(staged publish),由维护者做一次带 2FA 的批准。
注意这个时间点带来的一个尴尬:**你要配 Trusted Publishing,但现在配不了,因为改配置这个操作本身已经被限制了。**你必须用一个带 2FA 的维护者会话在 npmjs.com 网页上手动完成。也就是说,替换 token 的准备工作,不能用你正在替换的那个 token 来做。
所以两阶段之间的这段时间是给迁移用的,不是给你观望用的。
顺带提一句,这次调整只影响 npm granular access token。GitHub Personal Access Token、GitHub App Token、Actions 里的 GITHUB_TOKEN 都不受影响,别搞混了一起改。
迁移路径怎么选
两条路,取决于你的发布是否要求人工介入。
完全自动化且不需要人工批准:Trusted Publishing(OIDC)。
CI 平台出示一个签名断言,证明某个仓库的某个 workflow 正在运行。registry 校验后换发一个只对这次运行有效的短期凭据。运行结束,凭据就没了。没有可以被偷走的东西,因为它从未长期存在过。
这是 npm 希望你到的终点。
需要人工卡一道:staged publish。
自动化流程把发布暂存起来,维护者用带 2FA 的会话批准后才真正上线。适合那些对发布时间点有要求的场景——比如要和市场节奏对齐的版本。
选择判断很简单:**如果一个脚本可以在没人看着的情况下把公开包推出去,那这个能力本身就是风险。**要么把凭据去掉(OIDC),要么把人加回来(staged publish)。
迁移之前先查三件事
第一,你有多少个绕过了 2FA 的 token,在哪些 workflow 里用。
不要凭记忆。去 npmjs.com 的 access tokens 页面把列表拉出来,然后对着 CI 配置文件一个个核。组织账号尤其容易积攒历史 token,有些是三年前某次救急建的,建的人早就离职了。
第二,确认托管 Runner 和 npm 版本。
自托管 Runner 目前不支持 Trusted Publishing,这是硬限制。如果你在用自托管 runner,要么切到 GitHub 托管 Runner,要么走 staged publish。
第三,改 Trusted Publishing 配置这件事,安排一个真人去做。
因为它没法脚本化——这正是这次变更的目的之一。别等到 2027 年 1 月发布突然断了才发现改不了配置。
顺手说清楚两件事
**发布凭据和你的 npm 账号本身是两层东西。**OIDC 解决的是 CI 里的发布凭据,但它不动你登录 npmjs.com 时用的那个第二重验证。如果你的 npm 账号还在用 TOTP 六位码,换手机的时候一样会丢。
这类东西的兜底永远是那三步:各平台的恢复码单独存一份不要只放手机里、确认你的验证器有迁移路径而且要提前确认而不是丢手机之后、换机之后先用新手机真实登录一次再清理旧设备。
**还有个边界值得说清楚。**二维码、TOTP 密钥、动态验证码和恢复码,不要公开、不要上传到任何解析网站、不要发到群里。这几样任意一个落到别人手里,第二重验证就等于没有了。
而且 TOTP 验证器管的是登录环节,它不替代PAT、GitHub App Token、SSH Key 这类机器凭据的治理。这次 npm 的事情正好说明:登录保护和自动化的发布凭据,是两套需要分别处理的问题。把它们混为一谈,就会出现"我明明开了 2FA,怎么还是被推了恶意包"这种困惑。