1 | const Fetcher = require('./fetcher.js')
|
---|
2 | const RemoteFetcher = require('./remote.js')
|
---|
3 | const _tarballFromResolved = Symbol.for('pacote.Fetcher._tarballFromResolved')
|
---|
4 | const pacoteVersion = require('../package.json').version
|
---|
5 | const npa = require('npm-package-arg')
|
---|
6 | const rpj = require('read-package-json-fast')
|
---|
7 | const pickManifest = require('npm-pick-manifest')
|
---|
8 | const ssri = require('ssri')
|
---|
9 | const Minipass = require('minipass')
|
---|
10 |
|
---|
11 | // Corgis are cute. 🐕🐶
|
---|
12 | const corgiDoc = 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*'
|
---|
13 | const fullDoc = 'application/json'
|
---|
14 |
|
---|
15 | const fetch = require('npm-registry-fetch')
|
---|
16 |
|
---|
17 | // TODO: memoize reg requests, so we don't even have to check cache
|
---|
18 |
|
---|
19 | const _headers = Symbol('_headers')
|
---|
20 | class RegistryFetcher extends Fetcher {
|
---|
21 | constructor (spec, opts) {
|
---|
22 | super(spec, opts)
|
---|
23 |
|
---|
24 | // you usually don't want to fetch the same packument multiple times in
|
---|
25 | // the span of a given script or command, no matter how many pacote calls
|
---|
26 | // are made, so this lets us avoid doing that. It's only relevant for
|
---|
27 | // registry fetchers, because other types simulate their packument from
|
---|
28 | // the manifest, which they memoize on this.package, so it's very cheap
|
---|
29 | // already.
|
---|
30 | this.packumentCache = this.opts.packumentCache || null
|
---|
31 |
|
---|
32 | // handle case when npm-package-arg guesses wrong.
|
---|
33 | if (this.spec.type === 'tag' &&
|
---|
34 | this.spec.rawSpec === '' &&
|
---|
35 | this.defaultTag !== 'latest')
|
---|
36 | this.spec = npa(`${this.spec.name}@${this.defaultTag}`)
|
---|
37 | this.registry = fetch.pickRegistry(spec, opts)
|
---|
38 | this.packumentUrl = this.registry.replace(/\/*$/, '/') +
|
---|
39 | this.spec.escapedName
|
---|
40 |
|
---|
41 | // XXX pacote <=9 has some logic to ignore opts.resolved if
|
---|
42 | // the resolved URL doesn't go to the same registry.
|
---|
43 | // Consider reproducing that here, to throw away this.resolved
|
---|
44 | // in that case.
|
---|
45 | }
|
---|
46 |
|
---|
47 | resolve () {
|
---|
48 | if (this.resolved)
|
---|
49 | return Promise.resolve(this.resolved)
|
---|
50 |
|
---|
51 | // fetching the manifest sets resolved and (usually) integrity
|
---|
52 | return this.manifest().then(() => {
|
---|
53 | if (this.resolved)
|
---|
54 | return this.resolved
|
---|
55 |
|
---|
56 | throw Object.assign(
|
---|
57 | new Error('Invalid package manifest: no `dist.tarball` field'),
|
---|
58 | { package: this.spec.toString() }
|
---|
59 | )
|
---|
60 | })
|
---|
61 | }
|
---|
62 |
|
---|
63 | [_headers] () {
|
---|
64 | return {
|
---|
65 | // npm will override UA, but ensure that we always send *something*
|
---|
66 | 'user-agent': this.opts.userAgent ||
|
---|
67 | `pacote/${pacoteVersion} node/${process.version}`,
|
---|
68 | ...(this.opts.headers || {}),
|
---|
69 | 'pacote-version': pacoteVersion,
|
---|
70 | 'pacote-req-type': 'packument',
|
---|
71 | 'pacote-pkg-id': `registry:${this.spec.name}`,
|
---|
72 | accept: this.fullMetadata ? fullDoc : corgiDoc,
|
---|
73 | }
|
---|
74 | }
|
---|
75 |
|
---|
76 | async packument () {
|
---|
77 | // note this might be either an in-flight promise for a request,
|
---|
78 | // or the actual packument, but we never want to make more than
|
---|
79 | // one request at a time for the same thing regardless.
|
---|
80 | if (this.packumentCache && this.packumentCache.has(this.packumentUrl))
|
---|
81 | return this.packumentCache.get(this.packumentUrl)
|
---|
82 |
|
---|
83 | // npm-registry-fetch the packument
|
---|
84 | // set the appropriate header for corgis if fullMetadata isn't set
|
---|
85 | // return the res.json() promise
|
---|
86 | const p = fetch(this.packumentUrl, {
|
---|
87 | ...this.opts,
|
---|
88 | headers: this[_headers](),
|
---|
89 | spec: this.spec,
|
---|
90 | // never check integrity for packuments themselves
|
---|
91 | integrity: null,
|
---|
92 | }).then(res => res.json().then(packument => {
|
---|
93 | packument._cached = res.headers.has('x-local-cache')
|
---|
94 | packument._contentLength = +res.headers.get('content-length')
|
---|
95 | if (this.packumentCache)
|
---|
96 | this.packumentCache.set(this.packumentUrl, packument)
|
---|
97 | return packument
|
---|
98 | })).catch(er => {
|
---|
99 | if (this.packumentCache)
|
---|
100 | this.packumentCache.delete(this.packumentUrl)
|
---|
101 | if (er.code === 'E404' && !this.fullMetadata) {
|
---|
102 | // possible that corgis are not supported by this registry
|
---|
103 | this.fullMetadata = true
|
---|
104 | return this.packument()
|
---|
105 | }
|
---|
106 | throw er
|
---|
107 | })
|
---|
108 | if (this.packumentCache)
|
---|
109 | this.packumentCache.set(this.packumentUrl, p)
|
---|
110 | return p
|
---|
111 | }
|
---|
112 |
|
---|
113 | manifest () {
|
---|
114 | if (this.package)
|
---|
115 | return Promise.resolve(this.package)
|
---|
116 |
|
---|
117 | return this.packument()
|
---|
118 | .then(packument => pickManifest(packument, this.spec.fetchSpec, {
|
---|
119 | ...this.opts,
|
---|
120 | defaultTag: this.defaultTag,
|
---|
121 | before: this.before,
|
---|
122 | }) /* XXX add ETARGET and E403 revalidation of cached packuments here */)
|
---|
123 | .then(mani => {
|
---|
124 | // add _resolved and _integrity from dist object
|
---|
125 | const { dist } = mani
|
---|
126 | if (dist) {
|
---|
127 | this.resolved = mani._resolved = dist.tarball
|
---|
128 | mani._from = this.from
|
---|
129 | const distIntegrity = dist.integrity ? ssri.parse(dist.integrity)
|
---|
130 | : dist.shasum ? ssri.fromHex(dist.shasum, 'sha1', {...this.opts})
|
---|
131 | : null
|
---|
132 | if (distIntegrity) {
|
---|
133 | if (!this.integrity)
|
---|
134 | this.integrity = distIntegrity
|
---|
135 | else if (!this.integrity.match(distIntegrity)) {
|
---|
136 | // only bork if they have algos in common.
|
---|
137 | // otherwise we end up breaking if we have saved a sha512
|
---|
138 | // previously for the tarball, but the manifest only
|
---|
139 | // provides a sha1, which is possible for older publishes.
|
---|
140 | // Otherwise, this is almost certainly a case of holding it
|
---|
141 | // wrong, and will result in weird or insecure behavior
|
---|
142 | // later on when building package tree.
|
---|
143 | for (const algo of Object.keys(this.integrity)) {
|
---|
144 | if (distIntegrity[algo]) {
|
---|
145 | throw Object.assign(new Error(
|
---|
146 | `Integrity checksum failed when using ${algo}: `+
|
---|
147 | `wanted ${this.integrity} but got ${distIntegrity}.`
|
---|
148 | ), { code: 'EINTEGRITY' })
|
---|
149 | }
|
---|
150 | }
|
---|
151 | // made it this far, the integrity is worthwhile. accept it.
|
---|
152 | // the setter here will take care of merging it into what we
|
---|
153 | // already had.
|
---|
154 | this.integrity = distIntegrity
|
---|
155 | }
|
---|
156 | }
|
---|
157 | }
|
---|
158 | if (this.integrity)
|
---|
159 | mani._integrity = String(this.integrity)
|
---|
160 | this.package = rpj.normalize(mani)
|
---|
161 | return this.package
|
---|
162 | })
|
---|
163 | }
|
---|
164 |
|
---|
165 | [_tarballFromResolved] () {
|
---|
166 | // we use a RemoteFetcher to get the actual tarball stream
|
---|
167 | return new RemoteFetcher(this.resolved, {
|
---|
168 | ...this.opts,
|
---|
169 | resolved: this.resolved,
|
---|
170 | pkgid: `registry:${this.spec.name}@${this.resolved}`,
|
---|
171 | })[_tarballFromResolved]()
|
---|
172 | }
|
---|
173 |
|
---|
174 | get types () {
|
---|
175 | return [
|
---|
176 | 'tag',
|
---|
177 | 'version',
|
---|
178 | 'range',
|
---|
179 | ]
|
---|
180 | }
|
---|
181 | }
|
---|
182 | module.exports = RegistryFetcher
|
---|