@0xrama/bnpm 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -0
- package/dist/package.json +45 -0
- package/dist/src/cache/archive.d.ts +15 -0
- package/dist/src/cache/archive.js +163 -0
- package/dist/src/cache/archive.js.map +1 -0
- package/dist/src/cache/commands.d.ts +15 -0
- package/dist/src/cache/commands.js +168 -0
- package/dist/src/cache/commands.js.map +1 -0
- package/dist/src/cache/quarantine.d.ts +18 -0
- package/dist/src/cache/quarantine.js +104 -0
- package/dist/src/cache/quarantine.js.map +1 -0
- package/dist/src/cache/store.d.ts +10 -0
- package/dist/src/cache/store.js +155 -0
- package/dist/src/cache/store.js.map +1 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +4 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/commands/diff.d.ts +34 -0
- package/dist/src/commands/diff.js +223 -0
- package/dist/src/commands/diff.js.map +1 -0
- package/dist/src/commands/doctor.d.ts +14 -0
- package/dist/src/commands/doctor.js +42 -0
- package/dist/src/commands/doctor.js.map +1 -0
- package/dist/src/commands/edit.d.ts +12 -0
- package/dist/src/commands/edit.js +100 -0
- package/dist/src/commands/edit.js.map +1 -0
- package/dist/src/commands/fund.d.ts +10 -0
- package/dist/src/commands/fund.js +32 -0
- package/dist/src/commands/fund.js.map +1 -0
- package/dist/src/commands/graph.d.ts +29 -0
- package/dist/src/commands/graph.js +90 -0
- package/dist/src/commands/graph.js.map +1 -0
- package/dist/src/commands/index.d.ts +20 -0
- package/dist/src/commands/index.js +1753 -0
- package/dist/src/commands/index.js.map +1 -0
- package/dist/src/commands/navigation.d.ts +2 -0
- package/dist/src/commands/navigation.js +47 -0
- package/dist/src/commands/navigation.js.map +1 -0
- package/dist/src/commands/outdated.d.ts +17 -0
- package/dist/src/commands/outdated.js +69 -0
- package/dist/src/commands/outdated.js.map +1 -0
- package/dist/src/commands/process.d.ts +11 -0
- package/dist/src/commands/process.js +189 -0
- package/dist/src/commands/process.js.map +1 -0
- package/dist/src/commands/prompts.d.ts +6 -0
- package/dist/src/commands/prompts.js +86 -0
- package/dist/src/commands/prompts.js.map +1 -0
- package/dist/src/commands/query.d.ts +48 -0
- package/dist/src/commands/query.js +639 -0
- package/dist/src/commands/query.js.map +1 -0
- package/dist/src/commands/rebuild.d.ts +16 -0
- package/dist/src/commands/rebuild.js +44 -0
- package/dist/src/commands/rebuild.js.map +1 -0
- package/dist/src/commands/sbom.d.ts +6 -0
- package/dist/src/commands/sbom.js +31 -0
- package/dist/src/commands/sbom.js.map +1 -0
- package/dist/src/commands/script-approvals.d.ts +22 -0
- package/dist/src/commands/script-approvals.js +128 -0
- package/dist/src/commands/script-approvals.js.map +1 -0
- package/dist/src/commands/shrinkwrap.d.ts +6 -0
- package/dist/src/commands/shrinkwrap.js +97 -0
- package/dist/src/commands/shrinkwrap.js.map +1 -0
- package/dist/src/commands/stage.d.ts +20 -0
- package/dist/src/commands/stage.js +40 -0
- package/dist/src/commands/stage.js.map +1 -0
- package/dist/src/commands/token.d.ts +2 -0
- package/dist/src/commands/token.js +47 -0
- package/dist/src/commands/token.js.map +1 -0
- package/dist/src/commands/trust.d.ts +5 -0
- package/dist/src/commands/trust.js +41 -0
- package/dist/src/commands/trust.js.map +1 -0
- package/dist/src/config/commands.d.ts +5 -0
- package/dist/src/config/commands.js +91 -0
- package/dist/src/config/commands.js.map +1 -0
- package/dist/src/config/configuration.d.ts +40 -0
- package/dist/src/config/configuration.js +157 -0
- package/dist/src/config/configuration.js.map +1 -0
- package/dist/src/config/interactive.d.ts +16 -0
- package/dist/src/config/interactive.js +19 -0
- package/dist/src/config/interactive.js.map +1 -0
- package/dist/src/config/paths.d.ts +22 -0
- package/dist/src/config/paths.js +46 -0
- package/dist/src/config/paths.js.map +1 -0
- package/dist/src/config/types.d.ts +15 -0
- package/dist/src/config/types.js +6 -0
- package/dist/src/config/types.js.map +1 -0
- package/dist/src/core/cli-parser.d.ts +102 -0
- package/dist/src/core/cli-parser.js +1148 -0
- package/dist/src/core/cli-parser.js.map +1 -0
- package/dist/src/core/cli-runner.d.ts +15 -0
- package/dist/src/core/cli-runner.js +261 -0
- package/dist/src/core/cli-runner.js.map +1 -0
- package/dist/src/core/exit-codes.d.ts +13 -0
- package/dist/src/core/exit-codes.js +13 -0
- package/dist/src/core/exit-codes.js.map +1 -0
- package/dist/src/core/output.d.ts +23 -0
- package/dist/src/core/output.js +123 -0
- package/dist/src/core/output.js.map +1 -0
- package/dist/src/installer/install.d.ts +73 -0
- package/dist/src/installer/install.js +596 -0
- package/dist/src/installer/install.js.map +1 -0
- package/dist/src/installer/mutations.d.ts +5 -0
- package/dist/src/installer/mutations.js +160 -0
- package/dist/src/installer/mutations.js.map +1 -0
- package/dist/src/linker/project-linker.d.ts +9 -0
- package/dist/src/linker/project-linker.js +223 -0
- package/dist/src/linker/project-linker.js.map +1 -0
- package/dist/src/lockfile/index.d.ts +27 -0
- package/dist/src/lockfile/index.js +221 -0
- package/dist/src/lockfile/index.js.map +1 -0
- package/dist/src/package/authoring.d.ts +31 -0
- package/dist/src/package/authoring.js +125 -0
- package/dist/src/package/authoring.js.map +1 -0
- package/dist/src/package/pack.d.ts +22 -0
- package/dist/src/package/pack.js +238 -0
- package/dist/src/package/pack.js.map +1 -0
- package/dist/src/package/provenance.d.ts +28 -0
- package/dist/src/package/provenance.js +162 -0
- package/dist/src/package/provenance.js.map +1 -0
- package/dist/src/package/publish.d.ts +28 -0
- package/dist/src/package/publish.js +189 -0
- package/dist/src/package/publish.js.map +1 -0
- package/dist/src/package/trusted-publishing.d.ts +7 -0
- package/dist/src/package/trusted-publishing.js +84 -0
- package/dist/src/package/trusted-publishing.js.map +1 -0
- package/dist/src/project/dependency-error.d.ts +3 -0
- package/dist/src/project/dependency-error.js +7 -0
- package/dist/src/project/dependency-error.js.map +1 -0
- package/dist/src/project/dev-engines.d.ts +10 -0
- package/dist/src/project/dev-engines.js +70 -0
- package/dist/src/project/dev-engines.js.map +1 -0
- package/dist/src/project/discovery.d.ts +6 -0
- package/dist/src/project/discovery.js +51 -0
- package/dist/src/project/discovery.js.map +1 -0
- package/dist/src/project/global.d.ts +3 -0
- package/dist/src/project/global.js +43 -0
- package/dist/src/project/global.js.map +1 -0
- package/dist/src/project/invalidation.d.ts +3 -0
- package/dist/src/project/invalidation.js +21 -0
- package/dist/src/project/invalidation.js.map +1 -0
- package/dist/src/project/link.d.ts +21 -0
- package/dist/src/project/link.js +152 -0
- package/dist/src/project/link.js.map +1 -0
- package/dist/src/project/manifest.d.ts +27 -0
- package/dist/src/project/manifest.js +378 -0
- package/dist/src/project/manifest.js.map +1 -0
- package/dist/src/project/pkg.d.ts +5 -0
- package/dist/src/project/pkg.js +94 -0
- package/dist/src/project/pkg.js.map +1 -0
- package/dist/src/project/recovery.d.ts +2 -0
- package/dist/src/project/recovery.js +82 -0
- package/dist/src/project/recovery.js.map +1 -0
- package/dist/src/project/workspaces.d.ts +4 -0
- package/dist/src/project/workspaces.js +59 -0
- package/dist/src/project/workspaces.js.map +1 -0
- package/dist/src/registry/account.d.ts +79 -0
- package/dist/src/registry/account.js +305 -0
- package/dist/src/registry/account.js.map +1 -0
- package/dist/src/registry/audit.d.ts +18 -0
- package/dist/src/registry/audit.js +74 -0
- package/dist/src/registry/audit.js.map +1 -0
- package/dist/src/registry/client.d.ts +19 -0
- package/dist/src/registry/client.js +102 -0
- package/dist/src/registry/client.js.map +1 -0
- package/dist/src/registry/configuration.d.ts +36 -0
- package/dist/src/registry/configuration.js +133 -0
- package/dist/src/registry/configuration.js.map +1 -0
- package/dist/src/registry/operations.d.ts +163 -0
- package/dist/src/registry/operations.js +502 -0
- package/dist/src/registry/operations.js.map +1 -0
- package/dist/src/registry/types.d.ts +30 -0
- package/dist/src/registry/types.js +2 -0
- package/dist/src/registry/types.js.map +1 -0
- package/dist/src/resolver/registry-resolver.d.ts +40 -0
- package/dist/src/resolver/registry-resolver.js +354 -0
- package/dist/src/resolver/registry-resolver.js.map +1 -0
- package/dist/src/resolver/source-provider.d.ts +17 -0
- package/dist/src/resolver/source-provider.js +336 -0
- package/dist/src/resolver/source-provider.js.map +1 -0
- package/dist/src/resolver/types.d.ts +28 -0
- package/dist/src/resolver/types.js +2 -0
- package/dist/src/resolver/types.js.map +1 -0
- package/dist/src/security/analyzer.d.ts +38 -0
- package/dist/src/security/analyzer.js +241 -0
- package/dist/src/security/analyzer.js.map +1 -0
- package/dist/src/security/audit.d.ts +19 -0
- package/dist/src/security/audit.js +61 -0
- package/dist/src/security/audit.js.map +1 -0
- package/dist/src/security/findings.d.ts +23 -0
- package/dist/src/security/findings.js +2 -0
- package/dist/src/security/findings.js.map +1 -0
- package/dist/src/security/integrity.d.ts +14 -0
- package/dist/src/security/integrity.js +40 -0
- package/dist/src/security/integrity.js.map +1 -0
- package/dist/src/security/policy.d.ts +16 -0
- package/dist/src/security/policy.js +24 -0
- package/dist/src/security/policy.js.map +1 -0
- package/dist/src/security/recent-release.d.ts +19 -0
- package/dist/src/security/recent-release.js +27 -0
- package/dist/src/security/recent-release.js.map +1 -0
- package/dist/src/security/script-runner.d.ts +13 -0
- package/dist/src/security/script-runner.js +73 -0
- package/dist/src/security/script-runner.js.map +1 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Better NPM
|
|
2
|
+
|
|
3
|
+
Better NPM (`bnpm`) is an npm-compatible package manager designed for fast installs and explicit supply-chain security decisions.
|
|
4
|
+
|
|
5
|
+
> Better NPM has a functional alpha command surface. It is not production-qualified yet; review the limitations below before using it for critical projects.
|
|
6
|
+
|
|
7
|
+
## Commands
|
|
8
|
+
|
|
9
|
+
- `bnpm install [spec...]` resolves and installs dependencies with bounded resolve, fetch/cache, byte-download, inspection, and linking progress; explicit specs are saved by default. Lockfile-only, non-mutating dry-run, and dev/optional/peer omit/include modes are supported.
|
|
10
|
+
- `bnpm ci` recreates the installation exactly from `bnpm-lock.yaml`.
|
|
11
|
+
- `bnpm add <spec...>` updates `package.json` and installs.
|
|
12
|
+
- `bnpm remove <name...>` removes dependencies and updates the installation.
|
|
13
|
+
- `bnpm update [name...]` refreshes all or selected dependencies within declared ranges.
|
|
14
|
+
- `bnpm outdated [name...]` reports current, wanted, and latest versions.
|
|
15
|
+
- `bnpm list [name...]` displays the installed graph; `bnpm why <name>` explains installation paths.
|
|
16
|
+
- `bnpm query <selector>` supports npm-style graph combinators, dependency groups, semver/manifest selectors, registry-enriched outdated and vulnerability filters, and expected-result assertions. `bnpm diff` emits verified unified patches for the local package or two registry, directory, remote-tarball, or secure Git sources with path, context, whitespace, prefix, and name-only controls; `bnpm find-dupes` reports duplicate identities.
|
|
17
|
+
- `bnpm bin`, `bnpm prefix`, and `bnpm root` print local or global installation paths.
|
|
18
|
+
- `bnpm run [--workspaces|--workspace <name>] [script] [-- args...]` lists scripts or analyzes and runs a project, selected workspaces, or every workspace; install mutations accept the same workspace selection and `--if-present` safely skips missing scripts. Scripts receive npm-compatible lifecycle and package environment variables.
|
|
19
|
+
- `bnpm test`, `start`, `stop`, `restart`, `install-test`, and `install-ci-test` provide npm-compatible script workflows.
|
|
20
|
+
- `bnpm audit` combines local static findings with npm registry advisories; `bnpm audit fix` applies safe in-range re-resolution and supports dry-run.
|
|
21
|
+
- `bnpm exec <bin> [-- args...]` runs an installed binary; repeated `--package <spec>` options install inspected packages ephemerally before execution.
|
|
22
|
+
- `bnpm explore <package> [-- command...]` runs a command inside an installed package.
|
|
23
|
+
- `bnpm edit <package>` opens a project-local installed instance and invalidates it for verified replacement on the next install.
|
|
24
|
+
- `bnpm pack [directory]` creates a deterministic npm-compatible tarball; use `--dry-run` or `--pack-destination` as needed.
|
|
25
|
+
- `bnpm publish [directory]` packs and publishes the exact verified artifact with scoped auth, tag, access, OTP, trusted-publisher OIDC, Sigstore provenance, and dry-run support.
|
|
26
|
+
- `bnpm stage` safely publishes, lists, inspects, downloads, approves, or rejects staged artifacts; `bnpm unpublish` handles exact registry removals.
|
|
27
|
+
- `bnpm access`, `owner`, `token`, `star`, `org`, `team`, `profile`, and `trust` expose bounded package and account administration, granular tokens, trusted publishers, passwords, and 2FA. Passwords use masked prompts and are never accepted as CLI operands.
|
|
28
|
+
- `bnpm login`, `bnpm logout`, and `bnpm whoami` manage web authentication, explicit legacy authentication, and identity.
|
|
29
|
+
- `bnpm view`, `bnpm search`, `bnpm dist-tag`, and `bnpm deprecate` provide bounded registry metadata and package-maintenance workflows.
|
|
30
|
+
- `bnpm init`/`bnpm create` creates default packages, safely registers new workspaces, or resolves and executes inspected `create-*` initializer packages with npm-compatible naming and argument forwarding. `bnpm version` runs npm-compatible authoring lifecycles, semantic/prerelease calculation, lockfile synchronization, transactional workspace selection, clean-tree checks, Git commits and tags (or manifest-only mode); `bnpm config` manages safe user settings.
|
|
31
|
+
- `bnpm shrinkwrap` exports the verified registry graph as deterministic npm lockfile v3 data.
|
|
32
|
+
- `bnpm approve-scripts` and `bnpm deny-scripts` explicitly mutate exact lockfile-bound lifecycle approvals.
|
|
33
|
+
- `bnpm prune`, `bnpm dedupe`, `bnpm rebuild`, `bnpm fund`, `bnpm cache add|ls|info|verify|clean`, `bnpm ping`, and `bnpm doctor` maintain and diagnose verified installations, storage, and registry access. Cache cleanup can target one package identity instead of deleting the entire store.
|
|
34
|
+
- `bnpm install-scripts approve|deny|ls|prune` manages exact integrity- and content-hash-bound lifecycle approvals; `approve-scripts` and `deny-scripts` remain concise aliases.
|
|
35
|
+
- `bnpm pkg`, `bnpm sbom`, `bnpm link`/`unlink`, and `bnpm completion` cover manifest updates, software bills of materials, live development links, and shell integration.
|
|
36
|
+
- `bnpmx <spec> [-- args...]` installs and inspects an ephemeral package, gives a decision-first summary of detected execution capabilities across its dependency graph, and requires confirmation before starting it. Use `bnpmx --details <spec>` for raw package and file evidence.
|
|
37
|
+
- `bnpmx check` scans every direct and transitive package recorded for the current project, reports runtime capabilities, lifecycle scripts, static findings, and registry advisories, and never executes package code.
|
|
38
|
+
|
|
39
|
+
Install, remove, update, outdated, list, why, audit, exec, bin, and prefix support `-g`/`--global` where applicable. Registry selection supports user/project `.npmrc`, scoped registries, path-scoped bearer/basic credentials, environment expansion, and one-shot `--registry` overrides. Dependency sources include registry packages, workspaces, bare, absolute, or `file:` local directories and package archives (saved canonically as relative `file:` requirements), HTTPS tarballs, and HTTPS/SSH Git repositories and hosted shortcuts, including semver tag selection, package subdirectories, validated recursive submodules, and approved prepare builds.
|
|
40
|
+
|
|
41
|
+
## Implemented safeguards
|
|
42
|
+
|
|
43
|
+
- Full-graph recent-publication checks with a first-use 1/6/24-hour policy, mature-version fallback during direct and transitive resolution, and exact non-cascading overrides.
|
|
44
|
+
- Bounded HTTPS metadata/tarball requests, redirects, retries, deadlines, archive sizes, and extraction ratios.
|
|
45
|
+
- Strong integrity verification before safe extraction and content-addressable promotion.
|
|
46
|
+
- Explainable high-confidence static findings for reverse shells, remote payload execution, miners, credential targeting, persistence, destructive commands, and obfuscated process execution.
|
|
47
|
+
- Pre-execution capability disclosure for ephemeral binaries, including AI-session history, credential/config access, network requests, local writes, subprocesses, and opaque native code.
|
|
48
|
+
- Lifecycle approval bound to package version, integrity, stage, command hash, and referenced-content hash.
|
|
49
|
+
- Deterministic lockfile security records, isolated package instances, offline/frozen installs, and interrupted-layout recovery.
|
|
50
|
+
|
|
51
|
+
Approved lifecycle scripts are **not sandboxed**. They run with the current user's normal operating-system permissions after an exact approval.
|
|
52
|
+
|
|
53
|
+
## Current limitations
|
|
54
|
+
|
|
55
|
+
- The implementation is still alpha. Initial live-project, adversarial-archive, concurrency, corruption-repair, and controlled performance checks pass, but the broader production matrix is not complete.
|
|
56
|
+
- The latest controlled isolated-cache fixture puts cold installs in pnpm's range while retaining static inspection; verified warm installs are about twice as fast. Results vary with registry conditions, and larger-project qualification remains incomplete.
|
|
57
|
+
- macOS is the primary development target. Linux and Windows interfaces exist, but neither is advertised as production-qualified.
|
|
58
|
+
- Native-platform release qualification is not complete. Web authentication is the default; password-only registries require the explicit `--auth-type=legacy` flow.
|
|
59
|
+
|
|
60
|
+
## Principles
|
|
61
|
+
|
|
62
|
+
- Resolve and install packages without delegating to another package manager.
|
|
63
|
+
- Use a global content-addressable store and isolated symlink layout.
|
|
64
|
+
- Quarantine and inspect package archives before linking them into a project.
|
|
65
|
+
- Never run dependency lifecycle scripts without informed approval.
|
|
66
|
+
- Warn about newly published versions throughout the dependency graph.
|
|
67
|
+
- Produce concrete, explainable findings rather than an opaque risk score.
|
|
68
|
+
|
|
69
|
+
## Development
|
|
70
|
+
|
|
71
|
+
Requires Node.js 22.22.2 or newer.
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
npm install
|
|
75
|
+
npm run check
|
|
76
|
+
npm test
|
|
77
|
+
npm run build
|
|
78
|
+
npm run benchmark
|
|
79
|
+
npm run verify:package
|
|
80
|
+
node dist/src/cli.js --help
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
See [`docs/product-spec.md`](docs/product-spec.md), [`docs/threat-model.md`](docs/threat-model.md), [`docs/architecture.md`](docs/architecture.md), and [`docs/releasing.md`](docs/releasing.md).
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@0xrama/bnpm",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A fast, security-focused npm-compatible package manager",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"bnpm": "dist/src/cli.js",
|
|
8
|
+
"bnpmx": "dist/src/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/src",
|
|
12
|
+
"dist/package.json",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22.22.2"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -p tsconfig.json",
|
|
23
|
+
"check": "tsc -p tsconfig.json --noEmit",
|
|
24
|
+
"test": "npm run build && node --test dist/test/*.test.js",
|
|
25
|
+
"benchmark": "npm run build && node scripts/benchmark.mjs",
|
|
26
|
+
"verify:package": "npm run build && node scripts/verify-package.mjs",
|
|
27
|
+
"start": "node dist/src/cli.js"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"npm-package-arg": "13.0.2",
|
|
31
|
+
"node-gyp": "13.0.1",
|
|
32
|
+
"npm-packlist": "11.3.0",
|
|
33
|
+
"semver": "7.8.5",
|
|
34
|
+
"sigstore": "5.0.0",
|
|
35
|
+
"tar-stream": "3.2.0",
|
|
36
|
+
"yaml": "^2.9.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^22.0.0",
|
|
40
|
+
"@types/npm-package-arg": "6.1.4",
|
|
41
|
+
"@types/semver": "7.7.1",
|
|
42
|
+
"@types/tar-stream": "3.1.4",
|
|
43
|
+
"typescript": "^7.0.2"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ArchiveLimits {
|
|
2
|
+
readonly maxEntries?: number;
|
|
3
|
+
readonly maxExpandedBytes?: number;
|
|
4
|
+
readonly maxFileBytes?: number;
|
|
5
|
+
readonly maxCompressionRatio?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface ExtractedArchive {
|
|
8
|
+
readonly path: string;
|
|
9
|
+
readonly entries: number;
|
|
10
|
+
readonly expandedBytes: number;
|
|
11
|
+
}
|
|
12
|
+
export declare class ArchiveError extends Error {
|
|
13
|
+
constructor(message: string);
|
|
14
|
+
}
|
|
15
|
+
export declare function extractPackageArchive(tarballPath: string, destination: string, limits?: ArchiveLimits): Promise<ExtractedArchive>;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import { mkdir, open, realpath, rm, stat } from "node:fs/promises";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { dirname, join, posix, resolve, sep } from "node:path";
|
|
5
|
+
import { createGunzip } from "node:zlib";
|
|
6
|
+
import tar from "tar-stream";
|
|
7
|
+
export class ArchiveError extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(`Archive error: ${message}`);
|
|
10
|
+
this.name = "ArchiveError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const defaults = {
|
|
14
|
+
maxEntries: 100_000,
|
|
15
|
+
maxExpandedBytes: 1024 * 1024 * 1024,
|
|
16
|
+
maxFileBytes: 128 * 1024 * 1024,
|
|
17
|
+
maxCompressionRatio: 200,
|
|
18
|
+
};
|
|
19
|
+
function safeRelativePath(name, expectedPrefix) {
|
|
20
|
+
if (name.includes("\0") || name.includes("\\") || name.startsWith("/") || /^[A-Za-z]:/.test(name)) {
|
|
21
|
+
throw new ArchiveError(`unsafe entry path ${JSON.stringify(name)}`);
|
|
22
|
+
}
|
|
23
|
+
const normalized = posix.normalize(name);
|
|
24
|
+
if (normalized === ".." || normalized.startsWith("../"))
|
|
25
|
+
throw new ArchiveError(`entry escapes package root: ${name}`);
|
|
26
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
27
|
+
const prefix = parts[0];
|
|
28
|
+
if (!prefix)
|
|
29
|
+
throw new ArchiveError(`archive entry has no package prefix: ${name}`);
|
|
30
|
+
if (expectedPrefix !== undefined && prefix !== expectedPrefix)
|
|
31
|
+
throw new ArchiveError(`entry is outside the archive package prefix: ${name}`);
|
|
32
|
+
const relative = parts.slice(1).join("/");
|
|
33
|
+
return relative.length === 0 ? { prefix } : { prefix, relative };
|
|
34
|
+
}
|
|
35
|
+
function contained(root, relative) {
|
|
36
|
+
const target = resolve(root, ...relative.split("/"));
|
|
37
|
+
if (target !== root && !target.startsWith(`${root}${sep}`))
|
|
38
|
+
throw new ArchiveError(`entry escapes extraction root: ${relative}`);
|
|
39
|
+
return target;
|
|
40
|
+
}
|
|
41
|
+
export async function extractPackageArchive(tarballPath, destination, limits = {}) {
|
|
42
|
+
const compressedBytes = (await stat(tarballPath)).size;
|
|
43
|
+
const maxEntries = limits.maxEntries ?? defaults.maxEntries;
|
|
44
|
+
const maxExpandedBytes = limits.maxExpandedBytes ?? defaults.maxExpandedBytes;
|
|
45
|
+
const maxFileBytes = limits.maxFileBytes ?? defaults.maxFileBytes;
|
|
46
|
+
const maxCompressionRatio = limits.maxCompressionRatio ?? defaults.maxCompressionRatio;
|
|
47
|
+
await mkdir(destination, { recursive: false, mode: 0o700 });
|
|
48
|
+
const root = await realpath(destination);
|
|
49
|
+
let entries = 0;
|
|
50
|
+
let expandedBytes = 0;
|
|
51
|
+
let archivePrefix;
|
|
52
|
+
const seen = new Map();
|
|
53
|
+
const extract = tar.extract();
|
|
54
|
+
let rejectCompletion = () => undefined;
|
|
55
|
+
const completion = new Promise((resolvePromise, reject) => {
|
|
56
|
+
rejectCompletion = reject;
|
|
57
|
+
extract.once("finish", resolvePromise);
|
|
58
|
+
extract.once("error", reject);
|
|
59
|
+
extract.on("entry", (header, stream, next) => {
|
|
60
|
+
void (async () => {
|
|
61
|
+
entries += 1;
|
|
62
|
+
if (entries > maxEntries)
|
|
63
|
+
throw new ArchiveError(`archive exceeds ${maxEntries} entries`);
|
|
64
|
+
const safePath = safeRelativePath(header.name, archivePrefix);
|
|
65
|
+
archivePrefix ??= safePath.prefix;
|
|
66
|
+
const relative = safePath.relative;
|
|
67
|
+
if (relative === undefined) {
|
|
68
|
+
stream.resume();
|
|
69
|
+
stream.once("end", next);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const target = contained(root, relative);
|
|
73
|
+
const prior = seen.get(relative);
|
|
74
|
+
if (header.type === "directory") {
|
|
75
|
+
if (prior && prior.type !== "directory")
|
|
76
|
+
throw new ArchiveError(`conflicting duplicate archive entry ${relative}`);
|
|
77
|
+
seen.set(relative, { type: "directory", size: 0, executable: false });
|
|
78
|
+
await mkdir(target, { recursive: true, mode: 0o755 });
|
|
79
|
+
stream.resume();
|
|
80
|
+
stream.once("end", next);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (header.type !== "file")
|
|
84
|
+
throw new ArchiveError(`unsupported ${header.type} entry ${relative}`);
|
|
85
|
+
const declaredSize = header.size ?? 0;
|
|
86
|
+
const executable = ((header.mode ?? 0) & 0o111) !== 0;
|
|
87
|
+
if (declaredSize > maxFileBytes)
|
|
88
|
+
throw new ArchiveError(`${relative} exceeds the per-file size limit`);
|
|
89
|
+
if (prior) {
|
|
90
|
+
if (prior.type !== "file" || prior.size !== declaredSize || prior.executable !== executable) {
|
|
91
|
+
throw new ArchiveError(`conflicting duplicate archive entry ${relative}`);
|
|
92
|
+
}
|
|
93
|
+
const duplicateHash = createHash("sha256");
|
|
94
|
+
let duplicateBytes = 0;
|
|
95
|
+
for await (const value of stream) {
|
|
96
|
+
const chunk = Buffer.from(value);
|
|
97
|
+
duplicateBytes += chunk.length;
|
|
98
|
+
expandedBytes += chunk.length;
|
|
99
|
+
if (duplicateBytes > maxFileBytes || expandedBytes > maxExpandedBytes)
|
|
100
|
+
throw new ArchiveError("archive exceeds expanded-size limits");
|
|
101
|
+
if (compressedBytes === 0 || expandedBytes > compressedBytes * maxCompressionRatio) {
|
|
102
|
+
throw new ArchiveError(`archive exceeds the ${maxCompressionRatio}:1 compression-ratio limit`);
|
|
103
|
+
}
|
|
104
|
+
duplicateHash.update(chunk);
|
|
105
|
+
}
|
|
106
|
+
if (duplicateBytes !== declaredSize || duplicateHash.digest("hex") !== prior.digest) {
|
|
107
|
+
throw new ArchiveError(`conflicting duplicate archive entry ${relative}`);
|
|
108
|
+
}
|
|
109
|
+
next();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
await mkdir(dirname(target), { recursive: true, mode: 0o755 });
|
|
113
|
+
const file = await open(target, "wx", executable ? 0o755 : 0o644);
|
|
114
|
+
const contentHash = createHash("sha256");
|
|
115
|
+
try {
|
|
116
|
+
let fileBytes = 0;
|
|
117
|
+
for await (const value of stream) {
|
|
118
|
+
const chunk = Buffer.from(value);
|
|
119
|
+
fileBytes += chunk.length;
|
|
120
|
+
expandedBytes += chunk.length;
|
|
121
|
+
if (fileBytes > maxFileBytes || expandedBytes > maxExpandedBytes)
|
|
122
|
+
throw new ArchiveError("archive exceeds expanded-size limits");
|
|
123
|
+
if (compressedBytes === 0 || expandedBytes > compressedBytes * maxCompressionRatio) {
|
|
124
|
+
throw new ArchiveError(`archive exceeds the ${maxCompressionRatio}:1 compression-ratio limit`);
|
|
125
|
+
}
|
|
126
|
+
contentHash.update(chunk);
|
|
127
|
+
await file.write(chunk);
|
|
128
|
+
}
|
|
129
|
+
if (fileBytes !== declaredSize)
|
|
130
|
+
throw new ArchiveError(`archive entry size mismatch for ${relative}`);
|
|
131
|
+
seen.set(relative, { type: "file", size: fileBytes, executable, digest: contentHash.digest("hex") });
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
await file.close();
|
|
135
|
+
}
|
|
136
|
+
next();
|
|
137
|
+
})().catch((error) => {
|
|
138
|
+
stream.resume();
|
|
139
|
+
extract.destroy();
|
|
140
|
+
rejectCompletion(error);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
const source = createReadStream(tarballPath);
|
|
145
|
+
const gunzip = createGunzip();
|
|
146
|
+
source.once("error", (error) => extract.destroy(error));
|
|
147
|
+
gunzip.once("error", (error) => extract.destroy(error));
|
|
148
|
+
source.pipe(gunzip).pipe(extract);
|
|
149
|
+
try {
|
|
150
|
+
await completion;
|
|
151
|
+
await stat(join(root, "package.json"));
|
|
152
|
+
return { path: root, entries, expandedBytes };
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
source.destroy();
|
|
156
|
+
gunzip.destroy();
|
|
157
|
+
await rm(destination, { recursive: true, force: true });
|
|
158
|
+
if (error.code === "ENOENT")
|
|
159
|
+
throw new ArchiveError("archive does not contain package/package.json");
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=archive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive.js","sourceRoot":"","sources":["../../../src/cache/archive.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { BnpmPaths } from "../config/paths.js";
|
|
2
|
+
export interface CacheVerification {
|
|
3
|
+
readonly entries: number;
|
|
4
|
+
readonly valid: number;
|
|
5
|
+
readonly corrupt: readonly string[];
|
|
6
|
+
}
|
|
7
|
+
export interface CacheEntry {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly integrity: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function listCache(paths: BnpmPaths, filter?: string): Promise<readonly CacheEntry[]>;
|
|
12
|
+
export declare function verifyCache(paths: BnpmPaths): Promise<CacheVerification>;
|
|
13
|
+
export declare function ensureCacheOwnership(paths: BnpmPaths): Promise<void>;
|
|
14
|
+
export declare function cleanCache(paths: BnpmPaths): Promise<readonly string[]>;
|
|
15
|
+
export declare function cleanCacheEntries(paths: BnpmPaths, filter: string): Promise<readonly CacheEntry[]>;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, resolve } from "node:path";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { verifyStoreEntry } from "./store.js";
|
|
5
|
+
import { storePath } from "./store.js";
|
|
6
|
+
export async function listCache(paths, filter) {
|
|
7
|
+
const entries = [];
|
|
8
|
+
try {
|
|
9
|
+
for (const algorithm of await readdir(paths.store, { withFileTypes: true })) {
|
|
10
|
+
if (!algorithm.isDirectory())
|
|
11
|
+
continue;
|
|
12
|
+
const parent = resolve(paths.store, algorithm.name);
|
|
13
|
+
for (const digest of await readdir(parent, { withFileTypes: true })) {
|
|
14
|
+
if (!digest.isDirectory() || digest.name.endsWith(".lock") || digest.name.includes(".tmp"))
|
|
15
|
+
continue;
|
|
16
|
+
const root = resolve(parent, digest.name);
|
|
17
|
+
try {
|
|
18
|
+
const metadata = JSON.parse(await readFile(resolve(root, ".bnpm-store.json"), "utf8"));
|
|
19
|
+
const manifest = JSON.parse(await readFile(resolve(root, "package.json"), "utf8"));
|
|
20
|
+
if (typeof metadata.integrity !== "string" || typeof manifest.name !== "string" || typeof manifest.version !== "string")
|
|
21
|
+
continue;
|
|
22
|
+
const id = `${manifest.name}@${manifest.version}`;
|
|
23
|
+
if (filter === undefined || id === filter || manifest.name === filter)
|
|
24
|
+
entries.push({ id, integrity: metadata.integrity });
|
|
25
|
+
if (entries.length > 100_000)
|
|
26
|
+
throw new Error("Cache contains more than 100000 store entries");
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error.code !== "ENOENT")
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error.code === "ENOENT")
|
|
37
|
+
return [];
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
return entries.sort((left, right) => left.id.localeCompare(right.id) || left.integrity.localeCompare(right.integrity));
|
|
41
|
+
}
|
|
42
|
+
export async function verifyCache(paths) {
|
|
43
|
+
const entries = [];
|
|
44
|
+
try {
|
|
45
|
+
for (const algorithm of await readdir(paths.store, { withFileTypes: true })) {
|
|
46
|
+
if (!algorithm.isDirectory())
|
|
47
|
+
continue;
|
|
48
|
+
const parent = resolve(paths.store, algorithm.name);
|
|
49
|
+
for (const digest of await readdir(parent, { withFileTypes: true })) {
|
|
50
|
+
if (!digest.isDirectory() || digest.name.endsWith(".lock") || digest.name.includes(".tmp"))
|
|
51
|
+
continue;
|
|
52
|
+
entries.push(resolve(parent, digest.name));
|
|
53
|
+
if (entries.length > 100_000)
|
|
54
|
+
throw new Error("Cache contains more than 100000 store entries");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (error.code === "ENOENT")
|
|
60
|
+
return { entries: 0, valid: 0, corrupt: [] };
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
const corrupt = [];
|
|
64
|
+
let valid = 0;
|
|
65
|
+
for (const entry of entries.sort()) {
|
|
66
|
+
try {
|
|
67
|
+
const metadata = JSON.parse(await readFile(resolve(entry, ".bnpm-store.json"), "utf8"));
|
|
68
|
+
if (typeof metadata.integrity !== "string" || !await verifyStoreEntry(entry, metadata.integrity))
|
|
69
|
+
corrupt.push(entry);
|
|
70
|
+
else
|
|
71
|
+
valid += 1;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
corrupt.push(entry);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { entries: entries.length, valid, corrupt };
|
|
78
|
+
}
|
|
79
|
+
function confined(path, expected) {
|
|
80
|
+
const absolute = resolve(path);
|
|
81
|
+
if (basename(absolute) !== expected || absolute === resolve("/") || dirname(absolute) === absolute)
|
|
82
|
+
throw new Error(`Refusing unsafe cache path ${absolute}`);
|
|
83
|
+
}
|
|
84
|
+
function cacheRoot(paths) {
|
|
85
|
+
const parents = [paths.store, paths.cache, paths.quarantine].map((path) => dirname(resolve(path)));
|
|
86
|
+
if (!parents.every((parent) => parent === parents[0]))
|
|
87
|
+
throw new Error("Refusing cache paths that do not share one root");
|
|
88
|
+
const root = parents[0] ?? resolve("/");
|
|
89
|
+
if (root === resolve("/") || (process.env.HOME && root === resolve(process.env.HOME)))
|
|
90
|
+
throw new Error(`Refusing unsafe cache root ${root}`);
|
|
91
|
+
return root;
|
|
92
|
+
}
|
|
93
|
+
export async function ensureCacheOwnership(paths) {
|
|
94
|
+
const root = cacheRoot(paths);
|
|
95
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
96
|
+
const marker = resolve(root, ".bnpm-cache-root");
|
|
97
|
+
try {
|
|
98
|
+
await writeFile(marker, "bnpm-cache-v1\n", { flag: "wx", mode: 0o600 });
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (error.code !== "EEXIST")
|
|
102
|
+
throw error;
|
|
103
|
+
if (await readFile(marker, "utf8") !== "bnpm-cache-v1\n")
|
|
104
|
+
throw new Error(`Cache ownership marker is invalid at ${marker}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export async function cleanCache(paths) {
|
|
108
|
+
const targets = [[paths.store, "store"], [paths.cache, "cache"], [paths.quarantine, "quarantine"]];
|
|
109
|
+
for (const [path, expected] of targets)
|
|
110
|
+
confined(path, expected);
|
|
111
|
+
const root = cacheRoot(paths);
|
|
112
|
+
if (await readFile(resolve(root, ".bnpm-cache-root"), "utf8").catch(() => "") !== "bnpm-cache-v1\n")
|
|
113
|
+
throw new Error(`Refusing unowned cache root ${root}`);
|
|
114
|
+
const removed = [];
|
|
115
|
+
for (const [path] of targets) {
|
|
116
|
+
const trash = `${path}.clean-${process.pid}-${randomBytes(6).toString("hex")}`;
|
|
117
|
+
try {
|
|
118
|
+
await rename(path, trash);
|
|
119
|
+
removed.push(path);
|
|
120
|
+
await rm(trash, { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (error.code !== "ENOENT")
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
await mkdir(path, { recursive: true, mode: 0o700 });
|
|
127
|
+
}
|
|
128
|
+
return removed;
|
|
129
|
+
}
|
|
130
|
+
async function makeRemovable(path) {
|
|
131
|
+
try {
|
|
132
|
+
await chmod(path, 0o700);
|
|
133
|
+
for (const entry of await readdir(path, { withFileTypes: true })) {
|
|
134
|
+
const child = resolve(path, entry.name);
|
|
135
|
+
if (entry.isDirectory())
|
|
136
|
+
await makeRemovable(child);
|
|
137
|
+
else
|
|
138
|
+
await chmod(child, 0o600);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (error.code !== "ENOENT")
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
export async function cleanCacheEntries(paths, filter) {
|
|
147
|
+
if (!filter || filter.includes("\0"))
|
|
148
|
+
throw new Error("A cache package name or exact name@version is required");
|
|
149
|
+
const root = cacheRoot(paths);
|
|
150
|
+
if (await readFile(resolve(root, ".bnpm-cache-root"), "utf8").catch(() => "") !== "bnpm-cache-v1\n")
|
|
151
|
+
throw new Error(`Refusing unowned cache root ${root}`);
|
|
152
|
+
const entries = await listCache(paths, filter);
|
|
153
|
+
for (const entry of entries) {
|
|
154
|
+
const stored = storePath(paths.store, entry.integrity);
|
|
155
|
+
const trash = `${stored}.clean-${process.pid}-${randomBytes(6).toString("hex")}`;
|
|
156
|
+
try {
|
|
157
|
+
await rename(stored, trash);
|
|
158
|
+
await makeRemovable(trash);
|
|
159
|
+
await rm(trash, { recursive: true, force: true });
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
if (error.code !== "ENOENT")
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return entries;
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=commands.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commands.js","sourceRoot":"","sources":["../../../src/cache/commands.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface QuarantineOptions {
|
|
2
|
+
root: string;
|
|
3
|
+
maxCompressedBytes?: number;
|
|
4
|
+
fetch?: typeof globalThis.fetch;
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
maxRedirects?: number;
|
|
7
|
+
maxRetries?: number;
|
|
8
|
+
timeoutMilliseconds?: number;
|
|
9
|
+
headers?: (url: URL) => Readonly<Record<string, string>>;
|
|
10
|
+
onProgress?: (bytes: number, totalBytes?: number) => void;
|
|
11
|
+
}
|
|
12
|
+
export interface QuarantinedTarball {
|
|
13
|
+
path: string;
|
|
14
|
+
bytes: number;
|
|
15
|
+
integrity: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function downloadToQuarantine(tarball: URL, integrity: string, options: QuarantineOptions): Promise<QuarantinedTarball>;
|
|
18
|
+
export declare function downloadUnverifiedToQuarantine(tarball: URL, options: QuarantineOptions): Promise<QuarantinedTarball>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, open, rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { assertDigest, selectExpectedIntegrity } from "../security/integrity.js";
|
|
5
|
+
const defaultMaxCompressedBytes = 100 * 1024 * 1024;
|
|
6
|
+
export async function downloadToQuarantine(tarball, integrity, options) {
|
|
7
|
+
return download(tarball, options, integrity);
|
|
8
|
+
}
|
|
9
|
+
export async function downloadUnverifiedToQuarantine(tarball, options) {
|
|
10
|
+
return download(tarball, options);
|
|
11
|
+
}
|
|
12
|
+
async function download(tarball, options, integrity) {
|
|
13
|
+
if (tarball.protocol !== "https:")
|
|
14
|
+
throw new Error("Tarball URL must use HTTPS");
|
|
15
|
+
const expected = integrity === undefined ? undefined : selectExpectedIntegrity(integrity);
|
|
16
|
+
const maxBytes = options.maxCompressedBytes ?? defaultMaxCompressedBytes;
|
|
17
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
18
|
+
await mkdir(options.root, { recursive: true });
|
|
19
|
+
const directory = join(options.root, randomUUID());
|
|
20
|
+
const path = join(directory, "package.tgz");
|
|
21
|
+
await mkdir(directory, { mode: 0o700 });
|
|
22
|
+
let file;
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetchTarball(tarball, fetchImpl, options);
|
|
25
|
+
if (!response.ok || !response.body) {
|
|
26
|
+
throw new Error(`Tarball download failed with ${response.status}`);
|
|
27
|
+
}
|
|
28
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
29
|
+
if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
|
|
30
|
+
throw new Error(`Tarball exceeds the ${maxBytes} byte compressed-size limit`);
|
|
31
|
+
}
|
|
32
|
+
file = await open(path, "wx", 0o600);
|
|
33
|
+
const hash = createHash(expected?.algorithm ?? "sha512");
|
|
34
|
+
let bytes = 0;
|
|
35
|
+
options.onProgress?.(0, Number.isFinite(declaredLength) && declaredLength >= 0 ? declaredLength : undefined);
|
|
36
|
+
for await (const value of response.body) {
|
|
37
|
+
const chunk = Buffer.from(value);
|
|
38
|
+
bytes += chunk.length;
|
|
39
|
+
if (bytes > maxBytes)
|
|
40
|
+
throw new Error(`Tarball exceeds the ${maxBytes} byte compressed-size limit`);
|
|
41
|
+
hash.update(chunk);
|
|
42
|
+
await writeAll(file, chunk);
|
|
43
|
+
options.onProgress?.(bytes, Number.isFinite(declaredLength) && declaredLength >= 0 ? declaredLength : undefined);
|
|
44
|
+
}
|
|
45
|
+
const digest = hash.digest();
|
|
46
|
+
if (expected)
|
|
47
|
+
assertDigest(expected, digest);
|
|
48
|
+
await file.close();
|
|
49
|
+
file = undefined;
|
|
50
|
+
return { path, bytes, integrity: expected?.serialized ?? `sha512-${digest.toString("base64")}` };
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
await file?.close().catch(() => undefined);
|
|
54
|
+
await rm(directory, { recursive: true, force: true });
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function fetchTarball(url, fetchImpl, options) {
|
|
59
|
+
const maxRetries = options.maxRetries ?? 2;
|
|
60
|
+
const maxRedirects = options.maxRedirects ?? 5;
|
|
61
|
+
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
|
|
62
|
+
let current = url;
|
|
63
|
+
try {
|
|
64
|
+
for (let redirects = 0; redirects <= maxRedirects; redirects += 1) {
|
|
65
|
+
const timeout = AbortSignal.timeout(options.timeoutMilliseconds ?? 60_000);
|
|
66
|
+
const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
|
|
67
|
+
const headers = options.headers?.(current);
|
|
68
|
+
const response = await fetchImpl(current, { ...(headers === undefined ? {} : { headers }), redirect: "manual", signal });
|
|
69
|
+
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
70
|
+
if (redirects === maxRedirects)
|
|
71
|
+
throw new Error("Tarball download exceeded the redirect limit");
|
|
72
|
+
const location = response.headers.get("location");
|
|
73
|
+
if (!location)
|
|
74
|
+
throw new Error("Tarball redirect did not provide a location");
|
|
75
|
+
current = new URL(location, current);
|
|
76
|
+
if (current.protocol !== "https:")
|
|
77
|
+
throw new Error("Tarball redirect must use HTTPS");
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if ([408, 429, 500, 502, 503, 504].includes(response.status) && attempt < maxRetries) {
|
|
81
|
+
await response.body?.cancel();
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
return response;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (attempt === maxRetries || options.signal?.aborted || (error instanceof Error && error.message.includes("redirect")))
|
|
89
|
+
throw error;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
throw new Error("Tarball download failed after bounded retries");
|
|
94
|
+
}
|
|
95
|
+
async function writeAll(file, buffer) {
|
|
96
|
+
let offset = 0;
|
|
97
|
+
while (offset < buffer.length) {
|
|
98
|
+
const { bytesWritten } = await file.write(buffer, offset, buffer.length - offset);
|
|
99
|
+
if (bytesWritten === 0)
|
|
100
|
+
throw new Error("Unable to write quarantined tarball");
|
|
101
|
+
offset += bytesWritten;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=quarantine.js.map
|