node-vault-client 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-vault-client",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "A Vault Client implemented in pure javascript for HashiCorp Vault. It supports variety of Auth Backends and performs lease renewal for issued auth token.",
5
5
  "repository": {
6
6
  "url": "git+https://github.com/namecheap/node-vault-client.git"
@@ -15,6 +15,7 @@
15
15
  "scripts": {
16
16
  "test": "mocha --exit \"test/**/*.test.mjs\"",
17
17
  "test:unit": "mocha \"test/*.test.mjs\"",
18
+ "test:e2e": "mocha --exit \"test/e2e/**/*.test.mjs\"",
18
19
  "coverage": "c8 npm run test:unit",
19
20
  "lint": "eslint src/",
20
21
  "version": "git add -A ."
@@ -34,13 +35,9 @@
34
35
  "license": "Apache-2.0",
35
36
  "dependencies": {
36
37
  "@aws-sdk/credential-providers": "^3.382.0",
37
- "assign-deep": "^1.0.1",
38
38
  "aws4": "^1.8.0",
39
- "bluebird": "^3.7.0",
40
- "lodash": "^4.17.15",
41
- "long-timeout": "^0.1.1",
42
- "pretty-ms": "^2.1.0",
43
- "url-join": "^2.0.5"
39
+ "lodash": "^4.17.21",
40
+ "long-timeout": "^0.1.1"
44
41
  },
45
42
  "peerDependencies": {
46
43
  "config": ">=1 <4"
@@ -55,8 +52,8 @@
55
52
  "c8": "^11.0.0",
56
53
  "chai": "^6.0.0",
57
54
  "deep-freeze": "^0.0.1",
58
- "eslint": "^9.0.0",
59
- "globals": "^16.0.0",
55
+ "eslint": "^9.39.4",
56
+ "globals": "^17.6.0",
60
57
  "mocha": "^11.7.6",
61
58
  "sinon": "^22.0.0",
62
59
  "sinon-chai": "^4.0.0"
@@ -1,8 +1,27 @@
1
1
  'use strict';
2
2
 
3
- const urljoin = require('url-join');
4
3
  const _ = require('lodash');
5
4
 
5
+ /**
6
+ * Joins URL segments with single slashes while preserving the leading
7
+ * protocol (e.g. "https://"). Replaces the former `url-join` dependency.
8
+ */
9
+ function joinUrl(...parts) {
10
+ return parts
11
+ .map((part, i) => {
12
+ let segment = String(part);
13
+ if (i > 0) {
14
+ segment = segment.replace(/^\/+/, '');
15
+ }
16
+ if (i < parts.length - 1) {
17
+ segment = segment.replace(/\/+$/, '');
18
+ }
19
+ return segment;
20
+ })
21
+ .filter((segment) => segment.length > 0)
22
+ .join('/');
23
+ }
24
+
6
25
  class VaultApiClient {
7
26
 
8
27
  /**
@@ -35,7 +54,7 @@ class VaultApiClient {
35
54
  data = data === undefined ? null : data;
36
55
  headers = headers === undefined ? {} : headers;
37
56
 
38
- const uri = urljoin(this.__config.url, this.__config.apiVersion, path);
57
+ const uri = joinUrl(this.__config.url, this.__config.apiVersion, path);
39
58
 
40
59
  const requestOptions = this.__config.requestOptions || {};
41
60
 
@@ -80,10 +99,12 @@ class VaultApiClient {
80
99
  throw error;
81
100
  }
82
101
 
83
- this._logger.debug('%s %s response body:\n%s',
102
+ // Do not log the response body: Vault responses carry secret
103
+ // material (auth tokens, secret reads). Log status only.
104
+ this._logger.debug('%s %s -> %d',
84
105
  method,
85
106
  uri,
86
- JSON.stringify(body, null, ' ')
107
+ response.status
87
108
  );
88
109
  return body;
89
110
  });
@@ -1,9 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const path = require('path');
4
- const Bluebird = require('bluebird');
5
4
  const _ = require('lodash');
6
- const assignDeep = require('assign-deep');
7
5
 
8
6
  const errors = require('./errors');
9
7
 
@@ -29,45 +27,54 @@ class VaultNodeConfig {
29
27
  let requiredData = {};
30
28
 
31
29
  this.__traverse(substitutionMap, (key, val) => {
32
- const splitRes = val.split('#');
33
- const path = splitRes[0], value = splitRes[1];
34
- if (path === undefined || value === undefined) {
35
- throw new errors.InvalidArgumentsError('Invalid format of substitution value');
36
- }
37
-
38
- requiredData[path] = null;
30
+ const { vaultPath } = this.__parseSubstitutionValue(val);
31
+ requiredData[vaultPath] = null;
39
32
  });
40
33
 
41
- let promises = _.mapValues(requiredData, (value, path) => this.__vault.read(path));
34
+ const vaultPaths = Object.keys(requiredData);
42
35
 
43
- return Bluebird.props(promises).then(results => {
44
- requiredData = _.mapValues(requiredData, (value, path) => results[path].getData());
36
+ return Promise.all(vaultPaths.map((vaultPath) => this.__vault.read(vaultPath))).then((leases) => {
37
+ const results = _.zipObject(vaultPaths, leases);
38
+ requiredData = _.mapValues(requiredData, (value, vaultPath) => results[vaultPath].getData());
45
39
 
46
40
  this.__traverse(substitutionMap, (key, val, obj) => {
47
- const splitRes = val.split('#');
48
- const path = splitRes[0], value = splitRes[1];
49
- const res = requiredData[path][value];
41
+ const { vaultPath, value } = this.__parseSubstitutionValue(val);
42
+ const res = requiredData[vaultPath][value];
50
43
  if (res === undefined) {
51
44
  throw new errors.VaultError(`Can't find substitution for "${val}"`);
52
45
  }
53
46
 
54
- obj[key] = requiredData[path][value];
47
+ obj[key] = requiredData[vaultPath][value];
55
48
  });
56
49
 
57
- return assignDeep(this.__nodeConfig, substitutionMap);
50
+ return _.merge(this.__nodeConfig, substitutionMap);
58
51
  });
59
52
  }
60
53
 
54
+ /**
55
+ * Splits a substitution value of the form "<vaultPath>#<value>".
56
+ *
57
+ * @private
58
+ */
59
+ __parseSubstitutionValue(val) {
60
+ const [vaultPath, value] = val.split('#');
61
+ if (!vaultPath || !value) {
62
+ throw new errors.InvalidArgumentsError('Invalid format of substitution value');
63
+ }
64
+
65
+ return { vaultPath, value };
66
+ }
67
+
61
68
  /**
62
69
  * @private
63
70
  */
64
71
  __getSubstitutionMap() {
65
- let config_dir = this.__nodeConfig.util.initParam('NODE_CONFIG_DIR', path.join( process.cwd(), 'config') );
66
- if (config_dir.indexOf('.') === 0) {
67
- config_dir = path.join(process.cwd() , config_dir);
72
+ let configDir = this.__nodeConfig.util.initParam('NODE_CONFIG_DIR', path.join(process.cwd(), 'config'));
73
+ if (configDir.indexOf('.') === 0) {
74
+ configDir = path.join(process.cwd(), configDir);
68
75
  }
69
76
 
70
- const fullFilename = path.join(config_dir, 'custom-vault-variables.js');
77
+ const fullFilename = path.join(configDir, 'custom-vault-variables.js');
71
78
 
72
79
  let fileContent;
73
80
  try {
@@ -84,12 +91,8 @@ class VaultNodeConfig {
84
91
  }
85
92
 
86
93
  __traverse(o, func) {
87
- for (let i in o) {
88
- if (!Object.hasOwn(o, i)) {
89
- continue;
90
- }
91
-
92
- if (o[i] !== null && typeof(o[i]) === "object") {
94
+ for (const i of Object.keys(o)) {
95
+ if (o[i] !== null && typeof o[i] === 'object') {
93
96
  //going one step down in the object tree!!
94
97
  this.__traverse(o[i], func);
95
98
  } else if (typeof o[i] === 'string') {
@@ -1,10 +1,26 @@
1
1
  'use strict';
2
2
 
3
3
  const lt = require('long-timeout');
4
- const prettyMs = require('pretty-ms');
5
4
  const AuthToken = require('./AuthToken');
6
5
  const errors = require('../errors');
7
6
 
7
+ /**
8
+ * Formats a millisecond duration as a short human-readable string
9
+ * (e.g. "1s", "2m 5s", "1h 3m"). Replaces the former `pretty-ms` dependency.
10
+ */
11
+ function humanizeMs(ms) {
12
+ const totalSeconds = Math.round(ms / 1000);
13
+ if (totalSeconds < 60) {
14
+ return `${totalSeconds}s`;
15
+ }
16
+ const totalMinutes = Math.floor(totalSeconds / 60);
17
+ if (totalMinutes < 60) {
18
+ return `${totalMinutes}m ${totalSeconds % 60}s`;
19
+ }
20
+ const hours = Math.floor(totalMinutes / 60);
21
+ return `${hours}h ${totalMinutes % 60}m`;
22
+ }
23
+
8
24
  class VaultBaseAuth {
9
25
 
10
26
  /**
@@ -135,7 +151,7 @@ class VaultBaseAuth {
135
151
 
136
152
  this._log.debug(
137
153
  'sleeping for %s',
138
- prettyMs(timer)
154
+ humanizeMs(timer)
139
155
  );
140
156
  }
141
157
 
@@ -28,18 +28,15 @@ class VaultKubernetesAuth extends VaultBaseAuth {
28
28
 
29
29
  const k8sJwtToken = fs.readFileSync(this.__tokenPath).toString();
30
30
  this._log.debug(
31
- 'receive K8s token: %s',
32
- k8sJwtToken
31
+ 'read K8s service-account JWT from "%s" (%d bytes)',
32
+ this.__tokenPath, k8sJwtToken.length
33
33
  );
34
34
 
35
35
  return this.__apiClient.makeRequest('POST', `/auth/${this._mount}/login`, {
36
36
  role: this.__role,
37
37
  jwt: k8sJwtToken,
38
38
  }).then((res) => {
39
- this._log.debug(
40
- 'receive token: %s',
41
- res.auth.client_token
42
- );
39
+ this._log.debug('received Vault client token from Kubernetes login');
43
40
 
44
41
  return this._getTokenEntity(res.auth.client_token);
45
42
  });