libnpmaccess
Advanced tools
Comparing version 7.0.0-pre.0 to 7.0.0-pre.1
254
lib/index.js
'use strict' | ||
const Minipass = require('minipass') | ||
const npa = require('npm-package-arg') | ||
const npmFetch = require('npm-registry-fetch') | ||
const validate = require('aproba') | ||
const eu = encodeURIComponent | ||
const npar = spec => { | ||
const npar = (spec) => { | ||
spec = npa(spec) | ||
if (!spec.registry) { | ||
throw new Error('`spec` must be a registry spec') | ||
throw new Error('must use package name only') | ||
} | ||
return spec | ||
} | ||
const mapJSON = (value, [key]) => { | ||
if (value === 'read') { | ||
return [key, 'read-only'] | ||
} else if (value === 'write') { | ||
return [key, 'read-write'] | ||
} else { | ||
return [key, value] | ||
const parseTeam = (scopeTeam) => { | ||
let slice = 0 | ||
if (scopeTeam.startsWith('@')) { | ||
slice = 1 | ||
} | ||
const [scope, team] = scopeTeam.slice(slice).split(':').map(encodeURIComponent) | ||
return { scope, team } | ||
} | ||
const cmd = module.exports = {} | ||
const getPackages = async (scopeTeam, opts) => { | ||
const { scope, team } = parseTeam(scopeTeam) | ||
cmd.public = (spec, opts) => setAccess(spec, 'public', opts) | ||
cmd.restricted = (spec, opts) => setAccess(spec, 'restricted', opts) | ||
function setAccess (spec, access, opts = {}) { | ||
return Promise.resolve().then(() => { | ||
spec = npar(spec) | ||
validate('OSO', [spec, access, opts]) | ||
const uri = `/-/package/${eu(spec.name)}/access` | ||
return npmFetch(uri, { | ||
...opts, | ||
method: 'POST', | ||
body: { access }, | ||
spec, | ||
}).then(() => true) | ||
}) | ||
let uri | ||
if (team) { | ||
uri = `/-/team/${scope}/${team}/package` | ||
} else { | ||
uri = `/-/org/${scope}/package` | ||
} | ||
try { | ||
return await npmFetch.json(uri, opts) | ||
} catch (err) { | ||
if (err.code === 'E404') { | ||
uri = `/-/user/${scope}/package` | ||
return npmFetch.json(uri, opts) | ||
} | ||
throw err | ||
} | ||
} | ||
cmd.grant = (spec, entity, permissions, opts = {}) => { | ||
return Promise.resolve().then(() => { | ||
spec = npar(spec) | ||
const { scope, team } = splitEntity(entity) | ||
validate('OSSSO', [spec, scope, team, permissions, opts]) | ||
if (permissions !== 'read-write' && permissions !== 'read-only') { | ||
throw new Error( | ||
'`permissions` must be `read-write` or `read-only`. Got `' | ||
+ permissions + '` instead') | ||
} | ||
const uri = `/-/team/${eu(scope)}/${eu(team)}/package` | ||
return npmFetch(uri, { | ||
...opts, | ||
method: 'PUT', | ||
body: { package: spec.name, permissions }, | ||
scope, | ||
spec, | ||
ignoreBody: true, | ||
}) | ||
.then(() => true) | ||
}) | ||
const getCollaborators = async (pkg, opts) => { | ||
const spec = npar(pkg) | ||
const uri = `/-/package/${spec.escapedName}/collaborators` | ||
return npmFetch.json(uri, opts) | ||
} | ||
cmd.revoke = (spec, entity, opts = {}) => { | ||
return Promise.resolve().then(() => { | ||
spec = npar(spec) | ||
const { scope, team } = splitEntity(entity) | ||
validate('OSSO', [spec, scope, team, opts]) | ||
const uri = `/-/team/${eu(scope)}/${eu(team)}/package` | ||
return npmFetch(uri, { | ||
...opts, | ||
method: 'DELETE', | ||
body: { package: spec.name }, | ||
scope, | ||
spec, | ||
ignoreBody: true, | ||
}) | ||
.then(() => true) | ||
}) | ||
const getVisibility = async (pkg, opts) => { | ||
const spec = npar(pkg) | ||
const uri = `/-/package/${spec.escapedName}/visibility` | ||
return npmFetch.json(uri, opts) | ||
} | ||
cmd.lsPackages = (entity, opts) => { | ||
return cmd.lsPackages.stream(entity, opts) | ||
.collect() | ||
.then(data => { | ||
return data.reduce((acc, [key, val]) => { | ||
if (!acc) { | ||
acc = {} | ||
} | ||
acc[key] = val | ||
return acc | ||
}, null) | ||
}) | ||
const setAccess = async (pkg, access, opts) => { | ||
const spec = npar(pkg) | ||
const uri = `/-/package/${spec.escapedName}/access` | ||
await npmFetch(uri, { | ||
...opts, | ||
method: 'POST', | ||
body: { access }, | ||
spec, | ||
ignoreBody: true, | ||
}) | ||
return true | ||
} | ||
cmd.lsPackages.stream = (entity, opts = {}) => { | ||
validate('SO|SZ', [entity, opts]) | ||
const { scope, team } = splitEntity(entity) | ||
let uri | ||
if (team) { | ||
uri = `/-/team/${eu(scope)}/${eu(team)}/package` | ||
} else { | ||
uri = `/-/org/${eu(scope)}/package` | ||
const setMfa = async (pkg, level, opts) => { | ||
const spec = npar(pkg) | ||
const body = {} | ||
switch (level) { | ||
case 'none': | ||
body.publish_requires_tfa = false | ||
break | ||
case 'publish': | ||
// tfa is required, automation tokens can not override tfa | ||
body.publish_requires_tfa = true | ||
body.automation_token_overrides_tfa = false | ||
break | ||
case 'automation': | ||
// tfa is required, automation tokens can override tfa | ||
body.publish_requires_tfa = true | ||
body.automation_token_overrides_tfa = true | ||
break | ||
default: | ||
throw new Error(`Invalid mfa setting ${level}`) | ||
} | ||
const nextOpts = { | ||
const uri = `/-/package/${spec.escapedName}/access` | ||
await npmFetch(uri, { | ||
...opts, | ||
query: { format: 'cli' }, | ||
mapJSON, | ||
} | ||
const ret = new Minipass({ objectMode: true }) | ||
npmFetch.json.stream(uri, '*', nextOpts) | ||
.on('error', err => { | ||
if (err.code === 'E404' && !team) { | ||
uri = `/-/user/${eu(scope)}/package` | ||
npmFetch.json.stream(uri, '*', nextOpts) | ||
.on('error', streamErr => ret.emit('error', streamErr)) | ||
.pipe(ret) | ||
} else { | ||
ret.emit('error', err) | ||
} | ||
}) | ||
.pipe(ret) | ||
return ret | ||
} | ||
cmd.lsCollaborators = (spec, user, opts) => { | ||
return Promise.resolve().then(() => { | ||
return cmd.lsCollaborators.stream(spec, user, opts) | ||
.collect() | ||
.then(data => { | ||
return data.reduce((acc, [key, val]) => { | ||
if (!acc) { | ||
acc = {} | ||
} | ||
acc[key] = val | ||
return acc | ||
}, null) | ||
}) | ||
method: 'POST', | ||
body, | ||
spec, | ||
ignoreBody: true, | ||
}) | ||
return true | ||
} | ||
cmd.lsCollaborators.stream = (spec, user, opts) => { | ||
if (typeof user === 'object' && !opts) { | ||
opts = user | ||
user = undefined | ||
} else if (!opts) { | ||
opts = {} | ||
const setPermissions = async (scopeTeam, pkg, permissions, opts) => { | ||
const spec = npar(pkg) | ||
const { scope, team } = parseTeam(scopeTeam) | ||
if (!scope || !team) { | ||
throw new Error('team must be in format `scope:team`') | ||
} | ||
spec = npar(spec) | ||
validate('OSO|OZO', [spec, user, opts]) | ||
const uri = `/-/package/${eu(spec.name)}/collaborators` | ||
return npmFetch.json.stream(uri, '*', { | ||
const uri = `/-/team/${scope}/${team}/package` | ||
await npmFetch(uri, { | ||
...opts, | ||
query: { format: 'cli', user: user || undefined }, | ||
mapJSON, | ||
method: 'PUT', | ||
body: { package: spec.name, permissions }, | ||
scope, | ||
spec, | ||
ignoreBody: true, | ||
}) | ||
return true | ||
} | ||
cmd.tfaRequired = (spec, opts) => setRequires2fa(spec, true, opts) | ||
cmd.tfaNotRequired = (spec, opts) => setRequires2fa(spec, false, opts) | ||
function setRequires2fa (spec, required, opts = {}) { | ||
return Promise.resolve().then(() => { | ||
spec = npar(spec) | ||
validate('OBO', [spec, required, opts]) | ||
const uri = `/-/package/${eu(spec.name)}/access` | ||
return npmFetch(uri, { | ||
...opts, | ||
method: 'POST', | ||
body: { publish_requires_tfa: required }, | ||
spec, | ||
ignoreBody: true, | ||
}).then(() => true) | ||
const removePermissions = async (scopeTeam, pkg, opts) => { | ||
const spec = npar(pkg) | ||
const { scope, team } = parseTeam(scopeTeam) | ||
const uri = `/-/team/${scope}/${team}/package` | ||
await npmFetch(uri, { | ||
...opts, | ||
method: 'DELETE', | ||
body: { package: spec.name }, | ||
scope, | ||
spec, | ||
ignoreBody: true, | ||
}) | ||
return true | ||
} | ||
cmd.edit = () => { | ||
throw new Error('Not implemented yet') | ||
module.exports = { | ||
getCollaborators, | ||
getPackages, | ||
getVisibility, | ||
removePermissions, | ||
setAccess, | ||
setMfa, | ||
setPermissions, | ||
} | ||
function splitEntity (entity = '') { | ||
const [, scope, team] = entity.match(/^@?([^:]+)(?::(.*))?$/) || [] | ||
return { scope, team } | ||
} |
{ | ||
"name": "libnpmaccess", | ||
"version": "7.0.0-pre.0", | ||
"version": "7.0.0-pre.1", | ||
"description": "programmatic library for `npm access` commands", | ||
@@ -9,3 +9,2 @@ "author": "GitHub Inc.", | ||
"scripts": { | ||
"postpublish": "git push origin --follow-tags", | ||
"lint": "eslint \"**/*.js\"", | ||
@@ -21,3 +20,3 @@ "test": "tap", | ||
"@npmcli/eslint-config": "^3.1.0", | ||
"@npmcli/template-oss": "4.0.0", | ||
"@npmcli/template-oss": "4.1.2", | ||
"nock": "^13.2.4", | ||
@@ -34,4 +33,2 @@ "tap": "^16.0.1" | ||
"dependencies": { | ||
"aproba": "^2.0.0", | ||
"minipass": "^3.1.1", | ||
"npm-package-arg": "^9.0.1", | ||
@@ -49,4 +46,4 @@ "npm-registry-fetch": "^13.0.0" | ||
"//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", | ||
"version": "4.0.0" | ||
"version": "4.1.2" | ||
} | ||
} |
235
README.md
@@ -9,4 +9,4 @@ # libnpmaccess | ||
library that provides programmatic access to the guts of the npm CLI's `npm | ||
access` command and its various subcommands. This includes managing account 2FA, | ||
listing packages and permissions, looking at package collaborators, and defining | ||
access` command. This includes managing account mfa settings, listing | ||
packages and permissions, looking at package collaborators, and defining | ||
package permissions for users, orgs, and teams. | ||
@@ -18,231 +18,78 @@ | ||
const access = require('libnpmaccess') | ||
const opts = { '//registry.npmjs.org/:_authToken: 'npm_token } | ||
// List all packages @zkat has access to on the npm registry. | ||
console.log(Object.keys(await access.lsPackages('zkat'))) | ||
console.log(Object.keys(await access.getPackages('zkat', opts))) | ||
``` | ||
## Table of Contents | ||
* [Installing](#install) | ||
* [Example](#example) | ||
* [Contributing](#contributing) | ||
* [API](#api) | ||
* [access opts](#opts) | ||
* [`public()`](#public) | ||
* [`restricted()`](#restricted) | ||
* [`grant()`](#grant) | ||
* [`revoke()`](#revoke) | ||
* [`tfaRequired()`](#tfa-required) | ||
* [`tfaNotRequired()`](#tfa-not-required) | ||
* [`lsPackages()`](#ls-packages) | ||
* [`lsPackages.stream()`](#ls-packages-stream) | ||
* [`lsCollaborators()`](#ls-collaborators) | ||
* [`lsCollaborators.stream()`](#ls-collaborators-stream) | ||
### Install | ||
`$ npm install libnpmaccess` | ||
### API | ||
#### <a name="opts"></a> `opts` for `libnpmaccess` commands | ||
#### `opts` for all `libnpmaccess` commands | ||
`libnpmaccess` uses [`npm-registry-fetch`](https://npm.im/npm-registry-fetch). | ||
All options are passed through directly to that library, so please refer to [its | ||
own `opts` | ||
All options are passed through directly to that library, so please refer | ||
to [its own `opts` | ||
documentation](https://www.npmjs.com/package/npm-registry-fetch#fetch-options) | ||
for options that can be passed in. | ||
A couple of options of note for those in a hurry: | ||
#### `spec` parameter for all `libnpmaccess` commands | ||
* `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. | ||
* `opts.otp` - certain operations will require an OTP token to be passed in. If a `libnpmaccess` command fails with `err.code === EOTP`, please retry the request with `{otp: <2fa token>}` | ||
#### <a name="public"></a> `> access.public(spec, [opts]) -> Promise<Boolean>` | ||
`spec` must be an [`npm-package-arg`](https://npm.im/npm-package-arg)-compatible | ||
registry spec. | ||
Makes package described by `spec` public. | ||
#### `access.getCollaborators(spec, opts) -> Promise<Object>` | ||
##### Example | ||
Gets collaborators for a given package | ||
```javascript | ||
await access.public('@foo/bar', {token: 'myregistrytoken'}) | ||
// `@foo/bar` is now public | ||
``` | ||
#### `access.getPackages(user|scope|team, opts) -> Promise<Object>` | ||
#### <a name="restricted"></a> `> access.restricted(spec, [opts]) -> Promise<Boolean>` | ||
Gets all packages for a given user, scope, or team. | ||
`spec` must be an [`npm-package-arg`](https://npm.im/npm-package-arg)-compatible | ||
registry spec. | ||
Teams should be in the format `scope:team` or `@scope:team` | ||
Makes package described by `spec` private/restricted. | ||
Users and scopes can be in the format `@scope` or `scope` | ||
##### Example | ||
#### `access.getVisibility(spec, opts) -> Promise<Object>` | ||
```javascript | ||
await access.restricted('@foo/bar', {token: 'myregistrytoken'}) | ||
// `@foo/bar` is now private | ||
``` | ||
Gets the visibility of a given package | ||
#### <a name="grant"></a> `> access.grant(spec, team, permissions, [opts]) -> Promise<Boolean>` | ||
#### `access.removePermissions(team, spec, opts) -> Promise<Boolean>` | ||
`spec` must be an [`npm-package-arg`](https://npm.im/npm-package-arg)-compatible | ||
registry spec. `team` must be a fully-qualified team name, in the `scope:team` | ||
format, with or without the `@` prefix, and the team must be a valid team within | ||
that scope. `permissions` must be one of `'read-only'` or `'read-write'`. | ||
Removes the access for a given team to a package. | ||
Grants `read-only` or `read-write` permissions for a certain package to a team. | ||
Teams should be in the format `scope:team` or `@scope:team` | ||
##### Example | ||
#### `access.setAccess(package, access, opts) -> Promise<Boolean>` | ||
```javascript | ||
await access.grant('@foo/bar', '@foo:myteam', 'read-write', { | ||
token: 'myregistrytoken' | ||
}) | ||
// `@foo/bar` is now read/write enabled for the @foo:myteam team. | ||
``` | ||
Sets access level for package described by `spec`. | ||
#### <a name="revoke"></a> `> access.revoke(spec, team, [opts]) -> Promise<Boolean>` | ||
The npm registry accepts the following `access` levels: | ||
`spec` must be an [`npm-package-arg`](https://npm.im/npm-package-arg)-compatible | ||
registry spec. `team` must be a fully-qualified team name, in the `scope:team` | ||
format, with or without the `@` prefix, and the team must be a valid team within | ||
that scope. `permissions` must be one of `'read-only'` or `'read-write'`. | ||
`public`: package is public | ||
`private`: package is private | ||
Removes access to a package from a certain team. | ||
The npm registry also only allows scoped packages to have their access | ||
level set. | ||
##### Example | ||
#### access.setMfa(spec, level, opts) -> Promise<Boolean>` | ||
```javascript | ||
await access.revoke('@foo/bar', '@foo:myteam', { | ||
token: 'myregistrytoken' | ||
}) | ||
// @foo:myteam can no longer access `@foo/bar` | ||
``` | ||
Sets the publishing mfa requirements for a given package. Level must be one of the | ||
following | ||
#### <a name="tfa-required"></a> `> access.tfaRequired(spec, [opts]) -> Promise<Boolean>` | ||
`none`: mfa is not required to publish this package. | ||
`publish`: mfa is required to publish this package, automation tokens | ||
cannot be used to publish. | ||
`automation`: mfa is required to publish this package, automation tokens | ||
may also be used for publishing from continuous integration workflows. | ||
`spec` must be an [`npm-package-arg`](https://npm.im/npm-package-arg)-compatible | ||
registry spec. | ||
#### access.setPermissions(team, spec, permssions, opts) -> Promise<Boolean>` | ||
Makes it so publishing or managing a package requires using 2FA tokens to | ||
complete operations. | ||
Sets permissions levels for a given team to a package. | ||
##### Example | ||
Teams should be in the format `scope:team` or `@scope:team` | ||
```javascript | ||
await access.tfaRequires('lodash', {token: 'myregistrytoken'}) | ||
// Publishing or changing dist-tags on `lodash` now require OTP to be enabled. | ||
``` | ||
The npm registry accepts the following `permissions`: | ||
#### <a name="tfa-not-required"></a> `> access.tfaNotRequired(spec, [opts]) -> Promise<Boolean>` | ||
`spec` must be an [`npm-package-arg`](https://npm.im/npm-package-arg)-compatible | ||
registry spec. | ||
Disabled the package-level 2FA requirement for `spec`. Note that you will need | ||
to pass in an `otp` token in `opts` in order to complete this operation. | ||
##### Example | ||
```javascript | ||
await access.tfaNotRequired('lodash', {otp: '123654', token: 'myregistrytoken'}) | ||
// Publishing or editing dist-tags on `lodash` no longer requires OTP to be | ||
// enabled. | ||
``` | ||
#### <a name="ls-packages"></a> `> access.lsPackages(entity, [opts]) -> Promise<Object | null>` | ||
`entity` must be either a valid org or user name, or a fully-qualified team name | ||
in the `scope:team` format, with or without the `@` prefix. | ||
Lists out packages a user, org, or team has access to, with corresponding | ||
permissions. Packages that the access token does not have access to won't be | ||
listed. | ||
In order to disambiguate between users and orgs, two requests may end up being | ||
made when listing orgs or users. | ||
For a streamed version of these results, see | ||
[`access.lsPackages.stream()`](#ls-package-stream). | ||
##### Example | ||
```javascript | ||
await access.lsPackages('zkat', { | ||
token: 'myregistrytoken' | ||
}) | ||
// Lists all packages `@zkat` has access to on the registry, and the | ||
// corresponding permissions. | ||
``` | ||
#### <a name="ls-packages-stream"></a> `> access.lsPackages.stream(scope, [team], [opts]) -> Stream` | ||
`entity` must be either a valid org or user name, or a fully-qualified team name | ||
in the `scope:team` format, with or without the `@` prefix. | ||
Streams out packages a user, org, or team has access to, with corresponding | ||
permissions, with each stream entry being formatted like `[packageName, | ||
permissions]`. Packages that the access token does not have access to won't be | ||
listed. | ||
In order to disambiguate between users and orgs, two requests may end up being | ||
made when listing orgs or users. | ||
The returned stream is a valid `asyncIterator`. | ||
##### Example | ||
```javascript | ||
for await (let [pkg, perm] of access.lsPackages.stream('zkat')) { | ||
console.log('zkat has', perm, 'access to', pkg) | ||
} | ||
// zkat has read-write access to eggplant | ||
// zkat has read-only access to @npmcorp/secret | ||
``` | ||
#### <a name="ls-collaborators"></a> `> access.lsCollaborators(spec, [user], [opts]) -> Promise<Object | null>` | ||
`spec` must be an [`npm-package-arg`](https://npm.im/npm-package-arg)-compatible | ||
registry spec. `user` must be a valid user name, with or without the `@` | ||
prefix. | ||
Lists out access privileges for a certain package. Will only show permissions | ||
for packages to which you have at least read access. If `user` is passed in, the | ||
list is filtered only to teams _that_ user happens to belong to. | ||
For a streamed version of these results, see [`access.lsCollaborators.stream()`](#ls-collaborators-stream). | ||
##### Example | ||
```javascript | ||
await access.lsCollaborators('@npm/foo', 'zkat', { | ||
token: 'myregistrytoken' | ||
}) | ||
// Lists all teams with access to @npm/foo that @zkat belongs to. | ||
``` | ||
#### <a name="ls-collaborators-stream"></a> `> access.lsCollaborators.stream(spec, [user], [opts]) -> Stream` | ||
`spec` must be an [`npm-package-arg`](https://npm.im/npm-package-arg)-compatible | ||
registry spec. `user` must be a valid user name, with or without the `@` | ||
prefix. | ||
Stream out access privileges for a certain package, with each entry in `[user, | ||
permissions]` format. Will only show permissions for packages to which you have | ||
at least read access. If `user` is passed in, the list is filtered only to teams | ||
_that_ user happens to belong to. | ||
The returned stream is a valid `asyncIterator`. | ||
##### Example | ||
```javascript | ||
for await (let [usr, perm] of access.lsCollaborators.stream('npm')) { | ||
console.log(usr, 'has', perm, 'access to npm') | ||
} | ||
// zkat has read-write access to npm | ||
// iarna has read-write access to npm | ||
``` | ||
`read-only`: Read only permissions | ||
`read-write`: Read and write (aka publish) permissions |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
2
8300
128
94
1
- Removedaproba@^2.0.0
- Removedminipass@^3.1.1
- Removedaproba@2.0.0(transitive)