Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/npm/cli.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Fritz <fritzy@github.com>2021-12-16 21:01:56 +0300
committerNathan Fritz <fritzy@github.com>2021-12-16 21:05:19 +0300
commitd7265045730555c03b3142c004c7438e9577028c (patch)
tree035d81b3124bdaa09c21854934bf2b2b50e52e44 /workspaces/libnpmpublish
parentd8aac8448e983692cacb427e03f4688cd1b62e30 (diff)
Bring in all libnpm modules + arborist as workspaces (#4166)
Added libnpm workspaces and arborist
Diffstat (limited to 'workspaces/libnpmpublish')
-rw-r--r--workspaces/libnpmpublish/.eslintrc.js14
-rw-r--r--workspaces/libnpmpublish/.gitignore23
-rw-r--r--workspaces/libnpmpublish/.npmrc3
-rw-r--r--workspaces/libnpmpublish/CHANGELOG.md91
-rw-r--r--workspaces/libnpmpublish/LICENSE13
-rw-r--r--workspaces/libnpmpublish/README.md105
-rw-r--r--workspaces/libnpmpublish/SECURITY.md3
-rw-r--r--workspaces/libnpmpublish/lib/index.js4
-rw-r--r--workspaces/libnpmpublish/lib/publish.js182
-rw-r--r--workspaces/libnpmpublish/lib/unpublish.js102
-rw-r--r--workspaces/libnpmpublish/package.json56
-rw-r--r--workspaces/libnpmpublish/test/fixtures/tnock.js12
-rw-r--r--workspaces/libnpmpublish/test/index.js6
-rw-r--r--workspaces/libnpmpublish/test/publish.js574
-rw-r--r--workspaces/libnpmpublish/test/unpublish.js242
15 files changed, 1430 insertions, 0 deletions
diff --git a/workspaces/libnpmpublish/.eslintrc.js b/workspaces/libnpmpublish/.eslintrc.js
new file mode 100644
index 000000000..022767bc3
--- /dev/null
+++ b/workspaces/libnpmpublish/.eslintrc.js
@@ -0,0 +1,14 @@
+// This file is automatically added by @npmcli/template-oss. Do not edit.
+
+const { readdirSync: readdir } = require('fs')
+
+const localConfigs = readdir(__dirname)
+ .filter((file) => file.startsWith('.eslintrc.local.'))
+ .map((file) => `./${file}`)
+
+module.exports = {
+ extends: [
+ '@npmcli',
+ ...localConfigs,
+ ],
+}
diff --git a/workspaces/libnpmpublish/.gitignore b/workspaces/libnpmpublish/.gitignore
new file mode 100644
index 000000000..6ed44c72b
--- /dev/null
+++ b/workspaces/libnpmpublish/.gitignore
@@ -0,0 +1,23 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+# ignore everything in the root
+/*
+
+# keep these
+!/.commitlintrc.js
+!/.npmrc
+!/.eslintrc*
+!/.github
+!**/.gitignore
+!/package.json
+!/docs
+!/bin
+!/lib
+!/map.js
+!/tap-snapshots
+!/test
+!/scripts
+!/README*
+!/LICENSE*
+!/SECURITY*
+!/CHANGELOG*
diff --git a/workspaces/libnpmpublish/.npmrc b/workspaces/libnpmpublish/.npmrc
new file mode 100644
index 000000000..878b7ddef
--- /dev/null
+++ b/workspaces/libnpmpublish/.npmrc
@@ -0,0 +1,3 @@
+;This file is automatically added by @npmcli/template-oss. Do not edit.
+
+package-lock=false
diff --git a/workspaces/libnpmpublish/CHANGELOG.md b/workspaces/libnpmpublish/CHANGELOG.md
new file mode 100644
index 000000000..57d21f840
--- /dev/null
+++ b/workspaces/libnpmpublish/CHANGELOG.md
@@ -0,0 +1,91 @@
+# Change Log
+
+<a name="3.0.1"></a>
+# [3.0.1](https://github.com/npm/libnpmpublish/compare/v3.0.0...v3.0.1) (2020-03-27)
+
+### Features
+
+* [`3e02307`](https://github.com/npm/libnpmpublish/commit/3e02307) chore: pack tarballs using libnpmpack ([@claudiahdz](https://github.com/claudiahdz))
+
+<a name="3.0.0"></a>
+# [3.0.0](https://github.com/npm/libnpmpublish/compare/v2.0.0...v3.0.0) (2020-03-09)
+
+### Breaking Changes
+
+* [`ecaeb0b`](https://github.com/npm/libnpmpublish/commit/ecaeb0b) feat: pack tarballs from source code using pacote v10 ([@claudiahdz](https://github.com/claudiahdz))
+
+* [`f6bf2b8`](https://github.com/npm/libnpmpublish/commit/f6bf2b8) feat: unpublish code refactor ([@claudiahdz](https://github.com/claudiahdz))
+
+### Miscellaneuous
+
+* [`5cea10f`](https://github.com/npm/libnpmpublish/commit/5cea10f) chore: basic project updates ([@claudiahdz](https://github.com/claudiahdz))
+* [`3010b93`](https://github.com/npm/libnpmpublish/commit/3010b93) chore: cleanup badges + contributing ([@ruyadorno](https://github.com/ruyadorno))
+
+---
+
+All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
+
+## [2.0.0](https://github.com/npm/libnpmpublish/compare/v1.1.3...v2.0.0) (2019-09-18)
+
+
+### ⚠ BREAKING CHANGES
+
+* This drops support for Node.js version 6.
+
+### Bug Fixes
+
+* audit warnings, drop support for Node.js v6 ([d9a1fb6](https://github.com/npm/libnpmpublish/commit/d9a1fb6))
+
+### [1.1.3](https://github.com/npm/libnpmpublish/compare/v1.1.2...v1.1.3) (2019-09-18)
+
+<a name="1.1.2"></a>
+## [1.1.2](https://github.com/npm/libnpmpublish/compare/v1.1.1...v1.1.2) (2019-07-16)
+
+
+
+<a name="1.1.1"></a>
+## [1.1.1](https://github.com/npm/libnpmpublish/compare/v1.1.0...v1.1.1) (2019-01-22)
+
+
+### Bug Fixes
+
+* **auth:** send username in correct key ([#3](https://github.com/npm/libnpmpublish/issues/3)) ([38422d0](https://github.com/npm/libnpmpublish/commit/38422d0))
+
+
+
+<a name="1.1.0"></a>
+# [1.1.0](https://github.com/npm/libnpmpublish/compare/v1.0.1...v1.1.0) (2018-08-31)
+
+
+### Features
+
+* **publish:** add support for publishConfig on manifests ([161723b](https://github.com/npm/libnpmpublish/commit/161723b))
+
+
+
+<a name="1.0.1"></a>
+## [1.0.1](https://github.com/npm/libnpmpublish/compare/v1.0.0...v1.0.1) (2018-08-31)
+
+
+### Bug Fixes
+
+* **opts:** remove unused opts ([2837098](https://github.com/npm/libnpmpublish/commit/2837098))
+
+
+
+<a name="1.0.0"></a>
+# 1.0.0 (2018-08-31)
+
+
+### Bug Fixes
+
+* **api:** use opts.algorithms, return true on success ([80fe34b](https://github.com/npm/libnpmpublish/commit/80fe34b))
+* **publish:** first test pass w/ bugfixes ([74135c9](https://github.com/npm/libnpmpublish/commit/74135c9))
+* **publish:** full coverage test and related fixes ([b5a3446](https://github.com/npm/libnpmpublish/commit/b5a3446))
+
+
+### Features
+
+* **docs:** add README with api docs ([553c13d](https://github.com/npm/libnpmpublish/commit/553c13d))
+* **publish:** add initial publish support. tests tbd ([5b3fe94](https://github.com/npm/libnpmpublish/commit/5b3fe94))
+* **unpublish:** add new api with unpublish support ([1c9d594](https://github.com/npm/libnpmpublish/commit/1c9d594))
diff --git a/workspaces/libnpmpublish/LICENSE b/workspaces/libnpmpublish/LICENSE
new file mode 100644
index 000000000..209e4477f
--- /dev/null
+++ b/workspaces/libnpmpublish/LICENSE
@@ -0,0 +1,13 @@
+Copyright npm, Inc
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/workspaces/libnpmpublish/README.md b/workspaces/libnpmpublish/README.md
new file mode 100644
index 000000000..0da46e89d
--- /dev/null
+++ b/workspaces/libnpmpublish/README.md
@@ -0,0 +1,105 @@
+# libnpmpublish
+
+[`libnpmpublish`](https://github.com/npm/libnpmpublish) is a Node.js
+library for programmatically publishing and unpublishing npm packages. Give
+it a manifest as an object and a tarball as a Buffer, and it'll put them on
+the registry for you.
+
+## Table of Contents
+
+* [Example](#example)
+* [Install](#install)
+* [API](#api)
+ * [publish/unpublish opts](#opts)
+ * [`publish()`](#publish)
+ * [`unpublish()`](#unpublish)
+
+## Example
+
+```js
+const { publish, unpublish } = require('libnpmpublish')
+```
+
+## Install
+
+`$ npm install libnpmpublish`
+
+### API
+
+#### <a name="opts"></a> `opts` for `libnpmpublish` commands
+
+`libnpmpublish` uses
+[`npm-registry-fetch`](https://npm.im/npm-registry-fetch). Most options
+are passed through directly to that library, so please refer to [its own
+`opts` documentation](http://npm.im/npm-registry-fetch#fetch-options) for
+options that can be passed in.
+
+A couple of options of note:
+
+* `opts.defaultTag` - registers the published package with the given tag,
+ defaults to `latest`.
+
+* `opts.access` - tells the registry whether this package should be
+ published as public or restricted. Only applies to scoped packages, which
+ default to restricted.
+
+* `opts.token` - can be passed in and will be used as the authentication
+ token for the registry. For other ways to pass in auth details, see the
+ n-r-f docs.
+
+#### <a name="publish"></a> `> libpub.publish(manifest, tarData, [opts]) -> Promise`
+
+Sends the package represented by the `manifest` and `tarData` to the
+configured registry.
+
+`manifest` should be the parsed `package.json` for the package being
+published (which can also be the manifest pulled from a packument, a git
+repo, tarball, etc.)
+
+`tarData` is a `Buffer` of the tarball being published.
+
+If `opts.npmVersion` is passed in, it will be used as the `_npmVersion`
+field in the outgoing packument. You may put your own user-agent string in
+there to identify your publishes.
+
+If `opts.algorithms` is passed in, it should be an array of hashing
+algorithms to generate `integrity` hashes for. The default is `['sha512']`,
+which means you end up with `dist.integrity = 'sha512-deadbeefbadc0ffee'`.
+Any algorithm supported by your current node version is allowed -- npm
+clients that do not support those algorithms will simply ignore the
+unsupported hashes.
+
+##### Example
+
+```js
+// note that pacote.manifest() and pacote.tarball() can also take
+// any spec that npm can install. a folder shown here, since that's
+// far and away the most common use case.
+const path = '/a/path/to/your/source/code'
+const pacote = require('pacote') // see: http://npm.im/pacote
+const manifest = await pacote.manifest(path)
+const tarData = await pacote.tarball(path)
+await libpub.publish(manifest, tarData, {
+ npmVersion: 'my-pub-script@1.0.2',
+ token: 'my-auth-token-here'
+}, opts)
+// Package has been published to the npm registry.
+```
+
+#### <a name="unpublish"></a> `> libpub.unpublish(spec, [opts]) -> Promise`
+
+Unpublishes `spec` from the appropriate registry. The registry in question may
+have its own limitations on unpublishing.
+
+`spec` should be either a string, or a valid
+[`npm-package-arg`](https://npm.im/npm-package-arg) parsed spec object. For
+legacy compatibility reasons, only `tag` and `version` specs will work as
+expected. `range` specs will fail silently in most cases.
+
+##### Example
+
+```js
+await libpub.unpublish('lodash', { token: 'i-am-the-worst'})
+//
+// `lodash` has now been unpublished, along with all its versions
+```
diff --git a/workspaces/libnpmpublish/SECURITY.md b/workspaces/libnpmpublish/SECURITY.md
new file mode 100644
index 000000000..a93106d0c
--- /dev/null
+++ b/workspaces/libnpmpublish/SECURITY.md
@@ -0,0 +1,3 @@
+<!-- This file is automatically added by @npmcli/template-oss. Do not edit. -->
+
+Please send vulnerability reports through [hackerone](https://hackerone.com/github).
diff --git a/workspaces/libnpmpublish/lib/index.js b/workspaces/libnpmpublish/lib/index.js
new file mode 100644
index 000000000..35687e026
--- /dev/null
+++ b/workspaces/libnpmpublish/lib/index.js
@@ -0,0 +1,4 @@
+module.exports = {
+ publish: require('./publish.js'),
+ unpublish: require('./unpublish.js'),
+}
diff --git a/workspaces/libnpmpublish/lib/publish.js b/workspaces/libnpmpublish/lib/publish.js
new file mode 100644
index 000000000..f6d88f732
--- /dev/null
+++ b/workspaces/libnpmpublish/lib/publish.js
@@ -0,0 +1,182 @@
+const { fixer } = require('normalize-package-data')
+const npmFetch = require('npm-registry-fetch')
+const npa = require('npm-package-arg')
+const semver = require('semver')
+const { URL } = require('url')
+const ssri = require('ssri')
+
+const publish = async (manifest, tarballData, opts) => {
+ if (manifest.private) {
+ throw Object.assign(
+ new Error(`This package has been marked as private
+Remove the 'private' field from the package.json to publish it.`),
+ { code: 'EPRIVATE' }
+ )
+ }
+
+ // 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 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 metadata = buildMetadata(reg, pubManifest, tarballData, opts)
+
+ try {
+ return await npmFetch(spec.escapedName, {
+ ...opts,
+ method: 'PUT',
+ body: metadata,
+ ignoreBody: 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,
+ })
+ }
+}
+
+const patchManifest = (_manifest, opts) => {
+ const { npmVersion } = opts
+ // we only update top-level fields, so a shallow clone is fine
+ const manifest = { ..._manifest }
+
+ manifest._nodeVersion = process.versions.node
+ if (npmVersion) {
+ manifest._npmVersion = npmVersion
+ }
+
+ 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
+}
+
+const 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,
+ }
+
+ root.versions[manifest.version] = manifest
+ const tag = manifest.tag || defaultTag
+ root['dist-tags'][tag] = manifest.version
+
+ 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 }
+ // 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()
+
+ // 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[tarballName] = {
+ content_type: 'application/octet-stream',
+ data: tarballData.toString('base64'),
+ length: tarballData.length,
+ }
+
+ return root
+}
+
+const patchMetadata = (current, newData) => {
+ const curVers = Object.keys(current.versions || {})
+ .map(v => semver.clean(v, true))
+ .concat(Object.keys(current.time || {})
+ .map(v => semver.valid(v, true) && semver.clean(v, true))
+ .filter(v => v))
+
+ const newVersion = Object.keys(newData.versions)[0]
+
+ if (curVers.indexOf(newVersion) !== -1) {
+ 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 || {}
+ current.versions[newVersion] = newData.versions[newVersion]
+ for (const i in newData) {
+ switch (i) {
+ // objects that copy over the new stuffs
+ case 'dist-tags':
+ case 'versions':
+ case '_attachments':
+ for (const j in newData[i]) {
+ current[i] = current[i] || {}
+ current[i][j] = newData[i][j]
+ }
+ break
+
+ // copy
+ default:
+ current[i] = newData[i]
+ break
+ }
+ }
+
+ return current
+}
+
+module.exports = publish
diff --git a/workspaces/libnpmpublish/lib/unpublish.js b/workspaces/libnpmpublish/lib/unpublish.js
new file mode 100644
index 000000000..7fbeea503
--- /dev/null
+++ b/workspaces/libnpmpublish/lib/unpublish.js
@@ -0,0 +1,102 @@
+'use strict'
+
+const npa = require('npm-package-arg')
+const npmFetch = require('npm-registry-fetch')
+const semver = require('semver')
+const { URL } = require('url')
+
+const unpublish = async (spec, opts) => {
+ spec = npa(spec)
+ // spec is used to pick the appropriate registry/auth combo.
+ opts = {
+ force: false,
+ ...opts,
+ spec,
+ }
+
+ try {
+ const pkgUri = spec.escapedName
+ const pkg = await npmFetch.json(pkgUri, {
+ ...opts,
+ query: { write: true },
+ })
+
+ const version = spec.rawSpec
+ const allVersions = pkg.versions || {}
+ const versionData = allVersions[version]
+
+ const rawSpecs = (!spec.rawSpec || spec.rawSpec === '*')
+ const onlyVersion = Object.keys(allVersions).length === 1
+ const noVersions = !Object.keys(allVersions).length
+
+ // if missing specific version,
+ // assumed unpublished
+ if (!versionData && !rawSpecs && !noVersions) {
+ return true
+ }
+
+ // unpublish all versions of a package:
+ // - no specs supplied "npm unpublish foo"
+ // - all specs ("*") "npm unpublish foo@*"
+ // - there was only one version
+ // - has no versions field on packument
+ if (rawSpecs || onlyVersion || noVersions) {
+ await npmFetch(`${pkgUri}/-rev/${pkg._rev}`, {
+ ...opts,
+ method: 'DELETE',
+ ignoreBody: true,
+ })
+ return true
+ } else {
+ const dist = allVersions[version].dist
+ delete allVersions[version]
+
+ const latestVer = pkg['dist-tags'].latest
+
+ // deleting dist tags associated to version
+ Object.keys(pkg['dist-tags']).forEach(tag => {
+ if (pkg['dist-tags'][tag] === version) {
+ delete pkg['dist-tags'][tag]
+ }
+ })
+
+ if (latestVer === version) {
+ pkg['dist-tags'].latest = Object.keys(
+ allVersions
+ ).sort(semver.compareLoose).pop()
+ }
+
+ delete pkg._revisions
+ delete pkg._attachments
+
+ // Update packument with removed versions
+ await npmFetch(`${pkgUri}/-rev/${pkg._rev}`, {
+ ...opts,
+ method: 'PUT',
+ body: pkg,
+ ignoreBody: true,
+ })
+
+ // Remove the tarball itself
+ const { _rev } = await npmFetch.json(pkgUri, {
+ ...opts,
+ query: { write: true },
+ })
+ const tarballUrl = new URL(dist.tarball).pathname.substr(1)
+ await npmFetch(`${tarballUrl}/-rev/${_rev}`, {
+ ...opts,
+ method: 'DELETE',
+ ignoreBody: true,
+ })
+ return true
+ }
+ } catch (err) {
+ if (err.code !== 'E404') {
+ throw err
+ }
+
+ return true
+ }
+}
+
+module.exports = unpublish
diff --git a/workspaces/libnpmpublish/package.json b/workspaces/libnpmpublish/package.json
new file mode 100644
index 000000000..ebedad68f
--- /dev/null
+++ b/workspaces/libnpmpublish/package.json
@@ -0,0 +1,56 @@
+{
+ "name": "libnpmpublish",
+ "version": "5.0.0",
+ "description": "Programmatic API for the bits behind npm publish and unpublish",
+ "author": "GitHub Inc.",
+ "main": "lib/index.js",
+ "contributors": [
+ "Kat Marchán <kzm@zkat.tech>",
+ "Claudia Hernández <claudia@npmjs.com>"
+ ],
+ "files": [
+ "bin",
+ "lib"
+ ],
+ "license": "ISC",
+ "scripts": {
+ "eslint": "eslint",
+ "lint": "eslint '**/*.js'",
+ "lintfix": "npm run lint -- --fix",
+ "preversion": "npm test",
+ "postversion": "npm publish",
+ "prepublishOnly": "git push origin --follow-tags",
+ "test": "tap",
+ "posttest": "npm run lint",
+ "postlint": "npm-template-check",
+ "snap": "tap"
+ },
+ "tap": {
+ "check-coverage": true
+ },
+ "devDependencies": {
+ "libnpmpack": "^3.0.0",
+ "lodash.clonedeep": "^4.5.0",
+ "nock": "^12.0.2",
+ "tap": "^15"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/npm/libnpmpublish.git"
+ },
+ "bugs": "https://github.com/npm/libnpmpublish/issues",
+ "homepage": "https://npmjs.com/package/libnpmpublish",
+ "dependencies": {
+ "normalize-package-data": "^3.0.2",
+ "npm-package-arg": "^8.1.2",
+ "npm-registry-fetch": "^11.0.0",
+ "semver": "^7.1.3",
+ "ssri": "^8.0.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16"
+ },
+ "templateOSS": {
+ "version": "2.4.1"
+ }
+}
diff --git a/workspaces/libnpmpublish/test/fixtures/tnock.js b/workspaces/libnpmpublish/test/fixtures/tnock.js
new file mode 100644
index 000000000..048639a50
--- /dev/null
+++ b/workspaces/libnpmpublish/test/fixtures/tnock.js
@@ -0,0 +1,12 @@
+'use strict'
+
+const nock = require('nock')
+
+module.exports = tnock
+function tnock (t, host) {
+ const server = nock(host)
+ t.teardown(function () {
+ server.done()
+ })
+ return server
+}
diff --git a/workspaces/libnpmpublish/test/index.js b/workspaces/libnpmpublish/test/index.js
new file mode 100644
index 000000000..7238e65a4
--- /dev/null
+++ b/workspaces/libnpmpublish/test/index.js
@@ -0,0 +1,6 @@
+const t = require('tap')
+const index = require('../lib/index.js')
+t.strictSame(index, {
+ publish: require('../lib/publish.js'),
+ unpublish: require('../lib/unpublish.js'),
+})
diff --git a/workspaces/libnpmpublish/test/publish.js b/workspaces/libnpmpublish/test/publish.js
new file mode 100644
index 000000000..fdd20f899
--- /dev/null
+++ b/workspaces/libnpmpublish/test/publish.js
@@ -0,0 +1,574 @@
+'use strict'
+
+const t = require('tap')
+const ssri = require('ssri')
+const crypto = require('crypto')
+const pack = require('libnpmpack')
+const cloneDeep = require('lodash.clonedeep')
+
+const publish = require('../lib/publish.js')
+const tnock = require('./fixtures/tnock.js')
+
+const testDir = t.testdir({
+ 'package.json': JSON.stringify({
+ name: 'libnpmpublish',
+ version: '1.0.0',
+ }, null, 2),
+ 'index.js': 'hello',
+})
+
+const OPTS = {
+ registry: 'https://mock.reg/',
+}
+
+const REG = OPTS.registry
+
+t.test('basic publish', async t => {
+ const manifest = {
+ name: 'libnpmpublish',
+ version: '1.0.0',
+ description: 'some stuff',
+ }
+
+ const tarData = await pack(`file:${testDir}`, { ...OPTS })
+ const shasum = crypto.createHash('sha1').update(tarData).digest('hex')
+ const integrity = ssri.fromData(tarData, { algorithms: ['sha512'] })
+ const packument = {
+ _id: 'libnpmpublish',
+ name: 'libnpmpublish',
+ description: 'some stuff',
+ 'dist-tags': {
+ latest: '1.0.0',
+ },
+ versions: {
+ '1.0.0': {
+ _id: 'libnpmpublish@1.0.0',
+ _nodeVersion: process.versions.node,
+ name: 'libnpmpublish',
+ version: '1.0.0',
+ description: 'some stuff',
+ dist: {
+ shasum,
+ integrity: integrity.toString(),
+ tarball: 'http://mock.reg/libnpmpublish/-/libnpmpublish-1.0.0.tgz',
+ },
+ },
+ },
+ access: 'public',
+ _attachments: {
+ 'libnpmpublish-1.0.0.tgz': {
+ content_type: 'application/octet-stream',
+ data: tarData.toString('base64'),
+ length: tarData.length,
+ },
+ },
+ }
+
+ const srv = tnock(t, REG)
+ srv.put('/libnpmpublish', body => {
+ t.same(body, packument, 'posted packument matches expectations')
+ return true
+ }, {
+ authorization: 'Bearer deadbeef',
+ }).reply(201, {})
+
+ const ret = await publish(manifest, tarData, {
+ ...OPTS,
+ token: 'deadbeef',
+ })
+ t.ok(ret, 'publish succeeded')
+})
+
+t.test('scoped publish', async t => {
+ const manifest = {
+ name: '@claudiahdz/libnpmpublish',
+ version: '1.0.0',
+ description: 'some stuff',
+ }
+
+ const tarData = await pack(`file:${testDir}`, { ...OPTS })
+ const shasum = crypto.createHash('sha1').update(tarData).digest('hex')
+ const integrity = ssri.fromData(tarData, { algorithms: ['sha512'] })
+ const packument = {
+ _id: '@claudiahdz/libnpmpublish',
+ name: '@claudiahdz/libnpmpublish',
+ description: 'some stuff',
+ 'dist-tags': {
+ latest: '1.0.0',
+ },
+ versions: {
+ '1.0.0': {
+ _id: '@claudiahdz/libnpmpublish@1.0.0',
+ _nodeVersion: process.versions.node,
+ _npmVersion: '6.13.7',
+ name: '@claudiahdz/libnpmpublish',
+ version: '1.0.0',
+ description: 'some stuff',
+ dist: {
+ shasum,
+ integrity: integrity.toString(),
+ tarball: 'http://mock.reg/@claudiahdz/libnpmpublish/'
+ + '-/@claudiahdz/libnpmpublish-1.0.0.tgz',
+ },
+ },
+ },
+ access: 'restricted',
+ _attachments: {
+ '@claudiahdz/libnpmpublish-1.0.0.tgz': {
+ content_type: 'application/octet-stream',
+ data: tarData.toString('base64'),
+ length: tarData.length,
+ },
+ },
+ }
+
+ const srv = tnock(t, REG)
+ srv.put('/@claudiahdz%2flibnpmpublish', body => {
+ t.same(body, packument, 'posted packument matches expectations')
+ return true
+ }, {
+ authorization: 'Bearer deadbeef',
+ }).reply(201, {})
+
+ const ret = await publish(manifest, tarData, {
+ ...OPTS,
+ npmVersion: '6.13.7',
+ token: 'deadbeef',
+ })
+ t.ok(ret, 'publish succeeded')
+})
+
+t.test('retry after a conflict', async t => {
+ const REV = '72-47f2986bfd8e8b55068b204588bbf484'
+ const manifest = {
+ name: 'libnpmpublish',
+ version: '1.0.0',
+ description: 'some stuff',
+ }
+
+ const tarData = await pack(`file:${testDir}`, { ...OPTS })
+ const shasum = crypto.createHash('sha1').update(tarData).digest('hex')
+ const integrity = ssri.fromData(tarData, { algorithms: ['sha512'] })
+
+ const basePackument = {
+ name: 'libnpmpublish',
+ description: 'some stuff',
+ access: 'public',
+ _id: 'libnpmpublish',
+ 'dist-tags': {},
+ versions: {},
+ _attachments: {},
+ }
+ const currentPackument = cloneDeep({
+ ...basePackument,
+ time: {
+ modified: new Date().toISOString(),
+ created: new Date().toISOString(),
+ '1.0.1': new Date().toISOString(),
+ },
+ 'dist-tags': { latest: '1.0.1' },
+ versions: {
+ '1.0.1': {
+ _id: 'libnpmpublish@1.0.1',
+ _nodeVersion: process.versions.node,
+ _npmVersion: '13.7.0',
+ name: 'libnpmpublish',
+ version: '1.0.1',
+ description: 'some stuff',
+ dist: {
+ shasum,
+ integrity: integrity.toString(),
+ tarball: 'http://mock.reg/libnpmpublish/-/libnpmpublish-1.0.1.tgz',
+ },
+ },
+ },
+ _attachments: {
+ 'libnpmpublish-1.0.1.tgz': {
+ content_type: 'application/octet-stream',
+ data: tarData.toString('base64'),
+ length: tarData.length,
+ },
+ },
+ })
+ const newPackument = cloneDeep({
+ ...basePackument,
+ 'dist-tags': { latest: '1.0.0' },
+ versions: {
+ '1.0.0': {
+ _id: 'libnpmpublish@1.0.0',
+ _nodeVersion: process.versions.node,
+ _npmVersion: '6.13.7',
+ name: 'libnpmpublish',
+ version: '1.0.0',
+ description: 'some stuff',
+ dist: {
+ shasum,
+ integrity: integrity.toString(),
+ tarball: 'http://mock.reg/libnpmpublish/-/libnpmpublish-1.0.0.tgz',
+ },
+ },
+ },
+ _attachments: {
+ 'libnpmpublish-1.0.0.tgz': {
+ content_type: 'application/octet-stream',
+ data: tarData.toString('base64'),
+ length: tarData.length,
+ },
+ },
+ })
+ const mergedPackument = cloneDeep({
+ ...basePackument,
+ time: currentPackument.time,
+ 'dist-tags': { latest: '1.0.0' },
+ versions: { ...currentPackument.versions, ...newPackument.versions },
+ _attachments: { ...currentPackument._attachments, ...newPackument._attachments },
+ })
+
+ const srv = tnock(t, REG)
+ srv.put('/libnpmpublish', body => {
+ t.notOk(body._rev, 'no _rev in initial post')
+ t.same(body, newPackument, 'got conflicting packument')
+ return true
+ }).reply(409, { error: 'gimme _rev plz' })
+
+ srv.get('/libnpmpublish?write=true').reply(200, {
+ _rev: REV,
+ ...currentPackument,
+ })
+
+ srv.put('/libnpmpublish', body => {
+ t.same(body, {
+ _rev: REV,
+ ...mergedPackument,
+ }, 'posted packument includes _rev and a merged version')
+ return true
+ }).reply(201, {})
+
+ const ret = await publish(manifest, tarData, {
+ ...OPTS,
+ token: 'deadbeef',
+ npmVersion: '6.13.7',
+ })
+
+ t.ok(ret, 'publish succeeded')
+})
+
+t.test('retry after a conflict -- no versions on remote', async t => {
+ const REV = '72-47f2986bfd8e8b55068b204588bbf484'
+ const manifest = {
+ name: 'libnpmpublish',
+ version: '1.0.0',
+ description: 'some stuff',
+ }
+
+ const tarData = await pack(`file:${testDir}`, { ...OPTS })
+ const shasum = crypto.createHash('sha1').update(tarData).digest('hex')
+ const integrity = ssri.fromData(tarData, { algorithms: ['sha512'] })
+
+ const basePackument = {
+ name: 'libnpmpublish',
+ description: 'some stuff',
+ access: 'public',
+ _id: 'libnpmpublish',
+ }
+ const currentPackument = { ...basePackument }
+ const newPackument = cloneDeep({
+ ...basePackument,
+ 'dist-tags': { latest: '1.0.0' },
+ versions: {
+ '1.0.0': {
+ _id: 'libnpmpublish@1.0.0',
+ _nodeVersion: process.versions.node,
+ _npmVersion: '6.13.7',
+ name: 'libnpmpublish',
+ version: '1.0.0',
+ description: 'some stuff',
+ dist: {
+ shasum,
+ integrity: integrity.toString(),
+ tarball: 'http://mock.reg/libnpmpublish/-/libnpmpublish-1.0.0.tgz',
+ },
+ },
+ },
+ _attachments: {
+ 'libnpmpublish-1.0.0.tgz': {
+ content_type: 'application/octet-stream',
+ data: tarData.toString('base64'),
+ length: tarData.length,
+ },
+ },
+ })
+ const mergedPackument = cloneDeep({
+ ...basePackument,
+ 'dist-tags': { latest: '1.0.0' },
+ versions: { ...newPackument.versions },
+ _attachments: { ...newPackument._attachments },
+ })
+
+ const srv = tnock(t, REG)
+ srv.put('/libnpmpublish', body => {
+ t.notOk(body._rev, 'no _rev in initial post')
+ t.same(body, newPackument, 'got conflicting packument')
+ return true
+ }).reply(409, { error: 'gimme _rev plz' })
+
+ srv.get('/libnpmpublish?write=true').reply(200, {
+ _rev: REV,
+ ...currentPackument,
+ })
+
+ srv.put('/libnpmpublish', body => {
+ t.same(body, {
+ _rev: REV,
+ ...mergedPackument,
+ }, 'posted packument includes _rev and a merged version')
+ return true
+ }).reply(201, {})
+
+ const ret = await publish(manifest, tarData, {
+ ...OPTS,
+ npmVersion: '6.13.7',
+ token: 'deadbeef',
+ })
+
+ t.ok(ret, 'publish succeeded')
+})
+
+t.test('version conflict', async t => {
+ const REV = '72-47f2986bfd8e8b55068b204588bbf484'
+ const manifest = {
+ name: 'libnpmpublish',
+ version: '1.0.0',
+ description: 'some stuff',
+ }
+
+ const tarData = await pack(`file:${testDir}`, { ...OPTS })
+ const shasum = crypto.createHash('sha1').update(tarData).digest('hex')
+ const integrity = ssri.fromData(tarData, { algorithms: ['sha512'] })
+ const basePackument = {
+ name: 'libnpmpublish',
+ description: 'some stuff',
+ access: 'public',
+ _id: 'libnpmpublish',
+ 'dist-tags': {},
+ versions: {},
+ _attachments: {},
+ }
+ const newPackument = cloneDeep(Object.assign({}, basePackument, {
+ 'dist-tags': { latest: '1.0.0' },
+ versions: {
+ '1.0.0': {
+ _id: 'libnpmpublish@1.0.0',
+ _nodeVersion: process.versions.node,
+ _npmVersion: '6.13.7',
+ name: 'libnpmpublish',
+ version: '1.0.0',
+ description: 'some stuff',
+ dist: {
+ shasum,
+ integrity: integrity.toString(),
+ tarball: 'http://mock.reg/libnpmpublish/-/libnpmpublish-1.0.0.tgz',
+ },
+ },
+ },
+ _attachments: {
+ 'libnpmpublish-1.0.0.tgz': {
+ content_type: 'application/octet-stream',
+ data: tarData.toString('base64'),
+ length: tarData.length,
+ },
+ },
+ }))
+
+ const srv = tnock(t, REG)
+ srv.put('/libnpmpublish', body => {
+ t.notOk(body._rev, 'no _rev in initial post')
+ t.same(body, newPackument, 'got conflicting packument')
+ return true
+ }).reply(409, { error: 'gimme _rev plz' })
+
+ srv.get('/libnpmpublish?write=true').reply(200, {
+ _rev: REV,
+ ...newPackument,
+ })
+
+ try {
+ await publish(manifest, tarData, {
+ ...OPTS,
+ npmVersion: '6.13.7',
+ token: 'deadbeef',
+ })
+ } catch (err) {
+ t.equal(err.code, 'EPUBLISHCONFLICT', 'got publish conflict code')
+ }
+})
+
+t.test('refuse if package marked private', async t => {
+ const manifest = {
+ name: 'libnpmpublish',
+ version: '1.0.0',
+ description: 'some stuff',
+ private: true,
+ }
+
+ try {
+ await publish(manifest, Buffer.from(''), {
+ ...OPTS,
+ npmVersion: '6.9.0',
+ token: 'deadbeef',
+ })
+ } catch (err) {
+ t.equal(err.code, 'EPRIVATE', 'got correct error code')
+ }
+})
+
+t.test('publish includes access', async t => {
+ const manifest = {
+ name: 'libnpmpublish',
+ version: '1.0.0',
+ description: 'some stuff',
+ }
+
+ const tarData = await pack(`file:${testDir}`, { ...OPTS })
+ const shasum = crypto.createHash('sha1').update(tarData).digest('hex')
+ const integrity = ssri.fromData(tarData, { algorithms: ['sha512'] })
+ const packument = {
+ name: 'libnpmpublish',
+ description: 'some stuff',
+ access: 'public',
+ _id: 'libnpmpublish',
+ 'dist-tags': {
+ latest: '1.0.0',
+ },
+ versions: {
+ '1.0.0': {
+ _id: 'libnpmpublish@1.0.0',
+ _nodeVersion: process.versions.node,
+ name: 'libnpmpublish',
+ version: '1.0.0',
+ description: 'some stuff',
+ dist: {
+ shasum,
+ integrity: integrity.toString(),
+ tarball: 'http://mock.reg/libnpmpublish/-/libnpmpublish-1.0.0.tgz',
+ },
+ },
+ },
+ _attachments: {
+ 'libnpmpublish-1.0.0.tgz': {
+ content_type: 'application/octet-stream',
+ data: tarData.toString('base64'),
+ length: tarData.length,
+ },
+ },
+ }
+
+ const srv = tnock(t, REG)
+ srv.put('/libnpmpublish', body => {
+ t.same(body, packument, 'posted packument matches expectations')
+ return true
+ }, {
+ authorization: 'Bearer deadbeef',
+ }).reply(201, {})
+
+ const ret = await publish(manifest, tarData, {
+ ...OPTS,
+ token: 'deadbeef',
+ access: 'public',
+ })
+
+ t.ok(ret, 'publish succeeded')
+})
+
+t.test('refuse if package is unscoped plus `restricted` access', async t => {
+ const manifest = {
+ name: 'libnpmpublish',
+ version: '1.0.0',
+ description: 'some stuff',
+ }
+
+ try {
+ await publish(manifest, Buffer.from(''), {
+ ...OPTS,
+ npmVersion: '6.13.7',
+ access: 'restricted',
+ })
+ } catch (err) {
+ t.equal(err.code, 'EUNSCOPED', 'got correct error code')
+ }
+})
+
+t.test('refuse if bad semver on manifest', async t => {
+ const manifest = {
+ name: 'libnpmpublish',
+ version: 'lmao',
+ description: 'some stuff',
+ }
+
+ try {
+ await publish(manifest, Buffer.from(''), OPTS)
+ } catch (err) {
+ t.equal(err.code, 'EBADSEMVER', 'got correct error code')
+ }
+})
+
+t.test('other error code', async t => {
+ const manifest = {
+ name: 'libnpmpublish',
+ version: '1.0.0',
+ description: 'some stuff',
+ }
+
+ const tarData = await pack(`file:${testDir}`, { ...OPTS })
+ const shasum = crypto.createHash('sha1').update(tarData).digest('hex')
+ const integrity = ssri.fromData(tarData, { algorithms: ['sha512'] })
+ const packument = {
+ name: 'libnpmpublish',
+ description: 'some stuff',
+ access: 'public',
+ _id: 'libnpmpublish',
+ 'dist-tags': {
+ latest: '1.0.0',
+ },
+ versions: {
+ '1.0.0': {
+ _id: 'libnpmpublish@1.0.0',
+ _nodeVersion: process.versions.node,
+ _npmVersion: '6.13.7',
+ name: 'libnpmpublish',
+ version: '1.0.0',
+ description: 'some stuff',
+ dist: {
+ shasum,
+ integrity: integrity.toString(),
+ tarball: 'http://mock.reg/libnpmpublish/-/libnpmpublish-1.0.0.tgz',
+ },
+ },
+ },
+ _attachments: {
+ 'libnpmpublish-1.0.0.tgz': {
+ content_type: 'application/octet-stream',
+ data: tarData.toString('base64'),
+ length: tarData.length,
+ },
+ },
+ }
+
+ const srv = tnock(t, REG)
+ srv.put('/libnpmpublish', body => {
+ t.same(body, packument, 'posted packument matches expectations')
+ return true
+ }, {
+ authorization: 'Bearer deadbeef',
+ }).reply(500, { error: 'go away' })
+
+ try {
+ await publish(manifest, tarData, {
+ ...OPTS,
+ npmVersion: '6.13.7',
+ token: 'deadbeef',
+ })
+ } catch (err) {
+ t.match(err.message, /go away/, 'no retry on non-409')
+ }
+})
diff --git a/workspaces/libnpmpublish/test/unpublish.js b/workspaces/libnpmpublish/test/unpublish.js
new file mode 100644
index 000000000..6fbe80276
--- /dev/null
+++ b/workspaces/libnpmpublish/test/unpublish.js
@@ -0,0 +1,242 @@
+'use strict'
+
+const t = require('tap')
+const tnock = require('./fixtures/tnock.js')
+
+const OPTS = {
+ registry: 'https://mock.reg/',
+}
+
+const REG = OPTS.registry
+const REV = '72-47f2986bfd8e8b55068b204588bbf484'
+const unpub = require('../lib/unpublish.js')
+
+t.test('basic test', async t => {
+ const doc = {
+ _id: 'foo',
+ _rev: REV,
+ name: 'foo',
+ 'dist-tags': {
+ latest: '1.0.0',
+ },
+ versions: {
+ '1.0.0': {
+ name: 'foo',
+ dist: {
+ tarball: `${REG}/foo/-/foo-1.0.0.tgz`,
+ },
+ },
+ },
+ }
+ const srv = tnock(t, REG)
+ srv.get('/foo?write=true').reply(200, doc)
+ srv.delete(`/foo/-rev/${REV}`).reply(201)
+ const ret = await unpub('foo', OPTS)
+ t.ok(ret, 'foo was unpublished')
+})
+
+t.test('scoped basic test', async t => {
+ const doc = {
+ _id: '@foo/bar',
+ _rev: REV,
+ name: '@foo/bar',
+ 'dist-tags': {
+ latest: '1.0.0',
+ },
+ versions: {
+ '1.0.0': {
+ name: '@foo/bar',
+ dist: {
+ tarball: `${REG}/@foo/bar/-/foo-1.0.0.tgz`,
+ },
+ },
+ },
+ }
+ const srv = tnock(t, REG)
+ srv.get('/@foo%2fbar?write=true').reply(200, doc)
+ srv.delete(`/@foo%2fbar/-rev/${REV}`).reply(201)
+ const ret = await unpub('@foo/bar', OPTS)
+ t.ok(ret, 'foo was unpublished')
+})
+
+t.test('unpublish specific, last version', async t => {
+ const doc = {
+ _id: 'foo',
+ _rev: REV,
+ name: 'foo',
+ 'dist-tags': {
+ latest: '1.0.0',
+ },
+ versions: {
+ '1.0.0': {
+ name: 'foo',
+ dist: {
+ tarball: `${REG}/foo/-/foo-1.0.0.tgz`,
+ },
+ },
+ },
+ }
+ const srv = tnock(t, REG)
+ srv.get('/foo?write=true').reply(200, doc)
+ srv.delete(`/foo/-rev/${REV}`).reply(201)
+ const ret = await unpub('foo@1.0.0', OPTS)
+ t.ok(ret, 'foo was unpublished')
+})
+
+t.test('unpublish specific version', async t => {
+ const doc = {
+ _id: 'foo',
+ _rev: REV,
+ _revisions: [1, 2, 3],
+ _attachments: [1, 2, 3],
+ name: 'foo',
+ 'dist-tags': {
+ latest: '1.0.1',
+ },
+ versions: {
+ '1.0.0': {
+ name: 'foo',
+ dist: {
+ tarball: `${REG}/foo/-/foo-1.0.0.tgz`,
+ },
+ },
+ '1.0.1': {
+ name: 'foo',
+ dist: {
+ tarball: `${REG}/foo/-/foo-1.0.1.tgz`,
+ },
+ },
+ },
+ }
+ const postEdit = {
+ _id: 'foo',
+ _rev: REV,
+ name: 'foo',
+ 'dist-tags': {
+ latest: '1.0.0',
+ },
+ versions: {
+ '1.0.0': {
+ name: 'foo',
+ dist: {
+ tarball: `${REG}/foo/-/foo-1.0.0.tgz`,
+ },
+ },
+ },
+ }
+
+ const srv = tnock(t, REG)
+ srv.get('/foo?write=true').reply(200, doc)
+ srv.put(`/foo/-rev/${REV}`, postEdit).reply(200)
+ srv.get('/foo?write=true').reply(200, postEdit)
+ srv.delete(`/foo/-/foo-1.0.1.tgz/-rev/${REV}`).reply(200)
+ const ret = await unpub('foo@1.0.1', OPTS)
+ t.ok(ret, 'foo was unpublished')
+})
+
+t.test('404 considered a success', async t => {
+ const srv = tnock(t, REG)
+ srv.get('/foo?write=true').reply(404)
+ const ret = await unpub('foo', OPTS)
+ t.ok(ret, 'foo was unpublished')
+})
+
+t.test('non-404 errors', async t => {
+ const srv = tnock(t, REG)
+ srv.get('/foo?write=true').reply(500)
+
+ try {
+ await unpub('foo', OPTS)
+ } catch (err) {
+ t.equal(err.code, 'E500', 'got right error from server')
+ }
+})
+
+t.test('packument with missing versions unpublishes whole thing', async t => {
+ const doc = {
+ _id: 'foo',
+ _rev: REV,
+ name: 'foo',
+ 'dist-tags': {
+ latest: '1.0.0',
+ },
+ }
+ const srv = tnock(t, REG)
+ srv.get('/foo?write=true').reply(200, doc)
+ srv.delete(`/foo/-rev/${REV}`).reply(201)
+ const ret = await unpub('foo@1.0.0', OPTS)
+ t.ok(ret, 'foo was unpublished')
+})
+
+t.test('packument with missing specific version assumed unpublished', async t => {
+ const doc = {
+ _id: 'foo',
+ _rev: REV,
+ name: 'foo',
+ 'dist-tags': {
+ latest: '1.0.0',
+ },
+ versions: {
+ '1.0.0': {
+ name: 'foo',
+ dist: {
+ tarball: `${REG}/foo/-/foo-1.0.0.tgz`,
+ },
+ },
+ },
+ }
+ const srv = tnock(t, REG)
+ srv.get('/foo?write=true').reply(200, doc)
+ const ret = await unpub('foo@1.0.1', OPTS)
+ t.ok(ret, 'foo was unpublished')
+})
+
+t.test('unpublish specific version without dist-tag update', async t => {
+ const doc = {
+ _id: 'foo',
+ _rev: REV,
+ _revisions: [1, 2, 3],
+ _attachments: [1, 2, 3],
+ name: 'foo',
+ 'dist-tags': {
+ latest: '1.0.0',
+ },
+ versions: {
+ '1.0.0': {
+ name: 'foo',
+ dist: {
+ tarball: `${REG}/foo/-/foo-1.0.0.tgz`,
+ },
+ },
+ '1.0.1': {
+ name: 'foo',
+ dist: {
+ tarball: `${REG}/foo/-/foo-1.0.1.tgz`,
+ },
+ },
+ },
+ }
+ const postEdit = {
+ _id: 'foo',
+ _rev: REV,
+ name: 'foo',
+ 'dist-tags': {
+ latest: '1.0.0',
+ },
+ versions: {
+ '1.0.0': {
+ name: 'foo',
+ dist: {
+ tarball: `${REG}/foo/-/foo-1.0.0.tgz`,
+ },
+ },
+ },
+ }
+ const srv = tnock(t, REG)
+ srv.get('/foo?write=true').reply(200, doc)
+ srv.put(`/foo/-rev/${REV}`, postEdit).reply(200)
+ srv.get('/foo?write=true').reply(200, postEdit)
+ srv.delete(`/foo/-/foo-1.0.1.tgz/-rev/${REV}`).reply(200)
+ const ret = await unpub('foo@1.0.1', OPTS)
+ t.ok(ret, 'foo was unpublished')
+})