diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index a61f5dbb82c67..edd39d0ba5da3 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -33,7 +33,7 @@ jobs: - run: npm i --ignore-scripts --no-audit --no-fund - name: Release Please id: release - run: npx --offline template-oss-release-please + run: npx --offline template-oss-release-please ${{ github.ref_name }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.release-please-manifest.json b/.release-please-manifest.json index efdb054e92edd..78ed38ff80b5b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,10 +1,10 @@ { - ".": "9.0.0-pre.0", - "workspaces/arborist": "6.0.0-pre.0", - "workspaces/libnpmaccess": "7.0.0-pre.0", + ".": "9.0.0-pre.1", + "workspaces/arborist": "6.0.0-pre.1", + "workspaces/libnpmaccess": "7.0.0-pre.1", "workspaces/libnpmdiff": "5.0.0-pre.0", - "workspaces/libnpmexec": "5.0.0-pre.0", - "workspaces/libnpmfund": "4.0.0-pre.0", + "workspaces/libnpmexec": "5.0.0-pre.1", + "workspaces/libnpmfund": "4.0.0-pre.1", "workspaces/libnpmhook": "9.0.0-pre.0", "workspaces/libnpmorg": "5.0.0-pre.0", "workspaces/libnpmpack": "5.0.0-pre.0", diff --git a/AUTHORS b/AUTHORS index 51a75460a7d80..313d036461b52 100644 --- a/AUTHORS +++ b/AUTHORS @@ -853,3 +853,4 @@ Kyle West Nathan Hughes Sandeep Meduru <73886592+sandeepmeduru@users.noreply.github.com> Kid <44045911+kidonng@users.noreply.github.com> +Hugh Lilly diff --git a/CHANGELOG.md b/CHANGELOG.md index 66f68b44fcc11..855e602bc19d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## [9.0.0-pre.1](https://github.com/npm/cli/compare/v9.0.0-pre.0...v9.0.0-pre.1) (2022-09-14) + +### ⚠️ BREAKING CHANGES + +* renames most of the `npm access` subcommands +* the api for libnpmaccess is different now + +### Features + +* [`9c32c6c`](https://github.com/npm/cli/commit/9c32c6c8d6fc5bdfd6af685731fe26920d7e5446) rewrite: rewrite `npm access` (@wraithgar) +* [`854521b`](https://github.com/npm/cli/commit/854521baa49ef88ff9586ec2cc5f1fbaee7fa364) rewrite: Rewrite libnpmaccess (@wraithgar) + +### Bug Fixes + +* [`c3d7549`](https://github.com/npm/cli/commit/c3d75499cfd4e3601c6ca31621b2f693af466c4d) add tag to publish log message (@wraithgar) + +### Documentation + +* [`fd0eebe`](https://github.com/npm/cli/commit/fd0eebe4c2b55dd69972aff7de1b4db14ea6799a) update registry docs header (@hughlilly) + +### Dependencies + +* [Workspace](https://github.com/npm/cli/compare/arborist-v6.0.0-pre.0...arborist-v6.0.0-pre.1): `@npmcli/arborist@6.0.0-pre.1` +* [Workspace](https://github.com/npm/cli/compare/libnpmaccess-v7.0.0-pre.0...libnpmaccess-v7.0.0-pre.1): `libnpmaccess@7.0.0-pre.1` +* [Workspace](https://github.com/npm/cli/compare/libnpmexec-v5.0.0-pre.0...libnpmexec-v5.0.0-pre.1): `libnpmexec@5.0.0-pre.1` +* [Workspace](https://github.com/npm/cli/compare/libnpmfund-v4.0.0-pre.0...libnpmfund-v4.0.0-pre.1): `libnpmfund@4.0.0-pre.1` + ## [9.0.0-pre.0](https://github.com/npm/cli/compare/v8.19.1...v9.0.0-pre.0) (2022-09-08) ### ⚠ BREAKING CHANGES 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/docs/content/commands/npm-access.md b/docs/content/commands/npm-access.md index f7a98af654714..bc481eac16336 100644 --- a/docs/content/commands/npm-access.md +++ b/docs/content/commands/npm-access.md @@ -11,15 +11,13 @@ description: Set access level on published packages ```bash -npm access public [] -npm access restricted [] +npm access list packages [|| [] +npm access list collaborators [ []] +npm access get status [] +npm access set status=public|private [] +npm access set mfa=none|publish|automation [] npm access grant [] npm access revoke [] -npm access 2fa-required [] -npm access 2fa-not-required [] -npm access ls-packages [||] -npm access ls-collaborators [ []] -npm access edit [] ``` @@ -91,12 +89,17 @@ Management of teams and team memberships is done with the `npm team` command. -#### `registry` +#### `json` -* Default: "https://registry.npmjs.org/" -* Type: URL +* Default: false +* Type: Boolean -The base URL of the npm registry. +Whether or not to output JSON data, rather than the normal output. + +* In `npm pkg set` it enables parsing set values with JSON.parse() before + saving them to your `package.json`. + +Not supported by all npm commands. @@ -115,6 +118,16 @@ password, npm will prompt on the command line for one. +#### `registry` + +* Default: "https://registry.npmjs.org/" +* Type: URL + +The base URL of the npm registry. + + + + ### See Also diff --git a/docs/content/using-npm/registry.md b/docs/content/using-npm/registry.md index 4a265db03f079..fd7e44de48eba 100644 --- a/docs/content/using-npm/registry.md +++ b/docs/content/using-npm/registry.md @@ -69,7 +69,7 @@ to force it to be published only to your internal/private registry. See [`package.json`](/configuring-npm/package-json) for more info on what goes in the package.json file. -### Where can I find my own, & other's, published packages? +### Where can I find my (and others') published packages? diff --git a/docs/package.json b/docs/package.json index 7950fed4eb1a9..74c575337f736 100644 --- a/docs/package.json +++ b/docs/package.json @@ -22,7 +22,7 @@ "@npmcli/eslint-config": "^3.1.0", "@npmcli/fs": "^2.1.0", "@npmcli/promise-spawn": "^3.0.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "cmark-gfm": "^0.9.0", "jsdom": "^18.1.0", "marked-man": "^0.7.0", @@ -59,6 +59,6 @@ "ciVersions": [ "16" ], - "version": "4.0.0" + "version": "4.1.2" } } diff --git a/lib/commands/access.js b/lib/commands/access.js index 3621861537171..d5ac5bb2f008e 100644 --- a/lib/commands/access.js +++ b/lib/commands/access.js @@ -1,223 +1,221 @@ const path = require('path') -const libaccess = require('libnpmaccess') +const libnpmaccess = require('libnpmaccess') +const npa = require('npm-package-arg') const readPackageJson = require('read-package-json-fast') +const localeCompare = require('@isaacs/string-locale-compare')('en') const otplease = require('../utils/otplease.js') const getIdentity = require('../utils/get-identity.js') -const log = require('../utils/log-shim.js') const BaseCommand = require('../base-command.js') -const subcommands = [ - 'public', - 'restricted', +const commands = [ + 'get', 'grant', + 'list', 'revoke', - 'ls-packages', - 'ls-collaborators', - 'edit', - '2fa-required', - '2fa-not-required', + 'set', ] -const deprecated = [ - '2fa-not-required', - '2fa-required', - 'ls-collaborators', - 'ls-packages', - 'public', - 'restricted', +const setCommands = [ + 'status=public', + 'status=private', + 'mfa=none', + 'mfa=publish', + 'mfa=automation', + '2fa=none', + '2fa=publish', + '2fa=automation', ] class Access extends BaseCommand { static description = 'Set access level on published packages' static name = 'access' static params = [ - 'registry', + 'json', 'otp', + 'registry', ] static ignoreImplicitWorkspace = true static usage = [ - 'public []', - 'restricted []', + 'list packages [|| []', + 'list collaborators [ []]', + 'get status []', + 'set status=public|private []', + 'set mfa=none|publish|automation []', 'grant []', 'revoke []', - '2fa-required []', - '2fa-not-required []', - 'ls-packages [||]', - 'ls-collaborators [ []]', - 'edit []', ] async completion (opts) { const argv = opts.conf.argv.remain if (argv.length === 2) { - return subcommands + return commands } switch (argv[2]) { case 'grant': - if (argv.length === 3) { - return ['read-only', 'read-write'] - } else { - return [] - } - - case 'public': - case 'restricted': - case 'ls-packages': - case 'ls-collaborators': - case 'edit': - case '2fa-required': - case '2fa-not-required': + return ['read-only', 'read-write'] case 'revoke': return [] + case 'list': + case 'ls': + return ['packages', 'collaborators'] + case 'get': + return ['status'] + case 'set': + return setCommands default: throw new Error(argv[2] + ' not recognized') } } - async exec ([cmd, ...args]) { + async exec ([cmd, subcmd, ...args]) { if (!cmd) { - throw this.usageError('Subcommand is required.') + throw this.usageError() } - - if (!subcommands.includes(cmd) || !this[cmd]) { - throw this.usageError(`${cmd} is not a recognized subcommand.`) + if (!commands.includes(cmd)) { + throw this.usageError(`${cmd} is not a valid access command`) } - - if (deprecated.includes(cmd)) { - log.warn('access', `${cmd} subcommand will be removed in the next version of npm`) + // All commands take at least one more parameter so we can do this check up front + if (!subcmd) { + throw this.usageError() } - return this[cmd](args, { - ...this.npm.flatOptions, - }) - } - - public ([pkg], opts) { - return this.modifyPackage(pkg, opts, libaccess.public) - } - - restricted ([pkg], opts) { - return this.modifyPackage(pkg, opts, libaccess.restricted) - } - - async grant ([perms, scopeteam, pkg], opts) { - if (!perms || (perms !== 'read-only' && perms !== 'read-write')) { - throw this.usageError('First argument must be either `read-only` or `read-write`.') - } - - if (!scopeteam) { - throw this.usageError('`` argument is required.') - } - - const [, scope, team] = scopeteam.match(/^@?([^:]+):(.*)$/) || [] - - if (!scope && !team) { - throw this.usageError( - 'Second argument used incorrect format.\n' + - 'Example: @example:developers' - ) - } - - return this.modifyPackage(pkg, opts, (pkgName, opts) => - libaccess.grant(pkgName, scopeteam, perms, opts), false) - } - - async revoke ([scopeteam, pkg], opts) { - if (!scopeteam) { - throw this.usageError('`` argument is required.') - } - - const [, scope, team] = scopeteam.match(/^@?([^:]+):(.*)$/) || [] - - if (!scope || !team) { - throw this.usageError( - 'First argument used incorrect format.\n' + - 'Example: @example:developers' - ) + switch (cmd) { + case 'grant': + if (!['read-only', 'read-write'].includes(subcmd)) { + throw this.usageError('grant must be either `read-only` or `read-write`') + } + if (!args[0]) { + throw this.usageError('`` argument is required') + } + return this.#grant(subcmd, args[0], args[1]) + case 'revoke': + return this.#revoke(subcmd, args[0]) + case 'list': + case 'ls': + if (subcmd === 'packages') { + return this.#listPackages(args[0], args[1]) + } + if (subcmd === 'collaborators') { + return this.#listCollaborators(args[0], args[1]) + } + throw this.usageError(`list ${subcmd} is not a valid access command`) + case 'get': + if (subcmd !== 'status') { + throw this.usageError(`get ${subcmd} is not a valid access command`) + } + return this.#getStatus(args[0]) + case 'set': + if (!setCommands.includes(subcmd)) { + throw this.usageError(`set ${subcmd} is not a valid access command`) + } + return this.#set(subcmd, args[0]) } - - return this.modifyPackage(pkg, opts, (pkgName, opts) => - libaccess.revoke(pkgName, scopeteam, opts)) - } - - get ['2fa-required'] () { - return this.tfaRequired - } - - tfaRequired ([pkg], opts) { - return this.modifyPackage(pkg, opts, libaccess.tfaRequired, false) } - get ['2fa-not-required'] () { - return this.tfaNotRequired + async #grant (permissions, scope, pkg) { + await libnpmaccess.setPermissions(scope, pkg, permissions) } - tfaNotRequired ([pkg], opts) { - return this.modifyPackage(pkg, opts, libaccess.tfaNotRequired, false) + async #revoke (scope, pkg) { + await libnpmaccess.removePermissions(scope, pkg) } - get ['ls-packages'] () { - return this.lsPackages - } - - async lsPackages ([owner], opts) { + async #listPackages (owner, pkg) { if (!owner) { - owner = await getIdentity(this.npm, opts) + owner = await getIdentity(this.npm, this.npm.flatOptions) } - - const pkgs = await libaccess.lsPackages(owner, opts) - - // TODO - print these out nicely (breaking change) - this.npm.output(JSON.stringify(pkgs, null, 2)) + const pkgs = await libnpmaccess.getPackages(owner, this.npm.flatOptions) + this.#output(pkgs, pkg) } - get ['ls-collaborators'] () { - return this.lsCollaborators + async #listCollaborators (pkg, user) { + const pkgName = await this.#getPackage(pkg, false) + const collabs = await libnpmaccess.getCollaborators(pkgName, this.npm.flatOptions) + this.#output(collabs, user) } - async lsCollaborators ([pkg, usr], opts) { - const pkgName = await this.getPackage(pkg, false) - const collabs = await libaccess.lsCollaborators(pkgName, usr, opts) + async #getStatus (pkg) { + const pkgName = await this.#getPackage(pkg, false) + const visibility = await libnpmaccess.getVisibility(pkgName, this.npm.flatOptions) + this.#output({ [pkgName]: visibility.public ? 'public' : 'private' }) + } - // TODO - print these out nicely (breaking change) - this.npm.output(JSON.stringify(collabs, null, 2)) + async #set (subcmd, pkg) { + const [subkey, subval] = subcmd.split('=') + switch (subkey) { + case 'mfa': + case '2fa': + return this.#setMfa(pkg, subval) + case 'status': + return this.#setStatus(pkg, subval) + } } - async edit () { - throw new Error('edit subcommand is not implemented') + async #setMfa (pkg, level) { + const pkgName = await this.#getPackage(pkg, false) + await otplease(this.npm, this.npm.flatOptions, (opts) => { + return libnpmaccess.setMfa(pkgName, level, opts) + }) } - modifyPackage (pkg, opts, fn, requireScope = true) { - return this.getPackage(pkg, requireScope) - .then(pkgName => otplease(this.npm, opts, opts => fn(pkgName, opts))) + async #setStatus (pkg, status) { + // only scoped packages can have their access changed + const pkgName = await this.#getPackage(pkg, true) + if (status === 'private') { + status = 'restricted' + } + await otplease(this.npm, this.npm.flatOptions, (opts) => { + return libnpmaccess.setAccess(pkgName, status, opts) + }) + return this.#getStatus(pkgName) } - async getPackage (name, requireScope) { - if (name && name.trim()) { - return name.trim() - } else { + async #getPackage (name, requireScope) { + if (!name) { try { const pkg = await readPackageJson(path.resolve(this.npm.prefix, 'package.json')) name = pkg.name } catch (err) { if (err.code === 'ENOENT') { - throw new Error( - 'no package name passed to command and no package.json found' - ) + throw Object.assign(new Error('no package name given and no package.json found'), { + code: 'ENOENT', + }) } else { throw err } } + } - if (requireScope && !name.match(/^@[^/]+\/.*$/)) { - throw this.usageError('This command is only available for scoped packages.') - } else { - return name + const spec = npa(name) + if (requireScope && !spec.scope) { + throw this.usageError('This command is only available for scoped packages.') + } + return name + } + + #output (items, limiter) { + const output = {} + const lookup = { + __proto__: null, + read: 'read-only', + write: 'read-write', + } + for (const item in items) { + const val = items[item] + output[item] = lookup[val] || val + } + if (this.npm.config.get('json')) { + this.npm.output(JSON.stringify(output, null, 2)) + } else { + for (const item of Object.keys(output).sort(localeCompare)) { + if (!limiter || limiter === item) { + this.npm.output(`${item}: ${output[item]}`) + } } } } diff --git a/lib/commands/deprecate.js b/lib/commands/deprecate.js index 068bfdbcec717..c41546eb1b85e 100644 --- a/lib/commands/deprecate.js +++ b/lib/commands/deprecate.js @@ -23,10 +23,10 @@ class Deprecate extends BaseCommand { } const username = await getIdentity(this.npm, this.npm.flatOptions) - const packages = await libaccess.lsPackages(username, this.npm.flatOptions) + const packages = await libaccess.getPackages(username, this.npm.flatOptions) return Object.keys(packages) .filter((name) => - packages[name] === 'read-write' && + packages[name] === 'write' && (opts.conf.argv.remain.length === 0 || name.startsWith(opts.conf.argv.remain[0]))) } diff --git a/lib/commands/publish.js b/lib/commands/publish.js index 3d17866a684a4..64b6dfc513c95 100644 --- a/lib/commands/publish.js +++ b/lib/commands/publish.js @@ -114,7 +114,10 @@ class Publish extends BaseCommand { } } - log.notice('', `Publishing to ${outputRegistry}${dryRun ? ' (dry-run)' : ''}`) + log.notice( + '', + `Publishing to ${outputRegistry} with tag ${defaultTag}${dryRun ? ' (dry-run)' : ''}` + ) if (!dryRun) { await otplease(this.npm, opts, opts => libpub(manifest, tarballData, opts)) diff --git a/lib/commands/unpublish.js b/lib/commands/unpublish.js index 0e5ef3dc5e91d..968bcf8018958 100644 --- a/lib/commands/unpublish.js +++ b/lib/commands/unpublish.js @@ -42,11 +42,11 @@ class Unpublish extends BaseCommand { return [] } - const access = await libaccess.lsPackages(username, opts) + const access = await libaccess.getPackages(username, opts) // do a bit of filtering at this point, so that we don't need // to fetch versions for more than one thing, but also don't // accidentally unpublish a whole project - let pkgs = Object.keys(access || {}) + let pkgs = Object.keys(access) if (!partialWord || !pkgs.length) { return pkgs } diff --git a/package-lock.json b/package-lock.json index 839a21597390d..0cb2bb110377a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "npm", - "version": "9.0.0-pre.0", + "version": "9.0.0-pre.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "npm", - "version": "9.0.0-pre.0", + "version": "9.0.0-pre.1", "bundleDependencies": [ "@isaacs/string-locale-compare", "@npmcli/arborist", @@ -91,7 +91,7 @@ ], "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^6.0.0-pre.0", + "@npmcli/arborist": "^6.0.0-pre.1", "@npmcli/ci-detect": "^2.0.0", "@npmcli/config": "^4.2.1", "@npmcli/fs": "^2.1.0", @@ -114,10 +114,10 @@ "init-package-json": "^3.0.2", "is-cidr": "^4.0.2", "json-parse-even-better-errors": "^2.3.1", - "libnpmaccess": "^7.0.0-pre.0", + "libnpmaccess": "^7.0.0-pre.1", "libnpmdiff": "^5.0.0-pre.0", - "libnpmexec": "^5.0.0-pre.0", - "libnpmfund": "^4.0.0-pre.0", + "libnpmexec": "^5.0.0-pre.1", + "libnpmfund": "^4.0.0-pre.1", "libnpmhook": "^9.0.0-pre.0", "libnpmorg": "^5.0.0-pre.0", "libnpmpack": "^5.0.0-pre.0", @@ -169,7 +169,7 @@ "devDependencies": { "@npmcli/eslint-config": "^3.1.0", "@npmcli/promise-spawn": "^3.0.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "fs-minipass": "^2.1.0", "licensee": "^8.2.0", "minimatch": "^5.1.0", @@ -189,7 +189,7 @@ "@npmcli/eslint-config": "^3.1.0", "@npmcli/fs": "^2.1.0", "@npmcli/promise-spawn": "^3.0.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "cmark-gfm": "^0.9.0", "jsdom": "^18.1.0", "marked-man": "^0.7.0", @@ -2362,15 +2362,16 @@ } }, "node_modules/@npmcli/template-oss": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/template-oss/-/template-oss-4.0.0.tgz", - "integrity": "sha512-4e4oYjMN3d1AN2PhDNZGAaIRAi/3p76jDdWREsuGZ8Du084QAcazl+uV2CKbDwsalOPeS46stDJwuuFG9riFzQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/template-oss/-/template-oss-4.1.2.tgz", + "integrity": "sha512-BS9YZeRtLsZ+lnCcQV3tZQa25K+MbUz/MgX5ZFzRmU+gTgCGthfajXZ7r8jJTjSiNgrElS0Ty/+x6Ds6B7oFdw==", "dev": true, "hasInstallScript": true, "dependencies": { "@actions/core": "^1.9.1", "@commitlint/cli": "^17.1.1", "@commitlint/config-conventional": "^17.1.0", + "@isaacs/string-locale-compare": "^1.1.0", "@npmcli/fs": "^2.0.1", "@npmcli/git": "^3.0.0", "@npmcli/map-workspaces": "^2.0.2", @@ -13843,7 +13844,7 @@ "devDependencies": { "@npmcli/eslint-config": "^3.1.0", "@npmcli/promise-spawn": "^3.0.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "minify-registry-metadata": "^2.2.0", "rimraf": "^3.0.2", "tap": "^16.0.1", @@ -13855,7 +13856,7 @@ }, "workspaces/arborist": { "name": "@npmcli/arborist", - "version": "6.0.0-pre.0", + "version": "6.0.0-pre.1", "license": "ISC", "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", @@ -13900,7 +13901,7 @@ }, "devDependencies": { "@npmcli/eslint-config": "^3.1.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "benchmark": "^2.1.4", "chalk": "^4.1.0", "minify-registry-metadata": "^2.1.0", @@ -13913,17 +13914,15 @@ } }, "workspaces/libnpmaccess": { - "version": "7.0.0-pre.0", + "version": "7.0.0-pre.1", "license": "ISC", "dependencies": { - "aproba": "^2.0.0", - "minipass": "^3.1.1", "npm-package-arg": "^9.0.1", "npm-registry-fetch": "^13.0.0" }, "devDependencies": { "@npmcli/eslint-config": "^3.1.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "nock": "^13.2.4", "tap": "^16.0.1" }, @@ -13946,7 +13945,7 @@ }, "devDependencies": { "@npmcli/eslint-config": "^3.1.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "tap": "^16.0.1" }, "engines": { @@ -13954,10 +13953,10 @@ } }, "workspaces/libnpmexec": { - "version": "5.0.0-pre.0", + "version": "5.0.0-pre.1", "license": "ISC", "dependencies": { - "@npmcli/arborist": "^6.0.0-pre.0", + "@npmcli/arborist": "^6.0.0-pre.1", "@npmcli/ci-detect": "^2.0.0", "@npmcli/fs": "^2.1.1", "@npmcli/run-script": "^4.2.0", @@ -13974,7 +13973,7 @@ }, "devDependencies": { "@npmcli/eslint-config": "^3.1.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "bin-links": "^3.0.3", "minify-registry-metadata": "^2.2.0", "mkdirp": "^1.0.4", @@ -13985,14 +13984,14 @@ } }, "workspaces/libnpmfund": { - "version": "4.0.0-pre.0", + "version": "4.0.0-pre.1", "license": "ISC", "dependencies": { - "@npmcli/arborist": "^6.0.0-pre.0" + "@npmcli/arborist": "^6.0.0-pre.1" }, "devDependencies": { "@npmcli/eslint-config": "^3.1.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "tap": "^16.0.1" }, "engines": { @@ -14008,7 +14007,7 @@ }, "devDependencies": { "@npmcli/eslint-config": "^3.1.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "nock": "^13.2.4", "tap": "^16.0.1" }, @@ -14025,7 +14024,7 @@ }, "devDependencies": { "@npmcli/eslint-config": "^3.1.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "minipass": "^3.1.1", "nock": "^13.2.4", "tap": "^16.0.1" @@ -14044,7 +14043,7 @@ }, "devDependencies": { "@npmcli/eslint-config": "^3.1.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "nock": "^13.0.7", "tap": "^16.0.1" }, @@ -14064,7 +14063,7 @@ }, "devDependencies": { "@npmcli/eslint-config": "^3.1.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "libnpmpack": "^5.0.0-pre.0", "lodash.clonedeep": "^4.5.0", "nock": "^13.2.4", @@ -14082,7 +14081,7 @@ }, "devDependencies": { "@npmcli/eslint-config": "^3.1.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "nock": "^13.2.4", "tap": "^16.0.1" }, @@ -14099,7 +14098,7 @@ }, "devDependencies": { "@npmcli/eslint-config": "^3.1.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "nock": "^13.2.4", "tap": "^16.0.1" }, @@ -14119,7 +14118,7 @@ }, "devDependencies": { "@npmcli/eslint-config": "^3.1.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "require-inject": "^1.4.4", "tap": "^16.0.1" }, diff --git a/package.json b/package.json index 703868e646b01..048ec7ee9629b 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "9.0.0-pre.0", + "version": "9.0.0-pre.1", "name": "npm", "description": "a package manager for JavaScript", "workspaces": [ @@ -56,7 +56,7 @@ }, "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^6.0.0-pre.0", + "@npmcli/arborist": "^6.0.0-pre.1", "@npmcli/ci-detect": "^2.0.0", "@npmcli/config": "^4.2.1", "@npmcli/fs": "^2.1.0", @@ -79,10 +79,10 @@ "init-package-json": "^3.0.2", "is-cidr": "^4.0.2", "json-parse-even-better-errors": "^2.3.1", - "libnpmaccess": "^7.0.0-pre.0", + "libnpmaccess": "^7.0.0-pre.1", "libnpmdiff": "^5.0.0-pre.0", - "libnpmexec": "^5.0.0-pre.0", - "libnpmfund": "^4.0.0-pre.0", + "libnpmexec": "^5.0.0-pre.1", + "libnpmfund": "^4.0.0-pre.1", "libnpmhook": "^9.0.0-pre.0", "libnpmorg": "^5.0.0-pre.0", "libnpmpack": "^5.0.0-pre.0", @@ -206,7 +206,7 @@ "devDependencies": { "@npmcli/eslint-config": "^3.1.0", "@npmcli/promise-spawn": "^3.0.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "fs-minipass": "^2.1.0", "licensee": "^8.2.0", "minimatch": "^5.1.0", @@ -251,7 +251,7 @@ "templateOSS": { "rootRepo": false, "rootModule": false, - "version": "4.0.0", + "version": "4.1.2", "releaseTest": "release.yml" }, "license": "Artistic-2.0", diff --git a/release-please-config.json b/release-please-config.json index 87ff3b2cac37b..808e4a7a084de 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -1,8 +1,7 @@ { "bootstrap-sha": "141faf0c19eae382d0e19833129f5545fc2355c8", "plugins": [ - "node-workspace", - "workspace-deps" + "node-workspace" ], "changelog-sections": [ { diff --git a/smoke-tests/package.json b/smoke-tests/package.json index 1c3d787723cf6..73ffde9910e39 100644 --- a/smoke-tests/package.json +++ b/smoke-tests/package.json @@ -20,7 +20,7 @@ "devDependencies": { "@npmcli/eslint-config": "^3.1.0", "@npmcli/promise-spawn": "^3.0.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "minify-registry-metadata": "^2.2.0", "rimraf": "^3.0.2", "tap": "^16.0.1", @@ -30,7 +30,7 @@ "license": "ISC", "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "4.0.0", + "version": "4.1.2", "workspaceRepo": false }, "tap": { diff --git a/tap-snapshots/test/lib/commands/completion.js.test.cjs b/tap-snapshots/test/lib/commands/completion.js.test.cjs index fb4c53a0205fb..85a883bd58b26 100644 --- a/tap-snapshots/test/lib/commands/completion.js.test.cjs +++ b/tap-snapshots/test/lib/commands/completion.js.test.cjs @@ -172,11 +172,7 @@ Array [ ` exports[`test/lib/commands/completion.js TAP completion filtered subcommands > filtered subcommands 1`] = ` -Array [ - Array [ - "public", - ], -] +Array [] ` exports[`test/lib/commands/completion.js TAP completion flags > flags 1`] = ` @@ -220,15 +216,11 @@ exports[`test/lib/commands/completion.js TAP completion subcommand completion > Array [ Array [ String( - public - restricted + get grant + list revoke - ls-packages - ls-collaborators - edit - 2fa-required - 2fa-not-required + set ), ], ] diff --git a/tap-snapshots/test/lib/commands/publish.js.test.cjs b/tap-snapshots/test/lib/commands/publish.js.test.cjs index d85a1164e22bf..3b215960fa37e 100644 --- a/tap-snapshots/test/lib/commands/publish.js.test.cjs +++ b/tap-snapshots/test/lib/commands/publish.js.test.cjs @@ -51,7 +51,7 @@ Array [ ], Array [ "", - "Publishing to https://registry.npmjs.org/ (dry-run)", + "Publishing to https://registry.npmjs.org/ with tag latest (dry-run)", ], ] ` @@ -72,7 +72,7 @@ exports[`test/lib/commands/publish.js TAP json > must match snapshot 1`] = ` Array [ Array [ "", - "Publishing to https://registry.npmjs.org/", + "Publishing to https://registry.npmjs.org/ with tag latest", ], ] ` @@ -165,7 +165,7 @@ Array [ ], Array [ "", - "Publishing to https://registry.npmjs.org/", + "Publishing to https://registry.npmjs.org/ with tag latest", ], ] ` diff --git a/tap-snapshots/test/lib/load-all-commands.js.test.cjs b/tap-snapshots/test/lib/load-all-commands.js.test.cjs index d4a340431792a..038121be95435 100644 --- a/tap-snapshots/test/lib/load-all-commands.js.test.cjs +++ b/tap-snapshots/test/lib/load-all-commands.js.test.cjs @@ -9,18 +9,16 @@ exports[`test/lib/load-all-commands.js TAP load each command access > must match Set access level on published packages Usage: -npm access public [] -npm access restricted [] +npm access list packages [|| [] +npm access list collaborators [ []] +npm access get status [] +npm access set status=public|private [] +npm access set mfa=none|publish|automation [] npm access grant [] npm access revoke [] -npm access 2fa-required [] -npm access 2fa-not-required [] -npm access ls-packages [||] -npm access ls-collaborators [ []] -npm access edit [] Options: -[--registry ] [--otp ] +[--json] [--otp ] [--registry ] Run "npm help access" for more info ` diff --git a/tap-snapshots/test/lib/npm.js.test.cjs b/tap-snapshots/test/lib/npm.js.test.cjs index 68adb55516c72..1c3dbe39bd4ae 100644 --- a/tap-snapshots/test/lib/npm.js.test.cjs +++ b/tap-snapshots/test/lib/npm.js.test.cjs @@ -168,18 +168,16 @@ All commands: access Set access level on published packages Usage: - npm access public [] - npm access restricted [] + npm access list packages [|| [] + npm access list collaborators [ []] + npm access get status [] + npm access set status=public|private [] + npm access set mfa=none|publish|automation [] npm access grant [] npm access revoke [] - npm access 2fa-required [] - npm access 2fa-not-required [] - npm access ls-packages [||] - npm access ls-collaborators [ []] - npm access edit [] Options: - [--registry ] [--otp ] + [--json] [--otp ] [--registry ] Run "npm help access" for more info diff --git a/test/fixtures/mock-registry.js b/test/fixtures/mock-registry.js index 8fb5a055ff2d7..65d4759627aa6 100644 --- a/test/fixtures/mock-registry.js +++ b/test/fixtures/mock-registry.js @@ -67,21 +67,19 @@ class MockRegistry { } } - access ({ spec, access, publishRequires2fa }) { - const body = {} - if (access !== undefined) { - body.access = access - } - if (publishRequires2fa !== undefined) { - body.publish_requires_tfa = publishRequires2fa - } + setAccess ({ spec, body = {} }) { this.nock = this.nock.post( - `/-/package/${encodeURIComponent(spec)}/access`, + `/-/package/${npa(spec).escapedName}/access`, body ).reply(200) } - grant ({ spec, team, permissions }) { + getVisibility ({ spec, visibility }) { + this.nock = this.nock.get(`/-/package/${npa(spec).escapedName}/visibility`) + .reply(200, visibility) + } + + setPermissions ({ spec, team, permissions }) { if (team.startsWith('@')) { team = team.slice(1) } @@ -92,7 +90,7 @@ class MockRegistry { ).reply(200) } - revoke ({ spec, team }) { + removePermissions ({ spec, team }) { if (team.startsWith('@')) { team = team.slice(1) } @@ -141,27 +139,22 @@ class MockRegistry { } // team can be a team or a username - lsPackages ({ team, packages = {}, times = 1 }) { + getPackages ({ team, packages = {}, times = 1 }) { if (team.startsWith('@')) { team = team.slice(1) } - const [scope, teamName] = team.split(':') + const [scope, teamName] = team.split(':').map(encodeURIComponent) let uri if (teamName) { - uri = `/-/team/${encodeURIComponent(scope)}/${encodeURIComponent(teamName)}/package` + uri = `/-/team/${scope}/${teamName}/package` } else { - uri = `/-/org/${encodeURIComponent(scope)}/package` + uri = `/-/org/${scope}/package` } - this.nock = this.nock.get(uri).query({ format: 'cli' }).times(times).reply(200, packages) + this.nock = this.nock.get(uri).times(times).reply(200, packages) } - lsCollaborators ({ spec, user, collaborators = {} }) { - const query = { format: 'cli' } - if (user) { - query.user = user - } - this.nock = this.nock.get(`/-/package/${encodeURIComponent(spec)}/collaborators`) - .query(query) + getCollaborators ({ spec, collaborators = {} }) { + this.nock = this.nock.get(`/-/package/${npa(spec).escapedName}/collaborators`) .reply(200, collaborators) } diff --git a/test/lib/commands/access.js b/test/lib/commands/access.js index aa748b10681df..ae26f4c9332dc 100644 --- a/test/lib/commands/access.js +++ b/test/lib/commands/access.js @@ -3,6 +3,7 @@ const t = require('tap') const { load: loadMockNpm } = require('../../fixtures/mock-npm.js') const MockRegistry = require('../../fixtures/mock-registry.js') +const token = 'test-auth-token' const auth = { '//registry.npmjs.org/:_authToken': 'test-auth-token' } t.test('completion', async t => { @@ -13,20 +14,21 @@ t.test('completion', async t => { t.resolves(res, expect, argv.join(' ')) } - testComp(['npm', 'access'], [ - 'public', 'restricted', 'grant', 'revoke', 'ls-packages', - 'ls-collaborators', 'edit', '2fa-required', '2fa-not-required', + testComp(['npm', 'access'], ['list', 'get', 'set', 'grant', 'revoke']) + testComp(['npm', 'access', 'list'], ['packages', 'collaborators']) + testComp(['npm', 'access', 'ls'], ['packages', 'collaborators']) + testComp(['npm', 'access', 'get'], ['status']) + testComp(['npm', 'access', 'set'], [ + 'status=public', + 'status=private', + 'mfa=none', + 'mfa=publish', + 'mfa=automation', + '2fa=none', + '2fa=publish', + '2fa=automation', ]) testComp(['npm', 'access', 'grant'], ['read-only', 'read-write']) - testComp(['npm', 'access', 'grant', 'read-only'], []) - testComp(['npm', 'access', 'public'], []) - testComp(['npm', 'access', 'restricted'], []) - testComp(['npm', 'access', 'revoke'], []) - testComp(['npm', 'access', 'ls-packages'], []) - testComp(['npm', 'access', 'ls-collaborators'], []) - testComp(['npm', 'access', 'edit'], []) - testComp(['npm', 'access', '2fa-required'], []) - testComp(['npm', 'access', '2fa-not-required'], []) testComp(['npm', 'access', 'revoke'], []) await t.rejects( @@ -35,406 +37,337 @@ t.test('completion', async t => { ) }) -t.test('subcommand required', async t => { +t.test('command required', async t => { const { npm } = await loadMockNpm(t) - const access = await npm.cmd('access') - await t.rejects( - npm.exec('access', []), - access.usageError('Subcommand is required.') - ) + await t.rejects(npm.exec('access', []), { code: 'EUSAGE' }) }) -t.test('unrecognized subcommand', async t => { +t.test('unrecognized command', async t => { const { npm } = await loadMockNpm(t) await t.rejects( - npm.exec('access', ['blerg']), - /blerg is not a recognized subcommand/, - 'should throw EUSAGE on missing subcommand' - ) + npm.exec('access', ['blerg']), { code: 'EUSAGE' }) }) -t.test('edit', async t => { +t.test('subcommand required', async t => { const { npm } = await loadMockNpm(t) - await t.rejects( - npm.exec('access', ['edit', '@scoped/another']), - /edit subcommand is not implemented/, - 'should throw not implemented yet error' - ) + await t.rejects(npm.exec('access', ['get']), { code: 'EUSAGE' }) }) -t.test('access public on unscoped package', async t => { - const { npm } = await loadMockNpm(t, { - prefixDir: { - 'package.json': JSON.stringify({ - name: 'npm-access-public-pkg', - }), - }, - }) - await t.rejects( - npm.exec('access', ['public']), - /This command is only available for scoped packages/, - 'should throw scoped-restricted error' - ) +t.test('unrecognized subcommand', async t => { + const { npm } = await loadMockNpm(t) + await t.rejects(npm.exec('access', ['list', 'blerg']), { code: 'EUSAGE' }) }) -t.test('access public on scoped package', async t => { - const name = '@scoped/npm-access-public-pkg' - const { npm, joinedOutput, logs } = await loadMockNpm(t, { - config: { - ...auth, - }, - prefixDir: { - 'package.json': JSON.stringify({ name }), - }, - }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - authorization: 'test-auth-token', +t.test('grant', t => { + t.test('invalid permissions', async t => { + const { npm } = await loadMockNpm(t) + await t.rejects(npm.exec('access', ['grant', 'other']), { code: 'EUSAGE' }) }) - registry.access({ spec: name, access: 'public' }) - await npm.exec('access', ['public']) - t.match(logs.warn[0], ['access', 'public subcommand will be removed in the next version of npm']) - t.equal(joinedOutput(), '') -}) -t.test('access public on missing package.json', async t => { - const { npm } = await loadMockNpm(t) - await t.rejects( - npm.exec('access', ['public']), - /no package name passed to command and no package.json found/, - 'should throw no package.json found error' - ) -}) + t.test('no permissions', async t => { + const { npm } = await loadMockNpm(t) + await t.rejects(npm.exec('access', ['grant', 'read-only']), { code: 'EUSAGE' }) + }) -t.test('access public on invalid package.json', async t => { - const { npm } = await loadMockNpm(t, { - prefixDir: { - 'package.json': '{\n', - node_modules: {}, - }, + t.test('read-only', async t => { + const { npm } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + const permissions = 'read-only' + registry.setPermissions({ spec: '@npmcli/test-package', team: '@npm:test-team', permissions }) + await npm.exec('access', ['grant', permissions, '@npm:test-team', '@npmcli/test-package']) }) - await t.rejects( - npm.exec('access', ['public']), - { code: 'EJSONPARSE' }, - 'should throw failed to parse package.json' - ) + t.end() }) -t.test('access restricted on unscoped package', async t => { - const { npm } = await loadMockNpm(t, { - prefixDir: { - 'package.json': JSON.stringify({ - name: 'npm-access-restricted-pkg', - }), - }, +t.test('revoke', t => { + t.test('success', async t => { + const { npm } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.removePermissions({ spec: '@npmcli/test-package', team: '@npm:test-team' }) + await npm.exec('access', ['revoke', '@npm:test-team', '@npmcli/test-package']) }) - await t.rejects( - npm.exec('access', ['public']), - /This command is only available for scoped packages/, - 'should throw scoped-restricted error' - ) + t.end() }) -t.test('access restricted on scoped package', async t => { - const name = '@scoped/npm-access-restricted-pkg' - const { npm, joinedOutput, logs } = await loadMockNpm(t, { - config: { - ...auth, - }, - prefixDir: { - 'package.json': JSON.stringify({ name }), - }, +t.test('list', t => { + const packages = { + '@npmcli/test-package': 'read', + '@npmcli/other-package': 'write', + } + const collaborators = { + npm: 'write', + github: 'read', + } + t.test('invalid subcommand', async t => { + const { npm } = await loadMockNpm(t) + await t.rejects(npm.exec('access', ['list', 'other'], { code: 'EUSAGE' })) }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - authorization: 'test-auth-token', + + t.test('packages explicit user', async t => { + const { npm, outputs } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.getPackages({ team: '@npm:test-team', packages }) + await npm.exec('access', ['list', 'packages', '@npm:test-team']) + t.same(outputs, [ + ['@npmcli/other-package: read-write'], + ['@npmcli/test-package: read-only'], + ]) }) - registry.access({ spec: name, access: 'restricted' }) - await npm.exec('access', ['restricted']) - t.match(logs.warn[0], - ['access', 'restricted subcommand will be removed in the next version of npm'] - ) - t.equal(joinedOutput(), '') -}) -t.test('access restricted on missing package.json', async t => { - const { npm } = await loadMockNpm(t) - await t.rejects( - npm.exec('access', ['restricted']), - /no package name passed to command and no package.json found/, - 'should throw no package.json found error' - ) -}) + t.test('packages infer user', async t => { + const { npm, outputs } = await loadMockNpm(t, { config: { ...auth } }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.whoami({ username: 'npm' }) + registry.getPackages({ team: 'npm', packages }) + await npm.exec('access', ['list', 'packages']) + t.same(outputs, [ + ['@npmcli/other-package: read-write'], + ['@npmcli/test-package: read-only'], + ]) + }) -t.test('access restricted on invalid package.json', async t => { - const { npm } = await loadMockNpm(t, { - prefixDir: { - 'package.json': '{\n', - node_modules: {}, - }, + t.test('packages json', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { config: { json: true } }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.getPackages({ team: '@npm:test-team', packages }) + await npm.exec('access', ['list', 'packages', '@npm:test-team']) + t.same(JSON.parse(joinedOutput()), { + '@npmcli/test-package': 'read-only', + '@npmcli/other-package': 'read-write', + }) }) - await t.rejects( - npm.exec('access', ['restricted']), - { code: 'EJSONPARSE' }, - 'should throw failed to parse package.json' - ) -}) -t.test('access grant read-only', async t => { - const { npm, joinedOutput } = await loadMockNpm(t, { - config: { - ...auth, - }, + t.test('collaborators explicit package', async t => { + const { npm, outputs } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.getCollaborators({ spec: '@npmcli/test-package', collaborators }) + await npm.exec('access', ['list', 'collaborators', '@npmcli/test-package']) + t.same(outputs, [ + ['github: read-only'], + ['npm: read-write'], + ]) }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - authorization: 'test-auth-token', + + t.test('collaborators user', async t => { + const { npm, outputs } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.getCollaborators({ spec: '@npmcli/test-package', collaborators }) + await npm.exec('access', ['list', 'collaborators', '@npmcli/test-package', 'npm']) + t.same(outputs, [ + ['npm: read-write'], + ]) }) - registry.grant({ spec: '@scoped/another', team: 'myorg:myteam', permissions: 'read-only' }) - await npm.exec('access', ['grant', 'read-only', 'myorg:myteam', '@scoped/another']) - t.equal(joinedOutput(), '') + t.end() }) -t.test('access grant read-write', async t => { - const { npm, joinedOutput } = await loadMockNpm(t, { - config: { - ...auth, - }, - }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - authorization: 'test-auth-token', +t.test('get', t => { + t.test('invalid subcommand', async t => { + const { npm } = await loadMockNpm(t) + await t.rejects(npm.exec('access', ['get', 'other'], { code: 'EUSAGE' })) }) - registry.grant({ spec: '@scoped/another', team: 'myorg:myteam', permissions: 'read-write' }) - await npm.exec('access', ['grant', 'read-write', 'myorg:myteam', '@scoped/another']) - t.equal(joinedOutput(), '') -}) -t.test('access grant current cwd', async t => { - const { npm, joinedOutput } = await loadMockNpm(t, { - config: { - ...auth, - }, - prefixDir: { - 'package.json': JSON.stringify({ - name: 'yargs', - }), - }, + t.test('status explicit package', async t => { + const { npm, outputs } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.getVisibility({ spec: '@npmcli/test-package', visibility: { public: true } }) + await npm.exec('access', ['get', 'status', '@npmcli/test-package']) + t.same(outputs, [['@npmcli/test-package: public']]) }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - authorization: 'test-auth-token', + t.test('status implicit package', async t => { + const { npm, outputs } = await loadMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ name: '@npmcli/test-package' }), + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.getVisibility({ spec: '@npmcli/test-package', visibility: { public: true } }) + await npm.exec('access', ['get', 'status']) + t.same(outputs, [['@npmcli/test-package: public']]) }) - registry.grant({ spec: 'yargs', team: 'myorg:myteam', permissions: 'read-write' }) - await npm.exec('access', ['grant', 'read-write', 'myorg:myteam']) - t.equal(joinedOutput(), '') -}) - -t.test('access grant others', async t => { - const { npm } = await loadMockNpm(t) - await t.rejects( - npm.exec('access', [ - 'grant', - 'rerere', - 'myorg:myteam', - '@scoped/another', - ]), - /First argument must be either `read-only` or `read-write`./, - 'should throw unrecognized argument error' - ) -}) - -t.test('access grant missing team args', async t => { - const { npm } = await loadMockNpm(t) - await t.rejects( - npm.exec('access', [ - 'grant', - 'read-only', - undefined, - '@scoped/another', - ]), - /`` argument is required./, - 'should throw missing argument error' - ) -}) - -t.test('access grant malformed team arg', async t => { - const { npm } = await loadMockNpm(t) - await t.rejects( - npm.exec('access', [ - 'grant', - 'read-only', - 'foo', - '@scoped/another', - ]), - /Second argument used incorrect format.\n/, - 'should throw malformed arg error' - ) -}) - -t.test('access 2fa-required', async t => { - const { npm, joinedOutput, logs } = await loadMockNpm(t, { - config: { - ...auth, - }, + t.test('status no package', async t => { + const { npm } = await loadMockNpm(t) + await t.rejects( + npm.exec('access', ['get', 'status']), + { code: 'ENOENT' } + ) }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - authorization: 'test-auth-token', + t.test('status invalid package', async t => { + const { npm } = await loadMockNpm(t, { + prefixDir: { 'package.json': '[not:valid_json}' }, + }) + await t.rejects( + npm.exec('access', ['get', 'status']), + { code: 'EJSONPARSE' } + ) }) - registry.access({ spec: '@scope/pkg', publishRequires2fa: true }) - await npm.exec('access', ['2fa-required', '@scope/pkg']) - t.match(logs.warn[0], - ['access', '2fa-required subcommand will be removed in the next version of npm'] - ) - t.equal(joinedOutput(), '') + t.test('status json', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { config: { json: true } }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.getVisibility({ spec: '@npmcli/test-package', visibility: { public: true } }) + await npm.exec('access', ['get', 'status', '@npmcli/test-package']) + t.same(JSON.parse(joinedOutput()), { '@npmcli/test-package': 'public' }) + }) + t.end() }) -t.test('access 2fa-not-required', async t => { - const { npm, joinedOutput, logs } = await loadMockNpm(t, { - config: { - ...auth, - }, +t.test('set', t => { + t.test('status=public', async t => { + const { npm, outputs } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.setAccess({ spec: '@npmcli/test-package', body: { access: 'public' } }) + registry.getVisibility({ spec: '@npmcli/test-package', visibility: { public: true } }) + await npm.exec('access', ['set', 'status=public', '@npmcli/test-package']) + t.same(outputs, [['@npmcli/test-package: public']]) }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - authorization: 'test-auth-token', + t.test('status=private', async t => { + const { npm, outputs } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.setAccess({ spec: '@npmcli/test-package', body: { access: 'restricted' } }) + registry.getVisibility({ spec: '@npmcli/test-package', visibility: { public: false } }) + await npm.exec('access', ['set', 'status=private', '@npmcli/test-package']) + t.same(outputs, [['@npmcli/test-package: private']]) }) - registry.access({ spec: '@scope/pkg', publishRequires2fa: false }) - await npm.exec('access', ['2fa-not-required', '@scope/pkg']) - t.match(logs.warn[0], - ['access', '2fa-not-required subcommand will be removed in the next version of npm'] - ) - t.equal(joinedOutput(), '') -}) - -t.test('access revoke', async t => { - const { npm, joinedOutput } = await loadMockNpm(t, { - config: { - ...auth, - }, + t.test('status=invalid', async t => { + const { npm } = await loadMockNpm(t) + await t.rejects( + npm.exec('access', ['set', 'status=invalid', '@npmcli/test-package']), + { code: 'EUSAGE' } + ) }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - authorization: 'test-auth-token', + t.test('status non scoped package', async t => { + const { npm } = await loadMockNpm(t) + await t.rejects( + npm.exec('access', ['set', 'status=public', 'npm']), + { code: 'EUSAGE' } + ) }) - registry.revoke({ spec: '@scoped/another', team: 'myorg:myteam' }) - await npm.exec('access', ['revoke', 'myorg:myteam', '@scoped/another']) - t.equal(joinedOutput(), '') -}) - -t.test('access revoke missing team args', async t => { - const { npm } = await loadMockNpm(t) - await t.rejects( - npm.exec('access', [ - 'revoke', - undefined, - '@scoped/another', - ]), - /`` argument is required./, - 'should throw missing argument error' - ) -}) - -t.test('access revoke malformed team arg', async t => { - const { npm } = await loadMockNpm(t) - await t.rejects( - npm.exec('access', [ - 'revoke', - 'foo', - '@scoped/another', - ]), - /First argument used incorrect format.\n/, - 'should throw malformed arg error' - ) -}) - -t.test('npm access ls-packages with no team', async t => { - const { npm, joinedOutput, logs } = await loadMockNpm(t, { - config: { - ...auth, - }, + t.test('mfa=none', async t => { + const { npm } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.setAccess({ spec: '@npmcli/test-package', + body: { + publish_requires_tfa: false, + } }) + await npm.exec('access', ['set', 'mfa=none', '@npmcli/test-package']) }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - authorization: 'test-auth-token', + t.test('mfa=publish', async t => { + const { npm } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.setAccess({ spec: '@npmcli/test-package', + body: { + publish_requires_tfa: true, + automation_token_overrides_tfa: false, + } }) + await npm.exec('access', ['set', 'mfa=publish', '@npmcli/test-package']) }) - const team = 'foo' - const packages = { 'test-package': 'read-write' } - registry.whoami({ username: team }) - registry.lsPackages({ team, packages }) - await npm.exec('access', ['ls-packages']) - t.match(logs.warn[0], - ['access', 'ls-packages subcommand will be removed in the next version of npm'] - ) - t.match(JSON.parse(joinedOutput()), packages) -}) - -t.test('access ls-packages on team', async t => { - const { npm, joinedOutput } = await loadMockNpm(t, { - config: { - ...auth, - }, + t.test('mfa=automation', async t => { + const { npm } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.setAccess({ spec: '@npmcli/test-package', + body: { + publish_requires_tfa: true, + automation_token_overrides_tfa: true, + } }) + await npm.exec('access', ['set', 'mfa=automation', '@npmcli/test-package']) }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - authorization: 'test-auth-token', + t.test('mfa=invalid', async t => { + const { npm } = await loadMockNpm(t) + await t.rejects( + npm.exec('access', ['set', 'mfa=invalid']), + { code: 'EUSAGE' } + ) }) - const team = 'myorg:myteam' - const packages = { 'test-package': 'read-write' } - registry.lsPackages({ team, packages }) - await npm.exec('access', ['ls-packages', 'myorg:myteam']) - t.match(JSON.parse(joinedOutput()), packages) -}) - -t.test('access ls-collaborators on current', async t => { - const { npm, joinedOutput, logs } = await loadMockNpm(t, { - config: { - ...auth, - }, - prefixDir: { - 'package.json': JSON.stringify({ - name: 'yargs', - }), - }, + t.test('2fa=none', async t => { + const { npm } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.setAccess({ spec: '@npmcli/test-package', + body: { + publish_requires_tfa: false, + } }) + await npm.exec('access', ['set', '2fa=none', '@npmcli/test-package']) }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - authorization: 'test-auth-token', + t.test('2fa=publish', async t => { + const { npm } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.setAccess({ spec: '@npmcli/test-package', + body: { + publish_requires_tfa: true, + automation_token_overrides_tfa: false, + } }) + await npm.exec('access', ['set', '2fa=publish', '@npmcli/test-package']) }) - const collaborators = { 'test-user': 'read-write' } - registry.lsCollaborators({ spec: 'yargs', collaborators }) - await npm.exec('access', ['ls-collaborators']) - t.match(logs.warn[0], - ['access', 'ls-collaborators subcommand will be removed in the next version of npm'] - ) - t.match(JSON.parse(joinedOutput()), collaborators) -}) - -t.test('access ls-collaborators on spec', async t => { - const { npm, joinedOutput } = await loadMockNpm(t, { - config: { - ...auth, - }, + t.test('2fa=automation', async t => { + const { npm } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.setAccess({ spec: '@npmcli/test-package', + body: { + publish_requires_tfa: true, + automation_token_overrides_tfa: true, + } }) + await npm.exec('access', ['set', '2fa=automation', '@npmcli/test-package']) }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - authorization: 'test-auth-token', + t.test('2fa=invalid', async t => { + const { npm } = await loadMockNpm(t) + await t.rejects( + npm.exec('access', ['set', '2fa=invalid']), + { code: 'EUSAGE' } + ) }) - const collaborators = { 'test-user': 'read-write' } - registry.lsCollaborators({ spec: 'yargs', collaborators }) - await npm.exec('access', ['ls-collaborators', 'yargs']) - t.match(JSON.parse(joinedOutput()), collaborators) + + t.end() }) diff --git a/test/lib/commands/deprecate.js b/test/lib/commands/deprecate.js index 3a610a703a2dc..57ed9c93ba0f4 100644 --- a/test/lib/commands/deprecate.js +++ b/test/lib/commands/deprecate.js @@ -32,7 +32,7 @@ t.test('completion', async t => { }) registry.whoami({ username: user, times: 4 }) - registry.lsPackages({ team: user, packages, times: 4 }) + registry.getPackages({ team: user, packages, times: 4 }) await Promise.all([ testComp([], ['foo', 'bar', 'baz']), testComp(['b'], ['bar', 'baz']), @@ -42,9 +42,12 @@ t.test('completion', async t => { await testComp(['foo', 'something'], []) - registry.whoami({ statusCode: 404, body: {} }) + registry.whoami({ responseCode: 401, body: {} }) - t.rejects(testComp([], []), { code: 'EINVALIDTYPE' }) + await t.rejects( + testComp([], []), + { code: 'E401' } + ) }) t.test('no args', async t => { diff --git a/test/lib/commands/unpublish.js b/test/lib/commands/unpublish.js index 28f93ea3e77a4..9efd2a147d42f 100644 --- a/test/lib/commands/unpublish.js +++ b/test/lib/commands/unpublish.js @@ -424,7 +424,7 @@ t.test('completion', async t => { }) await registry.package({ manifest, query: { write: true } }) registry.whoami({ username: user }) - registry.nock.get('/-/org/test-user/package?format=cli').reply(200, { [pkg]: 'write' }) + registry.getPackages({ team: user, packages: { [pkg]: 'write' } }) await testComp(t, { argv: ['npm', 'unpublish'], @@ -446,7 +446,7 @@ t.test('completion', async t => { manifest.versions = {} await registry.package({ manifest, query: { write: true } }) registry.whoami({ username: user }) - registry.nock.get('/-/org/test-user/package?format=cli').reply(200, { [pkg]: 'write' }) + registry.getPackages({ team: user, packages: { [pkg]: 'write' } }) await testComp(t, { argv: ['npm', 'unpublish'], @@ -465,11 +465,12 @@ t.test('completion', async t => { authorization: 'test-auth-token', }) registry.whoami({ username: user }) - registry.nock.get('/-/org/test-user/package?format=cli').reply(200, { - [pkg]: 'write', - [`${pkg}a`]: 'write', - [`${pkg}b`]: 'write', - }) + registry.getPackages({ team: user, + packages: { + [pkg]: 'write', + [`${pkg}a`]: 'write', + [`${pkg}b`]: 'write', + } }) await testComp(t, { argv: ['npm', 'unpublish'], @@ -489,7 +490,7 @@ t.test('completion', async t => { authorization: 'test-auth-token', }) registry.whoami({ username: user }) - registry.nock.get('/-/org/test-user/package?format=cli').reply(200, {}) + registry.getPackages({ team: user, packages: {} }) await testComp(t, { argv: ['npm', 'unpublish'], @@ -506,10 +507,11 @@ t.test('completion', async t => { authorization: 'test-auth-token', }) registry.whoami({ username: user }) - registry.nock.get('/-/org/test-user/package?format=cli').reply(200, { - [pkg]: 'write', - [`${pkg}a`]: 'write', - }) + registry.getPackages({ team: user, + packages: { + [pkg]: 'write', + [`${pkg}a`]: 'write', + } }) await testComp(t, { argv: ['npm', 'unpublish'], @@ -519,23 +521,6 @@ t.test('completion', async t => { }) }) - t.test('no pkg names retrieved from user account', async t => { - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - authorization: 'test-auth-token', - }) - registry.whoami({ username: user }) - registry.nock.get('/-/org/test-user/package?format=cli').reply(200, null) - - await testComp(t, { - argv: ['npm', 'unpublish'], - partialWord: pkg, - expect: [], - title: 'should have no autocomplete', - }) - }) - t.test('logged out user', async t => { const registry = new MockRegistry({ tap: t, diff --git a/workspaces/arborist/CHANGELOG.md b/workspaces/arborist/CHANGELOG.md index 6afe93f2dde7d..c96cf00e7a8db 100644 --- a/workspaces/arborist/CHANGELOG.md +++ b/workspaces/arborist/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [6.0.0-pre.1](https://github.com/npm/cli/compare/arborist-v6.0.0-pre.0...arborist-v6.0.0-pre.1) (2022-09-14) + +### Bug Fixes + +* [`f3b0c43`](https://github.com/npm/cli/commit/f3b0c438d5b62b267f36c21d7b9fa57ae9507ef5) keep saveTypes separate for each `add` (@wraithgar) + ## [6.0.0-pre.0](https://github.com/npm/cli/compare/arborist-v5.6.1...arborist-v6.0.0-pre.0) (2022-09-08) ### ⚠ BREAKING CHANGES diff --git a/workspaces/arborist/lib/add-rm-pkg-deps.js b/workspaces/arborist/lib/add-rm-pkg-deps.js index 7b43c38e2492b..59d5e32547c29 100644 --- a/workspaces/arborist/lib/add-rm-pkg-deps.js +++ b/workspaces/arborist/lib/add-rm-pkg-deps.js @@ -5,23 +5,24 @@ const localeCompare = require('@isaacs/string-locale-compare')('en') const add = ({ pkg, add, saveBundle, saveType }) => { for (const { name, rawSpec } of add) { + let addSaveType = saveType // if the user does not give us a type, we infer which type(s) // to keep based on the same order of priority we do when // building the tree as defined in the _loadDeps method of // the node class. - if (!saveType) { - saveType = inferSaveType(pkg, name) + if (!addSaveType) { + addSaveType = inferSaveType(pkg, name) } - if (saveType === 'prod') { + if (addSaveType === 'prod') { // a production dependency can only exist as production (rpj ensures it // doesn't coexist w/ optional) deleteSubKey(pkg, 'devDependencies', name, 'dependencies') deleteSubKey(pkg, 'peerDependencies', name, 'dependencies') - } else if (saveType === 'dev') { + } else if (addSaveType === 'dev') { // a dev dependency may co-exist as peer, or optional, but not production deleteSubKey(pkg, 'dependencies', name, 'devDependencies') - } else if (saveType === 'optional') { + } else if (addSaveType === 'optional') { // an optional dependency may co-exist as dev (rpj ensures it doesn't // coexist w/ prod) deleteSubKey(pkg, 'peerDependencies', name, 'optionalDependencies') @@ -31,23 +32,23 @@ const add = ({ pkg, add, saveBundle, saveType }) => { deleteSubKey(pkg, 'optionalDependencies', name, 'peerDependencies') } - const depType = saveTypeMap.get(saveType) + const depType = saveTypeMap.get(addSaveType) pkg[depType] = pkg[depType] || {} if (rawSpec !== '' || pkg[depType][name] === undefined) { pkg[depType][name] = rawSpec || '*' } - if (saveType === 'optional') { + if (addSaveType === 'optional') { // Affordance for previous npm versions that require this behaviour pkg.dependencies = pkg.dependencies || {} pkg.dependencies[name] = pkg.optionalDependencies[name] } - if (saveType === 'peer' || saveType === 'peerOptional') { + if (addSaveType === 'peer' || addSaveType === 'peerOptional') { const pdm = pkg.peerDependenciesMeta || {} - if (saveType === 'peer' && pdm[name] && pdm[name].optional) { + if (addSaveType === 'peer' && pdm[name] && pdm[name].optional) { pdm[name].optional = false - } else if (saveType === 'peerOptional') { + } else if (addSaveType === 'peerOptional') { pdm[name] = pdm[name] || {} pdm[name].optional = true pkg.peerDependenciesMeta = pdm @@ -59,7 +60,7 @@ const add = ({ pkg, add, saveBundle, saveType }) => { } } - if (saveBundle && saveType !== 'peer' && saveType !== 'peerOptional') { + if (saveBundle && addSaveType !== 'peer' && addSaveType !== 'peerOptional') { // keep it sorted, keep it unique const bd = new Set(pkg.bundleDependencies || []) bd.add(name) diff --git a/workspaces/arborist/package.json b/workspaces/arborist/package.json index a6a1a0bc31124..9c58bff582367 100644 --- a/workspaces/arborist/package.json +++ b/workspaces/arborist/package.json @@ -1,6 +1,6 @@ { "name": "@npmcli/arborist", - "version": "6.0.0-pre.0", + "version": "6.0.0-pre.1", "description": "Manage node_modules trees", "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", @@ -42,7 +42,7 @@ }, "devDependencies": { "@npmcli/eslint-config": "^3.1.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "benchmark": "^2.1.4", "chalk": "^4.1.0", "minify-registry-metadata": "^2.1.0", @@ -100,6 +100,6 @@ }, "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "4.0.0" + "version": "4.1.2" } } diff --git a/workspaces/arborist/test/add-rm-pkg-deps.js b/workspaces/arborist/test/add-rm-pkg-deps.js index 77ea2bd61e550..b15b9f96d74a3 100644 --- a/workspaces/arborist/test/add-rm-pkg-deps.js +++ b/workspaces/arborist/test/add-rm-pkg-deps.js @@ -17,6 +17,22 @@ t.test('add', t => { } process.on('log', log) t.teardown(() => process.off('log', log)) + t.strictSame(add({ + pkg: { + dependencies: { bar: '1' }, + devDependencies: { foo: '2' }, + }, + add: [ + foo1, + bar, + ], + path: '/', + }), { + dependencies: { bar: '1' }, + devDependencies: { foo: '1' }, + }, 'inferred save types stay the same for each dependency') + + t.strictSame(logs, []) t.strictSame(add({ pkg: { dependencies: { bar: '1' }, diff --git a/workspaces/libnpmaccess/CHANGELOG.md b/workspaces/libnpmaccess/CHANGELOG.md index 8fd14a63f0066..4bfe6ce9ebb53 100644 --- a/workspaces/libnpmaccess/CHANGELOG.md +++ b/workspaces/libnpmaccess/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [7.0.0-pre.1](https://github.com/npm/cli/compare/libnpmaccess-v7.0.0-pre.0...libnpmaccess-v7.0.0-pre.1) (2022-09-14) + +### ⚠️ BREAKING CHANGES + +* the api for libnpmaccess is different now + +### Features + +* [`854521b`](https://github.com/npm/cli/commit/854521baa49ef88ff9586ec2cc5f1fbaee7fa364) rewrite: Rewrite libnpmaccess (@wraithgar) + ## [7.0.0-pre.0](https://github.com/npm/cli/compare/libnpmaccess-v6.0.4...libnpmaccess-v7.0.0-pre.0) (2022-09-08) ### ⚠ BREAKING CHANGES 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 0c2df73a1ab1a..93fa3bb973bf5 100644 --- a/workspaces/libnpmaccess/package.json +++ b/workspaces/libnpmaccess/package.json @@ -1,12 +1,11 @@ { "name": "libnpmaccess", - "version": "7.0.0-pre.0", + "version": "7.0.0-pre.1", "description": "programmatic library for `npm access` commands", "author": "GitHub Inc.", "license": "ISC", "main": "lib/index.js", "scripts": { - "postpublish": "git push origin --follow-tags", "lint": "eslint \"**/*.js\"", "test": "tap", "postlint": "template-oss-check", @@ -17,7 +16,7 @@ }, "devDependencies": { "@npmcli/eslint-config": "^3.1.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "nock": "^13.2.4", "tap": "^16.0.1" }, @@ -29,8 +28,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" }, @@ -43,6 +40,6 @@ ], "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "4.0.0" + "version": "4.1.2" } } 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() }) diff --git a/workspaces/libnpmdiff/package.json b/workspaces/libnpmdiff/package.json index ee3a56a29889d..c1665390b8c53 100644 --- a/workspaces/libnpmdiff/package.json +++ b/workspaces/libnpmdiff/package.json @@ -43,7 +43,7 @@ }, "devDependencies": { "@npmcli/eslint-config": "^3.1.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "tap": "^16.0.1" }, "dependencies": { @@ -58,6 +58,6 @@ }, "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "4.0.0" + "version": "4.1.2" } } diff --git a/workspaces/libnpmexec/CHANGELOG.md b/workspaces/libnpmexec/CHANGELOG.md index 91fce3939d765..79486595a645f 100644 --- a/workspaces/libnpmexec/CHANGELOG.md +++ b/workspaces/libnpmexec/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [5.0.0-pre.1](https://github.com/npm/cli/compare/libnpmexec-v5.0.0-pre.0...libnpmexec-v5.0.0-pre.1) (2022-09-14) + +### Dependencies + +* [Workspace](https://github.com/npm/cli/compare/arborist-v6.0.0-pre.0...arborist-v6.0.0-pre.1): `@npmcli/arborist@6.0.0-pre.1` + ## [5.0.0-pre.0](https://github.com/npm/cli/compare/libnpmexec-v4.0.12...libnpmexec-v5.0.0-pre.0) (2022-09-08) ### ⚠ BREAKING CHANGES diff --git a/workspaces/libnpmexec/package.json b/workspaces/libnpmexec/package.json index 8ac115d3fc732..39c185ee4a0f2 100644 --- a/workspaces/libnpmexec/package.json +++ b/workspaces/libnpmexec/package.json @@ -1,6 +1,6 @@ { "name": "libnpmexec", - "version": "5.0.0-pre.0", + "version": "5.0.0-pre.1", "files": [ "bin/", "lib/" @@ -47,14 +47,14 @@ }, "devDependencies": { "@npmcli/eslint-config": "^3.1.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "bin-links": "^3.0.3", "minify-registry-metadata": "^2.2.0", "mkdirp": "^1.0.4", "tap": "^16.0.1" }, "dependencies": { - "@npmcli/arborist": "^6.0.0-pre.0", + "@npmcli/arborist": "^6.0.0-pre.1", "@npmcli/ci-detect": "^2.0.0", "@npmcli/fs": "^2.1.1", "@npmcli/run-script": "^4.2.0", @@ -71,6 +71,6 @@ }, "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "4.0.0" + "version": "4.1.2" } } diff --git a/workspaces/libnpmfund/CHANGELOG.md b/workspaces/libnpmfund/CHANGELOG.md index a2d26703e4684..e10f4b45bb352 100644 --- a/workspaces/libnpmfund/CHANGELOG.md +++ b/workspaces/libnpmfund/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [4.0.0-pre.1](https://github.com/npm/cli/compare/libnpmfund-v4.0.0-pre.0...libnpmfund-v4.0.0-pre.1) (2022-09-14) + +### Dependencies + +* [Workspace](https://github.com/npm/cli/compare/arborist-v6.0.0-pre.0...arborist-v6.0.0-pre.1): `@npmcli/arborist@6.0.0-pre.1` + ## [4.0.0-pre.0](https://github.com/npm/cli/compare/libnpmfund-v3.0.3...libnpmfund-v4.0.0-pre.0) (2022-09-08) ### ⚠ BREAKING CHANGES diff --git a/workspaces/libnpmfund/package.json b/workspaces/libnpmfund/package.json index 9e69616c14552..271be4e686c9e 100644 --- a/workspaces/libnpmfund/package.json +++ b/workspaces/libnpmfund/package.json @@ -1,6 +1,6 @@ { "name": "libnpmfund", - "version": "4.0.0-pre.0", + "version": "4.0.0-pre.1", "main": "lib/index.js", "files": [ "bin/", @@ -42,17 +42,17 @@ }, "devDependencies": { "@npmcli/eslint-config": "^3.1.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "tap": "^16.0.1" }, "dependencies": { - "@npmcli/arborist": "^6.0.0-pre.0" + "@npmcli/arborist": "^6.0.0-pre.1" }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" }, "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "4.0.0" + "version": "4.1.2" } } diff --git a/workspaces/libnpmhook/package.json b/workspaces/libnpmhook/package.json index db94874c18bbb..a4f42a83e643a 100644 --- a/workspaces/libnpmhook/package.json +++ b/workspaces/libnpmhook/package.json @@ -37,7 +37,7 @@ }, "devDependencies": { "@npmcli/eslint-config": "^3.1.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "nock": "^13.2.4", "tap": "^16.0.1" }, @@ -46,6 +46,6 @@ }, "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "4.0.0" + "version": "4.1.2" } } diff --git a/workspaces/libnpmorg/package.json b/workspaces/libnpmorg/package.json index b188a5e27859b..748dd83f00e79 100644 --- a/workspaces/libnpmorg/package.json +++ b/workspaces/libnpmorg/package.json @@ -28,7 +28,7 @@ ], "devDependencies": { "@npmcli/eslint-config": "^3.1.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "minipass": "^3.1.1", "nock": "^13.2.4", "tap": "^16.0.1" @@ -49,6 +49,6 @@ }, "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "4.0.0" + "version": "4.1.2" } } diff --git a/workspaces/libnpmpack/package.json b/workspaces/libnpmpack/package.json index 545c2870a1ec3..f7d8fcfe28f7f 100644 --- a/workspaces/libnpmpack/package.json +++ b/workspaces/libnpmpack/package.json @@ -23,7 +23,7 @@ }, "devDependencies": { "@npmcli/eslint-config": "^3.1.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "nock": "^13.0.7", "tap": "^16.0.1" }, @@ -44,6 +44,6 @@ }, "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "4.0.0" + "version": "4.1.2" } } diff --git a/workspaces/libnpmpublish/package.json b/workspaces/libnpmpublish/package.json index 07bead1202f1f..6959a8bd98354 100644 --- a/workspaces/libnpmpublish/package.json +++ b/workspaces/libnpmpublish/package.json @@ -25,7 +25,7 @@ }, "devDependencies": { "@npmcli/eslint-config": "^3.1.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "libnpmpack": "^5.0.0-pre.0", "lodash.clonedeep": "^4.5.0", "nock": "^13.2.4", @@ -50,6 +50,6 @@ }, "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "4.0.0" + "version": "4.1.2" } } diff --git a/workspaces/libnpmsearch/package.json b/workspaces/libnpmsearch/package.json index a10a51ec568ef..3581ecc31e876 100644 --- a/workspaces/libnpmsearch/package.json +++ b/workspaces/libnpmsearch/package.json @@ -26,7 +26,7 @@ }, "devDependencies": { "@npmcli/eslint-config": "^3.1.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "nock": "^13.2.4", "tap": "^16.0.1" }, @@ -45,6 +45,6 @@ }, "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "4.0.0" + "version": "4.1.2" } } diff --git a/workspaces/libnpmteam/package.json b/workspaces/libnpmteam/package.json index 9c1f7cd5e9dbe..d9efe70197337 100644 --- a/workspaces/libnpmteam/package.json +++ b/workspaces/libnpmteam/package.json @@ -16,7 +16,7 @@ }, "devDependencies": { "@npmcli/eslint-config": "^3.1.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "nock": "^13.2.4", "tap": "^16.0.1" }, @@ -39,6 +39,6 @@ }, "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "4.0.0" + "version": "4.1.2" } } diff --git a/workspaces/libnpmversion/package.json b/workspaces/libnpmversion/package.json index ac3ecf47aeb4e..ec4e4bc8a73e6 100644 --- a/workspaces/libnpmversion/package.json +++ b/workspaces/libnpmversion/package.json @@ -28,7 +28,7 @@ }, "devDependencies": { "@npmcli/eslint-config": "^3.1.0", - "@npmcli/template-oss": "4.0.0", + "@npmcli/template-oss": "4.1.2", "require-inject": "^1.4.4", "tap": "^16.0.1" }, @@ -44,6 +44,6 @@ }, "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "4.0.0" + "version": "4.1.2" } }