• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict'
2
3const cloneDeep = require('lodash.clonedeep')
4const figgyPudding = require('figgy-pudding')
5const { fixer } = require('normalize-package-data')
6const getStream = require('get-stream')
7const npa = require('npm-package-arg')
8const npmAuth = require('npm-registry-fetch/auth.js')
9const npmFetch = require('npm-registry-fetch')
10const semver = require('semver')
11const ssri = require('ssri')
12const url = require('url')
13const validate = require('aproba')
14
15const PublishConfig = figgyPudding({
16  access: {},
17  algorithms: { default: ['sha512'] },
18  npmVersion: {},
19  tag: { default: 'latest' },
20  Promise: { default: () => Promise }
21})
22
23module.exports = publish
24function publish (manifest, tarball, opts) {
25  opts = PublishConfig(opts)
26  return new opts.Promise(resolve => resolve()).then(() => {
27    validate('OSO|OOO', [manifest, tarball, opts])
28    if (manifest.private) {
29      throw Object.assign(new Error(
30        'This package has been marked as private\n' +
31        "Remove the 'private' field from the package.json to publish it."
32      ), { code: 'EPRIVATE' })
33    }
34    const spec = npa.resolve(manifest.name, manifest.version)
35    // NOTE: spec is used to pick the appropriate registry/auth combo.
36    opts = opts.concat(manifest.publishConfig, { spec })
37    const reg = npmFetch.pickRegistry(spec, opts)
38    const auth = npmAuth(reg, opts)
39    const pubManifest = patchedManifest(spec, auth, manifest, opts)
40
41    // registry-frontdoor cares about the access level, which is only
42    // configurable for scoped packages
43    if (!spec.scope && opts.access === 'restricted') {
44      throw Object.assign(
45        new Error("Can't restrict access to unscoped packages."),
46        { code: 'EUNSCOPED' }
47      )
48    }
49
50    return slurpTarball(tarball, opts).then(tardata => {
51      const metadata = buildMetadata(
52        spec, auth, reg, pubManifest, tardata, opts
53      )
54      return npmFetch(spec.escapedName, opts.concat({
55        method: 'PUT',
56        body: metadata,
57        ignoreBody: true
58      })).catch(err => {
59        if (err.code !== 'E409') { throw err }
60        return npmFetch.json(spec.escapedName, opts.concat({
61          query: { write: true }
62        })).then(
63          current => patchMetadata(current, metadata, opts)
64        ).then(newMetadata => {
65          return npmFetch(spec.escapedName, opts.concat({
66            method: 'PUT',
67            body: newMetadata,
68            ignoreBody: true
69          }))
70        })
71      })
72    })
73  }).then(() => true)
74}
75
76function patchedManifest (spec, auth, base, opts) {
77  const manifest = cloneDeep(base)
78  manifest._nodeVersion = process.versions.node
79  if (opts.npmVersion) {
80    manifest._npmVersion = opts.npmVersion
81  }
82  if (auth.username || auth.email) {
83    // NOTE: This is basically pointless, but reproduced because it's what
84    // legacy does: tl;dr `auth.username` and `auth.email` are going to be
85    // undefined in any auth situation that uses tokens instead of plain
86    // auth. I can only assume some registries out there decided that
87    // _npmUser would be of any use to them, but _npmUser in packuments
88    // currently gets filled in by the npm registry itself, based on auth
89    // information.
90    manifest._npmUser = {
91      name: auth.username,
92      email: auth.email
93    }
94  }
95
96  fixer.fixNameField(manifest, { strict: true, allowLegacyCase: true })
97  const version = semver.clean(manifest.version)
98  if (!version) {
99    throw Object.assign(
100      new Error('invalid semver: ' + manifest.version),
101      { code: 'EBADSEMVER' }
102    )
103  }
104  manifest.version = version
105  return manifest
106}
107
108function buildMetadata (spec, auth, registry, manifest, tardata, opts) {
109  const root = {
110    _id: manifest.name,
111    name: manifest.name,
112    description: manifest.description,
113    'dist-tags': {},
114    versions: {},
115    readme: manifest.readme || ''
116  }
117
118  if (opts.access) root.access = opts.access
119
120  if (!auth.token) {
121    root.maintainers = [{ name: auth.username, email: auth.email }]
122    manifest.maintainers = JSON.parse(JSON.stringify(root.maintainers))
123  }
124
125  root.versions[ manifest.version ] = manifest
126  const tag = manifest.tag || opts.tag
127  root['dist-tags'][tag] = manifest.version
128
129  const tbName = manifest.name + '-' + manifest.version + '.tgz'
130  const tbURI = manifest.name + '/-/' + tbName
131  const integrity = ssri.fromData(tardata, {
132    algorithms: [...new Set(['sha1'].concat(opts.algorithms))]
133  })
134
135  manifest._id = manifest.name + '@' + manifest.version
136  manifest.dist = manifest.dist || {}
137  // Don't bother having sha1 in the actual integrity field
138  manifest.dist.integrity = integrity['sha512'][0].toString()
139  // Legacy shasum support
140  manifest.dist.shasum = integrity['sha1'][0].hexDigest()
141  manifest.dist.tarball = url.resolve(registry, tbURI)
142    .replace(/^https:\/\//, 'http://')
143
144  root._attachments = {}
145  root._attachments[ tbName ] = {
146    'content_type': 'application/octet-stream',
147    'data': tardata.toString('base64'),
148    'length': tardata.length
149  }
150
151  return root
152}
153
154function patchMetadata (current, newData, opts) {
155  const curVers = Object.keys(current.versions || {}).map(v => {
156    return semver.clean(v, true)
157  }).concat(Object.keys(current.time || {}).map(v => {
158    if (semver.valid(v, true)) { return semver.clean(v, true) }
159  })).filter(v => v)
160
161  const newVersion = Object.keys(newData.versions)[0]
162
163  if (curVers.indexOf(newVersion) !== -1) {
164    throw ConflictError(newData.name, newData.version)
165  }
166
167  current.versions = current.versions || {}
168  current.versions[newVersion] = newData.versions[newVersion]
169  for (var i in newData) {
170    switch (i) {
171      // objects that copy over the new stuffs
172      case 'dist-tags':
173      case 'versions':
174      case '_attachments':
175        for (var j in newData[i]) {
176          current[i] = current[i] || {}
177          current[i][j] = newData[i][j]
178        }
179        break
180
181      // ignore these
182      case 'maintainers':
183        break
184
185      // copy
186      default:
187        current[i] = newData[i]
188    }
189  }
190  const maint = newData.maintainers && JSON.parse(JSON.stringify(newData.maintainers))
191  newData.versions[newVersion].maintainers = maint
192  return current
193}
194
195function slurpTarball (tarSrc, opts) {
196  if (Buffer.isBuffer(tarSrc)) {
197    return opts.Promise.resolve(tarSrc)
198  } else if (typeof tarSrc === 'string') {
199    return opts.Promise.resolve(Buffer.from(tarSrc, 'base64'))
200  } else if (typeof tarSrc.pipe === 'function') {
201    return getStream.buffer(tarSrc)
202  } else {
203    return opts.Promise.reject(Object.assign(
204      new Error('invalid tarball argument. Must be a Buffer, a base64 string, or a binary stream'), {
205        code: 'EBADTAR'
206      }))
207  }
208}
209
210function ConflictError (pkgid, version) {
211  return Object.assign(new Error(
212    `Cannot publish ${pkgid}@${version} over existing version.`
213  ), {
214    code: 'EPUBLISHCONFLICT',
215    pkgid,
216    version
217  })
218}
219