pnpm unpublish 完整支持注册表 2FA/OTP 挑战:从 ERR_PNPM_UNAUTHORIZED 到 Web 认证与一次性密码复用

发布时间:2026/9/20 7:39:58
pnpm unpublish 完整支持注册表 2FA/OTP 挑战:从 ERR_PNPM_UNAUTHORIZED 到 Web 认证与一次性密码复用 包管理器开发工具CLI【免费下载链接】pnpmFast, disk space efficient package manager项目地址https://gitcode.com/gh_mirrors/pn/pnpm点击查看免费下载导读本文聚焦 pnpm 在unpublish取消发布命令上的认证能力升级当注册表对已登录用户返回 401 且该 401 是一次性密码OTP挑战时pnpm 不再直接报ERR_PNPM_UNAUTHORIZED失败而是自动完成双重认证——要么启动基于 Web 的认证流程EOTP要么提示输入经典一次性密码并且把获取到的密码复用到本次运行内的每一个注册表请求。读者读完本文将掌握pnpm unpublish的完整参数语义、2FA 挑战的两条处理路径、密码复用机制OtpSession、非交互终端下的错误处理ERR_PNPM_OTP_NON_INTERACTIVE以及对应的源码与测试依据。该变更记录在仓库的 changeset 文件 .changeset/unpublish-otp-challenge.md 中涉及pnpm/network.web-authminor、pnpm/registry-access.clientpatch、pnpm/registry-access.commandspatch、pacquetpatch与pnpmpatch等包。unpublish 命令的认证背景与本次变更动因pnpm unpublish用于从注册表移除某个包或某段版本范围。此前当注册表要求双重认证2FA而用户已经登录时命令会直接以ERR_PNPM_UNAUTHORIZED失败——也就是说“已登录”与“通过 2FA 挑战”被混为一谈用户明明持有有效凭据却无法完成取消发布操作。本次 changeset 明确了修复目标对应上游 issue #14464登录状态下的 401 若为OTP 挑战而非真正的凭据错误则启动认证完成流程挑战携带authUrl/doneUrl时走Web 认证流程挑战为经典一次性密码时提示输入 OTP获得的密码被本次运行中的所有请求复用避免每个请求都重复认证。ERR_PNPM_UNAUTHORIZED在仓库中依然存在但它现在只代表“未登录”这类真正的认证失败而非 OTP 挑战。可以从 Rust CLI 源码 pnpm/crates/cli/src/cli_args/unpublish.rs 以及 TypeScript 测试 pnpm11/registry-access/commands/test/unpublish.ts 中看到该错误码的既有语义。unpublish 命令参数全解UnpublishArgs的定义位于 pnpm/crates/cli/src/cli_args/unpublish.rs四个参数与认证直接相关的是--otp参数类型说明--registry URL可选npm 注册表的基础 URL用于覆盖配置中的默认注册表--otp code可选一次性密码用于要求双重认证的注册表。传了--otp时命令走legacy认证类型直接接受经典验证码--force布尔无论当前发布了多少版本都从注册表移除整个包不传该标志时 pnpm 拒绝整体取消发布params位置参数要移除的包可带版本范围如pkg1.x裸包名表示移除全部已发布版本认证类型在源码中通过AuthType枚举建模unpublish.rsAuthType::Web默认值对应 npm 的auth-type: web用于 Web 认证挑战AuthType::Legacy用户显式传入--otp时启用对应auth-type: legacy接受经典一次性密码。每个请求都会携带npm-auth-type请求头值为web或legacy若本次请求带有 OTP则额外携带npm-otp请求头见 pnpm/crates/cli/src/cli_args/unpublish/mutation.rs。401 挑战识别什么才是“OTP 挑战”并非所有 401 都值得走认证流程。otp_challenge_from_unauthorized_body位于 pnpm/crates/network-web-auth/src/with_otp_handling.rs负责解析 401 响应体判定规则如下Web 认证挑战响应体是 JSON 且同时包含authUrl与doneUrl两个字符串字段即判定为 EOTP 挑战挑战体携带这两个 URL经典 OTP 挑战响应体转小写后包含 npm 经典措辞one-time pass判定为经典 OTP 挑战挑战体为空其余情况如{error:unauthorized}、Bad token、空 body 等都不是挑战按普通认证失败处理。对应的单元测试见 pnpm/crates/network-web-auth/src/with_otp_handling/tests.rs覆盖了上述三种判定及边界情况。在 unpublish 的实现里401 响应会先被读取带DEPRECATION_ERROR_BODY_LIMIT长度限制然后交给该函数分类是挑战则抛给OtpSession处理不是挑战则用write_error_for_status生成普通注册表错误mutation.rs。Web 认证流程与经典 OTP 提示挑战被确认后resolve_otp_challengewith_otp_handling.rs负责满足它两条路径为Web 流程当挑战体同时携带合法http/https的authUrl与doneUrl时pnpm 打印带认证 URL 的消息format_auth_url_message询问是否在浏览器中打开该 URL随后轮询doneUrl等待用户完成浏览器认证并取得令牌poll_for_web_auth_token。URL 会经过canonical_http_url净化只允许http/https协议防止恶意注册表注入javascript:等危险 scheme并剥离 URL 中的 userinfo避免凭据形状的数据进入日志经典 OTP挑战体为空或缺少 URL时在终端提示This operation requires a one-time password.并等待输入用户取消输入则按“挑战未满足”处理不重试。两条路径都要求终端可交互stdin与stdout必须都是 TTY。若注册表要求额外认证而 pnpm 运行在非交互终端中会报ERR_PNPM_OTP_NON_INTERACTIVEOtpNonInteractiveErrorwith_otp_handling.rs诊断信息会给出两条修复建议在交互式终端重跑命令或改用--otp提供经典一次性密码。一次性密码的会话级复用OtpSession本次 changeset 的关键设计是“密码复用”——对应源码中的OtpSessionwith_otp_handling.rs会话创建时不含 OTP第一次请求运行时把已配置的--otp若有随请求发送若请求返回挑战会话通过上述两条路径之一取得新密码并缓存到self.otp同一会话后续的每一个操作都带上缓存密码——OtpSession::run在每次调用操作前都会把当前持有的 OTP 传入operation(self.otp.clone())并在操作返回挑战时更新缓存后重试一次重试时若注册表再次索要 OTP则判定为ERR_PNPM_OTP_SECOND_CHALLENGEOtpSecondChallengeError提示这是注册表的异常行为。OtpSession文档注释点明了设计意图一批操作只花费“一次在场证明”one proof of presence而不是每个操作各一次当缓存的密码不再被接受经典 OTP 通常在一分钟内过期时随后的挑战会获取新密码并仅对触发该挑战的操作做一次重试。unpublish 正是这种“一次认证、多请求复用”的典型场景部分版本取消发布会先执行一次PUT更新 packument再对每个被移除版本的 tarball 执行DELETE每次删除还会先重新拉取 packument 以拿到最新的_rev——见 unpublish.rs。如果每个请求都重新认证一次取消发布会触发多次 2FA而MutationContext把OtpSession与所有变更请求共享让整次运行只认证一次mutation.rs。请求与重试细节头部、重试与超时send_oncemutation.rs构造每次变更请求方法DELETE package/-rev/rev整包删除或PUT package/-rev/rev版本子集更新body 为更新后的 packument JSON头部始终携带npm-auth-type有authorization时携带本次请求有生效 OTP 时携带npm-otp请求经由send_with_retry发送重试选项取自配置。Web 认证流程自身的轮询也有一套独立的网络选项web_auth_fetch_optionsmutation.rs把fetch_timeout、fetch_retries、fetch_retry_factor、fetch_retry_maxtimeout、fetch_retry_mintimeout等配置映射到WebAuthFetchOptions供轮询doneUrl时使用。测试与验证无终端与 --otp 两种路径Rust 集成测试 pnpm/crates/cli/tests/suite/unpublish.rs 用脚本化的 Web 认证宿主UnpublishHosttrait生产环境为真实WebAuthHost测试中为可编排的 fake验证了两条关键路径非交互终端的 Web 挑战spawn 出的 CLI 进程没有 TTY注册表返回 OTP 挑战时stderr 断言包含ERR_PNPM_OTP_NON_INTERACTIVE而不是ERR_PNPM_UNAUTHORIZED——证明“已登录但 2FA 未过”不再与“未登录”混淆--otp的 legacy 路径the_otp_flag_sends_the_code_under_the_legacy_auth_type断言请求携带npm-otp: 123456且本次请求不带 Web 挑战所需的npm-otp缺失匹配——验证经典一次性密码在legacy认证类型下直接前置发送。UnpublishHost的抽象unpublish.rs把Clock、Sleep、WebAuthFetch、StdinIsTty、StdoutIsTty、EnterKeyListener、OpenUrl、PromptOtp等能力组合成一个 trait使测试可以在不真实打开浏览器的情况下编排整个挑战-应答流程。使用建议与限制交互式终端下默认无需任何额外参数遇到 Web 挑战时跟随提示在浏览器完成认证即可遇到经典 OTP 时按提示输入验证码自动化/CI 等非交互环境若注册表使用经典一次性密码请显式传入--otp code否则命令会以ERR_PNPM_OTP_NON_INTERACTIVE退出密码只在本次运行的会话内复用不会写入任何配置文件需要整体移除整个包时务必带上--force否则会得到ERR_PNPM_UNPUBLISH_CONFIRMpnpm11/registry-access/commands/test/unpublish.ts中同样断言了该保护行为若注册表返回 405说明该包只允许弃用deprecate而不允许整体删除ERR_PNPM_UNPUBLISH_FORBIDDEN。上述行为以当前仓库pnpm Rust 实现 pnpm/crates/cli/src/cli_args/unpublish.rs、Web 认证核心 pnpm/crates/network-web-auth/src/with_otp_handling.rs 及测试 pnpm/crates/cli/tests/suite/unpublish.rs为准注册表端的挑战格式需符合 npm 的 EOTP 约定authUrl/doneUrl或经典one-time pass措辞。赞分享包管理器开发工具CLI【免费下载链接】pnpmFast, disk space efficient package manager项目地址https://gitcode.com/gh_mirrors/pn/pnpm点击查看免费下载相关推荐Wasp 邮箱认证完整实战指南从注册登录到邮件验证与密码重置Wasp 邮箱认证完整实战指南从注册登录到邮件验证与密码重置 本指南以 Wasp 全栈框架当前仓库 waspc https://link.gitcode.cWeb框架后端前端CLI开发工具NocoBase 密码认证用户注册、注册表单设置与忘记密码流程全解NocoBase 密码认证用户注册、注册表单设置与忘记密码流程全解 本篇基于 NocoBase 官方文档《密码认证》并结合开源仓库中 nocobase/pl低代码后端前端人工智能AI 应用工作流自动化基于 Passport.js 与 bcrypt 的 Express 用户认证实战从注册、登录到密码加密基于 Passport.js 与 bcrypt 的 Express 用户认证实战从注册、登录到密码加密 本文是一份面向 Node.js 后端开发者的完整认证实文档教程教育上一篇Fooocus-MRE从零开始的AI绘画终极指南让创意不再被技术束缚下一篇主Issue重构教程章节结构创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考