Skip to main content

配置

pnpr 使用 YAML 配置。当你没有传递 -c 时,pnpr 会在其配置目录中查找全局 config.yaml,否则会回退到打包的默认值(该默认值是针对 pnpm 自己的测试注册表设计的——实际使用请编写你自己的配置)。

🌐 pnpr uses a YAML config. When you don't pass -c, pnpr looks for a global config.yaml in its config directory and otherwise falls back to a bundled default (which is shaped for pnpm's own test registry — write your own config for real use).

一个最小配置,在本地托管私有作用域,并将其他所有内容路由到公共 npm 注册表:

🌐 A minimal config that hosts a private scope locally and routes everything else to the public npm registry:

storage: ./storage

registries:
local:
type: hosted
packages:
# The names this registry serves, plus their access rules.
'@mycompany/*':
access: $authenticated
publish: $authenticated

npmjs:
type: upstream
url: https://registry.npmjs.org/
public: true

main:
type: router
sources: [local, npmjs]

defaultRegistry: main

-c 通过它:

🌐 Pass it with -c:

pnpr -c ./pnpr.yaml

storagecache

🌐 storage and cache

pnpr 保存两种数据:

🌐 pnpr keeps two kinds of data:

  • storage — the source of truth: packages published to this server plus anything served in static mode. Back this up and keep it on a durable volume.

  • cache — the disposable mirror of upstream registries plus the resolver store, resolver cache, lockfile-verdict cache, and S3 upload staging scratch. Safe to wipe at any time. Defaults to a .pnpr-cache subdirectory of storage; point it at a separate, ephemeral volume to keep cached upstream content off the durable disk.

storage: ./storage
#cache: ./cache

托管商店也可以存放在兼容 S3 的对象存储中——参见 存储后端

🌐 The hosted store can instead live in an S3-compatible object store — see Storage backends.

registriesdefaultRegistry

🌐 registries and defaultRegistry

registries: 地图是 pnpr 的唯一路由表面。每个可寻址的注册源都是一个命名注册表,以 npm 注册表的形式在 https://<pnpr>/~<name>/ 上公开。有三种类型:

  • hosted — a registry pnpr itself is the authoritative origin for. The only kind that accepts writes (publish, dist-tags, unpublish).

  • upstream — exactly one external registry origin, proxied and cached.

  • router — an ordered list of concrete source registries; a package resolves to the first listed source that claims its name.

该模型由一个不变量控制:来源必须声明,绝不推断。每个具体的注册表都会通过其 packages: 映射 声明它所服务的软件包名称——即其命名空间——一个软件包解析到唯一声明的来源,并且没有任何配置可以表示跨来源回退——无论是在“未找到”还是“不可用”的情况下。命名空间在注册表的每一条路径上都受到强制:对命名空间之外名称的读取会在存储或上游查询之前直接返回 404,而命名空间外的发布会被拒绝。这从根本上防止了依赖混淆类攻击。

🌐 The model is governed by one invariant: provenance is declared, never inferred. Every concrete registry declares the package names it serves — its namespace — through its packages: map, a package resolves to exactly one declared origin, and no configuration can express a cross-origin fall-through — not on "not found" and not on "unavailable". The namespace is enforced at the registry, on every path to it: a read of a name outside the namespace is a definitive 404 answered before storage or the upstream is consulted, and an off-namespace publish is rejected. This closes the dependency-confusion class of attacks by construction.

defaultRegistry 指定无路径基础 URL (https://<pnpr>/) 所别名的注册表——通常是一个路由。如果省略,裸主机不提供注册表,客户端必须访问一个 /~<name>/ URL。

注册表名称作为单个 URL 路径段 /~<name>/ 使用,因此它必须是一个 URL 安全的段:不能为空,不能是 ...,不能以 . 开头,也不能包含 /\:%?#、空格或控制字符。

packages 地图

🌐 The packages map

每个具体的注册表(托管或上游)都可以选择性地使用 packages: 映射。它的键是注册表的命名空间 —— 它提供的包名 —— 而它的值是每个包的访问规则。一次声明即可进行路由、过滤和授权。当省略该映射时,注册表将提供每个名称。

🌐 Every concrete registry (hosted or upstream) takes an optional packages: map. Its keys are the registry's namespace — the package names it serves — and its values are the per-package access rules. One declaration routes, filters, and authorizes. When the map is omitted, the registry serves every name.

Keys 使用一种刻意的小型模式语言,因此可以静态地检查覆盖范围:

🌐 Keys use a deliberately small pattern language, so coverage can be checked statically:

模式匹配项
**每个包名。
@*/*每个带作用域的包,任意作用域。
@scope/*一个作用域内的每个包。
foo, @scope/foo就是那个包。

任何其他通配符都是配置错误,而不是默默地永远不匹配的字面量。

🌐 Any other wildcard is a config error rather than a silently-never-matching literal.

最具体的匹配键获胜:精确名称胜过 @scope/*@scope/* 胜过 @*/*@*/* 胜过 **。键的顺序没有意义——在每个具体性层级中最多只有一个键可以匹配名称,因此获胜规则是唯一的,通过重新排序映射(无论是通过格式化工具,还是 YAML 的往返操作)都不会改变应用的规则。重复键是一种配置错误。

🌐 The most specific matching key wins: an exact name beats @scope/*, which beats @*/*, which beats **. Key order carries no meaning — at most one key per specificity tier can match a name, so the winning rule is unique and reordering the map (by a formatter, or a YAML round-trip) can never change which rule applies. A duplicate key is a config error.

每个值可以设置 accesspublishunpublish。常见的值有 $all(任何人)、$authenticated(已登录用户)和 $anonymous;列表也可以指定用户和 。当省略某个字段时,access 回退到注册表级别的 access: 默认值,publish 的默认值为 $authenticated,而 unpublish 的默认值为无人。

🌐 Each value can set access, publish, and unpublish. Common values are $all (anyone), $authenticated (logged-in users), and $anonymous; a list can also name users and groups. When a field is omitted, access falls back to the registry-level access: default, publish defaults to $authenticated, and unpublish defaults to nobody.

托管注册表

🌐 Hosted registries

registries:
private:
type: hosted
org: mycompany
access: $authenticated
packages:
'@mycompany/*':
publish: $authenticated
unpublish: $authenticated
# An explicit entry fully decides its names — this opens one
# package on an otherwise-private registry.
'@mycompany/open-sdk':
access: $all
描述
org此注册表的包存储命名空间,因此两个托管注册表可以保存相同的 name@version 而不会发生冲突。省略则表示平面 storage 根。必须是单个路径安全段。
access对未设置自己权限的 packages: 条目的默认读取访问权限。默认值为 $all
packages注册表的命名空间和每个包的规则 — 见 上文。只有这些名称可以从此注册表提供或发布。

两个托管注册表不能共享 org 命名空间——在配置加载时会被拒绝。

🌐 Two hosted registries cannot share an org namespace — that's rejected at config load.

拒绝的形式取决于谁发出的拒绝。对注册表级别 access: 默认也被拒绝的调用者,会被 404 掩盖——一个全面私有的注册表从不确认哪个包名存在。对于那些被显式 packages: 条目拒绝,而默认情况下本可允许的调用者,会被明显拒绝(匿名调用者为 401,已认证调用者为 403),因此客户端可以提示输入凭据。

🌐 The shape of a denial depends on who denied it. A caller that the registry-level access: default also denies is masked with a 404 — a blanket-private registry never confirms which package names exist. A caller denied by an explicit packages: entry while the default would admit them is rejected loudly (401 for anonymous callers, 403 for authenticated ones), so clients can prompt for credentials.

上游注册表

🌐 Upstream registries

上游注册表是一个外部来源 —— 一个 URL,一个凭证,一个缓存命名空间。它要么是 public(匿名获取,无凭证),要么是私有的(携带服务器拥有的凭证和一个 access: 策略,指定谁可以使用它):

🌐 An upstream registry is one external origin — one URL, one credential, one cache namespace. It is either public (fetched anonymously, no credential) or private (carries a server-owned credential and an access: policy naming who may use it):

registries:
npmjs:
type: upstream
url: https://registry.npmjs.org/
public: true

corp:
type: upstream
url: https://npm.corp.example.com/
auth:
type: bearer
token_env: CORP_NPM_TOKEN
access: $authenticated
packages:
'@corp/*': {}
描述
url上游注册表 URL。
public标记一个匿名的、全世界可读的来源(例如公共 npm 注册表)。公共上游不发送任何凭证或自定义头;在声明 public: true 的同时声明 authaccessheaders 是配置错误。
auth私有源的服务器拥有的凭证。typebearerbasic;提供 token,或 token_env: true 来读取 NPM_TOKEN,或 token_env: NAME 来读取命名的环境变量。
headers发送到上游的额外请求头。如果设置了 headers.Authorization,它将覆盖由 auth 派生的头。
access哪些 pnpr 用户可以访问此注册表 /~<name>/(并通过它解析)。非 public 上游所必需。
packages此上游通过 pnpr 提供的名称 — 见 上文
maxage每个注册表的包信息新鲜度窗口。覆盖 pnpr 的全局包信息 TTL / --packument-ttl-secs 用于此注册表。
timeout每个请求的上游截止时间。默认值为 30s
max_fails上游断路器开启前的连续失败次数。默认值为 20 禁用断路器。
fail_timeout在电路再次被探测前的冷却时间。默认值为 5m
cache是否将从此注册表获取的压缩包进行本地缓存。默认值为 truefalse 将经过验证的压缩包通过临时文件流传输。

间隔值可以接受像 30s5m1h30m 这样的字符串,或仅秒数的数字。

🌐 Interval values accept strings such as 30s, 5m, 1h30m, or a bare number of seconds.

有两个特定于上游的规则适用于 packages: 映射。即使在 public: true 上游,也允许每包 access 值 —— public 描述了 fetch(匿名、无凭证),而一个规则的 access 决定了谁可以 通过 pnpr 读取名称。并且 publish/unpublish 值在任何上游都会被拒绝:不能在上面进行写入。

🌐 Two upstream-specific rules apply to the packages: map. Per-package access values are allowed even on a public: true upstream — public describes the fetch (anonymous, credential-free), while a rule's access gates who may read the name through pnpr. And publish/unpublish values are rejected on any upstream: no write can land there.

私有上游注册表不仅仅是一个代理:它是该源的pnpr 管理的凭证。一个由服务器持有的上游令牌通过注册表的 access: 策略分发给整个团队——客户端使用自己的令牌进行 pnpr 身份验证,并且永远不会看到上游凭证。在私有上游上声明 packages: 命名空间也限制了该凭证的使用范围:调用者不能通过它拉取任意公共名称。pnpr 从不将客户端的凭证转发到上游,而且服务器拥有的凭证只会通过上游自身的方案附加(https:// 注册表的令牌绝不会通过明文的 http:// 发送)。

🌐 A private upstream registry is more than a proxy: it is a pnpr-managed credential for that origin. One upstream token, held by the server, is fanned out to a whole team through the registry's access: policy — clients authenticate to pnpr with their own tokens and never see the upstream credential. Declaring a packages: namespace on a private upstream also bounds what that credential can be used for: a caller can't pull arbitrary public names through it. pnpr never forwards a client's credentials upstream, and the server-owned credential is only ever attached over the upstream's own scheme (an https:// registry's token is never sent over plain http://).

每个上游都有自己的缓存命名空间:一个 public 上游使用一个在重启之间共享且稳定、不含秘密的命名空间,而私有上游的缓存则由注册表及其凭证的 HMAC 生成键 —— 因此旋转上游令牌会自动切换到一个新的命名空间,并且私有注册表的内容永远不会通过公共路径或其他注册表提供。

🌐 Each upstream gets its own cache namespace: a public upstream uses a stable, secret-free namespace shared across restarts, while a private upstream's cache is keyed by an HMAC of the registry and its credential — so rotating the upstream token automatically moves to a fresh namespace, and a private registry's content can never be served on a public path or through another registry.

路由

🌐 Routers

路由是一个有序的 sources: 具体注册表列表。一个包会解析到 第一个列出其 packages: 键声明其名称的源,具有权威性 — 后续的源永远不会被查询,而没有任何源声明的名称则是确定的 404:

🌐 A router is an ordered sources: list of concrete registries. A package resolves to the first listed source whose packages: keys claim its name, authoritatively — later sources are never consulted, and a name that no source claims is a definitive 404:

registries:
main:
type: router
sources: [private, corp, npmjs]

一个没有 packages: 映射的源声称每个名称——即通用名称——并且必须列在最后。路由可以对竞争的声明进行排序,但它永远不能将名称分配给不声明它的注册表:命名空间存在于具体的注册表上,并在每条路径上在那里被执行。

🌐 A source with no packages: map claims every name — the catch-all — and must be listed last. A router can order competing claims, but it can never assign a name to a registry that doesn't claim it: the namespace lives on the concrete registry, and is enforced there on every path.

因为源顺序是关键——错误排序的路由是将私有作用域发送到公共源的唯一途径——pnpr 拒绝启动(并在配置重载时失败)在路由无效的情况下:

🌐 Because source order is load-bearing — a misordered router is the one way a mistake could send a private scope to a public origin — pnpr refuses to start (and fails a config reload) on an invalid router:

  • a source that is unreachable — everything it claims is already covered by earlier sources (including a catch-all that isn't last);

  • two sources claiming an identical pattern (genuinely ambiguous provenance, rejected in either order);

  • a duplicate source;

  • a source that is undefined, the router itself, or another router (sources must be hosted or upstream registries — no nesting, no cycles);

  • a router with no sources.

groups

静态组/团队成员资格。每个访问列表在任何接受用户名的位置都可以接受组名——无论是每个包的 packages: 规则,还是注册表级别的 access: 列表:

🌐 Static group/team memberships. Every access list accepts group names anywhere it accepts usernames — the per-package packages: rules and the registry-level access: lists alike:

groups:
platform: alice bob
frontend: [carol, dave]

registries:
corp:
type: upstream
url: https://npm.corp.example.com/
auth:
type: bearer
token_env: CORP_NPM_TOKEN
access: platform

auth

默认情况下,用户存储在 htpasswd 文件中,令牌存储在本地 SQLite 数据库中:

🌐 By default users are stored in an htpasswd file and tokens in a local SQLite database:

auth:
htpasswd:
file: ./htpasswd
# Self-registration is disabled when omitted or set to -1.
# Set a non-negative cap to allow new users.
max_users: -1
tokens:
# Optional. Defaults to a tokens.db sibling of the htpasswd file.
file: ./tokens.db

省略 max_users 意味着注册被禁用,而不是无限注册。现有用户仍然可以登录。当省略 auth.tokens.file 且设置了 auth.htpasswd.file 时,令牌数据库默认位于与 htpasswd 文件相邻的 tokens.db

🌐 Omitted max_users means registration disabled, not unlimited registration. Existing users can still log in. When auth.tokens.file is omitted and auth.htpasswd.file is set, the token database defaults to tokens.db next to the htpasswd file.

要在多个无状态 pnpr 副本之间共享认证状态,请将用户和令牌移入共享的 SQL 数据库 —— 参见 认证后端

🌐 To share auth state across several stateless pnpr replicas, move users and tokens into a shared SQL database — see Auth backends.

secret

secret: ${PNPR_SECRET}

pnpr 私有缓存命名空间的 HMAC 密钥——用于私有解析缓存条目和私有每注册表缓存目录的密钥,因此磁盘上的路径既不显示注册表名称也不显示凭据。它必须至少为 16 字节。如果省略,会为每个进程生成一个新的随机密钥,这意味着私有缓存条目仅在该进程的生命周期内有效——设置一个显式密钥以在重启时保持私有缓存热状态。

🌐 The HMAC key for pnpr's private cache namespaces — private resolution-cache entries and private per-registry cache directories are keyed with it, so on-disk paths reveal neither registry names nor credentials. It must be at least 16 bytes. When omitted, a fresh random secret is generated per process, which means private cache entries only survive for that process's lifetime — set an explicit secret to keep private caches warm across restarts.

注册表表面和 resolver

🌐 The registry surface and resolver

pnpr 暴露了两个 HTTP 接口:

🌐 pnpr exposes two HTTP surfaces:

  • The npm registry surface — packument and tarball reads, publish, unpublish, dist-tags, and search, on the path-less base and on every /~<name>/ endpoint. It has no config toggle: it is served exactly when at least one registry is declared under registries:. The CLI flag --disable-registry turns it off for one process.

  • The resolver surface — the pnpr install-accelerator routes (GET /-/pnpr, POST /-/pnpr/v0/resolve, and POST /-/pnpr/v0/verify-lockfile):

resolver:
enabled: true

解析器默认启用;CLI 标志 --disable-resolver 会覆盖该设置。

🌐 The resolver is enabled by default; the CLI flag --disable-resolver overrides the setting.

必须提供某些内容:一个没有注册表的配置(或通过标志禁用注册表界面)并且解析器被禁用,是启动时错误。

🌐 Something must be served: a config with no registries (or the registry surface disabled by flag) and the resolver disabled is a startup error.

健康端点(/-/ping)和账户端点(登录、whoami 和令牌管理)始终可用,无论启用了哪些界面——因此即使是仅解析器服务器,pnpm login 也能工作。请参阅 HTTP 端点

🌐 The health endpoint (/-/ping) and the account endpoints (login, whoami, and token management) are always served, whichever surfaces are enabled — so pnpm login works even against a resolver-only server. See HTTP endpoints.

routes

解析器路由分类(参见 安装加速)默认将官方 npm 注册表视为公共路由。如果客户端针对其他提供匿名可读内容的注册表进行解析,请将它们声明为公共路由,以便它们的解析结果可以缓存并在所有调用者之间共享:

🌐 Resolver route classification (see Install acceleration) treats the official npm registry as public out of the box. If clients resolve against other registries that serve anonymously-readable content, declare them as public routes so their resolutions can be cached and shared across all callers:

routes:
public:
- registry: https://registry.mirror.example.com/
- registry: https://npm.corp.example.com/
package: '@oss/*'

每条规则可以指定一个 registry 来源、一个 package 通配符,或者两者都指定。规则在遇到拼写错误时会关闭:如果 registry URL 或 package 通配符存在但无法解析,会丢弃整条规则(并发出警告),而不是扩大它。

🌐 Each rule may name a registry origin, a package glob, or both. Rules fail closed on a typo: a present-but-unparsable registry URL or package glob drops the whole rule (with a warning) rather than widening it.

osv

本地 OSV 检查可以在不调用实时 OSV API 的情况下隐藏或拒绝已知存在漏洞的 npm 版本:

🌐 Local OSV checks can hide or reject known vulnerable npm versions without live OSV API calls:

osv:
enabled: true
path: ./osv/npm/all.zip

path 可能指向 OSV npm 数据库的压缩包或已解压的 JSON 目录。当省略时,pnpr 会查找 <cache>/osv/npm/all.zip。同样的功能也可以通过 CLI 使用 --osv--osv-db 启用。

log

log:
type: stdout
format: pretty # or `json`
level: error # trace, debug, http, info, warn, or error

RUST_LOG 总是会覆盖已配置的级别。请参阅 CLI 参考

环境变量替换

🌐 Environment variable substitution

配置中的任何 ${ENV_VAR} 都会在解析前从环境中替换,因此可以将机密信息保留在文件之外:

🌐 Any ${ENV_VAR} in the config is substituted from the environment before parsing, so secrets can be kept out of the file:

s3:
accessKeyId: ${PNPR_S3_ACCESS_KEY_ID}
secretAccessKey: ${PNPR_S3_SECRET_ACCESS_KEY}