pacote 13.3.0 → 13.4.0
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 +4 -0
- package/lib/registry.js +114 -77
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -168,6 +168,10 @@ resolved, and other properties, as they are determined.
|
|
|
168
168
|
is unlikely to change in the span of a single command.
|
|
169
169
|
* `silent` A boolean that determines whether the banner is displayed
|
|
170
170
|
when calling `@npmcli/run-script`.
|
|
171
|
+
* `verifySignatures` A boolean that will make pacote verify the
|
|
172
|
+
integrity signature of a manifest, if present. There must be a
|
|
173
|
+
configured `_keys` entry in the config that is scoped to the
|
|
174
|
+
registry the manifest is being fetched from.
|
|
171
175
|
|
|
172
176
|
|
|
173
177
|
### Advanced API
|
package/lib/registry.js
CHANGED
|
@@ -7,6 +7,7 @@ const npa = require('npm-package-arg')
|
|
|
7
7
|
const rpj = require('read-package-json-fast')
|
|
8
8
|
const pickManifest = require('npm-pick-manifest')
|
|
9
9
|
const ssri = require('ssri')
|
|
10
|
+
const crypto = require('crypto')
|
|
10
11
|
|
|
11
12
|
// Corgis are cute. 🐕🐶
|
|
12
13
|
const corgiDoc = 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*'
|
|
@@ -14,8 +15,6 @@ const fullDoc = 'application/json'
|
|
|
14
15
|
|
|
15
16
|
const fetch = require('npm-registry-fetch')
|
|
16
17
|
|
|
17
|
-
// TODO: memoize reg requests, so we don't even have to check cache
|
|
18
|
-
|
|
19
18
|
const _headers = Symbol('_headers')
|
|
20
19
|
class RegistryFetcher extends Fetcher {
|
|
21
20
|
constructor (spec, opts) {
|
|
@@ -39,28 +38,30 @@ class RegistryFetcher extends Fetcher {
|
|
|
39
38
|
this.packumentUrl = removeTrailingSlashes(this.registry) + '/' +
|
|
40
39
|
this.spec.escapedName
|
|
41
40
|
|
|
41
|
+
const parsed = new URL(this.registry)
|
|
42
|
+
const regKey = `//${parsed.host}${parsed.pathname}`
|
|
43
|
+
// unlike the nerf-darted auth keys, this one does *not* allow a mismatch
|
|
44
|
+
// of trailing slashes. It must match exactly.
|
|
45
|
+
if (this.opts[`${regKey}:_keys`]) {
|
|
46
|
+
this.registryKeys = this.opts[`${regKey}:_keys`]
|
|
47
|
+
}
|
|
48
|
+
|
|
42
49
|
// XXX pacote <=9 has some logic to ignore opts.resolved if
|
|
43
50
|
// the resolved URL doesn't go to the same registry.
|
|
44
51
|
// Consider reproducing that here, to throw away this.resolved
|
|
45
52
|
// in that case.
|
|
46
53
|
}
|
|
47
54
|
|
|
48
|
-
resolve () {
|
|
49
|
-
if
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// fetching the manifest sets resolved and (usually) integrity
|
|
54
|
-
return this.manifest().then(() => {
|
|
55
|
-
if (this.resolved) {
|
|
56
|
-
return this.resolved
|
|
57
|
-
}
|
|
58
|
-
|
|
55
|
+
async resolve () {
|
|
56
|
+
// fetching the manifest sets resolved and (if present) integrity
|
|
57
|
+
await this.manifest()
|
|
58
|
+
if (!this.resolved) {
|
|
59
59
|
throw Object.assign(
|
|
60
60
|
new Error('Invalid package manifest: no `dist.tarball` field'),
|
|
61
61
|
{ package: this.spec.toString() }
|
|
62
62
|
)
|
|
63
|
-
}
|
|
63
|
+
}
|
|
64
|
+
return this.resolved
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
[_headers] () {
|
|
@@ -87,91 +88,127 @@ class RegistryFetcher extends Fetcher {
|
|
|
87
88
|
// npm-registry-fetch the packument
|
|
88
89
|
// set the appropriate header for corgis if fullMetadata isn't set
|
|
89
90
|
// return the res.json() promise
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
try {
|
|
92
|
+
const res = await fetch(this.packumentUrl, {
|
|
93
|
+
...this.opts,
|
|
94
|
+
headers: this[_headers](),
|
|
95
|
+
spec: this.spec,
|
|
96
|
+
// never check integrity for packuments themselves
|
|
97
|
+
integrity: null,
|
|
98
|
+
})
|
|
99
|
+
const packument = await res.json()
|
|
97
100
|
packument._cached = res.headers.has('x-local-cache')
|
|
98
101
|
packument._contentLength = +res.headers.get('content-length')
|
|
99
102
|
if (this.packumentCache) {
|
|
100
103
|
this.packumentCache.set(this.packumentUrl, packument)
|
|
101
104
|
}
|
|
102
105
|
return packument
|
|
103
|
-
}
|
|
106
|
+
} catch (err) {
|
|
104
107
|
if (this.packumentCache) {
|
|
105
108
|
this.packumentCache.delete(this.packumentUrl)
|
|
106
109
|
}
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
this.fullMetadata = true
|
|
110
|
-
return this.packument()
|
|
110
|
+
if (err.code !== 'E404' || this.fullMetadata) {
|
|
111
|
+
throw err
|
|
111
112
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
this.packumentCache.set(this.packumentUrl, p)
|
|
113
|
+
// possible that corgis are not supported by this registry
|
|
114
|
+
this.fullMetadata = true
|
|
115
|
+
return this.packument()
|
|
116
116
|
}
|
|
117
|
-
return p
|
|
118
117
|
}
|
|
119
118
|
|
|
120
|
-
manifest () {
|
|
119
|
+
async manifest () {
|
|
121
120
|
if (this.package) {
|
|
122
|
-
return
|
|
121
|
+
return this.package
|
|
123
122
|
}
|
|
124
123
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
), { code: 'EINTEGRITY' })
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
// made it this far, the integrity is worthwhile. accept it.
|
|
160
|
-
// the setter here will take care of merging it into what we
|
|
161
|
-
// already had.
|
|
162
|
-
this.integrity = distIntegrity
|
|
124
|
+
const packument = await this.packument()
|
|
125
|
+
const mani = await pickManifest(packument, this.spec.fetchSpec, {
|
|
126
|
+
...this.opts,
|
|
127
|
+
defaultTag: this.defaultTag,
|
|
128
|
+
before: this.before,
|
|
129
|
+
})
|
|
130
|
+
/* XXX add ETARGET and E403 revalidation of cached packuments here */
|
|
131
|
+
|
|
132
|
+
// add _resolved and _integrity from dist object
|
|
133
|
+
const { dist } = mani
|
|
134
|
+
if (dist) {
|
|
135
|
+
this.resolved = mani._resolved = dist.tarball
|
|
136
|
+
mani._from = this.from
|
|
137
|
+
const distIntegrity = dist.integrity ? ssri.parse(dist.integrity)
|
|
138
|
+
: dist.shasum ? ssri.fromHex(dist.shasum, 'sha1', { ...this.opts })
|
|
139
|
+
: null
|
|
140
|
+
if (distIntegrity) {
|
|
141
|
+
if (this.integrity && !this.integrity.match(distIntegrity)) {
|
|
142
|
+
// only bork if they have algos in common.
|
|
143
|
+
// otherwise we end up breaking if we have saved a sha512
|
|
144
|
+
// previously for the tarball, but the manifest only
|
|
145
|
+
// provides a sha1, which is possible for older publishes.
|
|
146
|
+
// Otherwise, this is almost certainly a case of holding it
|
|
147
|
+
// wrong, and will result in weird or insecure behavior
|
|
148
|
+
// later on when building package tree.
|
|
149
|
+
for (const algo of Object.keys(this.integrity)) {
|
|
150
|
+
if (distIntegrity[algo]) {
|
|
151
|
+
throw Object.assign(new Error(
|
|
152
|
+
`Integrity checksum failed when using ${algo}: ` +
|
|
153
|
+
`wanted ${this.integrity} but got ${distIntegrity}.`
|
|
154
|
+
), { code: 'EINTEGRITY' })
|
|
163
155
|
}
|
|
164
156
|
}
|
|
165
157
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
158
|
+
// made it this far, the integrity is worthwhile. accept it.
|
|
159
|
+
// the setter here will take care of merging it into what we already
|
|
160
|
+
// had.
|
|
161
|
+
this.integrity = distIntegrity
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (this.integrity) {
|
|
165
|
+
mani._integrity = String(this.integrity)
|
|
166
|
+
if (dist.signatures) {
|
|
167
|
+
if (this.opts.verifySignatures) {
|
|
168
|
+
if (this.registryKeys) {
|
|
169
|
+
// validate and throw on error, then set _signatures
|
|
170
|
+
const message = `${mani._id}:${mani._integrity}`
|
|
171
|
+
for (const signature of dist.signatures) {
|
|
172
|
+
const publicKey = this.registryKeys.filter(key => (key.keyid === signature.keyid))[0]
|
|
173
|
+
if (!publicKey) {
|
|
174
|
+
throw Object.assign(new Error(
|
|
175
|
+
`${mani._id} has a signature with keyid: ${signature.keyid} ` +
|
|
176
|
+
'but no corresponding public key can be found.'
|
|
177
|
+
), { code: 'EMISSINGSIGNATUREKEY' })
|
|
178
|
+
}
|
|
179
|
+
const validPublicKey =
|
|
180
|
+
!publicKey.expires || (Date.parse(publicKey.expires) > Date.now())
|
|
181
|
+
if (!validPublicKey) {
|
|
182
|
+
throw Object.assign(new Error(
|
|
183
|
+
`${mani._id} has a signature with keyid: ${signature.keyid} ` +
|
|
184
|
+
`but the corresponding public key has expired ${publicKey.expires}`
|
|
185
|
+
), { code: 'EEXPIREDSIGNATUREKEY' })
|
|
186
|
+
}
|
|
187
|
+
const verifier = crypto.createVerify('SHA256')
|
|
188
|
+
verifier.write(message)
|
|
189
|
+
verifier.end()
|
|
190
|
+
const valid = verifier.verify(
|
|
191
|
+
publicKey.pemkey,
|
|
192
|
+
signature.sig,
|
|
193
|
+
'base64'
|
|
194
|
+
)
|
|
195
|
+
if (!valid) {
|
|
196
|
+
throw Object.assign(new Error(
|
|
197
|
+
'Integrity checksum signature failed: ' +
|
|
198
|
+
`key ${publicKey.keyid} signature ${signature.sig}`
|
|
199
|
+
), { code: 'EINTEGRITYSIGNATURE' })
|
|
200
|
+
}
|
|
201
|
+
}
|
|
169
202
|
mani._signatures = dist.signatures
|
|
170
203
|
}
|
|
204
|
+
// if no keys, don't set _signatures
|
|
205
|
+
} else {
|
|
206
|
+
mani._signatures = dist.signatures
|
|
171
207
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
this.package = rpj.normalize(mani)
|
|
211
|
+
return this.package
|
|
175
212
|
}
|
|
176
213
|
|
|
177
214
|
[_tarballFromResolved] () {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pacote",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.4.0",
|
|
4
4
|
"description": "JavaScript package downloader",
|
|
5
5
|
"author": "GitHub Inc.",
|
|
6
6
|
"bin": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@npmcli/eslint-config": "^3.0.1",
|
|
29
|
-
"@npmcli/template-oss": "3.
|
|
29
|
+
"@npmcli/template-oss": "3.5.0",
|
|
30
30
|
"hosted-git-info": "^5.0.0",
|
|
31
31
|
"mutate-fs": "^2.1.1",
|
|
32
32
|
"nock": "^13.2.4",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
},
|
|
75
75
|
"templateOSS": {
|
|
76
76
|
"//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
|
|
77
|
-
"version": "3.
|
|
77
|
+
"version": "3.5.0",
|
|
78
78
|
"windowsCI": false
|
|
79
79
|
}
|
|
80
80
|
}
|