Files
pnpm/Cargo.toml
Zoltan Kochan 70554b8677 feat(pnpr): config-selectable networked-SQLite auth backend (#12199 phase 3) (#12206)
## What

Implements the **auth half** of [#12199](https://github.com/pnpm/pnpm/issues/12199) (phase 3) — making pnpr's remaining per-instance state pluggable so the registry can run as stateless, horizontally-scaled replicas.

### Auth records behind config-selected backends
Users + tokens now sit behind narrow async `UserBackend` / `TokenBackend` traits, built once at startup into `Arc<dyn …>` handles (the same build-once pattern #12198 used for the hosted store). Three implementations:

- **Local** (default) — today's htpasswd file + SQLite token DB, or in-memory when no file is configured. Unchanged behavior.
- **Networked SQLite (libsql / Turso)** — `LibsqlAuth` stores **both** records in one shared database, so several stateless replicas observe a consistent set of users and tokens. The `tokens` table DDL is shared verbatim with the local backend (a DB can migrate between them); users — which the local backend keeps in htpasswd — move into a `users` table.

Selected via a new top-level YAML block:

```yaml
backend:
  libsql:
    url: ${PNPR_LIBSQL_URL}
    authToken: ${PNPR_LIBSQL_TOKEN}
    # optional embedded replica for local-fast hot-path reads:
    replicaPath: ./auth-replica.db
    syncIntervalSecs: 60
```

When the block is absent, auth stays on local disk exactly as before.

### Embedded-replica read acceleration
Token lookups are on the request hot path, so against a remote primary every read would be a network round-trip. With `replicaPath` set, `LibsqlAuth` builds a libsql **embedded replica**: reads hit a local file that libsql keeps current in the background; writes go to the primary. `syncIntervalSecs` is the freshness knob that bounds token-revocation lag.

### Async access path
`identify` / `enforce_access` are now async (a networked lookup is async). `enforce_access` is split into an async `resolve_identity` + a sync `authorize`, so the search endpoint resolves the caller once and authorizes each candidate synchronously (no async-in-`retain`).

### Concurrent-publish guard (cross-cutting follow-up from the issue)
Closes the same-instance lost-update window in the three read-modify-write packument flows (publish, dist-tag change, partial-unpublish): a striped per-package lock serializes same-package writers on one instance while letting different packages proceed in parallel. The **cross-replica** half (S3 `If-Match` / ETag CAS) is documented in-code as the remaining piece — the issue files it under "fix when we get there," and it belongs with the multi-writer S3 publish work, not this auth branch.

## Tests
All green — `cargo test -p pnpr`:
- **176 lib unit tests** incl. new `LibsqlAuth` tests (run against an in-memory libsql DB — same driver + SQL, no server) and `backend.libsql` config-parsing tests (incl. the replica options).
- New `concurrent_publishes_of_distinct_versions_all_survive` integration test for the publish guard.
- Existing auth_persistence / auth_user_endpoints / auth_publish / server / s3_backend suites pass.
- Clean under `cargo fmt`, `clippy`, `RUSTDOCFLAGS=-D warnings cargo doc`, **Dylint perfectionist**, and `taplo`.

## Docs
`backend.libsql` (incl. embedded replica) documented in the bundled `config.yaml` and the `pnpr` npm README, mirroring how the S3 backend was documented in #12198.
2026-06-05 09:15:17 +02:00

206 lines
10 KiB
TOML

[workspace]
resolver = "2"
members = ["pacquet/crates/*", "pacquet/tasks/*", "pnpr/crates/*"]
[workspace.package]
authors = ["Yagiz Nizipli <yagiz@nizipli.com"]
description = "Pacquet"
edition = "2024"
homepage = "https://github.com/pnpm/pacquet"
keywords = ["nodejs", "package", "manager", "pnpm", "npm"]
license = "MIT"
repository = "https://github.com/pnpm/pacquet"
[workspace.dependencies]
# Crates
pacquet-pnpr-client = { path = "pacquet/crates/pnpr-client" }
pacquet-catalogs-config = { path = "pacquet/crates/catalogs-config" }
pacquet-catalogs-protocol-parser = { path = "pacquet/crates/catalogs-protocol-parser" }
pacquet-catalogs-resolver = { path = "pacquet/crates/catalogs-resolver" }
pacquet-catalogs-types = { path = "pacquet/crates/catalogs-types" }
pacquet-cli = { path = "pacquet/crates/cli" }
pacquet-cmd-shim = { path = "pacquet/crates/cmd-shim" }
pacquet-crypto-hash = { path = "pacquet/crates/crypto-hash" }
pacquet-crypto-shasums-file = { path = "pacquet/crates/crypto-shasums-file" }
pacquet-engine-runtime-bun-resolver = { path = "pacquet/crates/engine-runtime-bun-resolver" }
pacquet-engine-runtime-deno-resolver = { path = "pacquet/crates/engine-runtime-deno-resolver" }
pacquet-engine-runtime-node-resolver = { path = "pacquet/crates/engine-runtime-node-resolver" }
pacquet-env-replace = { path = "pacquet/crates/env-replace" }
pacquet-fs = { path = "pacquet/crates/fs" }
pacquet-registry = { path = "pacquet/crates/registry" }
pacquet-tarball = { path = "pacquet/crates/tarball" }
pacquet-testing-utils = { path = "pacquet/crates/testing-utils" }
pacquet-package-manifest = { path = "pacquet/crates/package-manifest" }
pacquet-package-manager = { path = "pacquet/crates/package-manager" }
pacquet-package-is-installable = { path = "pacquet/crates/package-is-installable" }
pacquet-lockfile = { path = "pacquet/crates/lockfile" }
pacquet-lockfile-preferred-versions = { path = "pacquet/crates/lockfile-preferred-versions" }
pacquet-lockfile-verification = { path = "pacquet/crates/lockfile-verification" }
pacquet-modules-yaml = { path = "pacquet/crates/modules-yaml" }
pacquet-network = { path = "pacquet/crates/network" }
pacquet-config = { path = "pacquet/crates/config" }
pacquet-config-dir = { path = "pacquet/crates/config-dir" }
pacquet-config-parse-overrides = { path = "pacquet/crates/config-parse-overrides" }
pacquet-executor = { path = "pacquet/crates/executor" }
pacquet-exportable-manifest = { path = "pacquet/crates/exportable-manifest" }
pacquet-directory-fetcher = { path = "pacquet/crates/directory-fetcher" }
pacquet-git-fetcher = { path = "pacquet/crates/git-fetcher" }
pacquet-deps-path = { path = "pacquet/crates/deps-path" }
pacquet-detect-libc = { path = "pacquet/crates/detect-libc" }
pacquet-diagnostics = { path = "pacquet/crates/diagnostics" }
pacquet-graph-hasher = { path = "pacquet/crates/graph-hasher" }
pacquet-hooks = { path = "pacquet/crates/hooks" }
pacquet-store-dir = { path = "pacquet/crates/store-dir" }
pacquet-reporter = { path = "pacquet/crates/reporter" }
pacquet-patching = { path = "pacquet/crates/patching" }
pacquet-real-hoist = { path = "pacquet/crates/real-hoist" }
pacquet-resolving-default-resolver = { path = "pacquet/crates/resolving-default-resolver" }
pacquet-resolving-deps-resolver = { path = "pacquet/crates/resolving-deps-resolver" }
pacquet-resolving-git-resolver = { path = "pacquet/crates/resolving-git-resolver" }
pacquet-resolving-jsr-specifier-parser = { path = "pacquet/crates/resolving-jsr-specifier-parser" }
pacquet-resolving-local-resolver = { path = "pacquet/crates/resolving-local-resolver" }
pacquet-resolving-npm-resolver = { path = "pacquet/crates/resolving-npm-resolver" }
pacquet-resolving-parse-wanted-dependency = { path = "pacquet/crates/resolving-parse-wanted-dependency" }
pacquet-resolving-resolver-base = { path = "pacquet/crates/resolving-resolver-base" }
pacquet-resolving-tarball-resolver = { path = "pacquet/crates/resolving-tarball-resolver" }
pacquet-workspace = { path = "pacquet/crates/workspace" }
pacquet-workspace-manifest-writer = { path = "pacquet/crates/workspace-manifest-writer" }
pacquet-workspace-projects-filter = { path = "pacquet/crates/workspace-projects-filter" }
pacquet-workspace-projects-graph = { path = "pacquet/crates/workspace-projects-graph" }
pacquet-workspace-range-resolver = { path = "pacquet/crates/workspace-range-resolver" }
pacquet-workspace-spec = { path = "pacquet/crates/workspace-spec" }
pacquet-workspace-state = { path = "pacquet/crates/workspace-state" }
# Tasks
pacquet-registry-mock = { path = "pacquet/tasks/registry-mock" }
# Registry (sibling project — pnpm-compatible registry server)
pnpr = { path = "pnpr/crates/pnpr" }
pnpr-fixtures = { path = "pnpr/crates/pnpr-fixtures" }
# Dependencies
async-recursion = { version = "1.1.1" }
async-trait = { version = "0.1.83" }
axum = { version = "0.8.7", default-features = false, features = [
"http1",
"tokio",
"json",
"matched-path",
"original-uri",
] }
clap = { version = "4", features = ["derive", "string"] }
command-extra = { version = "1.0.0" }
base64 = { version = "0.22.1" }
bcrypt = { version = "0.19.1" }
bytes = { version = "1.11.0" }
chrono = { version = "0.4.44", default-features = false, features = ["clock"] }
dashmap = { version = "6.1.0" }
derive_more = { version = "2.1.1", features = ["full"] }
dialoguer = { version = "0.11.0", default-features = false }
diffy = { version = "0.5.0" }
dunce = { version = "1.0.5" }
home = { version = "0.5.12" }
httpdate = { version = "1.0.3" }
ignore = { version = "0.4.25" }
indexmap = { version = "2.14.0", features = ["serde"] }
insta = { version = "1.47.2", features = ["yaml", "glob", "walkdir"] }
itertools = { version = "0.14.0" }
libsql = { version = "0.9.30", default-features = false, features = ["core", "remote", "replication"] }
futures-util = { version = "0.3.32" }
flate2 = { version = "1.1.9" }
gethostname = { version = "1" }
getrandom = { version = "0.4.2" }
miette = { version = "7.6.0", features = ["fancy"] }
num_cpus = { version = "1.17.0" }
object_store = { version = "0.12", features = ["aws"] }
os_display = { version = "0.1.4" }
owo-colors = { version = "4", features = ["supports-colors"] }
reflink-copy = { version = "0.1.29" }
junction = { version = "2.0.0" }
libc = { version = "0.2.186" }
reqwest = { version = "0.13", default-features = false, features = [
"gzip",
"hickory-dns",
"json",
"rustls",
"socks",
"stream",
] }
node-semver = { version = "2.2.0" }
pathdiff = { version = "0.2.3" }
pipe-trait = { version = "0.4.0" }
rayon = { version = "1.12.0" }
rmp-serde = { version = "1.3.0" }
rusqlite = { version = "0.39.0", features = ["bundled"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = { version = "1.0.150", features = ["preserve_order"] }
serde-saphyr = { version = "0.0.26" }
# 0.11 removes the LowerHex impl on Output; revisit after upstream/consumers catch up
sha2 = { version = "0.10.9" }
smart-default = { version = "0.7.1" }
split-first-char = { version = "2.0.1" }
ssri = { version = "9.2.0" }
strum = { version = "0.28.0", features = ["derive"] }
sysinfo = { version = "0.39.2" }
tabled = { version = "0.17" }
tar = { version = "0.4.46" }
text-block-macros = { version = "0.2.0" }
tower = { version = "0.5.3" }
tower-http = { version = "0.6.11", features = ["compression-gzip", "trace"] }
tracing = { version = "0.1.44" }
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "fs", "io-util", "net", "signal", "sync"] }
walkdir = { version = "2.5.0" }
yaml_serde = { version = "0.10.4" }
yamlpatch = { version = "1.25.2" }
yamlpath = { version = "1.25.2" }
wax = { version = "0.7.0" }
which = { version = "8.0.2" }
zip = { version = "8", default-features = false, features = ["deflate"] }
zune-inflate = { version = "0.2.54" }
# Dev dependencies
assert_cmd = { version = "2.2.2" }
criterion = { version = "0.8.2", features = ["async_tokio"] }
pretty_assertions = { version = "1.4.1" }
project-root = { version = "0.2.2" }
tempfile = { version = "3.27.0" }
mockito = { version = "1.7.2" }
[workspace.metadata.workspaces]
allow_branch = "main"
# Declares the `dylint_lib = "perfectionist"` cfg used by the
# `cfg_attr(dylint_lib = "perfectionist", feature(register_tool))` /
# `register_tool(perfectionist)` lines at each pacquet crate's root.
# Those `cfg_attr`s register the perfectionist tool name under dylint's
# nightly toolchain (so `#[expect(perfectionist::lint, reason = "...")]`
# at use sites compiles cleanly without a wrapper); this `check-cfg`
# entry tells stable `cargo check` that the cfg name is expected even
# though it's never set off-dylint.
[workspace.lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(dylint_lib, values("perfectionist"))'] }
[workspace.lints.clippy]
clone_on_ref_ptr = "warn"
if_then_some_else_none = "warn"
needless_collect = "warn"
or_fun_call = "warn"
redundant_clone = "warn"
unnecessary_lazy_evaluations = "warn"
[profile.release]
opt-level = 3
lto = "fat"
codegen-units = 1
strip = "symbols"
debug = false
panic = "abort" # Let it crash and force ourselves to write safe Rust.
# Use the `--profile release-debug` flag to show symbols in release mode.
# e.g. `cargo build --profile release-debug`
[profile.release-debug]
inherits = "release"
strip = false
debug = true