publish.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. const { fixer } = require('normalize-package-data')
  2. const npmFetch = require('npm-registry-fetch')
  3. const npa = require('npm-package-arg')
  4. const semver = require('semver')
  5. const { URL } = require('url')
  6. const ssri = require('ssri')
  7. const publish = async (manifest, tarballData, opts) => {
  8. if (manifest.private) {
  9. throw Object.assign(
  10. new Error(`This package has been marked as private
  11. Remove the 'private' field from the package.json to publish it.`),
  12. { code: 'EPRIVATE' }
  13. )
  14. }
  15. // spec is used to pick the appropriate registry/auth combo
  16. const spec = npa.resolve(manifest.name, manifest.version)
  17. opts = {
  18. defaultTag: 'latest',
  19. // if scoped, restricted by default
  20. access: spec.scope ? 'restricted' : 'public',
  21. algorithms: ['sha512'],
  22. ...opts,
  23. spec,
  24. }
  25. const reg = npmFetch.pickRegistry(spec, opts)
  26. const pubManifest = patchManifest(manifest, opts)
  27. // registry-frontdoor cares about the access level,
  28. // which is only configurable for scoped packages
  29. if (!spec.scope && opts.access === 'restricted') {
  30. throw Object.assign(
  31. new Error("Can't restrict access to unscoped packages."),
  32. { code: 'EUNSCOPED' }
  33. )
  34. }
  35. const metadata = buildMetadata(reg, pubManifest, tarballData, opts)
  36. try {
  37. return await npmFetch(spec.escapedName, {
  38. ...opts,
  39. method: 'PUT',
  40. body: metadata,
  41. ignoreBody: true,
  42. })
  43. } catch (err) {
  44. if (err.code !== 'E409')
  45. throw err
  46. // if E409, we attempt exactly ONE retry, to protect us
  47. // against malicious activity like trying to publish
  48. // a bunch of new versions of a package at the same time
  49. // and/or spamming the registry
  50. const current = await npmFetch.json(spec.escapedName, {
  51. ...opts,
  52. query: { write: true },
  53. })
  54. const newMetadata = patchMetadata(current, metadata, opts)
  55. return npmFetch(spec.escapedName, {
  56. ...opts,
  57. method: 'PUT',
  58. body: newMetadata,
  59. ignoreBody: true,
  60. })
  61. }
  62. }
  63. const patchManifest = (_manifest, opts) => {
  64. const { npmVersion } = opts
  65. // we only update top-level fields, so a shallow clone is fine
  66. const manifest = { ..._manifest }
  67. manifest._nodeVersion = process.versions.node
  68. if (npmVersion)
  69. manifest._npmVersion = npmVersion
  70. fixer.fixNameField(manifest, { strict: true, allowLegacyCase: true })
  71. const version = semver.clean(manifest.version)
  72. if (!version) {
  73. throw Object.assign(
  74. new Error('invalid semver: ' + manifest.version),
  75. { code: 'EBADSEMVER' }
  76. )
  77. }
  78. manifest.version = version
  79. return manifest
  80. }
  81. const buildMetadata = (registry, manifest, tarballData, opts) => {
  82. const { access, defaultTag, algorithms } = opts
  83. const root = {
  84. _id: manifest.name,
  85. name: manifest.name,
  86. description: manifest.description,
  87. 'dist-tags': {},
  88. versions: {},
  89. access,
  90. }
  91. root.versions[manifest.version] = manifest
  92. const tag = manifest.tag || defaultTag
  93. root['dist-tags'][tag] = manifest.version
  94. const tarballName = `${manifest.name}-${manifest.version}.tgz`
  95. const tarballURI = `${manifest.name}/-/${tarballName}`
  96. const integrity = ssri.fromData(tarballData, {
  97. algorithms: [...new Set(['sha1'].concat(algorithms))],
  98. })
  99. manifest._id = `${manifest.name}@${manifest.version}`
  100. manifest.dist = { ...manifest.dist }
  101. // Don't bother having sha1 in the actual integrity field
  102. manifest.dist.integrity = integrity.sha512[0].toString()
  103. // Legacy shasum support
  104. manifest.dist.shasum = integrity.sha1[0].hexDigest()
  105. // NB: the CLI always fetches via HTTPS if the registry is HTTPS,
  106. // regardless of what's here. This makes it so that installing
  107. // from an HTTP-only mirror doesn't cause problems, though.
  108. manifest.dist.tarball = new URL(tarballURI, registry).href
  109. .replace(/^https:\/\//, 'http://')
  110. root._attachments = {}
  111. root._attachments[tarballName] = {
  112. content_type: 'application/octet-stream',
  113. data: tarballData.toString('base64'),
  114. length: tarballData.length,
  115. }
  116. return root
  117. }
  118. const patchMetadata = (current, newData) => {
  119. const curVers = Object.keys(current.versions || {})
  120. .map(v => semver.clean(v, true))
  121. .concat(Object.keys(current.time || {})
  122. .map(v => semver.valid(v, true) && semver.clean(v, true))
  123. .filter(v => v))
  124. const newVersion = Object.keys(newData.versions)[0]
  125. if (curVers.indexOf(newVersion) !== -1) {
  126. const { name: pkgid, version } = newData
  127. throw Object.assign(
  128. new Error(
  129. `Cannot publish ${pkgid}@${version} over existing version.`
  130. ), {
  131. code: 'EPUBLISHCONFLICT',
  132. pkgid,
  133. version,
  134. })
  135. }
  136. current.versions = current.versions || {}
  137. current.versions[newVersion] = newData.versions[newVersion]
  138. for (const i in newData) {
  139. switch (i) {
  140. // objects that copy over the new stuffs
  141. case 'dist-tags':
  142. case 'versions':
  143. case '_attachments':
  144. for (const j in newData[i]) {
  145. current[i] = current[i] || {}
  146. current[i][j] = newData[i][j]
  147. }
  148. break
  149. // copy
  150. default:
  151. current[i] = newData[i]
  152. break
  153. }
  154. }
  155. return current
  156. }
  157. module.exports = publish