diff options
Diffstat (limited to 'deps/npm/node_modules/libnpmpublish/publish.js')
-rw-r--r-- | deps/npm/node_modules/libnpmpublish/publish.js | 249 |
1 files changed, 114 insertions, 135 deletions
diff --git a/deps/npm/node_modules/libnpmpublish/publish.js b/deps/npm/node_modules/libnpmpublish/publish.js index de5af4f5d3..8a382e4ad9 100644 --- a/deps/npm/node_modules/libnpmpublish/publish.js +++ b/deps/npm/node_modules/libnpmpublish/publish.js @@ -1,96 +1,98 @@ '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 cloneDeep = require('lodash.clonedeep') +const npa = require('npm-package-arg') +const pack = require('libnpmpack') const semver = require('semver') +const { URL } = require('url') +const util = require('util') 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 } -}) +const statAsync = util.promisify(require('fs').stat) 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' } - ) - } +async function publish (folder, manifest, 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' } + ) + } - 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 - })) - }) - }) + // spec is used to pick the appropriate registry/auth combo + const spec = npa.resolve(manifest.name, manifest.version) + opts = { + defaultTag: 'latest', + // if scoped, restricted by default + access: spec.scope ? 'restricted' : 'public', + algorithms: ['sha512'], + ...opts, + spec + } + + const stat = await statAsync(folder) + // checks if it's a dir + if (!stat.isDirectory()) { + throw Object.assign( + new Error('not a directory'), + { code: 'ENOTDIR' } + ) + } + + const reg = npmFetch.pickRegistry(spec, opts) + const pubManifest = patchManifest(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' } + ) + } + + const tarballData = await pack(`file:${folder}`, { ...opts }) + const metadata = buildMetadata(reg, pubManifest, tarballData, opts) + + try { + return await npmFetch(spec.escapedName, { + ...opts, + method: 'PUT', + body: metadata, + ignoreBody: true }) - }).then(() => true) + } catch (err) { + if (err.code !== 'E409') { throw err } + // if E409, we attempt exactly ONE retry, to protect us + // against malicious activity like trying to publish + // a bunch of new versions of a package at the same time + // and/or spamming the registry + const current = await npmFetch.json(spec.escapedName, { + ...opts, + query: { write: true } + }) + const newMetadata = patchMetadata(current, metadata, opts) + return npmFetch(spec.escapedName, { + ...opts, + method: 'PUT', + body: newMetadata, + ignoreBody: true + }) + } } -function patchedManifest (spec, auth, base, opts) { - const manifest = cloneDeep(base) +function patchManifest (_manifest, opts) { + const { npmVersion } = opts + const manifest = cloneDeep(_manifest) + 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 - } + if (npmVersion) { + manifest._npmVersion = npmVersion } fixer.fixNameField(manifest, { strict: true, allowLegacyCase: true }) @@ -105,53 +107,52 @@ function patchedManifest (spec, auth, base, opts) { return manifest } -function buildMetadata (spec, auth, registry, manifest, tardata, opts) { +function buildMetadata (registry, manifest, tarballData, opts) { + const { access, defaultTag, algorithms } = opts const root = { _id: manifest.name, name: manifest.name, description: manifest.description, 'dist-tags': {}, versions: {}, + access, 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.versions[manifest.version] = manifest + const tag = manifest.tag || defaultTag 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))] + const tarballName = `${manifest.name}-${manifest.version}.tgz` + const tarballURI = `${manifest.name}/-/${tarballName}` + const integrity = ssri.fromData(tarballData, { + algorithms: [...new Set(['sha1'].concat(algorithms))] }) - manifest._id = manifest.name + '@' + manifest.version - manifest.dist = manifest.dist || {} + 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() + manifest.dist.integrity = integrity.sha512[0].toString() // Legacy shasum support - manifest.dist.shasum = integrity['sha1'][0].hexDigest() - manifest.dist.tarball = url.resolve(registry, tbURI) + manifest.dist.shasum = integrity.sha1[0].hexDigest() + + // NB: the CLI always fetches via HTTPS if the registry is HTTPS, + // regardless of what's here. This makes it so that installing + // from an HTTP-only mirror doesn't cause problems, though. + manifest.dist.tarball = new URL(tarballURI, registry).href .replace(/^https:\/\//, 'http://') root._attachments = {} - root._attachments[ tbName ] = { - 'content_type': 'application/octet-stream', - 'data': tardata.toString('base64'), - 'length': tardata.length + root._attachments[tarballName] = { + content_type: 'application/octet-stream', + data: tarballData.toString('base64'), + length: tarballData.length } return root } -function patchMetadata (current, newData, opts) { +function patchMetadata (current, newData) { const curVers = Object.keys(current.versions || {}).map(v => { return semver.clean(v, true) }).concat(Object.keys(current.time || {}).map(v => { @@ -161,7 +162,15 @@ function patchMetadata (current, newData, opts) { const newVersion = Object.keys(newData.versions)[0] if (curVers.indexOf(newVersion) !== -1) { - throw ConflictError(newData.name, newData.version) + const { name: pkgid, version } = newData + throw Object.assign( + new Error( + `Cannot publish ${pkgid}@${version} over existing version.` + ), { + code: 'EPUBLISHCONFLICT', + pkgid, + version + }) } current.versions = current.versions || {} @@ -178,41 +187,11 @@ function patchMetadata (current, newData, opts) { } 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 - }) + return current } |