A crafted lockfile alias could reach several install-time filesystem joins. With --trust-lockfile or a frozen lockfile, traversal segments could create links outside the intended project or node_modules boundary. This patch validates dependency names and every virtual-store slot before creating directories, links, bins, or hoisted entries.
Security boundary
A shared safe-join helper rejects traversal, absolute, platform-specific, and reserved dependency names before filesystem materialization.
Direct and transitive dependency links, package links, bin destinations, and public/private hoist destinations use the same containment rule.
Global virtual-store slots validate the complete slot path, including version-derived components, before directory creation.
Snapshot slots and package names are checked before store initialization and before the current-lockfile fast path, closing the warm-install bypass.
Before the patch, pacquet install --frozen-lockfile --trust-lockfile accepted a ../../escaped-link dependency key and created a symlink outside the project. With this patch, the same lockfile is rejected before materialization and no outside link is created.
Files changed
pacquet/crates/package-manager/src/safe_join_modules_dir.rs defines the shared containment rule.
Install, symlink, bin, hoist, virtual-store, and frozen-lockfile paths call that helper before filesystem materialization.
The corresponding tests.rs files cover every sink, including warm installs and global virtual-store slots.
Focused regressions cover direct and transitive aliases, bins, hoists, package names, global virtual-store version traversal, and a poisoned prior-install slot.
cargo fmt --all -- --check and git diff --check: passed.
Compatibility
Valid unscoped and scoped dependency aliases continue to work. The reproduced escape was specific to pacquet, so this branch does not change the TypeScript CLI or the lockfile format.