From 6643a3d09e4cbe7c162a2064b6a99350c491ee57 Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@arkins.net>
Date: Thu, 5 Jul 2018 15:44:42 +0200
Subject: [PATCH] feat: npm token substitution in npmrc

If an encrypted npmToken is found alongside an unencrypted npmrc in config, then the token will replace any `${NPM_TOKEN}` placeholder found, or be appended to the end of the file. This enables large npmrc files to be defined in config without needing to enrypt the entire thing.

Closes #1796
---
 lib/config/decrypt.js                         | 22 +++++++++++++++-
 .../config/__snapshots__/decrypt.spec.js.snap |  7 +++++
 test/config/decrypt.spec.js                   | 26 +++++++++++++++++++
 website/docs/private-modules.md               | 10 +++----
 4 files changed, 58 insertions(+), 7 deletions(-)
 create mode 100644 test/config/__snapshots__/decrypt.spec.js.snap

diff --git a/lib/config/decrypt.js b/lib/config/decrypt.js
index 94f0f7bd9d..dccb44c2aa 100644
--- a/lib/config/decrypt.js
+++ b/lib/config/decrypt.js
@@ -25,7 +25,27 @@ function decryptConfig(config, privateKey) {
                 { token: maskToken(token) },
                 'Migrating npmToken to npmrc'
               );
-              decryptedConfig.npmrc = `//registry.npmjs.org/:_authToken=${token}\n`;
+              if (decryptedConfig.npmrc) {
+                /* eslint-disable no-template-curly-in-string */
+                if (decryptedConfig.npmrc.includes('${NPM_TOKEN}')) {
+                  logger.debug('Replacing ${NPM_TOKEN} with decrypted token');
+                  decryptedConfig.npmrc = decryptedConfig.npmrc.replace(
+                    '${NPM_TOKEN}',
+                    token
+                  );
+                } else {
+                  logger.debug(
+                    'Appending _authToken= to end of existing npmrc'
+                  );
+                  decryptedConfig.npmrc = decryptedConfig.npmrc.replace(
+                    /\n?$/,
+                    `\n_authToken=${token}\n`
+                  );
+                }
+                /* eslint-enable no-template-curly-in-string */
+              } else {
+                decryptedConfig.npmrc = `//registry.npmjs.org/:_authToken=${token}\n`;
+              }
             } else {
               decryptedConfig[eKey] = decryptedStr;
             }
diff --git a/test/config/__snapshots__/decrypt.spec.js.snap b/test/config/__snapshots__/decrypt.spec.js.snap
new file mode 100644
index 0000000000..df59cc5018
--- /dev/null
+++ b/test/config/__snapshots__/decrypt.spec.js.snap
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`config/decrypt decryptConfig() appends npm token in npmrc 1`] = `
+"foo=bar
+_authToken=abcdef-ghijklm-nopqf-stuvwxyz
+"
+`;
diff --git a/test/config/decrypt.spec.js b/test/config/decrypt.spec.js
index c96616a8fe..0040dd207e 100644
--- a/test/config/decrypt.spec.js
+++ b/test/config/decrypt.spec.js
@@ -33,6 +33,32 @@ describe('config/decrypt', () => {
       expect(res.encrypted).not.toBeDefined();
       expect(res.a).not.toBeDefined();
     });
+    it('replaces npm token placeholder in npmrc', () => {
+      config.privateKey = privateKey;
+      config.npmrc = '//registry.npmjs.org/:_authToken=${NPM_TOKEN}\n'; // eslint-disable-line no-template-curly-in-string
+      config.encrypted = {
+        npmToken:
+          'FLA9YHIzpE7YetAg/P0X46npGRCMqn7hgyzwX5ZQ9wYgu9BRRbTiBVsUIFTyM5BuP1Q22slT2GkWvFvum7GU236Y6QiT7Nr8SLvtsJn2XUuq8H7REFKzdy3+wqyyWbCErYTFyY1dcPM7Ht+CaGDWdd8u/FsoX7AdMRs/X1jNUo6iSmlUiyGlYDKF+QMnCJom1VPVgZXWsGKdjI2MLny991QMaiv0VajmFIh4ENv4CtXOl/1twvIl/6XTXAaqpJJKDTPZEuydi+PHDZmal2RAOfrkH4m0UURa7SlfpUlIg+EaqbNGp85hCYXLwRcEET1OnYr3rH1oYkcYJ40any1tvQ==',
+      };
+      const res = decryptConfig(config, privateKey);
+      expect(res.encrypted).not.toBeDefined();
+      expect(res.npmToken).not.toBeDefined();
+      expect(res.npmrc).toEqual(
+        '//registry.npmjs.org/:_authToken=abcdef-ghijklm-nopqf-stuvwxyz\n'
+      );
+    });
+    it('appends npm token in npmrc', () => {
+      config.privateKey = privateKey;
+      config.npmrc = 'foo=bar\n'; // eslint-disable-line no-template-curly-in-string
+      config.encrypted = {
+        npmToken:
+          'FLA9YHIzpE7YetAg/P0X46npGRCMqn7hgyzwX5ZQ9wYgu9BRRbTiBVsUIFTyM5BuP1Q22slT2GkWvFvum7GU236Y6QiT7Nr8SLvtsJn2XUuq8H7REFKzdy3+wqyyWbCErYTFyY1dcPM7Ht+CaGDWdd8u/FsoX7AdMRs/X1jNUo6iSmlUiyGlYDKF+QMnCJom1VPVgZXWsGKdjI2MLny991QMaiv0VajmFIh4ENv4CtXOl/1twvIl/6XTXAaqpJJKDTPZEuydi+PHDZmal2RAOfrkH4m0UURa7SlfpUlIg+EaqbNGp85hCYXLwRcEET1OnYr3rH1oYkcYJ40any1tvQ==',
+      };
+      const res = decryptConfig(config, privateKey);
+      expect(res.encrypted).not.toBeDefined();
+      expect(res.npmToken).not.toBeDefined();
+      expect(res.npmrc).toMatchSnapshot();
+    });
     it('decrypts nested', () => {
       config.privateKey = privateKey;
       config.packageFiles = [
diff --git a/website/docs/private-modules.md b/website/docs/private-modules.md
index 393860e880..08d7e1f05e 100644
--- a/website/docs/private-modules.md
+++ b/website/docs/private-modules.md
@@ -115,10 +115,8 @@ The configure it like:
 }
 ```
 
-## Future npm authentication approaches
+Renovate will then use the following logic:
 
-#### Webhooks from npm registry
-
-The npm registry allows for owners of packages to send webhooks to custom destinations whenever the package is updated. Using this approach, it would be possible to notify the Renovate App API of updates to your private npm modules and we store these in our database.
-
-An important downside of this approach to be aware of is that this could solve only Use #1 (module lookup) and not Use #2 (Lock file generation). As it seems inevitable that most projects will adopt lock files - especially projects advanced enough to be using private npm modules - this solution is taking a lower priority compared to the first two, because it may ultimately not be required if lock file support becomes as widespread as expected.
+1.  If no `npmrc` string is present in config then one will be created with the `_authToken` pointing to the default npmjs registry
+2.  If an `npmrc` string is present and contains `${NPM_TOKEN}` then that placeholder will be replaced with the decrypted token
+3.  If an `npmrc` string is present but doesn't contain `${NPM_TOKEN}` then the file will have `_authToken=<token>` appended to it
-- 
GitLab