From 854521baa49ef88ff9586ec2cc5f1fbaee7fa364 Mon Sep 17 00:00:00 2001 From: Gar Date: Tue, 30 Aug 2022 11:39:48 -0700 Subject: [PATCH] feat(rewrite): Rewrite libnpmaccess BREAKING CHANGE: the api for libnpmaccess is different now It is aligned more with how npm uses it, consolidating the mfa functions into a single command, and renames the functions to be easier to eventually consolidate into a registry client library. See the README for the new api. --- DEPENDENCIES.md | 2 - package-lock.json | 2 - workspaces/libnpmaccess/README.md | 235 +++---------- workspaces/libnpmaccess/lib/index.js | 254 ++++++-------- workspaces/libnpmaccess/package.json | 2 - workspaces/libnpmaccess/test/index.js | 465 ++++++++------------------ 6 files changed, 276 insertions(+), 684 deletions(-) diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 1ddd85b3d411d..105761dcb15bd 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -323,8 +323,6 @@ graph LR; init-package-json-->validate-npm-package-name; is-cidr-->cidr-regex; is-core-module-->has; - libnpmaccess-->aproba; - libnpmaccess-->minipass; libnpmaccess-->nock; libnpmaccess-->npm-package-arg; libnpmaccess-->npm-registry-fetch; diff --git a/package-lock.json b/package-lock.json index bf31065aeb9a5..e0a2304732744 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13917,8 +13917,6 @@ "version": "7.0.0-pre.0", "license": "ISC", "dependencies": { - "aproba": "^2.0.0", - "minipass": "^3.1.1", "npm-package-arg": "^9.0.1", "npm-registry-fetch": "^13.0.0" }, diff --git a/workspaces/libnpmaccess/README.md b/workspaces/libnpmaccess/README.md index 3e35562cfddbc..060016bc2a0b6 100644 --- a/workspaces/libnpmaccess/README.md +++ b/workspaces/libnpmaccess/README.md @@ -6,241 +6,88 @@ [`libnpmaccess`](https://github.com/npm/libnpmaccess) is a Node.js 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. ## Example ```javascript 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 -#### `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: - -* `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>}` - -#### `> access.public(spec, [opts]) -> Promise` +#### `spec` parameter for all `libnpmaccess` commands `spec` must be an [`npm-package-arg`](https://npm.im/npm-package-arg)-compatible registry spec. -Makes package described by `spec` public. - -##### Example - -```javascript -await access.public('@foo/bar', {token: 'myregistrytoken'}) -// `@foo/bar` is now public -``` - -#### `> access.restricted(spec, [opts]) -> Promise` - -`spec` must be an [`npm-package-arg`](https://npm.im/npm-package-arg)-compatible -registry spec. - -Makes package described by `spec` private/restricted. - -##### Example +#### `access.getCollaborators(spec, opts) -> Promise` -```javascript -await access.restricted('@foo/bar', {token: 'myregistrytoken'}) -// `@foo/bar` is now private -``` +Gets collaborators for a given package -#### `> access.grant(spec, team, permissions, [opts]) -> Promise` +#### `access.getPackages(user|scope|team, opts) -> Promise` -`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'`. +Gets all packages for a given user, scope, or team. -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 +Users and scopes can be in the format `@scope` or `scope` -```javascript -await access.grant('@foo/bar', '@foo:myteam', 'read-write', { - token: 'myregistrytoken' -}) -// `@foo/bar` is now read/write enabled for the @foo:myteam team. -``` +#### `access.getVisibility(spec, opts) -> Promise` -#### `> access.revoke(spec, team, [opts]) -> Promise` +Gets the visibility of a given package -`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'`. +#### `access.removePermissions(team, spec, opts) -> Promise` -Removes access to a package from a certain team. +Removes the access for a given team to a package. -##### Example +Teams should be in the format `scope:team` or `@scope:team` -```javascript -await access.revoke('@foo/bar', '@foo:myteam', { - token: 'myregistrytoken' -}) -// @foo:myteam can no longer access `@foo/bar` -``` +#### `access.setAccess(package, access, opts) -> Promise` -#### `> access.tfaRequired(spec, [opts]) -> Promise` +Sets access level for package described by `spec`. -`spec` must be an [`npm-package-arg`](https://npm.im/npm-package-arg)-compatible -registry spec. - -Makes it so publishing or managing a package requires using 2FA tokens to -complete operations. +The npm registry accepts the following `access` levels: -##### Example +`public`: package is public +`private`: package is private -```javascript -await access.tfaRequires('lodash', {token: 'myregistrytoken'}) -// Publishing or changing dist-tags on `lodash` now require OTP to be enabled. -``` +The npm registry also only allows scoped packages to have their access +level set. -#### `> access.tfaNotRequired(spec, [opts]) -> Promise` +#### access.setMfa(spec, level, opts) -> Promise` -`spec` must be an [`npm-package-arg`](https://npm.im/npm-package-arg)-compatible -registry spec. +Sets the publishing mfa requirements for a given package. Level must be one of the +following -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. +`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. -##### Example +#### access.setPermissions(team, spec, permssions, opts) -> Promise` -```javascript -await access.tfaNotRequired('lodash', {otp: '123654', token: 'myregistrytoken'}) -// Publishing or editing dist-tags on `lodash` no longer requires OTP to be -// enabled. -``` +Sets permissions levels for a given team to a package. -#### `> access.lsPackages(entity, [opts]) -> Promise` +Teams should be in the format `scope:team` or `@scope:team` -`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. +The npm registry accepts the following `permissions`: -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. -``` - -#### `> 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 -``` - -#### `> access.lsCollaborators(spec, [user], [opts]) -> Promise` - -`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. -``` - -#### `> 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 diff --git a/workspaces/libnpmaccess/lib/index.js b/workspaces/libnpmaccess/lib/index.js index 71219d0098cfe..fca0e47279bfb 100644 --- a/workspaces/libnpmaccess/lib/index.js +++ b/workspaces/libnpmaccess/lib/index.js @@ -1,186 +1,140 @@ '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) - }) -} - -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') + 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) } - 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) - }) + throw err + } } -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 getCollaborators = async (pkg, opts) => { + const spec = npar(pkg) + const uri = `/-/package/${spec.escapedName}/collaborators` + 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 getVisibility = async (pkg, opts) => { + const spec = npar(pkg) + const uri = `/-/package/${spec.escapedName}/visibility` + return npmFetch.json(uri, opts) } -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 nextOpts = { +const setAccess = async (pkg, access, opts) => { + const spec = npar(pkg) + 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: { access }, + 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 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}`) } - spec = npar(spec) - validate('OSO|OZO', [spec, user, opts]) - const uri = `/-/package/${eu(spec.name)}/collaborators` - return npmFetch.json.stream(uri, '*', { + const uri = `/-/package/${spec.escapedName}/access` + await npmFetch(uri, { ...opts, - query: { format: 'cli', user: user || undefined }, - mapJSON, + method: 'POST', + body, + 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 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`') + } + const uri = `/-/team/${scope}/${team}/package` + await npmFetch(uri, { + ...opts, + method: 'PUT', + body: { package: spec.name, permissions }, + scope, + spec, + ignoreBody: true, }) + return true } -cmd.edit = () => { - throw new Error('Not implemented yet') +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 } -function splitEntity (entity = '') { - const [, scope, team] = entity.match(/^@?([^:]+)(?::(.*))?$/) || [] - return { scope, team } +module.exports = { + getCollaborators, + getPackages, + getVisibility, + removePermissions, + setAccess, + setMfa, + setPermissions, } diff --git a/workspaces/libnpmaccess/package.json b/workspaces/libnpmaccess/package.json index d05156710256b..8e73fd2676dd9 100644 --- a/workspaces/libnpmaccess/package.json +++ b/workspaces/libnpmaccess/package.json @@ -29,8 +29,6 @@ "bugs": "https://github.com/npm/libnpmaccess/issues", "homepage": "https://npmjs.com/package/libnpmaccess", "dependencies": { - "aproba": "^2.0.0", - "minipass": "^3.1.1", "npm-package-arg": "^9.0.1", "npm-registry-fetch": "^13.0.0" }, diff --git a/workspaces/libnpmaccess/test/index.js b/workspaces/libnpmaccess/test/index.js index 689788d5269f7..bf73c3c8c929f 100644 --- a/workspaces/libnpmaccess/test/index.js +++ b/workspaces/libnpmaccess/test/index.js @@ -10,353 +10,150 @@ const OPTS = { registry: REG, } -t.test('access public', async t => { - tnock(t, REG).post( - '/-/package/%40foo%2Fbar/access', { access: 'public' } - ).reply(200) - await t.resolves(access.public('@foo/bar', OPTS)) -}) - -t.test('access public - failure', async t => { - tnock(t, REG).post( - '/-/package/%40foo%2Fbar/access', { access: 'public' } - ).reply(418) - await t.rejects( - access.public('@foo/bar', OPTS), - { statusCode: 418 }, - 'fails with code from registry' - ) -}) - -t.test('access restricted', async t => { - tnock(t, REG).post( - '/-/package/%40foo%2Fbar/access', { access: 'restricted' } - ).reply(200) - await t.resolves(access.restricted('@foo/bar', OPTS)) -}) - -t.test('access restricted - failure', async t => { - tnock(t, REG).post( - '/-/package/%40foo%2Fbar/access', { access: 'restricted' } - ).reply(418) - await t.rejects( - access.restricted('@foo/bar', OPTS), - { statusCode: 418 }, - 'fails with code from registry') -}) - -t.test('access 2fa-required', async t => { - tnock(t, REG).post('/-/package/%40foo%2Fbar/access', { - publish_requires_tfa: true, - }).reply(200, { ok: true }) - await t.resolves(access.tfaRequired('@foo/bar', OPTS)) -}) - -t.test('access 2fa-not-required', async t => { - tnock(t, REG).post('/-/package/%40foo%2Fbar/access', { - publish_requires_tfa: false, - }).reply(200, { ok: true }) - await t.resolves(access.tfaNotRequired('@foo/bar', OPTS)) -}) - -t.test('access grant basic read-write', async t => { - tnock(t, REG).put('/-/team/myorg/myteam/package', { - package: '@foo/bar', - permissions: 'read-write', - }).reply(201) - await t.resolves(access.grant('@foo/bar', 'myorg:myteam', 'read-write', OPTS)) -}) - -t.test('access grant basic read-only', async t => { - tnock(t, REG).put('/-/team/myorg/myteam/package', { - package: '@foo/bar', - permissions: 'read-only', - }).reply(201) - await t.resolves(access.grant('@foo/bar', 'myorg:myteam', 'read-only', OPTS)) -}) - -t.test('access grant bad perm', async t => { - await t.rejects( - access.grant('@foo/bar', 'myorg:myteam', 'unknown', OPTS), - { message: /must be.*read-write.*read-only/ }, - 'only read-write and read-only are accepted' - ) -}) - -t.test('access grant no entity', async t => { - await t.rejects( - access.grant('@foo/bar', undefined, 'read-write', OPTS), - { message: /Expected string/ }, - 'passing undefined entity gives useful error' - ) -}) - -t.test('access grant basic unscoped', async t => { - tnock(t, REG).put('/-/team/myorg/myteam/package', { - package: 'bar', - permissions: 'read-write', - }).reply(201) - await t.resolves(access.grant('bar', 'myorg:myteam', 'read-write', OPTS)) -}) - -t.test('access grant no opts passed', async t => { - // NOTE: mocking real url, because no opts variable means `registry` value - // will be defauled to real registry url - tnock(t, 'https://registry.npmjs.org') - .put('/-/team/myorg/myteam/package', { - package: 'bar', - permissions: 'read-write', - }) - .reply(201) - await t.resolves(access.grant('bar', 'myorg:myteam', 'read-write')) -}) - -t.test('access revoke basic', async t => { - tnock(t, REG).delete('/-/team/myorg/myteam/package', { - package: '@foo/bar', - }).reply(200) - await t.resolves(access.revoke('@foo/bar', 'myorg:myteam', OPTS)) -}) - -t.test('access revoke basic unscoped', async t => { - tnock(t, REG).delete('/-/team/myorg/myteam/package', { - package: 'bar', - }).reply(200, { accessChanged: true }) - await t.resolves(access.revoke('bar', 'myorg:myteam', OPTS)) -}) - -t.test('access revoke no opts passed', async t => { - // NOTE: mocking real url, because no opts variable means `registry` value - // will be defauled to real registry url - tnock(t, 'https://registry.npmjs.org') - .delete('/-/team/myorg/myteam/package', { - package: 'bar', - }) - .reply(201) - await t.resolves(access.revoke('bar', 'myorg:myteam')) -}) - -t.test('ls-packages on team', async t => { - const serverPackages = { - '@foo/bar': 'write', - '@foo/util': 'read', - '@foo/other': 'shrödinger', - } - const clientPackages = { - '@foo/bar': 'read-write', - '@foo/util': 'read-only', - '@foo/other': 'shrödinger', - } - tnock(t, REG).get( - '/-/team/myorg/myteam/package?format=cli' - ).reply(200, serverPackages) - const data = await access.lsPackages('myorg:myteam', OPTS) - t.same(data, clientPackages, 'got client package info') -}) - -t.test('ls-packages on org', async t => { - const serverPackages = { - '@foo/bar': 'write', - '@foo/util': 'read', - '@foo/other': 'shrödinger', - } - const clientPackages = { - '@foo/bar': 'read-write', - '@foo/util': 'read-only', - '@foo/other': 'shrödinger', - } - tnock(t, REG).get( - '/-/org/myorg/package?format=cli' - ).reply(200, serverPackages) - const data = await access.lsPackages('myorg', OPTS) - t.same(data, clientPackages, 'got client package info') -}) - -t.test('ls-packages on user', async t => { - const serverPackages = { - '@foo/bar': 'write', - '@foo/util': 'read', - '@foo/other': 'shrödinger', - } - const clientPackages = { - '@foo/bar': 'read-write', - '@foo/util': 'read-only', - '@foo/other': 'shrödinger', - } - const srv = tnock(t, REG) - srv.get('/-/org/myuser/package?format=cli').reply(404, { error: 'not found' }) - srv.get('/-/user/myuser/package?format=cli').reply(200, serverPackages) - const data = await access.lsPackages('myuser', OPTS) - t.same(data, clientPackages, 'got client package info') -}) - -t.test('ls-packages error on team', async t => { - tnock(t, REG).get('/-/team/myorg/myteam/package?format=cli').reply(404) - await t.rejects( - access.lsPackages('myorg:myteam', OPTS), - { code: 'E404' }, - 'spit out 404 directly if team provided' - ) -}) - -t.test('ls-packages error on user', async t => { - const srv = tnock(t, REG) - srv.get('/-/org/myuser/package?format=cli').reply(404, { error: 'not found' }) - srv.get('/-/user/myuser/package?format=cli').reply(404, { error: 'not found' }) - await t.rejects( - access.lsPackages('myuser', OPTS), - { code: 'E404' }, - 'spit out 404 if both reqs fail' - ) -}) - -t.test('ls-packages bad response', async t => { - tnock(t, REG).get( - '/-/team/myorg/myteam/package?format=cli' - ).reply(200, JSON.stringify(null)) - const data = await access.lsPackages('myorg:myteam', OPTS) - t.same(data, null, 'succeeds with null') -}) - -t.test('ls-packages stream', async t => { - const serverPackages = { - '@foo/bar': 'write', - '@foo/util': 'read', - '@foo/other': 'shrödinger', - } - const clientPackages = [ - ['@foo/bar', 'read-write'], - ['@foo/util', 'read-only'], - ['@foo/other', 'shrödinger'], - ] - tnock(t, REG).get( - '/-/team/myorg/myteam/package?format=cli' - ).reply(200, serverPackages) - const data = await access.lsPackages.stream('myorg:myteam', OPTS).collect() - t.same(data, clientPackages, 'got streamed client package info') -}) - -t.test('ls-packages stream no opts', async t => { - const serverPackages = { - '@foo/bar': 'write', - '@foo/util': 'read', - '@foo/other': 'shrödinger', - } - const clientPackages = [ - ['@foo/bar', 'read-write'], - ['@foo/util', 'read-only'], - ['@foo/other', 'shrödinger'], - ] - // NOTE: mocking real url, because no opts variable means `registry` value - // will be defauled to real registry url - tnock(t, 'https://registry.npmjs.org') - .get('/-/team/myorg/myteam/package?format=cli') - .reply(200, serverPackages) - const data = await access.lsPackages.stream('myorg:myteam').collect() - t.same(data, clientPackages, 'got streamed client package info') +t.test('getCollaborators', t => { + t.test('success', async t => { + const collaborators = { + 'npm:myteam': 'write', + 'npm:anotherteam': 'read', + 'npm:thirdteam': 'special-case', + } + tnock(t, REG).get('/-/package/@npmcli%2ftest-package/collaborators').reply(200, collaborators) + const data = await access.getCollaborators('@npmcli/test-package', OPTS) + t.same(data, collaborators) + }) + t.test('non registry package', async t => { + await t.rejects(access.getCollaborators('./local', OPTS), /package name only/) + }) + t.end() }) -t.test('ls-collaborators', async t => { - const serverCollaborators = { - 'myorg:myteam': 'write', - 'myorg:anotherteam': 'read', - 'myorg:thirdteam': 'special-case', +t.test('getPackages', t => { + const packages = { + '@npmcli/test-package': 'write', + '@npmcli/util': 'read', + '@npmcli/other': 'shrödinger', } - const clientCollaborators = { - 'myorg:myteam': 'read-write', - 'myorg:anotherteam': 'read-only', - 'myorg:thirdteam': 'special-case', - } - tnock(t, REG).get( - '/-/package/%40foo%2Fbar/collaborators?format=cli' - ).reply(200, serverCollaborators) - const data = await access.lsCollaborators('@foo/bar', OPTS) - t.same(data, clientCollaborators, 'got collaborators') + t.test('team', async t => { + tnock(t, REG).get('/-/team/npm/myteam/package').reply(200, packages) + const data = await access.getPackages('npm:myteam', OPTS) + t.same(data, packages) + }) + t.test('org', async t => { + tnock(t, REG).get('/-/org/npm/package').reply(200, packages) + const data = await access.getPackages('npm', OPTS) + t.same(data, packages) + }) + t.test('user', async t => { + tnock(t, REG).get('/-/org/testuser/package').reply(404, {}) + tnock(t, REG).get('/-/user/testuser/package').reply(200, packages) + const data = await access.getPackages('testuser', OPTS) + t.same(data, packages) + }) + t.test('registry error', async t => { + tnock(t, REG).get('/-/org/npm/package').reply(500, {}) + await t.rejects(access.getPackages('npm', OPTS), { code: 'E500' }) + }) + t.end() }) -t.test('ls-collaborators stream', async t => { - const serverCollaborators = { - 'myorg:myteam': 'write', - 'myorg:anotherteam': 'read', - 'myorg:thirdteam': 'special-case', - } - const clientCollaborators = [ - ['myorg:myteam', 'read-write'], - ['myorg:anotherteam', 'read-only'], - ['myorg:thirdteam', 'special-case'], - ] - tnock(t, REG).get( - '/-/package/%40foo%2Fbar/collaborators?format=cli' - ).reply(200, serverCollaborators) - const data = await access.lsCollaborators.stream('@foo/bar', OPTS).collect() - t.same(data, clientCollaborators, 'got collaborators') +t.test('getVisibility', t => { + t.test('success', async t => { + const visibility = { public: true } + tnock(t, REG).get('/-/package/@npmcli%2ftest-package/visibility').reply(200, visibility) + const data = await access.getVisibility('@npmcli/test-package', OPTS) + t.same(data, visibility) + }) + t.test('non registry package', async t => { + await t.rejects(access.getVisibility('./local', OPTS), /package name only/) + }) + t.end() }) -t.test('ls-collaborators w/scope', async t => { - const serverCollaborators = { - 'myorg:myteam': 'write', - 'myorg:anotherteam': 'read', - 'myorg:thirdteam': 'special-case', - } - const clientCollaborators = { - 'myorg:myteam': 'read-write', - 'myorg:anotherteam': 'read-only', - 'myorg:thirdteam': 'special-case', - } - tnock(t, REG).get( - '/-/package/%40foo%2Fbar/collaborators?format=cli&user=zkat' - ).reply(200, serverCollaborators) - const data = await access.lsCollaborators('@foo/bar', 'zkat', OPTS) - t.same(data, clientCollaborators, 'got collaborators') +t.test('removePermissions', t => { + t.test('success', async t => { + tnock(t, REG).delete('/-/team/npm/myteam/package', { + package: '@npmcli/test-package', + }).reply(200) + await t.resolves(access.removePermissions('npm:myteam', '@npmcli/test-package', OPTS)) + }) + t.test('non registry spec', async t => { + await t.rejects(access.removePermissions('npm:myteam', './local', OPTS), /package name only/) + }) + t.end() }) -t.test('ls-collaborators w/o scope', async t => { - const serverCollaborators = { - 'myorg:myteam': 'write', - 'myorg:anotherteam': 'read', - 'myorg:thirdteam': 'special-case', - } - const clientCollaborators = { - 'myorg:myteam': 'read-write', - 'myorg:anotherteam': 'read-only', - 'myorg:thirdteam': 'special-case', - } - tnock(t, REG).get( - '/-/package/bar/collaborators?format=cli&user=zkat' - ).reply(200, serverCollaborators) - const data = await access.lsCollaborators('bar', 'zkat', OPTS) - t.same(data, clientCollaborators, 'got collaborators') +t.test('setAccess', t => { + t.test('public', async t => { + tnock(t, REG).post( + '/-/package/@npmcli%2ftest-package/access', { access: 'public' } + ).reply(200) + await t.resolves(access.setAccess('@npmcli/test-package', 'public', OPTS)) + }) + t.test('restricted', async t => { + tnock(t, REG).post( + '/-/package/@npmcli%2ftest-package/access', { access: 'restricted' } + ).reply(200) + await t.resolves(access.setAccess('@npmcli/test-package', 'restricted', OPTS)) + }) + t.test('non registry package', async t => { + await t.rejects(access.setAccess('./local', 'public', OPTS), /package name only/) + }) + t.end() }) -t.test('ls-collaborators bad response', async t => { - tnock(t, REG).get( - '/-/package/%40foo%2Fbar/collaborators?format=cli' - ).reply(200, JSON.stringify(null)) - const data = await access.lsCollaborators('@foo/bar', null, OPTS) - t.same(data, null, 'succeeds with null') +t.test('setMfa', t => { + t.test('none', async t => { + tnock(t, REG).post('/-/package/@npmcli%2ftest-package/access', { + publish_requires_tfa: false, + }).reply(200) + await t.resolves(access.setMfa('@npmcli/test-package', 'none', OPTS)) + }) + t.test('publish', async t => { + tnock(t, REG).post('/-/package/@npmcli%2ftest-package/access', { + publish_requires_tfa: true, + automation_token_overrides_tfa: false, + }).reply(200) + await t.resolves(access.setMfa('@npmcli/test-package', 'publish', OPTS)) + }) + t.test('automation', async t => { + tnock(t, REG).post('/-/package/@npmcli%2ftest-package/access', { + publish_requires_tfa: true, + automation_token_overrides_tfa: true, + }).reply(200) + await t.resolves(access.setMfa('@npmcli/test-package', 'automation', OPTS)) + }) + t.test('invalid', async t => { + await t.rejects(access.setMfa('@npmcli/test-package', 'invalid', OPTS), /Invalid mfa setting/) + }) + t.test('non registry spec', async t => { + await t.rejects(access.setMfa('./local', 'none', OPTS, /package name only/)) + }) + t.end() }) -t.test('error on non-registry specs', async t => { - await t.rejects(access.public('githubusername/reponame'), - /spec.*must be a registry spec/, 'registry spec required') - await t.rejects(access.restricted('foo/bar'), - /spec.*must be a registry spec/, 'registry spec required') - await t.rejects(access.grant('foo/bar', 'myorg', 'myteam', 'read-only'), - /spec.*must be a registry spec/, 'registry spec required') - await t.rejects(access.revoke('foo/bar', 'myorg', 'myteam'), - /spec.*must be a registry spec/, 'registry spec required') - await t.rejects(access.lsCollaborators('foo/bar'), - /spec.*must be a registry spec/, 'registry spec required') - await t.rejects(access.tfaRequired('foo/bar'), - /spec.*must be a registry spec/, 'registry spec required') - await t.rejects(access.tfaNotRequired('foo/bar'), - /spec.*must be a registry spec/, 'registry spec required') -}) +t.test('setPermissions', t => { + t.test('scope:team read-only', async t => { + tnock(t, REG).put('/-/team/npmcli/myteam/package', { + package: '@npmcli/test-package', + permissions: 'read-only', + }).reply(201) + await t.resolves( + access.setPermissions('npmcli:myteam', '@npmcli/test-package', 'read-only', OPTS) + ) + }) + t.test('scope only', async t => { + await t.rejects( + access.setPermissions('npmcli', '@npmcli/test-package', 'read-only', OPTS), + /scope:team/ + ) + }) + + t.test('no scope or team', async t => { + await t.rejects( + access.setPermissions('@:myteam', '@npmcli/test-package', 'read-only', OPTS), + /scope:team/ + ) + }) -t.test('edit', t => { - t.equal(typeof access.edit, 'function', 'access.edit exists') - t.throws(() => { - access.edit() - }, /Not implemented/, 'directly throws NIY message') t.end() })