node-vault-client 2.0.1 → 2.1.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/README.md CHANGED
@@ -47,14 +47,16 @@ vaultClient.read('secret/tst').then(lease => {
47
47
 
48
48
  ```javascript
49
49
  const vaultClient = VaultClient.boot('main', {
50
- api: { url: 'https://vault.example.com:8200/' },
50
+ api: {
51
+ url: 'https://vault.example.com:8200/',
52
+ namespace: 'some_namespace', // Optional. X-Vault-Namespace header (canonical location; auth.config.namespace is honored as a legacy fallback)
53
+ },
51
54
  auth: {
52
55
  type: 'iam',
53
56
  mount: 'aws', // Optional. Vault AWS auth mount point ("aws" by default)
54
57
  config: {
55
58
  role: 'my_iam_role',
56
59
  iam_server_id_header_value: 'https://vault.example.com:8200/', // Optional. X-Vault-AWS-IAM-Server-ID header
57
- namespace: 'some_namespace', // Optional. X-Vault-Namespace header
58
60
  region: 'eu-central-1', // Optional. AWS STS region (see below)
59
61
  credentials: { // Optional. Resolved from the AWS provider chain when omitted
60
62
  accessKeyId: process.env.AWS_ACCESS_KEY_ID,
@@ -136,6 +138,14 @@ const vaultClient = VaultClient.boot('main', {
136
138
  * [.read(path)](#VaultClient+read) ⇒ <code>Promise.&lt;Lease&gt;</code>
137
139
  * [.list(path)](#VaultClient+list) ⇒ <code>Promise.&lt;Lease&gt;</code>
138
140
  * [.write(path, data)](#VaultClient+write) ⇒ <code>Promise.&lt;Object&gt;</code>
141
+ * [.delete(path)](#VaultClient+delete) ⇒ <code>Promise.&lt;Object&gt;</code>
142
+ * [.update(path, data)](#VaultClient+update) ⇒ <code>Promise.&lt;Object&gt;</code>
143
+ * [.request(method, path, [data])](#VaultClient+request) ⇒ <code>Promise.&lt;Object&gt;</code>
144
+ * [.deleteVersions(path, versions)](#VaultClient+deleteVersions) ⇒ <code>Promise.&lt;Object&gt;</code>
145
+ * [.undeleteVersions(path, versions)](#VaultClient+undeleteVersions) ⇒ <code>Promise.&lt;Object&gt;</code>
146
+ * [.destroyVersions(path, versions)](#VaultClient+destroyVersions) ⇒ <code>Promise.&lt;Object&gt;</code>
147
+ * [.readMetadata(path)](#VaultClient+readMetadata) ⇒ <code>Promise.&lt;Object&gt;</code>
148
+ * [.deleteMetadata(path)](#VaultClient+deleteMetadata) ⇒ <code>Promise.&lt;Object&gt;</code>
139
149
  * [.close()](#VaultClient+close)
140
150
  * _static_
141
151
  * [.boot(name, [options])](#VaultClient.boot) ⇒ <code>VaultClient</code>
@@ -143,6 +153,11 @@ const vaultClient = VaultClient.boot('main', {
143
153
  * [.clear([name])](#VaultClient.clear)
144
154
  * [Lease](#Lease)
145
155
 
156
+ **Return contract**: [`read()`](#VaultClient+read) and [`list()`](#VaultClient+list) resolve to a
157
+ [`Lease`](#Lease) — use its accessors to extract the secret data. Every other data-plane method
158
+ (`write`, `delete`, `update`, `request` and the KV v2 helpers) resolves to the raw parsed Vault
159
+ response body, which may be empty/undefined for `204 No Content` responses.
160
+
146
161
  <a name="new_VaultClient_new"></a>
147
162
 
148
163
  #### new VaultClient(options)
@@ -156,12 +171,15 @@ Client constructor function.
156
171
  | options.api.url | <code>String</code> | | the url of the vault server |
157
172
  | [options.api.apiVersion] | <code>String</code> | `v1` | |
158
173
  | [options.api.requestOptions] | <code>Object</code> | | extra options merged into every HTTP request (see [Custom transport](#custom-transport-proxy--self-signed-tls)) |
174
+ | [options.api.namespace] | <code>String</code> | | Optional. Vault namespace, sent as the `X-Vault-Namespace` header on **every** request — login, token lookup/renewal, and all secret operations — for every auth type. This is the canonical location; `auth.config.namespace` is still honored for backward compatibility. |
175
+ | [options.api.kv.autoDetect] | <code>boolean</code> | `false` | auto-detect the KV version of each mount on first use (see [KV v2 & generic backends](#kv-v2--generic-backends)) |
176
+ | [options.api.engines] | <code>Object</code> | `{}` | static mount-to-version map, e.g. `{ secret: 2, legacy: 1 }` (see [KV v2 & generic backends](#kv-v2--generic-backends)) |
159
177
  | options.auth | <code>Object</code> | | |
160
178
  | options.auth.type | <code>String</code> | | |
161
179
  | [options.auth.mount] | <code>String</code> | | Vault auth backend mount point; default varies per method (e.g. "aws" for iam, "approle", "token", "kubernetes") |
162
180
  | options.auth.config | <code>Object</code> | | auth configuration variables |
163
- | [options.auth.config.namespace] | <code>String</code> | | Optional. Vault namespace, sent as the `X-Vault-Namespace` header on all secret read/list/write requests. Applies to every auth type, not just IAM. |
164
- | options.logger | <code>Object</code> | `false` | | Logger that supports "error", "info", "warn", "trace", "debug" methods. Uses `console` by default. Pass `false` to disable logging. |
181
+ | [options.auth.config.namespace] | <code>String</code> | | Optional. Legacy location for the Vault namespace (see `api.namespace`). Sent as the `X-Vault-Namespace` header on **every** request for every auth type. |
182
+ | [options.logger] | <code>Object</code> \| <code>false</code> | | Logger that supports "error", "info", "warn", "trace", "debug" methods. Uses `console` by default. Pass `false` to disable logging. |
165
183
 
166
184
  ##### Custom transport (proxy / self-signed TLS)
167
185
 
@@ -196,6 +214,8 @@ env var with no code change. Only disable verification
196
214
  (`new Agent({ connect: { rejectUnauthorized: false } })`) in throwaway/dev setups — it removes
197
215
  MITM protection.
198
216
 
217
+ <a name="VaultClient+fillNodeConfig"></a>
218
+
199
219
  #### vaultClient.fillNodeConfig() ⇒ <code>Promise</code>
200
220
  Populates Vault's values to NPM "config" module
201
221
 
@@ -236,9 +256,131 @@ Resolves to the raw parsed Vault response body, which may be empty/undefined for
236
256
 
237
257
  | Param | Type | Description |
238
258
  | --- | --- | --- |
239
- | path | | path used to write data |
259
+ | path | <code>string</code> | path used to write data |
240
260
  | data | <code>object</code> | data to write |
241
261
 
262
+ <a name="VaultClient+delete"></a>
263
+
264
+ #### vaultClient.delete(path) ⇒ <code>Promise.&lt;Object&gt;</code>
265
+ Deletes a secret
266
+
267
+ On KV v2 mounts this sends `DELETE` to the `data/` path, soft-deleting the latest version.
268
+ On KV v1 / non-KV mounts this sends `DELETE` to the raw path. Resolves to the raw parsed
269
+ Vault response body, which may be empty/undefined for `204 No Content` responses.
270
+
271
+ **Kind**: instance method of [<code>VaultClient</code>](#VaultClient)
272
+
273
+ | Param | Type | Description |
274
+ | --- | --- | --- |
275
+ | path | <code>string</code> | path to the secret |
276
+
277
+ <a name="VaultClient+update"></a>
278
+
279
+ #### vaultClient.update(path, data) ⇒ <code>Promise.&lt;Object&gt;</code>
280
+ Updates (merge-patches) a KV v2 secret
281
+
282
+ Sends `PATCH` with `Content-Type: application/merge-patch+json`, merging `data` into the
283
+ existing secret without overwriting keys that are not listed. KV v2 merge-patch operation —
284
+ KV v1 mounts do not support `PATCH` and Vault returns `405` there. Resolves to the raw
285
+ parsed Vault response body.
286
+
287
+ **Kind**: instance method of [<code>VaultClient</code>](#VaultClient)
288
+
289
+ | Param | Type | Description |
290
+ | --- | --- | --- |
291
+ | path | <code>string</code> | path to the secret |
292
+ | data | <code>object</code> | keys to merge into the existing secret |
293
+
294
+ <a name="VaultClient+request"></a>
295
+
296
+ #### vaultClient.request(method, path, [data]) ⇒ <code>Promise.&lt;Object&gt;</code>
297
+ Raw request — escape hatch for any Vault backend
298
+
299
+ Sends the literal API path with no KV path rewriting and no response unwrapping, and
300
+ resolves to the parsed response body. Use it for non-KV backends (e.g. Transit) or when you
301
+ have already constructed the complete Vault API path.
302
+
303
+ **Kind**: instance method of [<code>VaultClient</code>](#VaultClient)
304
+
305
+ | Param | Type | Description |
306
+ | --- | --- | --- |
307
+ | method | <code>string</code> | HTTP method (e.g. `GET`, `POST`) |
308
+ | path | <code>string</code> | literal API path, sent as-is |
309
+ | [data] | <code>object</code> | request body |
310
+
311
+ <a name="VaultClient+deleteVersions"></a>
312
+
313
+ #### vaultClient.deleteVersions(path, versions) ⇒ <code>Promise.&lt;Object&gt;</code>
314
+ Soft-deletes specific versions of a KV v2 secret
315
+
316
+ KV v2 only — rejects with `UnsupportedOperationError` on non-v2 mounts. Resolves to the raw
317
+ parsed Vault response body.
318
+
319
+ **Kind**: instance method of [<code>VaultClient</code>](#VaultClient)
320
+
321
+ | Param | Type | Description |
322
+ | --- | --- | --- |
323
+ | path | <code>string</code> | path to the secret |
324
+ | versions | <code>Array.&lt;number&gt;</code> | version numbers to soft-delete |
325
+
326
+ <a name="VaultClient+undeleteVersions"></a>
327
+
328
+ #### vaultClient.undeleteVersions(path, versions) ⇒ <code>Promise.&lt;Object&gt;</code>
329
+ Undeletes (restores) soft-deleted versions of a KV v2 secret
330
+
331
+ KV v2 only — rejects with `UnsupportedOperationError` on non-v2 mounts. Resolves to the raw
332
+ parsed Vault response body.
333
+
334
+ **Kind**: instance method of [<code>VaultClient</code>](#VaultClient)
335
+
336
+ | Param | Type | Description |
337
+ | --- | --- | --- |
338
+ | path | <code>string</code> | path to the secret |
339
+ | versions | <code>Array.&lt;number&gt;</code> | version numbers to restore |
340
+
341
+ <a name="VaultClient+destroyVersions"></a>
342
+
343
+ #### vaultClient.destroyVersions(path, versions) ⇒ <code>Promise.&lt;Object&gt;</code>
344
+ Permanently destroys specific versions of a KV v2 secret
345
+
346
+ The destroyed version data cannot be recovered. KV v2 only — rejects with
347
+ `UnsupportedOperationError` on non-v2 mounts. Resolves to the raw parsed Vault response body.
348
+
349
+ **Kind**: instance method of [<code>VaultClient</code>](#VaultClient)
350
+
351
+ | Param | Type | Description |
352
+ | --- | --- | --- |
353
+ | path | <code>string</code> | path to the secret |
354
+ | versions | <code>Array.&lt;number&gt;</code> | version numbers to destroy |
355
+
356
+ <a name="VaultClient+readMetadata"></a>
357
+
358
+ #### vaultClient.readMetadata(path) ⇒ <code>Promise.&lt;Object&gt;</code>
359
+ Reads KV v2 metadata for a secret
360
+
361
+ Resolves to the metadata document (`current_version`, the `versions` map, timestamps, etc.).
362
+ KV v2 only — rejects with `UnsupportedOperationError` on non-v2 mounts.
363
+
364
+ **Kind**: instance method of [<code>VaultClient</code>](#VaultClient)
365
+
366
+ | Param | Type | Description |
367
+ | --- | --- | --- |
368
+ | path | <code>string</code> | path to the secret |
369
+
370
+ <a name="VaultClient+deleteMetadata"></a>
371
+
372
+ #### vaultClient.deleteMetadata(path) ⇒ <code>Promise.&lt;Object&gt;</code>
373
+ Deletes all metadata and version history for a KV v2 secret (permanent)
374
+
375
+ KV v2 only — rejects with `UnsupportedOperationError` on non-v2 mounts. Resolves to the raw
376
+ parsed Vault response body, which may be empty/undefined for `204 No Content` responses.
377
+
378
+ **Kind**: instance method of [<code>VaultClient</code>](#VaultClient)
379
+
380
+ | Param | Type | Description |
381
+ | --- | --- | --- |
382
+ | path | <code>string</code> | path to the secret |
383
+
242
384
  <a name="VaultClient+close"></a>
243
385
 
244
386
  #### vaultClient.close()
@@ -315,6 +457,152 @@ If no name passed all named instances will be cleared.
315
457
  | --- | --- | --- |
316
458
  | [name] | <code>String</code> | Vault instance name, all instances will be cleared if no name were passed |
317
459
 
460
+ ## KV v2 & generic backends
461
+
462
+ ### Overview
463
+
464
+ By default the client behaves exactly as before (KV v1 / raw passthrough). To enable transparent
465
+ KV v2 support set `api.kv.autoDetect: true` **or** supply a static `api.engines` map. Either
466
+ option activates path-rewriting and response-unwrapping; callers do not need to know the engine
467
+ version.
468
+
469
+ ### Configuration options
470
+
471
+ | Option | Type | Default | Description |
472
+ |---|---|---|---|
473
+ | `api.kv.autoDetect` | `boolean` | `false` | Auto-detect the KV version of each mount on first use via `GET sys/internal/ui/mounts/<path>`. |
474
+ | `api.engines` | `Object` | `{}` | Static mount-to-version map, e.g. `{ secret: 2, legacy: 1 }`. Overrides detection; use this when the token lacks permission on `sys/internal/ui/mounts`. |
475
+
476
+ Both options can be combined: `engines` acts as an override — matching mounts skip detection
477
+ while unmatched mounts are auto-detected (when `autoDetect: true`).
478
+
479
+ ### Auto-detect example
480
+
481
+ ```javascript
482
+ const client = VaultClient.boot('main', {
483
+ api: {
484
+ url: 'https://vault.example.com:8200/',
485
+ kv: { autoDetect: true },
486
+ },
487
+ auth: { type: 'token', config: { token: '...' } },
488
+ });
489
+
490
+ // Works transparently on both KV v1 and KV v2 mounts
491
+ const lease = await client.read('secret/my-app/config');
492
+ console.log(lease.getData()); // the secret object
493
+ console.log(lease.getMetadata()); // KV v2 version metadata (undefined on v1)
494
+ ```
495
+
496
+ ### Static engines override example
497
+
498
+ ```javascript
499
+ const client = VaultClient.boot('main', {
500
+ api: {
501
+ url: 'https://vault.example.com:8200/',
502
+ engines: { secret: 2, legacy: 1 },
503
+ },
504
+ auth: { type: 'token', config: { token: '...' } },
505
+ });
506
+ ```
507
+
508
+ ### KV v2-specific methods
509
+
510
+ These methods require a KV v2 mount and throw `UnsupportedOperationError` on v1 / non-KV mounts.
511
+
512
+ ```javascript
513
+ // Soft-delete specific versions
514
+ await client.deleteVersions('secret/foo', [1, 2]);
515
+
516
+ // Restore soft-deleted versions
517
+ await client.undeleteVersions('secret/foo', [1]);
518
+
519
+ // Permanently destroy versions
520
+ await client.destroyVersions('secret/foo', [1, 2]);
521
+
522
+ // Read version metadata (current_version, versions map, etc.)
523
+ const meta = await client.readMetadata('secret/foo');
524
+
525
+ // Delete all metadata and version history (permanent)
526
+ await client.deleteMetadata('secret/foo');
527
+ ```
528
+
529
+ ### update() — merge-patch
530
+
531
+ ```javascript
532
+ // PATCH a subset of keys without overwriting others (KV v2)
533
+ await client.update('secret/foo', { password: 'new-value' });
534
+ // Sends PATCH secret/data/foo with Content-Type: application/merge-patch+json
535
+ ```
536
+
537
+ ### delete()
538
+
539
+ ```javascript
540
+ // Soft-delete the latest version on KV v2; DELETE on v1/passthrough
541
+ await client.delete('secret/foo');
542
+ ```
543
+
544
+ ### request() — raw escape hatch
545
+
546
+ For any Vault backend that does not benefit from KV path rewriting use `request()`. It sends the
547
+ literal path with no rewriting or response normalisation and returns the parsed body directly.
548
+
549
+ ```javascript
550
+ // Encrypt with Transit engine — path must not be rewritten
551
+ const result = await client.request('POST', 'transit/encrypt/my-key', {
552
+ plaintext: Buffer.from('hello').toString('base64'),
553
+ });
554
+ console.log(result.data.ciphertext);
555
+ ```
556
+
557
+ ### Lease.getMetadata()
558
+
559
+ `getMetadata()` is additive — existing code is unaffected.
560
+
561
+ ```javascript
562
+ const lease = await client.read('secret/my-app/db');
563
+ lease.getData(); // the secret values
564
+ lease.getMetadata(); // { version, created_time, deletion_time, destroyed, custom_metadata }
565
+ // undefined on KV v1 / passthrough mounts
566
+ ```
567
+
568
+ ### Path requirements when autoDetect / engines are active
569
+
570
+ When `autoDetect: true` or `api.engines` is set, the client rewrites logical paths to the
571
+ correct KV v2 API paths automatically (e.g. `secret/foo` → `secret/data/foo` for reads).
572
+ **Callers must pass logical paths — do not include the internal KV v2 segments** (`data/`,
573
+ `metadata/`, `delete/`, `undelete/`, `destroy/`) in the path argument:
574
+
575
+ ```javascript
576
+ // Correct — logical path only
577
+ await client.read('secret/my-app/config');
578
+
579
+ // Wrong — double-rewrite: 'secret/data/foo' becomes 'secret/data/data/foo' on the wire
580
+ await client.read('secret/data/foo');
581
+ ```
582
+
583
+ If you need to send a fully-literal Vault API path (e.g. when working with non-KV backends or
584
+ when you have already constructed the complete path), use `request()` which bypasses all path
585
+ rewriting:
586
+
587
+ ```javascript
588
+ // Literal path, no rewriting
589
+ await client.request('GET', 'secret/data/foo');
590
+ ```
591
+
592
+ ### Mount detection caching
593
+
594
+ - Each canonical mount is detected at most once per `VaultClient` instance.
595
+ - Concurrent first-touch requests for the same mount share a single in-flight detection promise.
596
+ - The detection endpoint used is `GET sys/internal/ui/mounts/<path>` (readable by any authenticated token).
597
+ - When the token lacks permission on that endpoint, set `api.engines` to skip detection.
598
+
599
+ ### Error classes
600
+
601
+ | Class | When thrown |
602
+ |---|---|
603
+ | `UnsupportedOperationError` | A v2-only method (`deleteVersions`, `undeleteVersions`, `destroyVersions`, `readMetadata`, `deleteMetadata`) is called against a non-v2 mount. |
604
+ | `VaultError` | Mount detection fails (e.g. permission denied) and no `api.engines` override was provided. |
605
+
318
606
  ## Contributing
319
607
 
320
608
  Contributions are welcome! Please read the [contributing guide](CONTRIBUTING.md) to get started,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-vault-client",
3
- "version": "2.0.1",
3
+ "version": "2.1.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,9 +15,10 @@
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
+ "test:e2e": "mocha --exit \"test/e2e/e2e.test.mjs\"",
19
+ "test:e2e:kv2": "mocha --exit \"test/e2e/e2e.kv2.test.mjs\"",
19
20
  "coverage": "c8 npm run test:unit",
20
- "lint": "eslint src/",
21
+ "lint": "eslint src/ test/",
21
22
  "version": "git add -A ."
22
23
  },
23
24
  "c8": {
@@ -29,33 +30,40 @@
29
30
  "html",
30
31
  "lcov"
31
32
  ],
32
- "all": true
33
+ "all": true,
34
+ "check-coverage": true,
35
+ "statements": 97,
36
+ "branches": 93,
37
+ "functions": 100,
38
+ "lines": 97
33
39
  },
34
40
  "author": "Namecheap <opensource@namecheap.com>",
35
41
  "license": "Apache-2.0",
36
42
  "dependencies": {
37
- "@aws-sdk/credential-providers": "^3.382.0",
38
- "aws4": "^1.8.0",
39
- "lodash": "^4.17.21",
43
+ "@aws-sdk/credential-providers": "^3.1100.0",
44
+ "aws4": "^1.13.2",
40
45
  "long-timeout": "^0.1.1"
41
46
  },
42
47
  "peerDependencies": {
43
48
  "config": ">=1 <4"
44
49
  },
45
50
  "overrides": {
46
- "serialize-javascript": "^7.0.5",
51
+ "serialize-javascript": "^7.0.7",
47
52
  "glob": "^13.0.0",
48
- "diff": "^8.0.3"
53
+ "diff": "^8.0.3",
54
+ "brace-expansion": "^5.0.9",
55
+ "js-yaml": "^4.3.0"
49
56
  },
50
57
  "devDependencies": {
51
- "@eslint/js": "^9.0.0",
52
- "c8": "^11.0.0",
53
- "chai": "^6.0.0",
58
+ "@eslint/js": "^9.39.5",
59
+ "c8": "^10.1.3",
60
+ "chai": "^6.2.2",
54
61
  "deep-freeze": "^0.0.1",
55
- "eslint": "^9.39.4",
56
- "globals": "^17.6.0",
62
+ "eslint": "^9.39.5",
63
+ "globals": "^17.8.0",
64
+ "lodash": "^4.18.1",
57
65
  "mocha": "^11.7.6",
58
- "sinon": "^22.0.0",
59
- "sinon-chai": "^4.0.0"
66
+ "sinon": "^22.1.0",
67
+ "sinon-chai": "^4.0.1"
60
68
  }
61
69
  }
package/src/Lease.js CHANGED
@@ -1,20 +1,20 @@
1
1
  'use strict';
2
2
 
3
- const _ = require('lodash');
4
-
5
3
  class Lease {
6
4
  constructor(
7
5
  requestId,
8
6
  leaseId,
9
7
  leaseDuration,
10
8
  renewable,
11
- data
9
+ data,
10
+ metadata
12
11
  ) {
13
12
  this.__requestId = requestId;
14
13
  this.__leaseId = leaseId;
15
14
  this.__leaseDuration = leaseDuration;
16
15
  this.__renewable = renewable;
17
16
  this.__data = data === undefined ? {} : data;
17
+ this.__metadata = metadata;
18
18
  }
19
19
 
20
20
  static fromResponse(response) {
@@ -23,7 +23,8 @@ class Lease {
23
23
  response.lease_id,
24
24
  response.lease_duration,
25
25
  response.renewable,
26
- response.data
26
+ response.data,
27
+ response.metadata
27
28
  );
28
29
  }
29
30
 
@@ -43,7 +44,7 @@ class Lease {
43
44
  * @returns {Object}
44
45
  */
45
46
  getData() {
46
- return _.cloneDeep(this.__data);
47
+ return structuredClone(this.__data);
47
48
  }
48
49
 
49
50
  /**
@@ -52,6 +53,16 @@ class Lease {
52
53
  isRenewable() {
53
54
  return this.__renewable;
54
55
  }
56
+
57
+ /**
58
+ * KV v2 version metadata object (created_time, version, etc.).
59
+ * Returns undefined for KV v1 / non-KV leases.
60
+ *
61
+ * @returns {Object|undefined}
62
+ */
63
+ getMetadata() {
64
+ return this.__metadata;
65
+ }
55
66
  }
56
67
 
57
68
  module.exports = Lease;