You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
182 lines
6.5 KiB
182 lines
6.5 KiB
const Fetcher = require('./fetcher.js')
|
|
const RemoteFetcher = require('./remote.js')
|
|
const _tarballFromResolved = Symbol.for('pacote.Fetcher._tarballFromResolved')
|
|
const pacoteVersion = require('../package.json').version
|
|
const npa = require('npm-package-arg')
|
|
const rpj = require('read-package-json-fast')
|
|
const pickManifest = require('npm-pick-manifest')
|
|
const ssri = require('ssri')
|
|
const Minipass = require('minipass')
|
|
|
|
// Corgis are cute. 🐕🐶
|
|
const corgiDoc = 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*'
|
|
const fullDoc = 'application/json'
|
|
|
|
const fetch = require('npm-registry-fetch')
|
|
|
|
// TODO: memoize reg requests, so we don't even have to check cache
|
|
|
|
const _headers = Symbol('_headers')
|
|
class RegistryFetcher extends Fetcher {
|
|
constructor (spec, opts) {
|
|
super(spec, opts)
|
|
|
|
// you usually don't want to fetch the same packument multiple times in
|
|
// the span of a given script or command, no matter how many pacote calls
|
|
// are made, so this lets us avoid doing that. It's only relevant for
|
|
// registry fetchers, because other types simulate their packument from
|
|
// the manifest, which they memoize on this.package, so it's very cheap
|
|
// already.
|
|
this.packumentCache = this.opts.packumentCache || null
|
|
|
|
// handle case when npm-package-arg guesses wrong.
|
|
if (this.spec.type === 'tag' &&
|
|
this.spec.rawSpec === '' &&
|
|
this.defaultTag !== 'latest')
|
|
this.spec = npa(`${this.spec.name}@${this.defaultTag}`)
|
|
this.registry = fetch.pickRegistry(spec, opts)
|
|
this.packumentUrl = this.registry.replace(/\/*$/, '/') +
|
|
this.spec.escapedName
|
|
|
|
// XXX pacote <=9 has some logic to ignore opts.resolved if
|
|
// the resolved URL doesn't go to the same registry.
|
|
// Consider reproducing that here, to throw away this.resolved
|
|
// in that case.
|
|
}
|
|
|
|
resolve () {
|
|
if (this.resolved)
|
|
return Promise.resolve(this.resolved)
|
|
|
|
// fetching the manifest sets resolved and (usually) integrity
|
|
return this.manifest().then(() => {
|
|
if (this.resolved)
|
|
return this.resolved
|
|
|
|
throw Object.assign(
|
|
new Error('Invalid package manifest: no `dist.tarball` field'),
|
|
{ package: this.spec.toString() }
|
|
)
|
|
})
|
|
}
|
|
|
|
[_headers] () {
|
|
return {
|
|
// npm will override UA, but ensure that we always send *something*
|
|
'user-agent': this.opts.userAgent ||
|
|
`pacote/${pacoteVersion} node/${process.version}`,
|
|
...(this.opts.headers || {}),
|
|
'pacote-version': pacoteVersion,
|
|
'pacote-req-type': 'packument',
|
|
'pacote-pkg-id': `registry:${this.spec.name}`,
|
|
accept: this.fullMetadata ? fullDoc : corgiDoc,
|
|
}
|
|
}
|
|
|
|
async packument () {
|
|
// note this might be either an in-flight promise for a request,
|
|
// or the actual packument, but we never want to make more than
|
|
// one request at a time for the same thing regardless.
|
|
if (this.packumentCache && this.packumentCache.has(this.packumentUrl))
|
|
return this.packumentCache.get(this.packumentUrl)
|
|
|
|
// npm-registry-fetch the packument
|
|
// set the appropriate header for corgis if fullMetadata isn't set
|
|
// return the res.json() promise
|
|
const p = fetch(this.packumentUrl, {
|
|
...this.opts,
|
|
headers: this[_headers](),
|
|
spec: this.spec,
|
|
// never check integrity for packuments themselves
|
|
integrity: null,
|
|
}).then(res => res.json().then(packument => {
|
|
packument._cached = res.headers.has('x-local-cache')
|
|
packument._contentLength = +res.headers.get('content-length')
|
|
if (this.packumentCache)
|
|
this.packumentCache.set(this.packumentUrl, packument)
|
|
return packument
|
|
})).catch(er => {
|
|
if (this.packumentCache)
|
|
this.packumentCache.delete(this.packumentUrl)
|
|
if (er.code === 'E404' && !this.fullMetadata) {
|
|
// possible that corgis are not supported by this registry
|
|
this.fullMetadata = true
|
|
return this.packument()
|
|
}
|
|
throw er
|
|
})
|
|
if (this.packumentCache)
|
|
this.packumentCache.set(this.packumentUrl, p)
|
|
return p
|
|
}
|
|
|
|
manifest () {
|
|
if (this.package)
|
|
return Promise.resolve(this.package)
|
|
|
|
return this.packument()
|
|
.then(packument => pickManifest(packument, this.spec.fetchSpec, {
|
|
...this.opts,
|
|
defaultTag: this.defaultTag,
|
|
before: this.before,
|
|
}) /* XXX add ETARGET and E403 revalidation of cached packuments here */)
|
|
.then(mani => {
|
|
// add _resolved and _integrity from dist object
|
|
const { dist } = mani
|
|
if (dist) {
|
|
this.resolved = mani._resolved = dist.tarball
|
|
mani._from = this.from
|
|
const distIntegrity = dist.integrity ? ssri.parse(dist.integrity)
|
|
: dist.shasum ? ssri.fromHex(dist.shasum, 'sha1', {...this.opts})
|
|
: null
|
|
if (distIntegrity) {
|
|
if (!this.integrity)
|
|
this.integrity = distIntegrity
|
|
else if (!this.integrity.match(distIntegrity)) {
|
|
// only bork if they have algos in common.
|
|
// otherwise we end up breaking if we have saved a sha512
|
|
// previously for the tarball, but the manifest only
|
|
// provides a sha1, which is possible for older publishes.
|
|
// Otherwise, this is almost certainly a case of holding it
|
|
// wrong, and will result in weird or insecure behavior
|
|
// later on when building package tree.
|
|
for (const algo of Object.keys(this.integrity)) {
|
|
if (distIntegrity[algo]) {
|
|
throw Object.assign(new Error(
|
|
`Integrity checksum failed when using ${algo}: `+
|
|
`wanted ${this.integrity} but got ${distIntegrity}.`
|
|
), { code: 'EINTEGRITY' })
|
|
}
|
|
}
|
|
// made it this far, the integrity is worthwhile. accept it.
|
|
// the setter here will take care of merging it into what we
|
|
// already had.
|
|
this.integrity = distIntegrity
|
|
}
|
|
}
|
|
}
|
|
if (this.integrity)
|
|
mani._integrity = String(this.integrity)
|
|
this.package = rpj.normalize(mani)
|
|
return this.package
|
|
})
|
|
}
|
|
|
|
[_tarballFromResolved] () {
|
|
// we use a RemoteFetcher to get the actual tarball stream
|
|
return new RemoteFetcher(this.resolved, {
|
|
...this.opts,
|
|
resolved: this.resolved,
|
|
pkgid: `registry:${this.spec.name}@${this.resolved}`,
|
|
})[_tarballFromResolved]()
|
|
}
|
|
|
|
get types () {
|
|
return [
|
|
'tag',
|
|
'version',
|
|
'range',
|
|
]
|
|
}
|
|
}
|
|
module.exports = RegistryFetcher
|