├── .github ├── settings.yml └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── package-lock.json ├── package.json └── test ├── fixtures └── tnock.js └── index.js /.github/settings.yml: -------------------------------------------------------------------------------- 1 | --- 2 | _extends: '.github:npm-cli/settings.yml' 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ################################################################################ 3 | # Template - Node CI 4 | # 5 | # Description: 6 | # This contains the basic information to: install dependencies, run tests, 7 | # get coverage, and run linting on a nodejs project. This template will run 8 | # over the MxN matrix of all operating systems, and all current LTS versions 9 | # of NodeJS. 10 | # 11 | # Dependencies: 12 | # This template assumes that your project is using the `tap` module for 13 | # testing. If you're not using this module, then the step that runs your 14 | # coverage will need to be adjusted. 15 | # 16 | ################################################################################ 17 | name: Node CI 18 | 19 | on: [push, pull_request] 20 | 21 | jobs: 22 | build: 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | node-version: [10.x, 12.x, 13.x] 27 | os: [ubuntu-latest, windows-latest, macOS-latest] 28 | 29 | runs-on: ${{ matrix.os }} 30 | 31 | steps: 32 | # Checkout the repository 33 | - uses: actions/checkout@v2 34 | # Installs the specific version of Node.js 35 | - name: Use Node.js ${{ matrix.node-version }} 36 | uses: actions/setup-node@v1 37 | with: 38 | node-version: ${{ matrix.node-version }} 39 | 40 | ################################################################################ 41 | # Install Dependencies 42 | # 43 | # ASSUMPTIONS: 44 | # - The project has a package-lock.json file 45 | # 46 | # Simply run the tests for the project. 47 | ################################################################################ 48 | - name: Install dependencies 49 | run: npm ci 50 | 51 | ################################################################################ 52 | # Run Testing 53 | # 54 | # ASSUMPTIONS: 55 | # - The project has `tap` as a devDependency 56 | # - There is a script called "test" in the package.json 57 | # 58 | # Simply run the tests for the project. 59 | ################################################################################ 60 | - name: Run tests 61 | run: npm test -- --no-coverage 62 | 63 | ################################################################################ 64 | # Run coverage check 65 | # 66 | # ASSUMPTIONS: 67 | # - The project has `tap` as a devDependency 68 | # - There is a script called "coverage" in the package.json 69 | # 70 | # Coverage should only be posted once, we are choosing the latest LTS of 71 | # node, and ubuntu as the matrix point to post coverage from. We limit 72 | # to the 'push' event so that coverage ins't posted twice from the 73 | # pull-request event, and push event (line 3). 74 | ################################################################################ 75 | - name: Run coverage report 76 | if: github.event_name == 'push' && matrix.node-version == '12.x' && matrix.os == 'ubuntu-latest' 77 | run: npm test 78 | env: 79 | # The environment variable name is leveraged by `tap` 80 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 81 | 82 | ################################################################################ 83 | # Run linting 84 | # 85 | # ASSUMPTIONS: 86 | # - There is a script called "lint" in the package.json 87 | # 88 | # We run linting AFTER we run testing and coverage checks, because if a step 89 | # fails in an GitHub Action, all other steps are not run. We don't want to 90 | # fail to run tests or coverage because of linting. It should be the lowest 91 | # priority of all the steps. 92 | ################################################################################ 93 | - name: Run linter 94 | run: npm run lint 95 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | coverage/ 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | 4 | ## [4.0.0](https://github.com/npm/libnpmaccess/compare/v3.0.2...v4.0.0) (2020-03-02) 5 | 6 | ### BREAKING CHANGES 7 | - `25ac61b` fix: remove figgy-pudding ([@claudiahdz](https://github.com/claudiahdz)) 8 | - `8d6f692` chore: rename opts.mapJson to opts.mapJSON ([@mikemimik](https://github.com/mikemimik)) 9 | 10 | ### Features 11 | - `257879a` chore: removed standard-version as a dep; updated scripts for version/publishing ([@mikemimik](https://github.com/mikemimik)) 12 | - `46c6740` fix: pull-request feedback; read full commit message ([@mikemimik](https://github.com/mikemimik)) 13 | - `778c102` chore: updated test, made case more clear ([@mikemimik](https://github.com/mikemimik)) 14 | - `6dc9852` fix: refactored 'pwrap' function out of code base; use native promises ([@mikemimik](https://github.com/mikemimik)) 15 | - `d2e7219` chore: updated package scripts; update CI workflow ([@mikemimik](https://github.com/mikemimik)) 16 | - `5872364` chore: renamed test/util/ to test/fixture/; tap will ignore now ([@mikemimik](https://github.com/mikemimik)) 17 | - `3c6b71d` chore: linted test file; made tap usage 'better' ([@mikemimik](https://github.com/mikemimik)) 18 | - `20f0858` fix: added default values to params for API functions (with tests) ([@mikemimik](https://github.com/mikemimik)) 19 | - `3218289` feat: replace get-stream with minipass ([@mikemimik](https://github.com/mikemimik)) 20 | 21 | ### Documentation 22 | - `6c8ffa0` docs: removed opts.Promise from docs; no longer in use ([@mikemimik](https://github.com/mikemimik)) 23 | - `311bff5` chore: added return types to function docs in README ([@mikemimik](https://github.com/mikemimik)) 24 | - `823726a` chore: removed travis badge, added github actions badge ([@mikemimik](https://github.com/mikemimik)) 25 | - `80e80ac` chore: updated README ([@mikemimik](https://github.com/mikemimik)) 26 | 27 | ### Dependencies 28 | - `baed2b9` deps: standard-version@7.1.0 (audit fix) ([@mikemimik](https://github.com/mikemimik)) 29 | - `65c2204` deps: nock@12.0.1 (audit fix) ([@mikemimik](https://github.com/mikemimik)) 30 | - `2668386` deps: npm-registry-fetch@8.0.0 ([@mikemimik](https://github.com/mikemimik)) 31 | - `ef093e2` deps: tap@14.10.6 ([@mikemimik](https://github.com/mikemimik)) 32 | 33 | ### Miscellanieous 34 | - `8e33902` chore: basic project updates ([@claudiahdz](https://github.com/claudiahdz)) 35 | - `50e1433` fix: update return value; add tests ([@mikemimik](https://github.com/mikemimik)) 36 | - `36d5c80` chore: updated gitignore; includes coverage folder ([@mikemimik](https://github.com/mikemimik)) 37 | 38 | --- 39 | 40 | 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. 41 | 42 | 43 | ## [3.0.2](https://github.com/npm/libnpmaccess/compare/v3.0.1...v3.0.2) (2019-07-16) 44 | 45 | 46 | 47 | 48 | ## [3.0.1](https://github.com/npm/libnpmaccess/compare/v3.0.0...v3.0.1) (2018-11-12) 49 | 50 | 51 | ### Bug Fixes 52 | 53 | * **ls-packages:** fix confusing splitEntity arg check ([1769090](https://github.com/npm/libnpmaccess/commit/1769090)) 54 | 55 | 56 | 57 | 58 | # [3.0.0](https://github.com/npm/libnpmaccess/compare/v2.0.1...v3.0.0) (2018-08-24) 59 | 60 | 61 | ### Features 62 | 63 | * **api:** overhaul API ergonomics ([1faf00a](https://github.com/npm/libnpmaccess/commit/1faf00a)) 64 | 65 | 66 | ### BREAKING CHANGES 67 | 68 | * **api:** all API calls where scope and team were separate, or 69 | where team was an extra, optional argument should now use a 70 | fully-qualified team name instead, in the `scope:team` format. 71 | 72 | 73 | 74 | 75 | ## [2.0.1](https://github.com/npm/libnpmaccess/compare/v2.0.0...v2.0.1) (2018-08-24) 76 | 77 | 78 | 79 | 80 | # [2.0.0](https://github.com/npm/libnpmaccess/compare/v1.2.2...v2.0.0) (2018-08-21) 81 | 82 | 83 | ### Bug Fixes 84 | 85 | * **json:** stop trying to parse response JSON ([20fdd84](https://github.com/npm/libnpmaccess/commit/20fdd84)) 86 | * **lsPackages:** team URL was wrong D: ([b52201c](https://github.com/npm/libnpmaccess/commit/b52201c)) 87 | 88 | 89 | ### BREAKING CHANGES 90 | 91 | * **json:** use cases where registries were returning JSON 92 | strings in the response body will no longer have an effect. All 93 | API functions except for lsPackages and lsCollaborators will return 94 | `true` on completion. 95 | 96 | 97 | 98 | 99 | ## [1.2.2](https://github.com/npm/libnpmaccess/compare/v1.2.1...v1.2.2) (2018-08-20) 100 | 101 | 102 | ### Bug Fixes 103 | 104 | * **docs:** tiny doc hiccup fix ([106396f](https://github.com/npm/libnpmaccess/commit/106396f)) 105 | 106 | 107 | 108 | 109 | ## [1.2.1](https://github.com/npm/libnpmaccess/compare/v1.2.0...v1.2.1) (2018-08-20) 110 | 111 | 112 | ### Bug Fixes 113 | 114 | * **docs:** document the stream interfaces ([c435aa2](https://github.com/npm/libnpmaccess/commit/c435aa2)) 115 | 116 | 117 | 118 | 119 | # [1.2.0](https://github.com/npm/libnpmaccess/compare/v1.1.0...v1.2.0) (2018-08-20) 120 | 121 | 122 | ### Bug Fixes 123 | 124 | * **readme:** fix up appveyor badge url ([42b45a1](https://github.com/npm/libnpmaccess/commit/42b45a1)) 125 | 126 | 127 | ### Features 128 | 129 | * **streams:** add streaming result support for lsPkg and lsCollab ([0f06f46](https://github.com/npm/libnpmaccess/commit/0f06f46)) 130 | 131 | 132 | 133 | 134 | # [1.1.0](https://github.com/npm/libnpmaccess/compare/v1.0.0...v1.1.0) (2018-08-17) 135 | 136 | 137 | ### Bug Fixes 138 | 139 | * **2fa:** escape package names correctly ([f2d83fe](https://github.com/npm/libnpmaccess/commit/f2d83fe)) 140 | * **grant:** fix permissions validation ([07f7435](https://github.com/npm/libnpmaccess/commit/07f7435)) 141 | * **ls-collaborators:** fix package name escaping + query ([3c02858](https://github.com/npm/libnpmaccess/commit/3c02858)) 142 | * **ls-packages:** add query + fix fallback request order ([bdc4791](https://github.com/npm/libnpmaccess/commit/bdc4791)) 143 | * **node6:** stop using Object.entries() ([4fec03c](https://github.com/npm/libnpmaccess/commit/4fec03c)) 144 | * **public/restricted:** body should be string, not bool ([cffc727](https://github.com/npm/libnpmaccess/commit/cffc727)) 145 | * **readme:** fix up title and badges ([2bd6113](https://github.com/npm/libnpmaccess/commit/2bd6113)) 146 | * **specs:** require specs to be registry specs ([7892891](https://github.com/npm/libnpmaccess/commit/7892891)) 147 | 148 | 149 | ### Features 150 | 151 | * **test:** add 100% coverage test suite ([22b5dec](https://github.com/npm/libnpmaccess/commit/22b5dec)) 152 | 153 | 154 | 155 | 156 | # 1.0.0 (2018-08-17) 157 | 158 | 159 | ### Bug Fixes 160 | 161 | * **test:** -100 is apparently bad now ([a5ab879](https://github.com/npm/libnpmaccess/commit/a5ab879)) 162 | 163 | 164 | ### Features 165 | 166 | * **impl:** initial implementation of api ([7039390](https://github.com/npm/libnpmaccess/commit/7039390)) 167 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright npm, Inc 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # We've Moved! 🚚 2 | The code for this repo is now a workspace in the npm CLI repo. 3 | 4 | [github.com/npm/cli](https://github.com/npm/cli) 5 | 6 | You can find the workspace in /workspaces/libnpmaccess 7 | 8 | Please file bugs and feature requests as issues on the CLI and tag the issue with "ws:libnpmaccess". 9 | 10 | [github.com/npm/cli/issues](https://github.com/npm/cli) 11 | 12 | # libnpmaccess 13 | 14 | [![npm version](https://img.shields.io/npm/v/libnpmaccess.svg)](https://npm.im/libnpmaccess) 15 | [![license](https://img.shields.io/npm/l/libnpmaccess.svg)](https://npm.im/libnpmaccess) 16 | [![GitHub Actions](https://github.com/npm/libnpmaccess/workflows/Node%20CI/badge.svg)](https://github.com/npm/libnpmaccess/actions?query=workflow%3A%22Node+CI%22) 17 | [![Coverage Status](https://coveralls.io/repos/github/npm/libnpmaccess/badge.svg?branch=latest)](https://coveralls.io/github/npm/libnpmaccess?branch=latest) 18 | 19 | [`libnpmaccess`](https://github.com/npm/libnpmaccess) is a Node.js 20 | library that provides programmatic access to the guts of the npm CLI's `npm 21 | access` command and its various subcommands. This includes managing account 2FA, 22 | listing packages and permissions, looking at package collaborators, and defining 23 | package permissions for users, orgs, and teams. 24 | 25 | ## Example 26 | 27 | ```javascript 28 | const access = require('libnpmaccess') 29 | 30 | // List all packages @zkat has access to on the npm registry. 31 | console.log(Object.keys(await access.lsPackages('zkat'))) 32 | ``` 33 | 34 | ## Table of Contents 35 | 36 | * [Installing](#install) 37 | * [Example](#example) 38 | * [Contributing](#contributing) 39 | * [API](#api) 40 | * [access opts](#opts) 41 | * [`public()`](#public) 42 | * [`restricted()`](#restricted) 43 | * [`grant()`](#grant) 44 | * [`revoke()`](#revoke) 45 | * [`tfaRequired()`](#tfa-required) 46 | * [`tfaNotRequired()`](#tfa-not-required) 47 | * [`lsPackages()`](#ls-packages) 48 | * [`lsPackages.stream()`](#ls-packages-stream) 49 | * [`lsCollaborators()`](#ls-collaborators) 50 | * [`lsCollaborators.stream()`](#ls-collaborators-stream) 51 | 52 | ### Install 53 | 54 | `$ npm install libnpmaccess` 55 | 56 | ### API 57 | 58 | #### `opts` for `libnpmaccess` commands 59 | 60 | `libnpmaccess` uses [`npm-registry-fetch`](https://npm.im/npm-registry-fetch). 61 | All options are passed through directly to that library, so please refer to [its 62 | own `opts` 63 | documentation](https://www.npmjs.com/package/npm-registry-fetch#fetch-options) 64 | for options that can be passed in. 65 | 66 | A couple of options of note for those in a hurry: 67 | 68 | * `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. 69 | * `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>}` 70 | 71 | #### `> access.public(spec, [opts]) -> Promise` 72 | 73 | `spec` must be an [`npm-package-arg`](https://npm.im/npm-package-arg)-compatible 74 | registry spec. 75 | 76 | Makes package described by `spec` public. 77 | 78 | ##### Example 79 | 80 | ```javascript 81 | await access.public('@foo/bar', {token: 'myregistrytoken'}) 82 | // `@foo/bar` is now public 83 | ``` 84 | 85 | #### `> access.restricted(spec, [opts]) -> Promise` 86 | 87 | `spec` must be an [`npm-package-arg`](https://npm.im/npm-package-arg)-compatible 88 | registry spec. 89 | 90 | Makes package described by `spec` private/restricted. 91 | 92 | ##### Example 93 | 94 | ```javascript 95 | await access.restricted('@foo/bar', {token: 'myregistrytoken'}) 96 | // `@foo/bar` is now private 97 | ``` 98 | 99 | #### `> access.grant(spec, team, permissions, [opts]) -> Promise` 100 | 101 | `spec` must be an [`npm-package-arg`](https://npm.im/npm-package-arg)-compatible 102 | registry spec. `team` must be a fully-qualified team name, in the `scope:team` 103 | format, with or without the `@` prefix, and the team must be a valid team within 104 | that scope. `permissions` must be one of `'read-only'` or `'read-write'`. 105 | 106 | Grants `read-only` or `read-write` permissions for a certain package to a team. 107 | 108 | ##### Example 109 | 110 | ```javascript 111 | await access.grant('@foo/bar', '@foo:myteam', 'read-write', { 112 | token: 'myregistrytoken' 113 | }) 114 | // `@foo/bar` is now read/write enabled for the @foo:myteam team. 115 | ``` 116 | 117 | #### `> access.revoke(spec, team, [opts]) -> Promise` 118 | 119 | `spec` must be an [`npm-package-arg`](https://npm.im/npm-package-arg)-compatible 120 | registry spec. `team` must be a fully-qualified team name, in the `scope:team` 121 | format, with or without the `@` prefix, and the team must be a valid team within 122 | that scope. `permissions` must be one of `'read-only'` or `'read-write'`. 123 | 124 | Removes access to a package from a certain team. 125 | 126 | ##### Example 127 | 128 | ```javascript 129 | await access.revoke('@foo/bar', '@foo:myteam', { 130 | token: 'myregistrytoken' 131 | }) 132 | // @foo:myteam can no longer access `@foo/bar` 133 | ``` 134 | 135 | #### `> access.tfaRequired(spec, [opts]) -> Promise` 136 | 137 | `spec` must be an [`npm-package-arg`](https://npm.im/npm-package-arg)-compatible 138 | registry spec. 139 | 140 | Makes it so publishing or managing a package requires using 2FA tokens to 141 | complete operations. 142 | 143 | ##### Example 144 | 145 | ```javascript 146 | await access.tfaRequires('lodash', {token: 'myregistrytoken'}) 147 | // Publishing or changing dist-tags on `lodash` now require OTP to be enabled. 148 | ``` 149 | 150 | #### `> access.tfaNotRequired(spec, [opts]) -> Promise` 151 | 152 | `spec` must be an [`npm-package-arg`](https://npm.im/npm-package-arg)-compatible 153 | registry spec. 154 | 155 | Disabled the package-level 2FA requirement for `spec`. Note that you will need 156 | to pass in an `otp` token in `opts` in order to complete this operation. 157 | 158 | ##### Example 159 | 160 | ```javascript 161 | await access.tfaNotRequired('lodash', {otp: '123654', token: 'myregistrytoken'}) 162 | // Publishing or editing dist-tags on `lodash` no longer requires OTP to be 163 | // enabled. 164 | ``` 165 | 166 | #### `> access.lsPackages(entity, [opts]) -> Promise` 167 | 168 | `entity` must be either a valid org or user name, or a fully-qualified team name 169 | in the `scope:team` format, with or without the `@` prefix. 170 | 171 | Lists out packages a user, org, or team has access to, with corresponding 172 | permissions. Packages that the access token does not have access to won't be 173 | listed. 174 | 175 | In order to disambiguate between users and orgs, two requests may end up being 176 | made when listing orgs or users. 177 | 178 | For a streamed version of these results, see 179 | [`access.lsPackages.stream()`](#ls-package-stream). 180 | 181 | ##### Example 182 | 183 | ```javascript 184 | await access.lsPackages('zkat', { 185 | token: 'myregistrytoken' 186 | }) 187 | // Lists all packages `@zkat` has access to on the registry, and the 188 | // corresponding permissions. 189 | ``` 190 | 191 | #### `> access.lsPackages.stream(scope, [team], [opts]) -> Stream` 192 | 193 | `entity` must be either a valid org or user name, or a fully-qualified team name 194 | in the `scope:team` format, with or without the `@` prefix. 195 | 196 | Streams out packages a user, org, or team has access to, with corresponding 197 | permissions, with each stream entry being formatted like `[packageName, 198 | permissions]`. Packages that the access token does not have access to won't be 199 | listed. 200 | 201 | In order to disambiguate between users and orgs, two requests may end up being 202 | made when listing orgs or users. 203 | 204 | The returned stream is a valid `asyncIterator`. 205 | 206 | ##### Example 207 | 208 | ```javascript 209 | for await (let [pkg, perm] of access.lsPackages.stream('zkat')) { 210 | console.log('zkat has', perm, 'access to', pkg) 211 | } 212 | // zkat has read-write access to eggplant 213 | // zkat has read-only access to @npmcorp/secret 214 | ``` 215 | 216 | #### `> access.lsCollaborators(spec, [user], [opts]) -> Promise` 217 | 218 | `spec` must be an [`npm-package-arg`](https://npm.im/npm-package-arg)-compatible 219 | registry spec. `user` must be a valid user name, with or without the `@` 220 | prefix. 221 | 222 | Lists out access privileges for a certain package. Will only show permissions 223 | for packages to which you have at least read access. If `user` is passed in, the 224 | list is filtered only to teams _that_ user happens to belong to. 225 | 226 | For a streamed version of these results, see [`access.lsCollaborators.stream()`](#ls-collaborators-stream). 227 | 228 | ##### Example 229 | 230 | ```javascript 231 | await access.lsCollaborators('@npm/foo', 'zkat', { 232 | token: 'myregistrytoken' 233 | }) 234 | // Lists all teams with access to @npm/foo that @zkat belongs to. 235 | ``` 236 | 237 | #### `> access.lsCollaborators.stream(spec, [user], [opts]) -> Stream` 238 | 239 | `spec` must be an [`npm-package-arg`](https://npm.im/npm-package-arg)-compatible 240 | registry spec. `user` must be a valid user name, with or without the `@` 241 | prefix. 242 | 243 | Stream out access privileges for a certain package, with each entry in `[user, 244 | permissions]` format. Will only show permissions for packages to which you have 245 | at least read access. If `user` is passed in, the list is filtered only to teams 246 | _that_ user happens to belong to. 247 | 248 | The returned stream is a valid `asyncIterator`. 249 | 250 | ##### Example 251 | 252 | ```javascript 253 | for await (let [usr, perm] of access.lsCollaborators.stream('npm')) { 254 | console.log(usr, 'has', perm, 'access to npm') 255 | } 256 | // zkat has read-write access to npm 257 | // iarna has read-write access to npm 258 | ``` 259 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Minipass = require('minipass') 4 | const npa = require('npm-package-arg') 5 | const npmFetch = require('npm-registry-fetch') 6 | const validate = require('aproba') 7 | 8 | const eu = encodeURIComponent 9 | const npar = spec => { 10 | spec = npa(spec) 11 | if (!spec.registry) { 12 | throw new Error('`spec` must be a registry spec') 13 | } 14 | return spec 15 | } 16 | const mapJSON = (value, [key]) => { 17 | if (value === 'read') { 18 | return [key, 'read-only'] 19 | } else if (value === 'write') { 20 | return [key, 'read-write'] 21 | } else { 22 | return [key, value] 23 | } 24 | } 25 | 26 | const cmd = module.exports = {} 27 | 28 | cmd.public = (spec, opts) => setAccess(spec, 'public', opts) 29 | cmd.restricted = (spec, opts) => setAccess(spec, 'restricted', opts) 30 | function setAccess (spec, access, opts = {}) { 31 | return Promise.resolve().then(() => { 32 | spec = npar(spec) 33 | validate('OSO', [spec, access, opts]) 34 | const uri = `/-/package/${eu(spec.name)}/access` 35 | return npmFetch(uri, { 36 | ...opts, 37 | method: 'POST', 38 | body: { access }, 39 | spec 40 | }).then(() => true) 41 | }) 42 | } 43 | 44 | cmd.grant = (spec, entity, permissions, opts = {}) => { 45 | return Promise.resolve().then(() => { 46 | spec = npar(spec) 47 | const { scope, team } = splitEntity(entity) 48 | validate('OSSSO', [spec, scope, team, permissions, opts]) 49 | if (permissions !== 'read-write' && permissions !== 'read-only') { 50 | throw new Error('`permissions` must be `read-write` or `read-only`. Got `' + permissions + '` instead') 51 | } 52 | const uri = `/-/team/${eu(scope)}/${eu(team)}/package` 53 | return npmFetch(uri, { 54 | ...opts, 55 | method: 'PUT', 56 | body: { package: spec.name, permissions }, 57 | scope, 58 | spec, 59 | ignoreBody: true 60 | }) 61 | .then(() => true) 62 | }) 63 | } 64 | 65 | cmd.revoke = (spec, entity, opts = {}) => { 66 | return Promise.resolve().then(() => { 67 | spec = npar(spec) 68 | const { scope, team } = splitEntity(entity) 69 | validate('OSSO', [spec, scope, team, opts]) 70 | const uri = `/-/team/${eu(scope)}/${eu(team)}/package` 71 | return npmFetch(uri, { 72 | ...opts, 73 | method: 'DELETE', 74 | body: { package: spec.name }, 75 | scope, 76 | spec, 77 | ignoreBody: true 78 | }) 79 | .then(() => true) 80 | }) 81 | } 82 | 83 | cmd.lsPackages = (entity, opts) => { 84 | return cmd.lsPackages.stream(entity, opts) 85 | .collect() 86 | .then(data => { 87 | return data.reduce((acc, [key, val]) => { 88 | if (!acc) { 89 | acc = {} 90 | } 91 | acc[key] = val 92 | return acc 93 | }, null) 94 | }) 95 | } 96 | 97 | cmd.lsPackages.stream = (entity, opts = {}) => { 98 | validate('SO|SZ', [entity, opts]) 99 | const { scope, team } = splitEntity(entity) 100 | let uri 101 | if (team) { 102 | uri = `/-/team/${eu(scope)}/${eu(team)}/package` 103 | } else { 104 | uri = `/-/org/${eu(scope)}/package` 105 | } 106 | const nextOpts = { 107 | ...opts, 108 | query: { format: 'cli' }, 109 | mapJSON 110 | } 111 | const ret = new Minipass({ objectMode: true }) 112 | npmFetch.json.stream(uri, '*', nextOpts) 113 | .on('error', err => { 114 | if (err.code === 'E404' && !team) { 115 | uri = `/-/user/${eu(scope)}/package` 116 | npmFetch.json.stream(uri, '*', nextOpts) 117 | .on('error', err => ret.emit('error', err)) 118 | .pipe(ret) 119 | } else { 120 | ret.emit('error', err) 121 | } 122 | }) 123 | .pipe(ret) 124 | return ret 125 | } 126 | 127 | cmd.lsCollaborators = (spec, user, opts) => { 128 | return Promise.resolve().then(() => { 129 | return cmd.lsCollaborators.stream(spec, user, opts) 130 | .collect() 131 | .then(data => { 132 | return data.reduce((acc, [key, val]) => { 133 | if (!acc) { 134 | acc = {} 135 | } 136 | acc[key] = val 137 | return acc 138 | }, null) 139 | }) 140 | }) 141 | } 142 | 143 | cmd.lsCollaborators.stream = (spec, user, opts) => { 144 | if (typeof user === 'object' && !opts) { 145 | opts = user 146 | user = undefined 147 | } else if (!opts) { 148 | opts = {} 149 | } 150 | spec = npar(spec) 151 | validate('OSO|OZO', [spec, user, opts]) 152 | const uri = `/-/package/${eu(spec.name)}/collaborators` 153 | return npmFetch.json.stream(uri, '*', { 154 | ...opts, 155 | query: { format: 'cli', user: user || undefined }, 156 | mapJSON 157 | }) 158 | } 159 | 160 | cmd.tfaRequired = (spec, opts) => setRequires2fa(spec, true, opts) 161 | cmd.tfaNotRequired = (spec, opts) => setRequires2fa(spec, false, opts) 162 | function setRequires2fa (spec, required, opts = {}) { 163 | return Promise.resolve().then(() => { 164 | spec = npar(spec) 165 | validate('OBO', [spec, required, opts]) 166 | const uri = `/-/package/${eu(spec.name)}/access` 167 | return npmFetch(uri, { 168 | ...opts, 169 | method: 'POST', 170 | body: { publish_requires_tfa: required }, 171 | spec, 172 | ignoreBody: true 173 | }).then(() => true) 174 | }) 175 | } 176 | 177 | cmd.edit = () => { 178 | throw new Error('Not implemented yet') 179 | } 180 | 181 | function splitEntity (entity = '') { 182 | const [, scope, team] = entity.match(/^@?([^:]+)(?::(.*))?$/) || [] 183 | return { scope, team } 184 | } 185 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "libnpmaccess", 3 | "version": "4.0.3", 4 | "description": "programmatic library for `npm access` commands", 5 | "author": "Kat Marchán ", 6 | "license": "ISC", 7 | "scripts": { 8 | "preversion": "npm test", 9 | "postversion": "npm publish", 10 | "postpublish": "git push origin --follow-tags", 11 | "lint": "standard", 12 | "test": "tap" 13 | }, 14 | "devDependencies": { 15 | "nock": "^12.0.1", 16 | "standard": "^14.3.0", 17 | "tap": "^14.11.0" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/npm/libnpmaccess.git" 22 | }, 23 | "bugs": "https://github.com/npm/libnpmaccess/issues", 24 | "homepage": "https://npmjs.com/package/libnpmaccess", 25 | "dependencies": { 26 | "aproba": "^2.0.0", 27 | "minipass": "^3.1.1", 28 | "npm-package-arg": "^8.1.2", 29 | "npm-registry-fetch": "^11.0.0" 30 | }, 31 | "engines": { 32 | "node": ">=10" 33 | }, 34 | "tap": { 35 | "check-coverage": true 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/fixtures/tnock.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const nock = require('nock') 4 | 5 | module.exports = tnock 6 | function tnock (t, host) { 7 | const server = nock(host) 8 | t.tearDown(function () { 9 | server.done() 10 | }) 11 | return server 12 | } 13 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('tap') 4 | const tnock = require('./fixtures/tnock.js') 5 | 6 | const access = require('../index.js') 7 | 8 | const REG = 'http://localhost:1337' 9 | const OPTS = { 10 | registry: REG 11 | } 12 | 13 | t.test('access public', t => { 14 | tnock(t, REG).post( 15 | '/-/package/%40foo%2Fbar/access', { access: 'public' } 16 | ).reply(200) 17 | return access.public('@foo/bar', OPTS).then(ret => { 18 | t.deepEqual(ret, true, 'request succeeded') 19 | }) 20 | }) 21 | 22 | t.test('access public - failure', t => { 23 | tnock(t, REG).post( 24 | '/-/package/%40foo%2Fbar/access', { access: 'public' } 25 | ).reply(418) 26 | return access.public('@foo/bar', OPTS) 27 | .catch(err => { 28 | t.equals(err.statusCode, 418, 'fails with code from registry') 29 | }) 30 | }) 31 | 32 | t.test('access restricted', t => { 33 | tnock(t, REG).post( 34 | '/-/package/%40foo%2Fbar/access', { access: 'restricted' } 35 | ).reply(200) 36 | return access.restricted('@foo/bar', OPTS).then(ret => { 37 | t.deepEqual(ret, true, 'request succeeded') 38 | }) 39 | }) 40 | 41 | t.test('access restricted - failure', t => { 42 | tnock(t, REG).post( 43 | '/-/package/%40foo%2Fbar/access', { access: 'restricted' } 44 | ).reply(418) 45 | return access.restricted('@foo/bar', OPTS) 46 | .catch(err => { 47 | t.equals(err.statusCode, 418, 'fails with code from registry') 48 | }) 49 | }) 50 | 51 | t.test('access 2fa-required', t => { 52 | tnock(t, REG).post('/-/package/%40foo%2Fbar/access', { 53 | publish_requires_tfa: true 54 | }).reply(200, { ok: true }) 55 | return access.tfaRequired('@foo/bar', OPTS).then(ret => { 56 | t.deepEqual(ret, true, 'request succeeded') 57 | }) 58 | }) 59 | 60 | t.test('access 2fa-not-required', t => { 61 | tnock(t, REG).post('/-/package/%40foo%2Fbar/access', { 62 | publish_requires_tfa: false 63 | }).reply(200, { ok: true }) 64 | return access.tfaNotRequired('@foo/bar', OPTS).then(ret => { 65 | t.deepEqual(ret, true, 'request succeeded') 66 | }) 67 | }) 68 | 69 | t.test('access grant basic read-write', t => { 70 | tnock(t, REG).put('/-/team/myorg/myteam/package', { 71 | package: '@foo/bar', 72 | permissions: 'read-write' 73 | }).reply(201) 74 | return access.grant( 75 | '@foo/bar', 'myorg:myteam', 'read-write', OPTS 76 | ).then(ret => { 77 | t.deepEqual(ret, true, 'request succeeded') 78 | }) 79 | }) 80 | 81 | t.test('access grant basic read-only', t => { 82 | tnock(t, REG).put('/-/team/myorg/myteam/package', { 83 | package: '@foo/bar', 84 | permissions: 'read-only' 85 | }).reply(201) 86 | return access.grant( 87 | '@foo/bar', 'myorg:myteam', 'read-only', OPTS 88 | ).then(ret => { 89 | t.deepEqual(ret, true, 'request succeeded') 90 | }) 91 | }) 92 | 93 | t.test('access grant bad perm', t => { 94 | return access.grant( 95 | '@foo/bar', 'myorg:myteam', 'unknown', OPTS 96 | ).then(ret => { 97 | throw new Error('should not have succeeded') 98 | }, err => { 99 | t.match( 100 | err.message, 101 | /must be.*read-write.*read-only/, 102 | 'only read-write and read-only are accepted' 103 | ) 104 | }) 105 | }) 106 | 107 | t.test('access grant no entity', t => { 108 | return access.grant( 109 | '@foo/bar', undefined, 'read-write', OPTS 110 | ).then(ret => { 111 | throw new Error('should not have succeeded') 112 | }, err => { 113 | t.match( 114 | err.message, 115 | /Expected string/, 116 | 'passing undefined entity gives useful error' 117 | ) 118 | }) 119 | }) 120 | 121 | t.test('access grant basic unscoped', t => { 122 | tnock(t, REG).put('/-/team/myorg/myteam/package', { 123 | package: 'bar', 124 | permissions: 'read-write' 125 | }).reply(201) 126 | return access.grant( 127 | 'bar', 'myorg:myteam', 'read-write', OPTS 128 | ).then(ret => { 129 | t.deepEqual(ret, true, 'request succeeded') 130 | }) 131 | }) 132 | 133 | t.test('access grant no opts passed', t => { 134 | // NOTE: mocking real url, because no opts variable means `registry` value 135 | // will be defauled to real registry url 136 | tnock(t, 'https://registry.npmjs.org') 137 | .put('/-/team/myorg/myteam/package', { 138 | package: 'bar', 139 | permissions: 'read-write' 140 | }) 141 | .reply(201) 142 | return access.grant('bar', 'myorg:myteam', 'read-write') 143 | .then(ret => { 144 | t.equals(ret, true, 'request succeeded') 145 | }) 146 | }) 147 | 148 | t.test('access revoke basic', t => { 149 | tnock(t, REG).delete('/-/team/myorg/myteam/package', { 150 | package: '@foo/bar' 151 | }).reply(200) 152 | return access.revoke('@foo/bar', 'myorg:myteam', OPTS).then(ret => { 153 | t.deepEqual(ret, true, 'request succeeded') 154 | }) 155 | }) 156 | 157 | t.test('access revoke basic unscoped', t => { 158 | tnock(t, REG).delete('/-/team/myorg/myteam/package', { 159 | package: 'bar' 160 | }).reply(200, { accessChanged: true }) 161 | return access.revoke('bar', 'myorg:myteam', OPTS).then(ret => { 162 | t.deepEqual(ret, true, 'request succeeded') 163 | }) 164 | }) 165 | 166 | t.test('access revoke no opts passed', t => { 167 | // NOTE: mocking real url, because no opts variable means `registry` value 168 | // will be defauled to real registry url 169 | tnock(t, 'https://registry.npmjs.org') 170 | .delete('/-/team/myorg/myteam/package', { 171 | package: 'bar' 172 | }) 173 | .reply(201) 174 | return access.revoke('bar', 'myorg:myteam') 175 | .then(ret => { 176 | t.equals(ret, true, 'request succeeded') 177 | }) 178 | }) 179 | 180 | t.test('ls-packages on team', t => { 181 | const serverPackages = { 182 | '@foo/bar': 'write', 183 | '@foo/util': 'read', 184 | '@foo/other': 'shrödinger' 185 | } 186 | const clientPackages = { 187 | '@foo/bar': 'read-write', 188 | '@foo/util': 'read-only', 189 | '@foo/other': 'shrödinger' 190 | } 191 | tnock(t, REG).get( 192 | '/-/team/myorg/myteam/package?format=cli' 193 | ).reply(200, serverPackages) 194 | return access.lsPackages('myorg:myteam', OPTS).then(data => { 195 | t.deepEqual(data, clientPackages, 'got client package info') 196 | }) 197 | }) 198 | 199 | t.test('ls-packages on org', t => { 200 | const serverPackages = { 201 | '@foo/bar': 'write', 202 | '@foo/util': 'read', 203 | '@foo/other': 'shrödinger' 204 | } 205 | const clientPackages = { 206 | '@foo/bar': 'read-write', 207 | '@foo/util': 'read-only', 208 | '@foo/other': 'shrödinger' 209 | } 210 | tnock(t, REG).get( 211 | '/-/org/myorg/package?format=cli' 212 | ).reply(200, serverPackages) 213 | return access.lsPackages('myorg', OPTS).then(data => { 214 | t.deepEqual(data, clientPackages, 'got client package info') 215 | }) 216 | }) 217 | 218 | t.test('ls-packages on user', t => { 219 | const serverPackages = { 220 | '@foo/bar': 'write', 221 | '@foo/util': 'read', 222 | '@foo/other': 'shrödinger' 223 | } 224 | const clientPackages = { 225 | '@foo/bar': 'read-write', 226 | '@foo/util': 'read-only', 227 | '@foo/other': 'shrödinger' 228 | } 229 | const srv = tnock(t, REG) 230 | srv.get('/-/org/myuser/package?format=cli').reply(404, { error: 'not found' }) 231 | srv.get('/-/user/myuser/package?format=cli').reply(200, serverPackages) 232 | return access.lsPackages('myuser', OPTS).then(data => { 233 | t.deepEqual(data, clientPackages, 'got client package info') 234 | }) 235 | }) 236 | 237 | t.test('ls-packages error on team', t => { 238 | tnock(t, REG).get('/-/team/myorg/myteam/package?format=cli').reply(404) 239 | return access.lsPackages('myorg:myteam', OPTS).then( 240 | () => { throw new Error('should not have succeeded') }, 241 | err => t.equal(err.code, 'E404', 'spit out 404 directly if team provided') 242 | ) 243 | }) 244 | 245 | t.test('ls-packages error on user', t => { 246 | const srv = tnock(t, REG) 247 | srv.get('/-/org/myuser/package?format=cli').reply(404, { error: 'not found' }) 248 | srv.get('/-/user/myuser/package?format=cli').reply(404, { error: 'not found' }) 249 | return access.lsPackages('myuser', OPTS).then( 250 | () => { throw new Error('should not have succeeded') }, 251 | err => t.equal(err.code, 'E404', 'spit out 404 if both reqs fail') 252 | ) 253 | }) 254 | 255 | t.test('ls-packages bad response', t => { 256 | tnock(t, REG).get( 257 | '/-/team/myorg/myteam/package?format=cli' 258 | ).reply(200, JSON.stringify(null)) 259 | return access.lsPackages('myorg:myteam', OPTS).then(data => { 260 | t.deepEqual(data, null, 'succeeds with null') 261 | }) 262 | }) 263 | 264 | t.test('ls-packages stream', t => { 265 | const serverPackages = { 266 | '@foo/bar': 'write', 267 | '@foo/util': 'read', 268 | '@foo/other': 'shrödinger' 269 | } 270 | const clientPackages = [ 271 | ['@foo/bar', 'read-write'], 272 | ['@foo/util', 'read-only'], 273 | ['@foo/other', 'shrödinger'] 274 | ] 275 | tnock(t, REG).get( 276 | '/-/team/myorg/myteam/package?format=cli' 277 | ).reply(200, serverPackages) 278 | return access.lsPackages.stream('myorg:myteam', OPTS) 279 | .collect() 280 | .then(data => { 281 | t.deepEqual(data, clientPackages, 'got streamed client package info') 282 | }) 283 | }) 284 | 285 | t.test('ls-packages stream no opts', t => { 286 | const serverPackages = { 287 | '@foo/bar': 'write', 288 | '@foo/util': 'read', 289 | '@foo/other': 'shrödinger' 290 | } 291 | const clientPackages = [ 292 | ['@foo/bar', 'read-write'], 293 | ['@foo/util', 'read-only'], 294 | ['@foo/other', 'shrödinger'] 295 | ] 296 | // NOTE: mocking real url, because no opts variable means `registry` value 297 | // will be defauled to real registry url 298 | tnock(t, 'https://registry.npmjs.org') 299 | .get('/-/team/myorg/myteam/package?format=cli') 300 | .reply(200, serverPackages) 301 | return access.lsPackages.stream('myorg:myteam') 302 | .collect() 303 | .then(data => { 304 | t.deepEqual(data, clientPackages, 'got streamed client package info') 305 | }) 306 | }) 307 | 308 | t.test('ls-collaborators', t => { 309 | const serverCollaborators = { 310 | 'myorg:myteam': 'write', 311 | 'myorg:anotherteam': 'read', 312 | 'myorg:thirdteam': 'special-case' 313 | } 314 | const clientCollaborators = { 315 | 'myorg:myteam': 'read-write', 316 | 'myorg:anotherteam': 'read-only', 317 | 'myorg:thirdteam': 'special-case' 318 | } 319 | tnock(t, REG).get( 320 | '/-/package/%40foo%2Fbar/collaborators?format=cli' 321 | ).reply(200, serverCollaborators) 322 | return access.lsCollaborators('@foo/bar', OPTS).then(data => { 323 | t.deepEqual(data, clientCollaborators, 'got collaborators') 324 | }) 325 | }) 326 | 327 | t.test('ls-collaborators stream', t => { 328 | const serverCollaborators = { 329 | 'myorg:myteam': 'write', 330 | 'myorg:anotherteam': 'read', 331 | 'myorg:thirdteam': 'special-case' 332 | } 333 | const clientCollaborators = [ 334 | ['myorg:myteam', 'read-write'], 335 | ['myorg:anotherteam', 'read-only'], 336 | ['myorg:thirdteam', 'special-case'] 337 | ] 338 | tnock(t, REG).get( 339 | '/-/package/%40foo%2Fbar/collaborators?format=cli' 340 | ).reply(200, serverCollaborators) 341 | return access.lsCollaborators.stream('@foo/bar', OPTS) 342 | .collect() 343 | .then(data => { 344 | t.deepEqual(data, clientCollaborators, 'got collaborators') 345 | }) 346 | }) 347 | 348 | t.test('ls-collaborators w/scope', t => { 349 | const serverCollaborators = { 350 | 'myorg:myteam': 'write', 351 | 'myorg:anotherteam': 'read', 352 | 'myorg:thirdteam': 'special-case' 353 | } 354 | const clientCollaborators = { 355 | 'myorg:myteam': 'read-write', 356 | 'myorg:anotherteam': 'read-only', 357 | 'myorg:thirdteam': 'special-case' 358 | } 359 | tnock(t, REG).get( 360 | '/-/package/%40foo%2Fbar/collaborators?format=cli&user=zkat' 361 | ).reply(200, serverCollaborators) 362 | return access.lsCollaborators('@foo/bar', 'zkat', OPTS).then(data => { 363 | t.deepEqual(data, clientCollaborators, 'got collaborators') 364 | }) 365 | }) 366 | 367 | t.test('ls-collaborators w/o scope', t => { 368 | const serverCollaborators = { 369 | 'myorg:myteam': 'write', 370 | 'myorg:anotherteam': 'read', 371 | 'myorg:thirdteam': 'special-case' 372 | } 373 | const clientCollaborators = { 374 | 'myorg:myteam': 'read-write', 375 | 'myorg:anotherteam': 'read-only', 376 | 'myorg:thirdteam': 'special-case' 377 | } 378 | tnock(t, REG).get( 379 | '/-/package/bar/collaborators?format=cli&user=zkat' 380 | ).reply(200, serverCollaborators) 381 | return access.lsCollaborators('bar', 'zkat', OPTS).then(data => { 382 | t.deepEqual(data, clientCollaborators, 'got collaborators') 383 | }) 384 | }) 385 | 386 | t.test('ls-collaborators bad response', t => { 387 | tnock(t, REG).get( 388 | '/-/package/%40foo%2Fbar/collaborators?format=cli' 389 | ).reply(200, JSON.stringify(null)) 390 | return access.lsCollaborators('@foo/bar', null, OPTS).then(data => { 391 | t.deepEqual(data, null, 'succeeds with null') 392 | }) 393 | }) 394 | 395 | t.test('error on non-registry specs', t => { 396 | const resolve = () => { throw new Error('should not succeed') } 397 | const reject = err => t.match( 398 | err.message, /spec.*must be a registry spec/, 'registry spec required' 399 | ) 400 | return Promise.all([ 401 | access.public('githubusername/reponame').then(resolve, reject), 402 | access.restricted('foo/bar').then(resolve, reject), 403 | access.grant('foo/bar', 'myorg', 'myteam', 'read-only').then(resolve, reject), 404 | access.revoke('foo/bar', 'myorg', 'myteam').then(resolve, reject), 405 | access.lsCollaborators('foo/bar').then(resolve, reject), 406 | access.tfaRequired('foo/bar').then(resolve, reject), 407 | access.tfaNotRequired('foo/bar').then(resolve, reject) 408 | ]) 409 | }) 410 | 411 | t.test('edit', t => { 412 | t.equal(typeof access.edit, 'function', 'access.edit exists') 413 | t.throws(() => { 414 | access.edit() 415 | }, /Not implemented/, 'directly throws NIY message') 416 | t.done() 417 | }) 418 | --------------------------------------------------------------------------------