diff --git a/AUTHORS b/AUTHORS index 9811e60d9382d..91d33cbbb38ea 100644 --- a/AUTHORS +++ b/AUTHORS @@ -837,3 +837,6 @@ Yonathan Randolph Julian Møller Ellehauge Lucas Werkmeister Seth Westphal +Mihai Crisan <61682879+crisanmm@users.noreply.github.com> +Kenichi Kamiya +Chen XI diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c0e3106fc85b..f28db5b85e775 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,38 @@ # Changelog +## v8.14.0 (2022-07-13) + +### Features + + * [`f032e1c`](https://github.com/npm/cli/commit/f032e1c0ada062e2139c8f057b24abb1ce2e4a33) [#4827](https://github.com/npm/cli/pull/4827) feat: add npm audit signatures ([@feelepxyz](https://github.com/feelepxyz)) + * [`e8102c1`](https://github.com/npm/cli/commit/e8102c1aae65a18e41253fbcdffe2eff0bedae53) [#5076](https://github.com/npm/cli/pull/5076) feat: Add `web` auth type ([@jumoel](https://github.com/jumoel)) + * [`e9b4214`](https://github.com/npm/cli/commit/e9b4214e1ddb1ad79fe6826cf2ce7ba385f0c274) [#5094](https://github.com/npm/cli/pull/5094) feat(arborist): add support for dependencies script ([@nlf](https://github.com/nlf)) + * [`c6c4ba3`](https://github.com/npm/cli/commit/c6c4ba3b62e2a0896a48329f4c7e13d9e44a2f80) [#5149](https://github.com/npm/cli/pull/5149) feat: notify on adduser of upcoming cmds, login and register ([@fritzy](https://github.com/fritzy)) + * [`e58f02f`](https://github.com/npm/cli/commit/e58f02f5e8263bf86ae1f07a863098d445e6d0cd) [#5149](https://github.com/npm/cli/pull/5149) feat: warn on config --auth-type=sso/saml/oauth, undeprecate --auth-type ([@fritzy](https://github.com/fritzy)) + +### Bug Fixes + + * [`52ec5ec`](https://github.com/npm/cli/commit/52ec5ec61fd3b266efd7a9c5712dd6a769a2d365) [#5154](https://github.com/npm/cli/pull/5154) fix: properly open package arg repo inside workspace ([@wraithgar](https://github.com/wraithgar)) + +### Documentation + + * [`9697f16`](https://github.com/npm/cli/commit/9697f16952b1bf02bb5455c36a1995277cbc0c97) [#5118](https://github.com/npm/cli/pull/5118) docs: typo in npm command ([@crisanmm](https://github.com/crisanmm)) + * [`da5a4ba`](https://github.com/npm/cli/commit/da5a4ba2c83af9a7e5e0fe38c32136adf396f557) [#5079](https://github.com/npm/cli/pull/5079) docs: update reference to deprecated spdx package ([@kachick](https://github.com/kachick)) + * [`25b3058`](https://github.com/npm/cli/commit/25b305830be0892bbbf0245aee2eebdb76ee2ce3) [#5043](https://github.com/npm/cli/pull/5043) docs: naming of files in example code should be consistent ([@xc1427](https://github.com/xc1427)) + * [`ac56fc4`](https://github.com/npm/cli/commit/ac56fc41bc2f91f51c8438f98893121e7a92ee46) [#5095](https://github.com/npm/cli/pull/5095) docs: document `dependencies` script ([@nlf](https://github.com/nlf)) + +### Dependencies + + * [`cb0db7c`](https://github.com/npm/cli/commit/cb0db7c3fd1d0a4c30db9f44e9ea9e69ec327fe8) [#5147](https://github.com/npm/cli/pull/5147) deps: `@npmcli/arborist@5.3.0` + * [`b8c0580`](https://github.com/npm/cli/commit/b8c0580e5df93aa519b3ec240bb85d59eee5ee37) [#5156](https://github.com/npm/cli/pull/5156) deps: `minipass@3.3.4` + * [`ad72611`](https://github.com/npm/cli/commit/ad726118755ef577cc0755499d35a5d3c74d54a6) [#5156](https://github.com/npm/cli/pull/5156) deps: `lru-cache@7.12.0` + * [`c94919d`](https://github.com/npm/cli/commit/c94919dd4874196d3a84eff4fab450a17dcd4867) [#5156](https://github.com/npm/cli/pull/5156) deps: `just-diff@5.0.3` + * [`18ddc57`](https://github.com/npm/cli/commit/18ddc57c7a54165d55c81b413ef9de981c790148) [#5156](https://github.com/npm/cli/pull/5156) deps: `just-diff-apply@5.3.1` + * [`a2d700b`](https://github.com/npm/cli/commit/a2d700b3cc7cebca2d1b0c16224af41da3689aaf) [#5156](https://github.com/npm/cli/pull/5156) deps: `npm-package-arg@9.1.0` + * [`99dc697`](https://github.com/npm/cli/commit/99dc697409e1eb42caaf0c0e38fa41635d89a871) [#5156](https://github.com/npm/cli/pull/5156) deps: `@npmcli/run-script@4.1.7` + * [`4a9f2dc`](https://github.com/npm/cli/commit/4a9f2dc9169fd330c4dcf2bad7890aaf4765bafa) [#5157](https://github.com/npm/cli/pull/5157) deps: `npm-registry-fetch@13.2.0` + * [`45a9bde`](https://github.com/npm/cli/commit/45a9bdee604073a3c5b4d3c6d90e22bf6672d6bf) [#5158](https://github.com/npm/cli/pull/5158) deps: `npm-profile@6.2.0` + ## v8.13.2 (2022-06-29) ### Documentation diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b32e47ce2f035..e12e300210841 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,15 +35,18 @@ $ node . run test **5. Open a [Pull Request](https://github.com/npm/cli/pulls) for your work & become the newest contributor to `npm`! 🎉** -## Test Coverage +## Pull Request Conventions -We use [`tap`](https://node-tap.org/) for testing & expect that every new feature or bug fix comes with corresponding tests that validate the solutions. We strive to have as close to, if not exactly, 100% code coverage. +We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). When opening a pull request, please be sure that either the pull request title, or each commit in the pull request, has one of the following prefixes: -**You can find out what the current test coverage percentage is by running...** + - `feat`: For when introducing a new feature. The result will be a new semver minor version of the package when it is next published. + - `fix`: For bug fixes. The result will be a new semver patch version of the package when it is next published. + - `docs`: For documentation updates. The result will be a new semver patch version of the package when it is next published. + - `chore`: For changes that do not affect the published module. Often these are changes to tests. The result will be *no* change to the version of the package when it is next published (as the commit does not affect the published version). -```bash -$ node . run check-coverage -``` +## Test Coverage + +We use [`tap`](https://node-tap.org/) for testing & expect that every new feature or bug fix comes with corresponding tests that validate the solutions. Tap also reports on code coverage and it will fail if that drops below 100%. ## Performance & Benchmarks diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index aa9c47d453448..421efae116298 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -131,6 +131,7 @@ graph LR; npm-bundled-->npm-normalize-package-bin; npm-install-checks-->semver; npm-package-arg-->hosted-git-info; + npm-package-arg-->proc-log; npm-package-arg-->semver; npm-package-arg-->validate-npm-package-name; npm-packlist-->ignore-walk; @@ -257,7 +258,7 @@ graph LR; cidr-regex-->ip-regex; cli-columns-->string-width; cli-columns-->strip-ansi; - cli-table3-->colors; + cli-table3-->colors-colors["@colors/colors"]; cli-table3-->string-width; cmd-shim-->mkdirp-infer-owner; color-convert-->color-name; @@ -405,6 +406,7 @@ graph LR; libnpmversion-->require-inject; libnpmversion-->semver; libnpmversion-->tap; + lru-cache-->yallist; make-fetch-happen-->agentkeepalive; make-fetch-happen-->cacache; make-fetch-happen-->http-cache-semantics; @@ -510,6 +512,7 @@ graph LR; npm-->npmcli-template-oss["@npmcli/template-oss"]; npm-->npmlog; npm-->opener; + npm-->p-map; npm-->pacote; npm-->parse-conflict-json; npm-->proc-log; @@ -535,6 +538,7 @@ graph LR; npm-bundled-->npm-normalize-package-bin; npm-install-checks-->semver; npm-package-arg-->hosted-git-info; + npm-package-arg-->proc-log; npm-package-arg-->semver; npm-package-arg-->validate-npm-package-name; npm-packlist-->glob; @@ -634,6 +638,7 @@ graph LR; npmcli-run-script-->npmcli-node-gyp["@npmcli/node-gyp"]; npmcli-run-script-->npmcli-promise-spawn["@npmcli/promise-spawn"]; npmcli-run-script-->read-package-json-fast; + npmcli-run-script-->which; npmlog-->are-we-there-yet; npmlog-->console-control-strings; npmlog-->gauge; diff --git a/docs/content/commands/npm-audit.md b/docs/content/commands/npm-audit.md index 206a33f53a688..48e0a3161e8f2 100644 --- a/docs/content/commands/npm-audit.md +++ b/docs/content/commands/npm-audit.md @@ -43,14 +43,55 @@ output, it simply changes the command's failure threshold. ### Audit Signatures -This command can also audit the integrity values of the packages in your -tree against any signatures present in the registry they were downloaded -from. npm will attempt to download the keys from `/-/npm/v1/keys` on -each the registry used to download any given package. It will then -check the `dist.signatures` object in the package itself, and verify the -`sig` present there using the `keyid` there, matching it with a key -returned from the registry. The command for this is `npm audit -signatures` +To ensure the integrity of packages you download from the public npm registry, or any registry that supports signatures, you can verify the registry signatures of downloaded packages using the npm CLI. + +Registry signatures can be verified using the following `audit` command: + +```bash +$ npm audit signatures +``` + +The npm CLI supports registry signatures and signing keys provided by any registry if the following conventions are followed: + +1. Signatures are provided in the package's `packument` in each published version within the `dist` object: + +```json +"dist":{ + "..omitted..": "..omitted..", + "signatures": [{ + "keyid": "SHA256:{{SHA256_PUBLIC_KEY}}", + "sig": "a312b9c3cb4a1b693e8ebac5ee1ca9cc01f2661c14391917dcb111517f72370809..." + }] +} +``` + +See this [example](https://registry.npmjs.org/light-cycle/1.4.3) of a signed package from the public npm registry. + +The `sig` is generated using the following template: `${package.name}@${package.version}:${package.dist.integrity}` and the `keyid` has to match one of the public signing keys below. + +2. Public signing keys are provided at `registry-host.tld/-/npm/v1/keys` in the following format: + +``` +{ + "keys": [{ + "expires": null, + "keyid": "SHA256:{{SHA256_PUBLIC_KEY}}", + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "key": "{{B64_PUBLIC_KEY}}" + }] +} +``` + +Keys response: + +- `expires`: null or a simplified extended ISO 8601 format: `YYYY-MM-DDTHH:mm:ss.sssZ` +- `keydid`: sha256 fingerprint of the public key +- `keytype`: only `ecdsa-sha2-nistp256` is currently supported by the npm CLI +- `scheme`: only `ecdsa-sha2-nistp256` is currently supported by the npm CLI +- `key`: base64 encoded public key + +See this example key's response from the public npm registry. ### Audit Endpoints diff --git a/docs/content/configuring-npm/folders.md b/docs/content/configuring-npm/folders.md index 218870765b262..5bab80ec169c5 100644 --- a/docs/content/configuring-npm/folders.md +++ b/docs/content/configuring-npm/folders.md @@ -202,7 +202,7 @@ For a graphical breakdown of what is installed where, use `npm ls`. #### Publishing Upon publishing, npm will look in the `node_modules` folder. If any of -the items there are not in the `bundledDependencies` array, then they will +the items there are not in the `bundleDependencies` array, then they will not be included in the package tarball. This allows a package maintainer to install all of their dependencies diff --git a/docs/content/configuring-npm/package-json.md b/docs/content/configuring-npm/package-json.md index 826f10ce82531..f0315d60efef4 100644 --- a/docs/content/configuring-npm/package-json.md +++ b/docs/content/configuring-npm/package-json.md @@ -829,14 +829,14 @@ if the `soy-milk` package is not installed on the host. This allows you to integrate and interact with a variety of host packages without requiring all of them to be installed. -### bundledDependencies +### bundleDependencies This defines an array of package names that will be bundled when publishing the package. In cases where you need to preserve npm packages locally or have them available through a single file download, you can bundle the packages in a -tarball file by specifying the package names in the `bundledDependencies` +tarball file by specifying the package names in the `bundleDependencies` array and executing `npm pack`. For example: @@ -847,7 +847,7 @@ If we define a package.json like this: { "name": "awesome-web-framework", "version": "1.0.0", - "bundledDependencies": [ + "bundleDependencies": [ "renderized", "super-streams" ] @@ -860,9 +860,9 @@ can be installed in a new project by executing `npm install awesome-web-framework-1.0.0.tgz`. Note that the package names do not include any versions, as that information is specified in `dependencies`. -If this is spelled `"bundleDependencies"`, then that is also honored. +If this is spelled `"bundledDependencies"`, then that is also honored. -Alternatively, `"bundledDependencies"` can be defined as a boolean value. A +Alternatively, `"bundleDependencies"` can be defined as a boolean value. A value of `true` will bundle all dependencies, a value of `false` will bundle none. diff --git a/docs/content/using-npm/config.md b/docs/content/using-npm/config.md index ffbf9be055719..e3e1bd6c73bb3 100644 --- a/docs/content/using-npm/config.md +++ b/docs/content/using-npm/config.md @@ -357,8 +357,9 @@ newlines replaced by the string "\n". For example: cert="-----BEGIN CERTIFICATE-----\nXXXX\nXXXX\n-----END CERTIFICATE-----" ``` -It is _not_ the path to a certificate file (and there is no "certfile" -option). +It is _not_ the path to a certificate file, though you can set a +registry-scoped "certfile" path like +"//other-registry.tld/:certfile=/path/to/cert.pem". @@ -946,7 +947,8 @@ format with newlines replaced by the string "\n". For example: key="-----BEGIN PRIVATE KEY-----\nXXXX\nXXXX\n-----END PRIVATE KEY-----" ``` -It is _not_ the path to a key file (and there is no "keyfile" option). +It is _not_ the path to a key file, though you can set a registry-scoped +"keyfile" path like "//other-registry.tld/:keyfile=/path/to/key.pem". diff --git a/lib/commands/adduser.js b/lib/commands/adduser.js index 5e23f40dc5968..2853269ef3dee 100644 --- a/lib/commands/adduser.js +++ b/lib/commands/adduser.js @@ -30,7 +30,7 @@ class AddUser extends BaseCommand { log.disableProgress() log.warn('adduser', - '`adduser` will be split into `login` and `register in a future version.' + '`adduser` will be split into `login` and `register` in a future version.' + ' `adduser` will become an alias of `register`.' + ' `login` (currently an alias) will become its own command.') log.notice('', `Log in on ${replaceInfo(registry)}`) diff --git a/lib/commands/publish.js b/lib/commands/publish.js index 0b6eed04b009b..950d7da96bcfa 100644 --- a/lib/commands/publish.js +++ b/lib/commands/publish.js @@ -61,8 +61,7 @@ class Publish extends BaseCommand { throw new Error('Tag name must not be a valid SemVer range: ' + defaultTag.trim()) } - const opts = { ...this.npm.flatOptions } - log.disableProgress() + const opts = { ...this.npm.flatOptions, progress: false } // you can publish name@version, ./foo.tgz, etc. // even though the default is the 'file:.' cwd. @@ -102,7 +101,7 @@ class Publish extends BaseCommand { const resolved = npa.resolve(manifest.name, manifest.version) const registry = npmFetch.pickRegistry(resolved, opts) const creds = this.npm.config.getCredentialsByURI(registry) - const noCreds = !creds.token && !creds.username + const noCreds = !(creds.token || creds.username || creds.certfile && creds.keyfile) const outputRegistry = replaceInfo(registry) if (noCreds) { diff --git a/lib/utils/config/definitions.js b/lib/utils/config/definitions.js index 665ed1efe5e61..7d6af2473f2bd 100644 --- a/lib/utils/config/definitions.js +++ b/lib/utils/config/definitions.js @@ -436,8 +436,8 @@ define('cert', { cert="-----BEGIN CERTIFICATE-----\\nXXXX\\nXXXX\\n-----END CERTIFICATE-----" \`\`\` - It is _not_ the path to a certificate file (and there is no "certfile" - option). + It is _not_ the path to a certificate file, though you can set a registry-scoped + "certfile" path like "//other-registry.tld/:certfile=/path/to/cert.pem". `, flatten, }) @@ -1118,7 +1118,8 @@ define('key', { key="-----BEGIN PRIVATE KEY-----\\nXXXX\\nXXXX\\n-----END PRIVATE KEY-----" \`\`\` - It is _not_ the path to a key file (and there is no "keyfile" option). + It is _not_ the path to a key file, though you can set a registry-scoped + "keyfile" path like "//other-registry.tld/:keyfile=/path/to/key.pem". `, flatten, }) diff --git a/lib/utils/get-identity.js b/lib/utils/get-identity.js index f4aedb89b3957..41d882473ab7b 100644 --- a/lib/utils/get-identity.js +++ b/lib/utils/get-identity.js @@ -9,8 +9,8 @@ module.exports = async (npm, opts) => { return creds.username } - // No username, but we have a token; fetch the username from registry - if (creds.token) { + // No username, but we have other credentials; fetch the username from registry + if (creds.token || creds.certfile && creds.keyfile) { const registryData = await npmFetch.json('/-/whoami', { ...opts }) return registryData.username } diff --git a/node_modules/@npmcli/config/lib/index.js b/node_modules/@npmcli/config/lib/index.js index 5b7ea68e91e36..8c2b181ca9c61 100644 --- a/node_modules/@npmcli/config/lib/index.js +++ b/node_modules/@npmcli/config/lib/index.js @@ -698,9 +698,11 @@ class Config { this.delete(`${nerfed}:_password`, 'user') this.delete(`${nerfed}:username`, 'user') this.delete(`${nerfed}:email`, 'user') + this.delete(`${nerfed}:certfile`, 'user') + this.delete(`${nerfed}:keyfile`, 'user') } - setCredentialsByURI (uri, { token, username, password, email }) { + setCredentialsByURI (uri, { token, username, password, email, certfile, keyfile }) { const nerfed = nerfDart(uri) const def = nerfDart(this.get('registry')) @@ -733,6 +735,11 @@ class Config { this.delete(`${nerfed}:-authtoken`, 'user') this.delete(`${nerfed}:_authtoken`, 'user') this.delete(`${nerfed}:email`, 'user') + if (certfile && keyfile) { + this.set(`${nerfed}:certfile`, certfile, 'user') + this.set(`${nerfed}:keyfile`, keyfile, 'user') + // cert/key may be used in conjunction with other credentials, thus no `else` + } if (token) { this.set(`${nerfed}:_authToken`, token, 'user') this.delete(`${nerfed}:_password`, 'user') @@ -750,7 +757,7 @@ class Config { // protects against shoulder-hacks if password is memorable, I guess? const encoded = Buffer.from(password, 'utf8').toString('base64') this.set(`${nerfed}:_password`, encoded, 'user') - } else { + } else if (!certfile || !keyfile) { throw new Error('No credentials to set.') } } @@ -765,6 +772,14 @@ class Config { creds.email = email } + const certfileReg = this.get(`${nerfed}:certfile`) + const keyfileReg = this.get(`${nerfed}:keyfile`) + if (certfileReg && keyfileReg) { + creds.certfile = certfileReg + creds.keyfile = keyfileReg + // cert/key may be used in conjunction with other credentials, thus no `return` + } + const tokenReg = this.get(`${nerfed}:_authToken`) || this.get(`${nerfed}:_authtoken`) || this.get(`${nerfed}:-authtoken`) || diff --git a/node_modules/@npmcli/config/package.json b/node_modules/@npmcli/config/package.json index 2cc04e05be8c9..2f561c12233dc 100644 --- a/node_modules/@npmcli/config/package.json +++ b/node_modules/@npmcli/config/package.json @@ -1,6 +1,6 @@ { "name": "@npmcli/config", - "version": "4.1.0", + "version": "4.2.0", "files": [ "bin/", "lib/" @@ -31,7 +31,7 @@ }, "devDependencies": { "@npmcli/eslint-config": "^3.0.1", - "@npmcli/template-oss": "3.3.2", + "@npmcli/template-oss": "3.5.0", "tap": "^16.0.1" }, "dependencies": { @@ -49,6 +49,6 @@ }, "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "3.3.2" + "version": "3.5.0" } } diff --git a/node_modules/make-fetch-happen/lib/cache/entry.js b/node_modules/make-fetch-happen/lib/cache/entry.js index 7a7572ba030c9..4307962b889d0 100644 --- a/node_modules/make-fetch-happen/lib/cache/entry.js +++ b/node_modules/make-fetch-happen/lib/cache/entry.js @@ -35,6 +35,7 @@ const KEEP_RESPONSE_HEADERS = [ 'etag', 'expires', 'last-modified', + 'link', 'location', 'pragma', 'vary', diff --git a/node_modules/make-fetch-happen/package.json b/node_modules/make-fetch-happen/package.json index e04c7645c4f2a..8b21901f34fc1 100644 --- a/node_modules/make-fetch-happen/package.json +++ b/node_modules/make-fetch-happen/package.json @@ -1,6 +1,6 @@ { "name": "make-fetch-happen", - "version": "10.1.8", + "version": "10.2.0", "description": "Opinionated, caching, retrying fetch client", "main": "lib/index.js", "files": [ diff --git a/node_modules/npm-registry-fetch/lib/auth.js b/node_modules/npm-registry-fetch/lib/auth.js index 17da6a17d7193..870ce0d923cd0 100644 --- a/node_modules/npm-registry-fetch/lib/auth.js +++ b/node_modules/npm-registry-fetch/lib/auth.js @@ -1,4 +1,5 @@ 'use strict' +const fs = require('fs') const npa = require('npm-package-arg') const { URL } = require('url') @@ -7,7 +8,8 @@ const { URL } = require('url') const regKeyFromURI = (uri, opts) => { const parsed = new URL(uri) // try to find a config key indicating we have auth for this registry - // can be one of :_authToken, :_auth, or :_password and :username + // can be one of :_authToken, :_auth, :_password and :username, or + // :certfile and :keyfile // We walk up the "path" until we're left with just //[:], // stopping when we reach '//'. let regKey = `//${parsed.host}${parsed.pathname}` @@ -26,7 +28,8 @@ const regKeyFromURI = (uri, opts) => { const hasAuth = (regKey, opts) => ( opts[`${regKey}:_authToken`] || opts[`${regKey}:_auth`] || - opts[`${regKey}:username`] && opts[`${regKey}:_password`] + opts[`${regKey}:username`] && opts[`${regKey}:_password`] || + opts[`${regKey}:certfile`] && opts[`${regKey}:keyfile`] ) const sameHost = (a, b) => { @@ -44,6 +47,17 @@ const getRegistry = opts => { return scopeReg || opts.registry } +const maybeReadFile = file => { + try { + return fs.readFileSync(file, 'utf8') + } catch (er) { + if (er.code !== 'ENOENT') { + throw er + } + return null + } +} + const getAuth = (uri, opts = {}) => { const { forceAuth } = opts if (!uri) { @@ -59,6 +73,8 @@ const getAuth = (uri, opts = {}) => { username: forceAuth.username, password: forceAuth._password || forceAuth.password, auth: forceAuth._auth || forceAuth.auth, + certfile: forceAuth.certfile, + keyfile: forceAuth.keyfile, }) } @@ -82,6 +98,8 @@ const getAuth = (uri, opts = {}) => { [`${regKey}:username`]: username, [`${regKey}:_password`]: password, [`${regKey}:_auth`]: auth, + [`${regKey}:certfile`]: certfile, + [`${regKey}:keyfile`]: keyfile, } = opts return new Auth({ @@ -90,15 +108,19 @@ const getAuth = (uri, opts = {}) => { auth, username, password, + certfile, + keyfile, }) } class Auth { - constructor ({ token, auth, username, password, scopeAuthKey }) { + constructor ({ token, auth, username, password, scopeAuthKey, certfile, keyfile }) { this.scopeAuthKey = scopeAuthKey this.token = null this.auth = null this.isBasicAuth = false + this.cert = null + this.key = null if (token) { this.token = token } else if (auth) { @@ -108,6 +130,15 @@ class Auth { this.auth = Buffer.from(`${username}:${p}`, 'utf8').toString('base64') this.isBasicAuth = true } + // mTLS may be used in conjunction with another auth method above + if (certfile && keyfile) { + const cert = maybeReadFile(certfile, 'utf-8') + const key = maybeReadFile(keyfile, 'utf-8') + if (cert && key) { + this.cert = cert + this.key = key + } + } } } diff --git a/node_modules/npm-registry-fetch/lib/index.js b/node_modules/npm-registry-fetch/lib/index.js index c788febc33afb..cc331a50c0963 100644 --- a/node_modules/npm-registry-fetch/lib/index.js +++ b/node_modules/npm-registry-fetch/lib/index.js @@ -112,10 +112,10 @@ function regFetch (uri, /* istanbul ignore next */ opts_ = {}) { cache: getCacheMode(opts), cachePath: opts.cache, ca: opts.ca, - cert: opts.cert, + cert: auth.cert || opts.cert, headers, integrity: opts.integrity, - key: opts.key, + key: auth.key || opts.key, localAddress: opts.localAddress, maxSockets: opts.maxSockets, memoize: opts.memoize, diff --git a/node_modules/npm-registry-fetch/package.json b/node_modules/npm-registry-fetch/package.json index 5f19697c3b19d..8a0189a9ef74d 100644 --- a/node_modules/npm-registry-fetch/package.json +++ b/node_modules/npm-registry-fetch/package.json @@ -1,6 +1,6 @@ { "name": "npm-registry-fetch", - "version": "13.2.0", + "version": "13.3.0", "description": "Fetch-based http client for use with npm registry APIs", "main": "lib", "files": [ diff --git a/package-lock.json b/package-lock.json index dab9fb2c9605f..bed9f6061630e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "npm", - "version": "8.13.2", + "version": "8.14.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "npm", - "version": "8.13.2", + "version": "8.14.0", "bundleDependencies": [ "@isaacs/string-locale-compare", "@npmcli/arborist", @@ -90,7 +90,7 @@ "@isaacs/string-locale-compare": "^1.1.0", "@npmcli/arborist": "^5.0.4", "@npmcli/ci-detect": "^2.0.0", - "@npmcli/config": "^4.1.0", + "@npmcli/config": "^4.2.0", "@npmcli/fs": "^2.1.0", "@npmcli/map-workspaces": "^2.0.3", "@npmcli/package-json": "^2.0.0", @@ -122,7 +122,7 @@ "libnpmsearch": "^5.0.2", "libnpmteam": "^4.0.2", "libnpmversion": "^3.0.1", - "make-fetch-happen": "^10.1.8", + "make-fetch-happen": "^10.2.0", "minipass": "^3.1.6", "minipass-pipeline": "^1.2.4", "mkdirp": "^1.0.4", @@ -135,7 +135,7 @@ "npm-package-arg": "^9.1.0", "npm-pick-manifest": "^7.0.1", "npm-profile": "^6.2.0", - "npm-registry-fetch": "^13.2.0", + "npm-registry-fetch": "^13.3.0", "npm-user-validate": "^1.0.1", "npmlog": "^6.0.2", "opener": "^1.5.2", @@ -867,9 +867,9 @@ } }, "node_modules/@npmcli/config": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/config/-/config-4.1.0.tgz", - "integrity": "sha512-cPQmIQ2Q0vuOfrenrA3isikdMFMAHgzlXV+EmvZ8f2JeJsU5xTU2bG7ipXECiMvPF9nM+QDnMLuIg8QLw9H4xg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@npmcli/config/-/config-4.2.0.tgz", + "integrity": "sha512-imWNz5dNWb2u+y41jyxL2WB389tkhu3a01Rchn16O/ur6GrnKySgOqdNG3N/9Z+mqxdISMEGKXI/POCauzz0dA==", "inBundle": true, "dependencies": { "@npmcli/map-workspaces": "^2.0.2", @@ -4612,9 +4612,9 @@ "peer": true }, "node_modules/make-fetch-happen": { - "version": "10.1.8", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.1.8.tgz", - "integrity": "sha512-0ASJbG12Au6+N5I84W+8FhGS6iM8MyzvZady+zaQAu+6IOaESFzCLLD0AR1sAFF3Jufi8bxm586ABN6hWd3k7g==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.0.tgz", + "integrity": "sha512-OnEfCLofQVJ5zgKwGk55GaqosqKjaR6khQlJY3dBAA+hM25Bc5CmX5rKUfVut+rYA3uidA7zb7AvcglU87rPRg==", "inBundle": true, "dependencies": { "agentkeepalive": "^4.2.1", @@ -5188,9 +5188,9 @@ } }, "node_modules/npm-registry-fetch": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-13.2.0.tgz", - "integrity": "sha512-NEKnK02Co31+cnDtnAvEdq9xn6E9yKPK/aOHXZieVbw/qVOcFd7su6kviZjImYoszjM2GykMfGMiyyPUQjUkag==", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-13.3.0.tgz", + "integrity": "sha512-10LJQ/1+VhKrZjIuY9I/+gQTvumqqlgnsCufoXETHAPFTS3+M+Z5CFhZRDHGavmJ6rOye3UvNga88vl8n1r6gg==", "inBundle": true, "dependencies": { "make-fetch-happen": "^10.0.6", diff --git a/package.json b/package.json index 1c1d76a9d2ae0..0f3a9fd583337 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "8.13.2", + "version": "8.14.0", "name": "npm", "description": "a package manager for JavaScript", "workspaces": [ @@ -58,7 +58,7 @@ "@isaacs/string-locale-compare": "^1.1.0", "@npmcli/arborist": "^5.0.4", "@npmcli/ci-detect": "^2.0.0", - "@npmcli/config": "^4.1.0", + "@npmcli/config": "^4.2.0", "@npmcli/fs": "^2.1.0", "@npmcli/map-workspaces": "^2.0.3", "@npmcli/package-json": "^2.0.0", @@ -90,7 +90,7 @@ "libnpmsearch": "^5.0.2", "libnpmteam": "^4.0.2", "libnpmversion": "^3.0.1", - "make-fetch-happen": "^10.1.8", + "make-fetch-happen": "^10.2.0", "minipass": "^3.1.6", "minipass-pipeline": "^1.2.4", "mkdirp": "^1.0.4", @@ -103,7 +103,7 @@ "npm-package-arg": "^9.1.0", "npm-pick-manifest": "^7.0.1", "npm-profile": "^6.2.0", - "npm-registry-fetch": "^13.2.0", + "npm-registry-fetch": "^13.3.0", "npm-user-validate": "^1.0.1", "npmlog": "^6.0.2", "opener": "^1.5.2", @@ -209,7 +209,7 @@ "tap": "^16.0.1" }, "scripts": { - "dependencies": "node scripts/bundle-and-gitignore-deps.js", + "dependencies": "node scripts/bundle-and-gitignore-deps.js && node scripts/dependency-graph.js", "dumpconf": "env | grep npm | sort | uniq", "preversion": "bash scripts/update-authors.sh && git add AUTHORS && git commit -m \"chore: update AUTHORS\" || true", "licenses": "licensee --production --errors-only", diff --git a/tap-snapshots/test/lib/commands/publish.js.test.cjs b/tap-snapshots/test/lib/commands/publish.js.test.cjs index f90cf3152e80f..d85a1164e22bf 100644 --- a/tap-snapshots/test/lib/commands/publish.js.test.cjs +++ b/tap-snapshots/test/lib/commands/publish.js.test.cjs @@ -56,7 +56,11 @@ Array [ ] ` -exports[`test/lib/commands/publish.js TAP has auth for scope configured registry > new package version 1`] = ` +exports[`test/lib/commands/publish.js TAP has mTLS auth for scope configured registry > new package version 1`] = ` ++ @npm/test-package@1.0.0 +` + +exports[`test/lib/commands/publish.js TAP has token auth for scope configured registry > new package version 1`] = ` + @npm/test-package@1.0.0 ` diff --git a/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs b/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs index 04d304a2254d9..89c9969d69424 100644 --- a/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs +++ b/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs @@ -404,8 +404,9 @@ newlines replaced by the string "\\n". For example: cert="-----BEGIN CERTIFICATE-----\\nXXXX\\nXXXX\\n-----END CERTIFICATE-----" \`\`\` -It is _not_ the path to a certificate file (and there is no "certfile" -option). +It is _not_ the path to a certificate file, though you can set a +registry-scoped "certfile" path like +"//other-registry.tld/:certfile=/path/to/cert.pem". ` exports[`test/lib/utils/config/definitions.js TAP > config description for ci-name 1`] = ` @@ -1016,7 +1017,8 @@ format with newlines replaced by the string "\\n". For example: key="-----BEGIN PRIVATE KEY-----\\nXXXX\\nXXXX\\n-----END PRIVATE KEY-----" \`\`\` -It is _not_ the path to a key file (and there is no "keyfile" option). +It is _not_ the path to a key file, though you can set a registry-scoped +"keyfile" path like "//other-registry.tld/:keyfile=/path/to/key.pem". ` exports[`test/lib/utils/config/definitions.js TAP > config description for legacy-bundling 1`] = ` diff --git a/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs b/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs index a291af6dedfae..a9247f49c0418 100644 --- a/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs +++ b/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs @@ -230,8 +230,9 @@ newlines replaced by the string "\\n". For example: cert="-----BEGIN CERTIFICATE-----\\nXXXX\\nXXXX\\n-----END CERTIFICATE-----" \`\`\` -It is _not_ the path to a certificate file (and there is no "certfile" -option). +It is _not_ the path to a certificate file, though you can set a +registry-scoped "certfile" path like +"//other-registry.tld/:certfile=/path/to/cert.pem". @@ -819,7 +820,8 @@ format with newlines replaced by the string "\\n". For example: key="-----BEGIN PRIVATE KEY-----\\nXXXX\\nXXXX\\n-----END PRIVATE KEY-----" \`\`\` -It is _not_ the path to a key file (and there is no "keyfile" option). +It is _not_ the path to a key file, though you can set a registry-scoped +"keyfile" path like "//other-registry.tld/:keyfile=/path/to/key.pem". diff --git a/test/lib/commands/publish.js b/test/lib/commands/publish.js index 3cbe962382e21..16b79df532d82 100644 --- a/test/lib/commands/publish.js +++ b/test/lib/commands/publish.js @@ -327,7 +327,7 @@ t.test('no auth for scope configured registry', async t => { ) }) -t.test('has auth for scope configured registry', async t => { +t.test('has token auth for scope configured registry', async t => { const spec = npa('@npm/test-package') const { npm, joinedOutput } = await loadMockNpm(t, { config: { @@ -356,6 +356,35 @@ t.test('has auth for scope configured registry', async t => { t.matchSnapshot(joinedOutput(), 'new package version') }) +t.test('has mTLS auth for scope configured registry', async t => { + const spec = npa('@npm/test-package') + const { npm, joinedOutput } = await loadMockNpm(t, { + config: { + '@npm:registry': alternateRegistry, + [`${alternateRegistry.slice(6)}/:certfile`]: '/some.cert', + [`${alternateRegistry.slice(6)}/:keyfile`]: '/some.key', + }, + prefixDir: { + 'package.json': JSON.stringify({ + name: '@npm/test-package', + version: '1.0.0', + }, null, 2), + }, + globals: ({ prefix }) => ({ + 'process.cwd': () => prefix, + }), + }) + const registry = new MockRegistry({ + tap: t, + registry: alternateRegistry, + }) + registry.nock.put(`/${spec.escapedName}`, body => { + return t.match(body, { name: '@npm/test-package' }) + }).reply(200, {}) + await npm.exec('publish', []) + t.matchSnapshot(joinedOutput(), 'new package version') +}) + t.test('workspaces', t => { const dir = { 'package.json': JSON.stringify( diff --git a/test/lib/commands/whoami.js b/test/lib/commands/whoami.js index ad7c223888df4..d63b49015f0d0 100644 --- a/test/lib/commands/whoami.js +++ b/test/lib/commands/whoami.js @@ -34,6 +34,20 @@ t.test('npm whoami --json', async t => { t.equal(JSON.parse(joinedOutput()), username, 'should print username') }) +t.test('npm whoami using mTLS', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { config: { + '//registry.npmjs.org/:certfile': '/some.cert', + '//registry.npmjs.org/:keyfile': '/some.key', + } }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.whoami({ username }) + await npm.exec('whoami', []) + t.equal(joinedOutput(), username, 'should print username') +}) + t.test('credentials from token', async t => { const { npm, joinedOutput } = await loadMockNpm(t, { config: { diff --git a/workspaces/arborist/tap-snapshots/test/arborist/rebuild.js.test.cjs b/workspaces/arborist/tap-snapshots/test/arborist/rebuild.js.test.cjs index c268752aad0f6..473a466789550 100644 --- a/workspaces/arborist/tap-snapshots/test/arborist/rebuild.js.test.cjs +++ b/workspaces/arborist/tap-snapshots/test/arborist/rebuild.js.test.cjs @@ -8,7 +8,7 @@ exports[`test/arborist/rebuild.js TAP verify dep flags in script environments > saved script results 1`] = ` Array [ Object { - "cmd": "{TMP}/postinstall{TIMESTAMP}", + "cmd": "{TMP}/postinstall{HASH}", "code": 0, "event": "postinstall", "pkg": Object { @@ -30,7 +30,7 @@ Array [ "stdout": "npm_package_dev\\n", }, Object { - "cmd": "{TMP}/postinstall{TIMESTAMP}", + "cmd": "{TMP}/postinstall{HASH}", "code": 0, "event": "postinstall", "pkg": Object { @@ -46,7 +46,7 @@ Array [ "stdout": "npm_package_dev_optional\\n", }, Object { - "cmd": "{TMP}/postinstall{TIMESTAMP}", + "cmd": "{TMP}/postinstall{HASH}", "code": 0, "event": "postinstall", "pkg": Object { @@ -66,7 +66,7 @@ Array [ ), }, Object { - "cmd": "{TMP}/postinstall{TIMESTAMP}", + "cmd": "{TMP}/postinstall{HASH}", "code": 0, "event": "postinstall", "pkg": Object { diff --git a/workspaces/arborist/tap-snapshots/test/spec-from-lock.js.test.cjs b/workspaces/arborist/tap-snapshots/test/spec-from-lock.js.test.cjs index 840f0eaa15021..c67ddf275ccb5 100644 --- a/workspaces/arborist/tap-snapshots/test/spec-from-lock.js.test.cjs +++ b/workspaces/arborist/tap-snapshots/test/spec-from-lock.js.test.cjs @@ -15,6 +15,7 @@ Result { "fetchSpec": "{..}/some/path", "gitCommittish": undefined, "gitRange": undefined, + "gitSubdir": undefined, "hosted": undefined, "name": "x", "raw": "x@file:../some/path", @@ -33,6 +34,7 @@ Result { "fetchSpec": "{CWD}/x-1.2.3.tgz", "gitCommittish": undefined, "gitRange": undefined, + "gitSubdir": undefined, "hosted": undefined, "name": "x", "raw": "x@x-1.2.3.tgz", @@ -51,6 +53,7 @@ Result { "fetchSpec": "/path/to/x-1.2.3.tgz", "gitCommittish": undefined, "gitRange": undefined, + "gitSubdir": undefined, "hosted": undefined, "name": "x", "raw": "x@/path/to/x-1.2.3.tgz", @@ -69,6 +72,7 @@ Result { "fetchSpec": "/path/to/x-1.2.3.tgz", "gitCommittish": undefined, "gitRange": undefined, + "gitSubdir": undefined, "hosted": undefined, "name": "x", "raw": "x@/path/to/x-1.2.3.tgz", @@ -87,6 +91,7 @@ Result { "fetchSpec": "ssh://git@github.com/isaacs/abbrev-js.git", "gitCommittish": "a9ee72ebc8fe3975f1b0c7aeb3a8f2a806a432eb", "gitRange": undefined, + "gitSubdir": undefined, "hosted": GitHost { "auth": null, "browsefiletemplate": "function browsefiletemplate", @@ -140,6 +145,7 @@ Result { "fetchSpec": "1.2.3", "gitCommittish": undefined, "gitRange": undefined, + "gitSubdir": undefined, "hosted": undefined, "name": "legacy", "raw": "legacy@1.2.3", @@ -158,6 +164,7 @@ Result { "fetchSpec": "{CWD}/foo.tgz", "gitCommittish": undefined, "gitRange": undefined, + "gitSubdir": undefined, "hosted": undefined, "name": "x", "raw": "x@foo.tgz", @@ -176,6 +183,7 @@ Result { "fetchSpec": "1.2.3", "gitCommittish": undefined, "gitRange": undefined, + "gitSubdir": undefined, "hosted": undefined, "name": "x", "raw": "x@1.2.3", @@ -194,6 +202,7 @@ Result { "fetchSpec": "1.2.3", "gitCommittish": undefined, "gitRange": undefined, + "gitSubdir": undefined, "hosted": undefined, "name": "x", "raw": "x@1.2.3", diff --git a/workspaces/arborist/test/arborist/rebuild.js b/workspaces/arborist/test/arborist/rebuild.js index cac811aacf99e..ba31c5a209c3d 100644 --- a/workspaces/arborist/test/arborist/rebuild.js +++ b/workspaces/arborist/test/arborist/rebuild.js @@ -189,7 +189,7 @@ t.test('verify dep flags in script environments', async t => { t.cleanSnapshot = (input) => { return input.replace(new RegExp(os.tmpdir().replace(/\\/g, '\\\\\\\\'), 'g'), '{TMP}') .replace(/\\\\/g, '/') - .replace(/(\d+)\.(?:sh|cmd)/g, '{TIMESTAMP}') + .replace(/-(.+)\.(?:sh|cmd)/g, '{HASH}') } t.matchSnapshot(saved, 'saved script results')