diff --git a/deps/npm/docs/content/commands/npm-audit.md b/deps/npm/docs/content/commands/npm-audit.md index 206a33f53a6..48e0a3161e8 100644 --- a/deps/npm/docs/content/commands/npm-audit.md +++ b/deps/npm/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/deps/npm/docs/content/configuring-npm/folders.md b/deps/npm/docs/content/configuring-npm/folders.md index 218870765b2..5bab80ec169 100644 --- a/deps/npm/docs/content/configuring-npm/folders.md +++ b/deps/npm/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/deps/npm/docs/content/configuring-npm/package-json.md b/deps/npm/docs/content/configuring-npm/package-json.md index 826f10ce825..f0315d60efe 100644 --- a/deps/npm/docs/content/configuring-npm/package-json.md +++ b/deps/npm/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/deps/npm/docs/content/using-npm/config.md b/deps/npm/docs/content/using-npm/config.md index ffbf9be0557..e3e1bd6c73b 100644 --- a/deps/npm/docs/content/using-npm/config.md +++ b/deps/npm/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/deps/npm/docs/output/commands/npm-audit.html b/deps/npm/docs/output/commands/npm-audit.html index ee315493a27..fa2c4ad52bc 100644 --- a/deps/npm/docs/output/commands/npm-audit.html +++ b/deps/npm/docs/output/commands/npm-audit.html @@ -171,13 +171,46 @@

Description

will cause the command to fail. This option does not filter the report 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:

+
$ 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:
  2. +
+
"dist":{
+  "..omitted..": "..omitted..",
+  "signatures": [{
+    "keyid": "SHA256:{{SHA256_PUBLIC_KEY}}",
+    "sig": "a312b9c3cb4a1b693e8ebac5ee1ca9cc01f2661c14391917dcb111517f72370809..."
+  }]
+}
+
+

See this example 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.

+
    +
  1. Public signing keys are provided at registry-host.tld/-/npm/v1/keys in the following format:
  2. +
+
{
+  "keys": [{
+    "expires": null,
+    "keyid": "SHA256:{{SHA256_PUBLIC_KEY}}",
+    "keytype": "ecdsa-sha2-nistp256",
+    "scheme": "ecdsa-sha2-nistp256",
+    "key": "{{B64_PUBLIC_KEY}}"
+  }]
+}
+
+

Keys response:

+ +

See this example key's response from the public npm registry.

Audit Endpoints

There are two audit endpoints that npm may use to fetch vulnerability information: the Bulk Advisory endpoint and the Quick Audit endpoint.

diff --git a/deps/npm/docs/output/commands/npm-ls.html b/deps/npm/docs/output/commands/npm-ls.html index 4483459262c..47b3bbc085e 100644 --- a/deps/npm/docs/output/commands/npm-ls.html +++ b/deps/npm/docs/output/commands/npm-ls.html @@ -166,7 +166,7 @@

Description

the results to only the paths to the packages named. Note that nested packages will also show the paths to the specified packages. For example, running npm ls promzard in npm's source tree will show:

-
npm@8.14.0 /path/to/npm
+
npm@8.15.0 /path/to/npm
 └─┬ init-package-json@0.0.4
   └── promzard@0.1.5
 
diff --git a/deps/npm/docs/output/commands/npm.html b/deps/npm/docs/output/commands/npm.html index cbe5fbee3e1..514017fd875 100644 --- a/deps/npm/docs/output/commands/npm.html +++ b/deps/npm/docs/output/commands/npm.html @@ -149,7 +149,7 @@

Table of contents

Version

-

8.14.0

+

8.15.0

Description

npm is the package manager for the Node JavaScript platform. It puts modules in place so that node can find them, and manages dependency diff --git a/deps/npm/docs/output/configuring-npm/folders.html b/deps/npm/docs/output/configuring-npm/folders.html index 6d722466c52..2968dacd5ee 100644 --- a/deps/npm/docs/output/configuring-npm/folders.html +++ b/deps/npm/docs/output/configuring-npm/folders.html @@ -291,7 +291,7 @@

Example

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 (and dev dependencies) locally, but only re-publish those items that diff --git a/deps/npm/docs/output/configuring-npm/package-json.html b/deps/npm/docs/output/configuring-npm/package-json.html index 665dad43671..354069b1a2c 100644 --- a/deps/npm/docs/output/configuring-npm/package-json.html +++ b/deps/npm/docs/output/configuring-npm/package-json.html @@ -142,7 +142,7 @@

package.json

Table of contents

- +

Description

@@ -772,19 +772,19 @@

peerDependenciesMeta

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:

If we define a package.json like this:

{
   "name": "awesome-web-framework",
   "version": "1.0.0",
-  "bundledDependencies": [
+  "bundleDependencies": [
     "renderized",
     "super-streams"
   ]
@@ -794,8 +794,8 @@ 

bundledDependencies

This file contains the dependencies renderized and super-streams which 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.

-

Alternatively, "bundledDependencies" can be defined as a boolean value. A +

If this is spelled "bundledDependencies", then that is also honored.

+

Alternatively, "bundleDependencies" can be defined as a boolean value. A value of true will bundle all dependencies, a value of false will bundle none.

optionalDependencies

diff --git a/deps/npm/docs/output/using-npm/config.html b/deps/npm/docs/output/using-npm/config.html index 900c08b3623..79bf005d61e 100644 --- a/deps/npm/docs/output/using-npm/config.html +++ b/deps/npm/docs/output/using-npm/config.html @@ -428,8 +428,9 @@

cert

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".

ci-name

@@ -907,7 +908,8 @@

key

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".

legacy-bundling

diff --git a/deps/npm/lib/auth/sso.js b/deps/npm/lib/auth/sso.js index 9812a18cb99..621ead5d21b 100644 --- a/deps/npm/lib/auth/sso.js +++ b/deps/npm/lib/auth/sso.js @@ -52,7 +52,7 @@ const login = async (npm, { creds, registry, scope }) => { authType: ssoType, } - const { token, sso } = await otplease(opts, + const { token, sso } = await otplease(npm, opts, opts => profile.loginCouch(auth.username, auth.password, opts) ) diff --git a/deps/npm/lib/commands/access.js b/deps/npm/lib/commands/access.js index bc8ce48bacd..0a80da8ddd0 100644 --- a/deps/npm/lib/commands/access.js +++ b/deps/npm/lib/commands/access.js @@ -180,7 +180,7 @@ class Access extends BaseCommand { modifyPackage (pkg, opts, fn, requireScope = true) { return this.getPackage(pkg, requireScope) - .then(pkgName => otplease(opts, opts => fn(pkgName, opts))) + .then(pkgName => otplease(this.npm, opts, opts => fn(pkgName, opts))) } async getPackage (name, requireScope) { diff --git a/deps/npm/lib/commands/adduser.js b/deps/npm/lib/commands/adduser.js index 5e23f40dc59..2853269ef3d 100644 --- a/deps/npm/lib/commands/adduser.js +++ b/deps/npm/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/deps/npm/lib/commands/deprecate.js b/deps/npm/lib/commands/deprecate.js index 862c214dbe5..068bfdbcec7 100644 --- a/deps/npm/lib/commands/deprecate.js +++ b/deps/npm/lib/commands/deprecate.js @@ -60,7 +60,7 @@ class Deprecate extends BaseCommand { packument.versions[v].deprecated = msg }) - return otplease(this.npm.flatOptions, opts => fetch(uri, { + return otplease(this.npm, this.npm.flatOptions, opts => fetch(uri, { ...opts, spec: p, method: 'PUT', diff --git a/deps/npm/lib/commands/dist-tag.js b/deps/npm/lib/commands/dist-tag.js index e74a3f1d435..8052e4f7e4e 100644 --- a/deps/npm/lib/commands/dist-tag.js +++ b/deps/npm/lib/commands/dist-tag.js @@ -116,7 +116,7 @@ class DistTag extends BaseCommand { }, spec, } - await otplease(reqOpts, reqOpts => regFetch(url, reqOpts)) + await otplease(this.npm, reqOpts, reqOpts => regFetch(url, reqOpts)) this.npm.output(`+${t}: ${spec.name}@${version}`) } @@ -142,7 +142,7 @@ class DistTag extends BaseCommand { method: 'DELETE', spec, } - await otplease(reqOpts, reqOpts => regFetch(url, reqOpts)) + await otplease(this.npm, reqOpts, reqOpts => regFetch(url, reqOpts)) this.npm.output(`-${tag}: ${spec.name}@${version}`) } diff --git a/deps/npm/lib/commands/hook.js b/deps/npm/lib/commands/hook.js index a4619802d84..bb3a34b8d2d 100644 --- a/deps/npm/lib/commands/hook.js +++ b/deps/npm/lib/commands/hook.js @@ -22,7 +22,7 @@ class Hook extends BaseCommand { static ignoreImplicitWorkspace = true async exec (args) { - return otplease({ + return otplease(this.npm, { ...this.npm.flatOptions, }, (opts) => { switch (args[0]) { diff --git a/deps/npm/lib/commands/org.js b/deps/npm/lib/commands/org.js index e2202a9e9cf..599b4b9c875 100644 --- a/deps/npm/lib/commands/org.js +++ b/deps/npm/lib/commands/org.js @@ -33,7 +33,7 @@ class Org extends BaseCommand { } async exec ([cmd, orgname, username, role], cb) { - return otplease({ + return otplease(this.npm, { ...this.npm.flatOptions, }, opts => { switch (cmd) { diff --git a/deps/npm/lib/commands/owner.js b/deps/npm/lib/commands/owner.js index 732bb40a300..824b64e044e 100644 --- a/deps/npm/lib/commands/owner.js +++ b/deps/npm/lib/commands/owner.js @@ -202,7 +202,7 @@ class Owner extends BaseCommand { const dataPath = `/${spec.escapedName}/-rev/${encodeURIComponent(data._rev)}` try { - const res = await otplease(this.npm.flatOptions, opts => { + const res = await otplease(this.npm, this.npm.flatOptions, opts => { return npmFetch.json(dataPath, { ...opts, method: 'PUT', diff --git a/deps/npm/lib/commands/profile.js b/deps/npm/lib/commands/profile.js index fcf0eb7d53f..27060cf73a6 100644 --- a/deps/npm/lib/commands/profile.js +++ b/deps/npm/lib/commands/profile.js @@ -221,7 +221,7 @@ class Profile extends BaseCommand { newUser[prop] = value - const result = await otplease(conf, conf => npmProfile.set(newUser, conf)) + const result = await otplease(this.npm, conf, conf => npmProfile.set(newUser, conf)) if (this.npm.config.get('json')) { this.npm.output(JSON.stringify({ [prop]: result[prop] }, null, 2)) diff --git a/deps/npm/lib/commands/publish.js b/deps/npm/lib/commands/publish.js index 579f5d6e74e..3d17866a684 100644 --- a/deps/npm/lib/commands/publish.js +++ b/deps/npm/lib/commands/publish.js @@ -61,7 +61,8 @@ class Publish extends BaseCommand { throw new Error('Tag name must not be a valid SemVer range: ' + defaultTag.trim()) } - const opts = { ...this.npm.flatOptions } + const opts = { ...this.npm.flatOptions, progress: false } + log.disableProgress() // you can publish name@version, ./foo.tgz, etc. // even though the default is the 'file:.' cwd. @@ -101,7 +102,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) { @@ -116,7 +117,7 @@ class Publish extends BaseCommand { log.notice('', `Publishing to ${outputRegistry}${dryRun ? ' (dry-run)' : ''}`) if (!dryRun) { - await otplease(opts, opts => libpub(manifest, tarballData, opts)) + await otplease(this.npm, opts, opts => libpub(manifest, tarballData, opts)) } if (spec.type === 'directory' && !ignoreScripts) { diff --git a/deps/npm/lib/commands/team.js b/deps/npm/lib/commands/team.js index 640002456ac..2d4fc663715 100644 --- a/deps/npm/lib/commands/team.js +++ b/deps/npm/lib/commands/team.js @@ -44,7 +44,7 @@ class Team extends BaseCommand { // XXX: "description" option to libnpmteam is used as a description of the // team, but in npm's options, this is a boolean meaning "show the // description in npm search output". Hence its being set to null here. - await otplease({ ...this.npm.flatOptions }, opts => { + await otplease(this.npm, { ...this.npm.flatOptions }, opts => { entity = entity.replace(/^@/, '') switch (cmd) { case 'create': return this.create(entity, opts) diff --git a/deps/npm/lib/commands/token.js b/deps/npm/lib/commands/token.js index edfb07b9d3a..cf3b8cbee53 100644 --- a/deps/npm/lib/commands/token.js +++ b/deps/npm/lib/commands/token.js @@ -121,7 +121,7 @@ class Token extends BaseCommand { }) await Promise.all( toRemove.map(key => { - return otplease(conf, conf => { + return otplease(this.npm, conf, conf => { return profile.removeToken(key, conf) }) }) @@ -146,7 +146,7 @@ class Token extends BaseCommand { const validCIDR = this.validateCIDRList(cidr) log.info('token', 'creating') return pulseTillDone.withPromise( - otplease(conf, conf => { + otplease(this.npm, conf, conf => { return profile.createToken(password, readonly, validCIDR, conf) }) ) diff --git a/deps/npm/lib/commands/unpublish.js b/deps/npm/lib/commands/unpublish.js index ab929d98cad..0e5ef3dc5e9 100644 --- a/deps/npm/lib/commands/unpublish.js +++ b/deps/npm/lib/commands/unpublish.js @@ -130,7 +130,7 @@ class Unpublish extends BaseCommand { } if (!dryRun) { - await otplease(opts, opts => libunpub(spec, opts)) + await otplease(this.npm, opts, opts => libunpub(spec, opts)) } if (!silent) { this.npm.output(`- ${pkgName}${pkgVersion}`) diff --git a/deps/npm/lib/utils/config/definitions.js b/deps/npm/lib/utils/config/definitions.js index 665ed1efe5e..7d6af2473f2 100644 --- a/deps/npm/lib/utils/config/definitions.js +++ b/deps/npm/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/deps/npm/lib/utils/get-identity.js b/deps/npm/lib/utils/get-identity.js index f4aedb89b39..41d882473ab 100644 --- a/deps/npm/lib/utils/get-identity.js +++ b/deps/npm/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/deps/npm/lib/utils/otplease.js b/deps/npm/lib/utils/otplease.js index 83985b6bc17..0e20e7a797a 100644 --- a/deps/npm/lib/utils/otplease.js +++ b/deps/npm/lib/utils/otplease.js @@ -1,17 +1,46 @@ -async function otplease (opts, fn) { +async function otplease (npm, opts, fn) { try { return await fn(opts) } catch (err) { - const readUserInfo = require('./read-user-info.js') - if (err.code !== 'EOTP' && (err.code !== 'E401' || !/one-time pass/.test(err.body))) { + if (!process.stdin.isTTY || !process.stdout.isTTY) { throw err - } else if (!process.stdin.isTTY || !process.stdout.isTTY) { - throw err - } else { + } + + if (isWebOTP(err)) { + const webAuth = require('./web-auth') + const openUrlPrompt = require('./open-url-prompt') + + const openerPromise = (url, emitter) => + openUrlPrompt( + npm, + url, + 'Authenticate your account at', + 'Press ENTER to open in the browser...', + emitter + ) + const otp = await webAuth(openerPromise, err.body.authUrl, err.body.doneUrl, opts) + return await fn({ ...opts, otp }) + } + + if (isClassicOTP(err)) { + const readUserInfo = require('./read-user-info.js') const otp = await readUserInfo.otp('This operation requires a one-time password.\nEnter OTP:') return await fn({ ...opts, otp }) } + + throw err + } +} + +function isWebOTP (err) { + if (!err.code === 'EOTP' || !err.body) { + return false } + return err.body.authUrl && err.body.doneUrl +} + +function isClassicOTP (err) { + return err.code === 'EOTP' || (err.code === 'E401' && /one-time pass/.test(err.body)) } module.exports = otplease diff --git a/deps/npm/lib/utils/web-auth.js b/deps/npm/lib/utils/web-auth.js new file mode 100644 index 00000000000..ce551687098 --- /dev/null +++ b/deps/npm/lib/utils/web-auth.js @@ -0,0 +1,20 @@ +const EventEmitter = require('events') +const { webAuthCheckLogin } = require('npm-profile') + +async function webAuth (opener, initialUrl, doneUrl, opts) { + const doneEmitter = new EventEmitter() + + const openPromise = opener(initialUrl, doneEmitter) + const webAuthCheckPromise = webAuthCheckLogin(doneUrl, { ...opts, cache: false }) + .then(authResult => { + // cancel open prompt if it's present + doneEmitter.emit('abort') + + return authResult.token + }) + + await openPromise + return await webAuthCheckPromise +} + +module.exports = webAuth diff --git a/deps/npm/man/man1/npm-audit.1 b/deps/npm/man/man1/npm-audit.1 index 63ef87d0a71..ac628b8a80c 100644 --- a/deps/npm/man/man1/npm-audit.1 +++ b/deps/npm/man/man1/npm-audit.1 @@ -31,14 +31,74 @@ will cause the command to fail\. This option does not filter the report output, it simply changes the command's failure threshold\. .SS Audit Signatures .P -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 \fB/\-/npm/v1/keys\fP on -each the registry used to download any given package\. It will then -check the \fBdist\.signatures\fP object in the package itself, and verify the -\fBsig\fP present there using the \fBkeyid\fP there, matching it with a key -returned from the registry\. The command for this is \fBnpm audit -signatures\fP +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\. +.P +Registry signatures can be verified using the following \fBaudit\fP command: +.P +.RS 2 +.nf +$ npm audit signatures +.fi +.RE +.P +The npm CLI supports registry signatures and signing keys provided by any registry if the following conventions are followed: +.RS 0 +.IP 1. 3 +Signatures are provided in the package's \fBpackument\fP in each published version within the \fBdist\fP object: + +.RE +.P +.RS 2 +.nf +"dist":{ + "\.\.omitted\.\.": "\.\.omitted\.\.", + "signatures": [{ + "keyid": "SHA256:{{SHA256_PUBLIC_KEY}}", + "sig": "a312b9c3cb4a1b693e8ebac5ee1ca9cc01f2661c14391917dcb111517f72370809\.\.\." + }] +} +.fi +.RE +.P +See this example \fIhttps://registry\.npmjs\.org/light\-cycle/1\.4\.3\fR of a signed package from the public npm registry\. +.P +The \fBsig\fP is generated using the following template: \fB${package\.name}@${package\.version}:${package\.dist\.integrity}\fP and the \fBkeyid\fP has to match one of the public signing keys below\. +.RS 0 +.IP 1. 3 +Public signing keys are provided at \fBregistry\-host\.tld/\-/npm/v1/keys\fP in the following format: + +.RE +.P +.RS 2 +.nf +{ + "keys": [{ + "expires": null, + "keyid": "SHA256:{{SHA256_PUBLIC_KEY}}", + "keytype": "ecdsa\-sha2\-nistp256", + "scheme": "ecdsa\-sha2\-nistp256", + "key": "{{B64_PUBLIC_KEY}}" + }] +} +.fi +.RE +.P +Keys response: +.RS 0 +.IP \(bu 2 +\fBexpires\fP: null or a simplified extended ISO 8601 format: \fBYYYY\-MM\-DDTHH:mm:ss\.sssZ\fP +.IP \(bu 2 +\fBkeydid\fP: sha256 fingerprint of the public key +.IP \(bu 2 +\fBkeytype\fP: only \fBecdsa\-sha2\-nistp256\fP is currently supported by the npm CLI +.IP \(bu 2 +\fBscheme\fP: only \fBecdsa\-sha2\-nistp256\fP is currently supported by the npm CLI +.IP \(bu 2 +\fBkey\fP: base64 encoded public key + +.RE +.P +See this example key's response from the public npm registry\|\. .SS Audit Endpoints .P There are two audit endpoints that npm may use to fetch vulnerability diff --git a/deps/npm/man/man1/npm-ls.1 b/deps/npm/man/man1/npm-ls.1 index ad4473f2d96..511f481a6ea 100644 --- a/deps/npm/man/man1/npm-ls.1 +++ b/deps/npm/man/man1/npm-ls.1 @@ -26,7 +26,7 @@ example, running \fBnpm ls promzard\fP in npm's source tree will show: .P .RS 2 .nf -npm@8\.14\.0 /path/to/npm +npm@8\.15\.0 /path/to/npm └─┬ init\-package\-json@0\.0\.4 └── promzard@0\.1\.5 .fi diff --git a/deps/npm/man/man1/npm.1 b/deps/npm/man/man1/npm.1 index b9f54eec1ba..984dbc49192 100644 --- a/deps/npm/man/man1/npm.1 +++ b/deps/npm/man/man1/npm.1 @@ -4,7 +4,7 @@ .SS Synopsis .SS Version .P -8\.14\.0 +8\.15\.0 .SS Description .P npm is the package manager for the Node JavaScript platform\. It puts diff --git a/deps/npm/man/man5/folders.5 b/deps/npm/man/man5/folders.5 index 3dda65825fa..924ee0fa243 100644 --- a/deps/npm/man/man5/folders.5 +++ b/deps/npm/man/man5/folders.5 @@ -198,7 +198,7 @@ For a graphical breakdown of what is installed where, use \fBnpm ls\fP\|\. .SS Publishing .P Upon publishing, npm will look in the \fBnode_modules\fP folder\. If any of -the items there are not in the \fBbundledDependencies\fP array, then they will +the items there are not in the \fBbundleDependencies\fP array, then they will not be included in the package tarball\. .P This allows a package maintainer to install all of their dependencies diff --git a/deps/npm/man/man5/package-json.5 b/deps/npm/man/man5/package-json.5 index 78bfc39af2e..0fd5174f6aa 100644 --- a/deps/npm/man/man5/package-json.5 +++ b/deps/npm/man/man5/package-json.5 @@ -924,14 +924,14 @@ Marking a peer dependency as optional ensures npm will not emit a warning if the \fBsoy\-milk\fP 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\. -.SS bundledDependencies +.SS bundleDependencies .P This defines an array of package names that will be bundled when publishing the package\. .P 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 \fBbundledDependencies\fP +tarball file by specifying the package names in the \fBbundleDependencies\fP array and executing \fBnpm pack\fP\|\. .P For example: @@ -943,7 +943,7 @@ If we define a package\.json like this: { "name": "awesome\-web\-framework", "version": "1\.0\.0", - "bundledDependencies": [ + "bundleDependencies": [ "renderized", "super\-streams" ] @@ -957,9 +957,9 @@ can be installed in a new project by executing \fBnpm install awesome\-web\-framework\-1\.0\.0\.tgz\fP\|\. Note that the package names do not include any versions, as that information is specified in \fBdependencies\fP\|\. .P -If this is spelled \fB"bundleDependencies"\fP, then that is also honored\. +If this is spelled \fB"bundledDependencies"\fP, then that is also honored\. .P -Alternatively, \fB"bundledDependencies"\fP can be defined as a boolean value\. A +Alternatively, \fB"bundleDependencies"\fP can be defined as a boolean value\. A value of \fBtrue\fP will bundle all dependencies, a value of \fBfalse\fP will bundle none\. .SS optionalDependencies diff --git a/deps/npm/man/man7/config.7 b/deps/npm/man/man7/config.7 index 1674743956f..bb0c65e9851 100644 --- a/deps/npm/man/man7/config.7 +++ b/deps/npm/man/man7/config.7 @@ -400,8 +400,9 @@ cert="\-\-\-\-\-BEGIN CERTIFICATE\-\-\-\-\-\\nXXXX\\nXXXX\\n\-\-\-\-\-END CERTIF .fi .RE .P -It is \fInot\fR the path to a certificate file (and there is no "certfile" -option)\. +It is \fInot\fR the path to a certificate file, though you can set a +registry\-scoped "certfile" path like +"//other\-registry\.tld/:certfile=/path/to/cert\.pem"\. .SS \fBci\-name\fP .RS 0 .IP \(bu 2 @@ -1012,7 +1013,8 @@ key="\-\-\-\-\-BEGIN PRIVATE KEY\-\-\-\-\-\\nXXXX\\nXXXX\\n\-\-\-\-\-END PRIVATE .fi .RE .P -It is \fInot\fR the path to a key file (and there is no "keyfile" option)\. +It is \fInot\fR the path to a key file, though you can set a registry\-scoped +"keyfile" path like "//other\-registry\.tld/:keyfile=/path/to/key\.pem"\. .SS \fBlegacy\-bundling\fP .RS 0 .IP \(bu 2 diff --git a/deps/npm/node_modules/@npmcli/config/lib/index.js b/deps/npm/node_modules/@npmcli/config/lib/index.js index 5b7ea68e91e..8c2b181ca9c 100644 --- a/deps/npm/node_modules/@npmcli/config/lib/index.js +++ b/deps/npm/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/deps/npm/node_modules/@npmcli/config/package.json b/deps/npm/node_modules/@npmcli/config/package.json index 2cc04e05be8..2f561c12233 100644 --- a/deps/npm/node_modules/@npmcli/config/package.json +++ b/deps/npm/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/deps/npm/node_modules/make-fetch-happen/lib/cache/entry.js b/deps/npm/node_modules/make-fetch-happen/lib/cache/entry.js index 7a7572ba030..4307962b889 100644 --- a/deps/npm/node_modules/make-fetch-happen/lib/cache/entry.js +++ b/deps/npm/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/deps/npm/node_modules/make-fetch-happen/package.json b/deps/npm/node_modules/make-fetch-happen/package.json index e04c7645c4f..8b21901f34f 100644 --- a/deps/npm/node_modules/make-fetch-happen/package.json +++ b/deps/npm/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/deps/npm/node_modules/npm-registry-fetch/lib/auth.js b/deps/npm/node_modules/npm-registry-fetch/lib/auth.js index 17da6a17d71..870ce0d923c 100644 --- a/deps/npm/node_modules/npm-registry-fetch/lib/auth.js +++ b/deps/npm/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/deps/npm/node_modules/npm-registry-fetch/lib/index.js b/deps/npm/node_modules/npm-registry-fetch/lib/index.js index c788febc33a..cc331a50c09 100644 --- a/deps/npm/node_modules/npm-registry-fetch/lib/index.js +++ b/deps/npm/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/deps/npm/node_modules/npm-registry-fetch/package.json b/deps/npm/node_modules/npm-registry-fetch/package.json index 5f19697c3b1..8a0189a9ef7 100644 --- a/deps/npm/node_modules/npm-registry-fetch/package.json +++ b/deps/npm/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/deps/npm/package.json b/deps/npm/package.json index 80e7a4fb0f6..969e8e160c2 100644 --- a/deps/npm/package.json +++ b/deps/npm/package.json @@ -1,5 +1,5 @@ { - "version": "8.14.0", + "version": "8.15.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/deps/npm/tap-snapshots/test/lib/commands/publish.js.test.cjs b/deps/npm/tap-snapshots/test/lib/commands/publish.js.test.cjs index e2d248edf5b..65a9ee02eb9 100644 --- a/deps/npm/tap-snapshots/test/lib/commands/publish.js.test.cjs +++ b/deps/npm/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/deps/npm/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs b/deps/npm/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs index 04d304a2254..89c9969d694 100644 --- a/deps/npm/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs +++ b/deps/npm/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/deps/npm/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs b/deps/npm/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs index a291af6dedf..a9247f49c04 100644 --- a/deps/npm/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs +++ b/deps/npm/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/deps/npm/test/lib/commands/publish.js b/deps/npm/test/lib/commands/publish.js index 3cbe962382e..16b79df532d 100644 --- a/deps/npm/test/lib/commands/publish.js +++ b/deps/npm/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/deps/npm/test/lib/commands/whoami.js b/deps/npm/test/lib/commands/whoami.js index ad7c223888d..d63b49015f0 100644 --- a/deps/npm/test/lib/commands/whoami.js +++ b/deps/npm/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/deps/npm/test/lib/utils/otplease.js b/deps/npm/test/lib/utils/otplease.js index 025084ab4f2..79eaa798e60 100644 --- a/deps/npm/test/lib/utils/otplease.js +++ b/deps/npm/test/lib/utils/otplease.js @@ -1,17 +1,25 @@ const t = require('tap') + +const { fake: mockNpm } = require('../../fixtures/mock-npm') const mockGlobals = require('../../fixtures/mock-globals') const readUserInfo = { otp: async () => '1234', } +const webAuth = async (opener) => { + opener() + return '1234' +} const otplease = t.mock('../../../lib/utils/otplease.js', { '../../../lib/utils/read-user-info.js': readUserInfo, + '../../../lib/utils/open-url-prompt.js': () => {}, + '../../../lib/utils/web-auth': webAuth, }) t.test('returns function results on success', async (t) => { const fn = () => 'test string' - const result = await otplease({}, fn) + const result = await otplease(null, {}, fn) t.equal('test string', result) }) @@ -26,7 +34,7 @@ t.test('returns function results on otp success', async (t) => { } throw Object.assign(new Error('nope'), { code: 'EOTP' }) } - const result = await otplease({}, fn) + const result = await otplease(null, {}, fn) t.equal('success', result) }) @@ -51,7 +59,31 @@ t.test('prompts for otp for EOTP', async (t) => { t.end() } - await otplease({ some: 'prop' }, fn) + await otplease(null, { some: 'prop' }, fn) +}) + +t.test('returns function results on webauth success', async (t) => { + mockGlobals(t, { + 'process.stdin': { isTTY: true }, + 'process.stdout': { isTTY: true }, + }) + + const npm = mockNpm({ config: { browser: 'firefox' } }) + const fn = ({ otp }) => { + if (otp) { + return 'success' + } + throw Object.assign(new Error('nope'), { + code: 'EOTP', + body: { + authUrl: 'https://www.example.com/auth', + doneUrl: 'https://www.example.com/done', + }, + }) + } + + const result = await otplease(npm, {}, fn) + t.equal('success', result) }) t.test('prompts for otp for 401', async (t) => { @@ -78,7 +110,7 @@ t.test('prompts for otp for 401', async (t) => { t.end() } - await otplease({ some: 'prop' }, fn) + await otplease(null, { some: 'prop' }, fn) }) t.test('does not prompt for non-otp errors', async (t) => { @@ -95,7 +127,11 @@ t.test('does not prompt for non-otp errors', async (t) => { throw new Error('nope') } - t.rejects(otplease({ some: 'prop' }, fn), { message: 'nope' }, 'rejects with the original error') + t.rejects( + otplease(null, { some: 'prop' }, fn), + { message: 'nope' }, + 'rejects with the original error' + ) }) t.test('does not prompt if stdin or stdout is not a tty', async (t) => { @@ -112,5 +148,9 @@ t.test('does not prompt if stdin or stdout is not a tty', async (t) => { throw Object.assign(new Error('nope'), { code: 'EOTP' }) } - t.rejects(otplease({ some: 'prop' }, fn), { message: 'nope' }, 'rejects with the original error') + t.rejects( + otplease(null, { some: 'prop' }, fn), + { message: 'nope' }, + 'rejects with the original error' + ) }) diff --git a/deps/npm/test/lib/utils/web-auth.js b/deps/npm/test/lib/utils/web-auth.js new file mode 100644 index 00000000000..ee8a17ecbc0 --- /dev/null +++ b/deps/npm/test/lib/utils/web-auth.js @@ -0,0 +1,32 @@ +const t = require('tap') + +const webAuthCheckLogin = async () => { + return { token: 'otp-token' } +} + +const webauth = t.mock('../../../lib/utils/web-auth.js', { + 'npm-profile': { webAuthCheckLogin }, +}) + +const initialUrl = 'https://example.com/auth' +const doneUrl = 'https://example.com/done' +const opts = {} + +t.test('returns token on success', async (t) => { + const opener = async () => {} + const result = await webauth(opener, initialUrl, doneUrl, opts) + t.equal(result, 'otp-token') +}) + +t.test('closes opener when auth check finishes', async (t) => { + const opener = (_url, emitter) => { + return new Promise((resolve, reject) => { + // the only way to finish this promise is to emit aboter on the emitter + emitter.addListener('abort', () => { + resolve() + }) + }) + } + const result = await webauth(opener, initialUrl, doneUrl, opts) + t.equal(result, 'otp-token') +})