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