summaryrefslogtreecommitdiff
path: root/deps/npm/node_modules/libnpmpublish/publish.js
blob: de5af4f5d3d6b2232b843327b97d96f57aaac501 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
'use strict'

const cloneDeep = require('lodash.clonedeep')
const figgyPudding = require('figgy-pudding')
const { fixer } = require('normalize-package-data')
const getStream = require('get-stream')
const npa = require('npm-package-arg')
const npmAuth = require('npm-registry-fetch/auth.js')
const npmFetch = require('npm-registry-fetch')
const semver = require('semver')
const ssri = require('ssri')
const url = require('url')
const validate = require('aproba')

const PublishConfig = figgyPudding({
  access: {},
  algorithms: { default: ['sha512'] },
  npmVersion: {},
  tag: { default: 'latest' },
  Promise: { default: () => Promise }
})

module.exports = publish
function publish (manifest, tarball, opts) {
  opts = PublishConfig(opts)
  return new opts.Promise(resolve => resolve()).then(() => {
    validate('OSO|OOO', [manifest, tarball, opts])
    if (manifest.private) {
      throw Object.assign(new Error(
        'This package has been marked as private\n' +
        "Remove the 'private' field from the package.json to publish it."
      ), { code: 'EPRIVATE' })
    }
    const spec = npa.resolve(manifest.name, manifest.version)
    // NOTE: spec is used to pick the appropriate registry/auth combo.
    opts = opts.concat(manifest.publishConfig, { spec })
    const reg = npmFetch.pickRegistry(spec, opts)
    const auth = npmAuth(reg, opts)
    const pubManifest = patchedManifest(spec, auth, manifest, opts)

    // registry-frontdoor cares about the access level, which is only
    // configurable for scoped packages
    if (!spec.scope && opts.access === 'restricted') {
      throw Object.assign(
        new Error("Can't restrict access to unscoped packages."),
        { code: 'EUNSCOPED' }
      )
    }

    return slurpTarball(tarball, opts).then(tardata => {
      const metadata = buildMetadata(
        spec, auth, reg, pubManifest, tardata, opts
      )
      return npmFetch(spec.escapedName, opts.concat({
        method: 'PUT',
        body: metadata,
        ignoreBody: true
      })).catch(err => {
        if (err.code !== 'E409') { throw err }
        return npmFetch.json(spec.escapedName, opts.concat({
          query: { write: true }
        })).then(
          current => patchMetadata(current, metadata, opts)
        ).then(newMetadata => {
          return npmFetch(spec.escapedName, opts.concat({
            method: 'PUT',
            body: newMetadata,
            ignoreBody: true
          }))
        })
      })
    })
  }).then(() => true)
}

function patchedManifest (spec, auth, base, opts) {
  const manifest = cloneDeep(base)
  manifest._nodeVersion = process.versions.node
  if (opts.npmVersion) {
    manifest._npmVersion = opts.npmVersion
  }
  if (auth.username || auth.email) {
    // NOTE: This is basically pointless, but reproduced because it's what
    // legacy does: tl;dr `auth.username` and `auth.email` are going to be
    // undefined in any auth situation that uses tokens instead of plain
    // auth. I can only assume some registries out there decided that
    // _npmUser would be of any use to them, but _npmUser in packuments
    // currently gets filled in by the npm registry itself, based on auth
    // information.
    manifest._npmUser = {
      name: auth.username,
      email: auth.email
    }
  }

  fixer.fixNameField(manifest, { strict: true, allowLegacyCase: true })
  const version = semver.clean(manifest.version)
  if (!version) {
    throw Object.assign(
      new Error('invalid semver: ' + manifest.version),
      { code: 'EBADSEMVER' }
    )
  }
  manifest.version = version
  return manifest
}

function buildMetadata (spec, auth, registry, manifest, tardata, opts) {
  const root = {
    _id: manifest.name,
    name: manifest.name,
    description: manifest.description,
    'dist-tags': {},
    versions: {},
    readme: manifest.readme || ''
  }

  if (opts.access) root.access = opts.access

  if (!auth.token) {
    root.maintainers = [{ name: auth.username, email: auth.email }]
    manifest.maintainers = JSON.parse(JSON.stringify(root.maintainers))
  }

  root.versions[ manifest.version ] = manifest
  const tag = manifest.tag || opts.tag
  root['dist-tags'][tag] = manifest.version

  const tbName = manifest.name + '-' + manifest.version + '.tgz'
  const tbURI = manifest.name + '/-/' + tbName
  const integrity = ssri.fromData(tardata, {
    algorithms: [...new Set(['sha1'].concat(opts.algorithms))]
  })

  manifest._id = manifest.name + '@' + manifest.version
  manifest.dist = manifest.dist || {}
  // Don't bother having sha1 in the actual integrity field
  manifest.dist.integrity = integrity['sha512'][0].toString()
  // Legacy shasum support
  manifest.dist.shasum = integrity['sha1'][0].hexDigest()
  manifest.dist.tarball = url.resolve(registry, tbURI)
    .replace(/^https:\/\//, 'http://')

  root._attachments = {}
  root._attachments[ tbName ] = {
    'content_type': 'application/octet-stream',
    'data': tardata.toString('base64'),
    'length': tardata.length
  }

  return root
}

function patchMetadata (current, newData, opts) {
  const curVers = Object.keys(current.versions || {}).map(v => {
    return semver.clean(v, true)
  }).concat(Object.keys(current.time || {}).map(v => {
    if (semver.valid(v, true)) { return semver.clean(v, true) }
  })).filter(v => v)

  const newVersion = Object.keys(newData.versions)[0]

  if (curVers.indexOf(newVersion) !== -1) {
    throw ConflictError(newData.name, newData.version)
  }

  current.versions = current.versions || {}
  current.versions[newVersion] = newData.versions[newVersion]
  for (var i in newData) {
    switch (i) {
      // objects that copy over the new stuffs
      case 'dist-tags':
      case 'versions':
      case '_attachments':
        for (var j in newData[i]) {
          current[i] = current[i] || {}
          current[i][j] = newData[i][j]
        }
        break

      // ignore these
      case 'maintainers':
        break

      // copy
      default:
        current[i] = newData[i]
    }
  }
  const maint = newData.maintainers && JSON.parse(JSON.stringify(newData.maintainers))
  newData.versions[newVersion].maintainers = maint
  return current
}

function slurpTarball (tarSrc, opts) {
  if (Buffer.isBuffer(tarSrc)) {
    return opts.Promise.resolve(tarSrc)
  } else if (typeof tarSrc === 'string') {
    return opts.Promise.resolve(Buffer.from(tarSrc, 'base64'))
  } else if (typeof tarSrc.pipe === 'function') {
    return getStream.buffer(tarSrc)
  } else {
    return opts.Promise.reject(Object.assign(
      new Error('invalid tarball argument. Must be a Buffer, a base64 string, or a binary stream'), {
        code: 'EBADTAR'
      }))
  }
}

function ConflictError (pkgid, version) {
  return Object.assign(new Error(
    `Cannot publish ${pkgid}@${version} over existing version.`
  ), {
    code: 'EPUBLISHCONFLICT',
    pkgid,
    version
  })
}