registry.js 6.5 KB

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