node-vault-client 3.0.2 → 3.2.0
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 +145 -22
- package/index.d.ts +3 -1
- package/package.json +9 -6
- package/src/Lease.js +10 -4
- package/src/MountResolver.js +30 -8
- package/src/VaultApiClient.js +291 -3
- package/src/VaultClient.js +239 -88
- package/src/auth/AuthToken.js +13 -4
- package/src/auth/VaultBaseAuth.js +142 -9
- package/src/auth/VaultIAMAuth.js +54 -0
- package/src/auth/VaultKubernetesAuth.js +21 -14
- package/src/errors.d.ts +5 -0
- package/src/errors.js +18 -0
- package/src/warnOnce.js +44 -0
package/README.md
CHANGED
|
@@ -116,6 +116,21 @@ when Vault's `sts_region` / `sts_endpoint` is configured for a non-`us-east-1` r
|
|
|
116
116
|
`SignatureDoesNotMatch — Credential should be scoped to a valid region`. Omitting `region`
|
|
117
117
|
preserves the previous (global-endpoint) behavior.
|
|
118
118
|
|
|
119
|
+
#### `iam_server_id_header_value`
|
|
120
|
+
|
|
121
|
+
Set it to the same value as `iam_server_id_header_value` in the mount's `auth/<mount>/config/client`.
|
|
122
|
+
The client then sends it in the signed request as `X-Vault-AWS-IAM-Server-ID`, so the signature
|
|
123
|
+
names your Vault server. A Vault server that enforces its own `iam_server_id_header_value` then
|
|
124
|
+
rejects a request captured from, or meant for, a different server; one that does not set the value
|
|
125
|
+
checks nothing. Vault rejects a login with a missing or wrong value only when the mount sets the
|
|
126
|
+
value. The header does not stop a replay to the same server while the signature is valid (about 15
|
|
127
|
+
minutes).
|
|
128
|
+
|
|
129
|
+
When `config.iam_server_id_header_value` is missing, empty or only whitespace, the first login
|
|
130
|
+
logs one warning and logs in as before, sending the value it has. Each logger gets the warning once
|
|
131
|
+
per Vault server, namespace and mount: clients on the default console logger share it, so the
|
|
132
|
+
process logs it once, and a client with `logger: false` logs nothing and leaves it to the others.
|
|
133
|
+
|
|
119
134
|
### AppRole auth
|
|
120
135
|
|
|
121
136
|
```javascript
|
|
@@ -423,6 +438,27 @@ that token expires, and this client does not renew secret leases — only the au
|
|
|
423
438
|
dynamic credentials (`database/creds/*`, cloud credentials) whose lease outlives the auth token,
|
|
424
439
|
leaving renewal on is what currently keeps them alive. KV reads are unaffected.
|
|
425
440
|
|
|
441
|
+
#### Tokens revoked on the Vault side
|
|
442
|
+
|
|
443
|
+
A token can stop working before its expiry, for example when an operator revokes it
|
|
444
|
+
(`vault token revoke`, `sys/leases/revoke-prefix`). Vault then answers 403 to every request that
|
|
445
|
+
carries it. When a secret request or a mount detection gets a 403, the client asks
|
|
446
|
+
`auth/token/lookup-self`, which every login already calls, whether Vault still accepts the token:
|
|
447
|
+
|
|
448
|
+
* If it does, the 403 was a policy denial. The call rejects with it as before, and no login is made.
|
|
449
|
+
* If it does not, the client drops the token and its renewal timer, logs `Vault no longer accepts
|
|
450
|
+
the token (accessor=…); logging in again` at `warn`, logs in, and sends the request once more.
|
|
451
|
+
The call then returns what that attempt got: its result, or its error (a failed login, a 404, a
|
|
452
|
+
timeout). Like any re-login, it replays the configured credential (see the table above).
|
|
453
|
+
|
|
454
|
+
Concurrent requests rejected with the same token share one lookup and one login. A 403 on the login
|
|
455
|
+
itself is returned as before. Under `token` auth there is no credential to log in with, so the 403
|
|
456
|
+
is returned as before, without a lookup.
|
|
457
|
+
|
|
458
|
+
So revoking a client's token no longer locks it out: it logs straight back in with the same
|
|
459
|
+
credential. To cut a client off, also revoke or rotate what it logs in with (the AppRole
|
|
460
|
+
`secret_id` or role, the Kubernetes or JWT role binding, the IAM role).
|
|
461
|
+
|
|
426
462
|
#### Tuning renewal instead of disabling it
|
|
427
463
|
|
|
428
464
|
Two further keys, also on `auth`, shape *how* renewal happens. Both are optional, and leaving them
|
|
@@ -502,9 +538,10 @@ Client constructor function.
|
|
|
502
538
|
| --- | --- | --- | --- |
|
|
503
539
|
| options | `Object` | | |
|
|
504
540
|
| options.api | <code>Object</code> | | |
|
|
505
|
-
| options.api.url | <code>String</code> | | the url of the vault server |
|
|
541
|
+
| options.api.url | <code>String</code> | | the url of the vault server. Plain `http://` to a host other than this machine logs a warning; see "Plain `http://` api.url" |
|
|
506
542
|
| [options.api.apiVersion] | <code>String</code> | `v1` | |
|
|
507
|
-
| [options.api.requestOptions] | <code>Object</code> | | extra options merged into every HTTP request (see [Custom transport](#custom-transport-proxy--self-signed-tls)) |
|
|
543
|
+
| [options.api.requestOptions] | <code>Object</code> | | extra options merged into every HTTP request (see [Custom transport](#custom-transport-proxy--self-signed-tls)); a `redirect` there replaces the same-origin redirect policy (see [Redirects and the Vault token](#redirects-and-the-vault-token)) |
|
|
544
|
+
| [options.api.timeout] | <code>number</code> | `120000` | milliseconds a request may take, response headers, body and redirects included, before it rejects with `RequestTimeoutError`; `0` turns the limit off, `null` gives the default (see [Request timeouts](#request-timeouts)) |
|
|
508
545
|
| [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. |
|
|
509
546
|
| [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)) |
|
|
510
547
|
| [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)) |
|
|
@@ -559,8 +596,35 @@ bundle undici 7.x, so pin `undici@^7`.
|
|
|
559
596
|
|
|
560
597
|
##### Request timeouts
|
|
561
598
|
|
|
562
|
-
|
|
563
|
-
`
|
|
599
|
+
Every request to Vault, including logins, `lookup-self` and background `renew-self`, has to
|
|
600
|
+
finish within `api.timeout` milliseconds: **120 000 (2 minutes) by default**. The limit covers the
|
|
601
|
+
whole request: response headers, the body and any redirects. A request that runs out of time
|
|
602
|
+
rejects with `RequestTimeoutError` (its `timeout` property holds the limit, `cause` the
|
|
603
|
+
`fetch()` rejection). With `api.kv.autoDetect`, a mount detection that times out fails the call
|
|
604
|
+
with the usual detection `VaultError`, with `RequestTimeoutError` on its `cause`. A renewal that
|
|
605
|
+
times out is handled like any other failed renewal.
|
|
606
|
+
|
|
607
|
+
`api.timeout` is in milliseconds; `null` or leaving it out gives the default, and a value under
|
|
608
|
+
1000 logs a warning in case it was meant as seconds. Anything but an integer from 0 to 2147483647
|
|
609
|
+
raises `InvalidArgumentsError` when the client is created.
|
|
610
|
+
|
|
611
|
+
The default sits above Vault's own default `max_request_duration` (90 s), so it does not cut
|
|
612
|
+
short a request that a Vault with default settings would still answer. Set it lower to fail
|
|
613
|
+
fast, or higher if your Vault allows longer requests:
|
|
614
|
+
|
|
615
|
+
```javascript
|
|
616
|
+
const vaultClient = VaultClient.boot('main', {
|
|
617
|
+
api: { url: 'https://vault.example.com:8200/', timeout: 10000 },
|
|
618
|
+
auth: { type: 'token', config: { token: '...' } },
|
|
619
|
+
});
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
`timeout: 0` turns the limit off, as it was before `api.timeout` existed. What remains then
|
|
623
|
+
are the timeouts of the dispatcher `fetch()` uses: Node's default one gives up after 300 s
|
|
624
|
+
without response headers (`TypeError: fetch failed`, `cause.code` `UND_ERR_HEADERS_TIMEOUT`) or
|
|
625
|
+
300 s without body data (`UND_ERR_BODY_TIMEOUT`). Those limits apply per phase, so a body that
|
|
626
|
+
keeps trickling in is never cut off. Any timeouts set on a custom dispatcher still apply
|
|
627
|
+
alongside `api.timeout`, and whichever fires first rejects the request:
|
|
564
628
|
|
|
565
629
|
```javascript
|
|
566
630
|
const { Agent } = require('undici');
|
|
@@ -574,24 +638,54 @@ const vaultClient = VaultClient.boot('main', {
|
|
|
574
638
|
});
|
|
575
639
|
```
|
|
576
640
|
|
|
577
|
-
A
|
|
578
|
-
`
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
641
|
+
A `signal` in `requestOptions` still aborts every request in flight when it fires (with the
|
|
642
|
+
signal's own reason, not `RequestTimeoutError`), so an `AbortController` can cancel requests
|
|
643
|
+
on shutdown. `fetch()` then receives the client's own per-request signal, which follows it. Do **not** use `signal: AbortSignal.timeout(ms)` there: it is created once and
|
|
644
|
+
shared by every request the client makes, so it works for the first calls and then aborts all
|
|
645
|
+
later ones. Use `api.timeout` instead.
|
|
646
|
+
|
|
647
|
+
##### Plain `http://` api.url
|
|
648
|
+
|
|
649
|
+
When `api.url` is `http://` and its host is not this machine, the client logs one warning on its
|
|
650
|
+
first request: `"api.url" uses plain http:// for <host:port>, which is not a loopback address; the
|
|
651
|
+
Vault token and secrets travel unencrypted. …` (on port 80 the message names the bare host).
|
|
652
|
+
Nothing else changes: the client connects and works as before. `localhost`, `127.0.0.0/8` (also
|
|
653
|
+
as `::ffff:127.x.y.z`), `::1`, and the unspecified addresses `0.0.0.0` and `::` count as this
|
|
654
|
+
machine; subdomains of `localhost` do not, since the system resolver can send them anywhere. The
|
|
655
|
+
warning names only the host and port, never credentials or a path from the URL. Each logger gets
|
|
656
|
+
it once per host and port: clients on the default console logger share it, so the process logs it
|
|
657
|
+
once, and a client with `logger: false` logs nothing and leaves the warning to the others. If the
|
|
658
|
+
logger throws, the client tries again on its next request. Only `api.url` is checked, not where a
|
|
659
|
+
proxy `dispatcher` or `requestOptions.redirect: 'follow'` sends the request.
|
|
660
|
+
|
|
661
|
+
If the connection is protected another way (a service mesh with mTLS, a TLS-terminating sidecar on
|
|
662
|
+
another host), filter that message in your `logger`, or switch to `https://`.
|
|
583
663
|
|
|
584
664
|
##### Redirects and the Vault token
|
|
585
665
|
|
|
586
|
-
Requests follow HTTP redirects
|
|
587
|
-
|
|
588
|
-
a
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
`
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
666
|
+
Requests follow HTTP redirects only within the origin (scheme, host and port) of `api.url`, the
|
|
667
|
+
way `fetch()` follows them: up to 20 hops, a 307 or 308 re-sends the method and body, and a 303,
|
|
668
|
+
or a 301 or 302 after a `POST`, becomes a `GET` without a body. A redirect to any other origin
|
|
669
|
+
rejects with a `VaultError` ("Refusing to follow a redirect from … to …") and nothing is sent
|
|
670
|
+
there. That includes a change of scheme on the same host: if a load balancer redirects an
|
|
671
|
+
`http://` `api.url` to `https://`, use the `https://` URL. Where `fetch()` itself gives up on a
|
|
672
|
+
redirect (more than 20 hops, or a `Location` that does not parse, is not http(s) or carries
|
|
673
|
+
credentials), the request rejects as it always has, with `TypeError: fetch failed` and the reason
|
|
674
|
+
on `cause`, so catch both if you handle redirect failures.
|
|
675
|
+
|
|
676
|
+
Earlier versions followed every redirect. Node's `fetch()` strips only `Authorization`,
|
|
677
|
+
`Cookie` and `Proxy-Authorization` when a redirect crosses origins, so `X-Vault-Token` went to the
|
|
678
|
+
new origin, a 307 or 308 replayed a login body with its credentials (the AppRole `secret_id`, the
|
|
679
|
+
Kubernetes or JWT token, the signed IAM request, a `distributedClaimAccessToken` Graph token), and
|
|
680
|
+
that origin's answer was returned as Vault's.
|
|
681
|
+
|
|
682
|
+
A Vault HA standby forwards requests to the active node. It answers with a 307 to the active
|
|
683
|
+
node's `api_addr` only when forwarding fails or the client sends `X-Vault-No-Request-Forwarding`.
|
|
684
|
+
If that address is another origin, point `api.url` at a load balancer that routes to the active
|
|
685
|
+
node. A `redirect` in `api.requestOptions` (other than `null`, which reads as not set) is passed
|
|
686
|
+
to `fetch()` as it is: `'follow'` restores
|
|
687
|
+
the old behaviour (then treat `api.url`, its DNS and anything in front of it as trusted with the
|
|
688
|
+
token), `'manual'` returns a redirect as a `VaultHttpError`, and `'error'` fails it.
|
|
595
689
|
|
|
596
690
|
<a name="VaultClient+fillNodeConfig"></a>
|
|
597
691
|
|
|
@@ -788,6 +882,14 @@ discarded rather than arming a fresh timer, so the client stops holding the even
|
|
|
788
882
|
same applies when a token is replaced by a new login — a renewal still running against the previous
|
|
789
883
|
token cannot overwrite the newer one or cancel its timer.
|
|
790
884
|
|
|
885
|
+
The same goes for a login that is in flight when `close()` runs, such as the first `read` of a
|
|
886
|
+
client closed before it settles: the read still resolves and the token is cached for later calls,
|
|
887
|
+
but no refresh timer is armed for it. A call made after `close()` that joins that same login
|
|
888
|
+
shares its token and gets no timer either, just as if `close()` had run right after the login. A
|
|
889
|
+
login that starts after `close()` arms one as before. As after any `close()`, the token is then
|
|
890
|
+
used until it expires; under `token` auth, which cannot log in again, later calls reject with
|
|
891
|
+
`AuthTokenExpiredError`, so close a client only once nothing will use it any more.
|
|
892
|
+
|
|
791
893
|
<a name="Lease"></a>
|
|
792
894
|
|
|
793
895
|
### Lease
|
|
@@ -795,7 +897,7 @@ token cannot overwrite the newer one or cancel its timer.
|
|
|
795
897
|
The object returned by `read()` and `list()` (they resolve to `Promise<Lease>`). Use its
|
|
796
898
|
accessors to extract the secret data:
|
|
797
899
|
|
|
798
|
-
* `getValue(key)` ⇒ <code
|
|
900
|
+
* `getValue(key)` ⇒ <code>\*</code> — value for a single key, as Vault returned it: usually a string, but `null`, `false`, `0`, objects and arrays come back as they are. Throws `Requested key does not exist` when the key is not an own property of the response data (for an array, such as a `list()` result, that means an index or `length`). A name such as `constructor` or `toString` counts only when Vault sent a field by that name, never as an inherited member.
|
|
799
901
|
* `getData()` ⇒ <code>Object</code> — a deep-cloned copy of the whole secret data object.
|
|
800
902
|
* `isRenewable()` ⇒ <code>boolean</code> — whether the underlying lease is renewable.
|
|
801
903
|
* `getMetadata()` ⇒ <code>Object</code> | <code>undefined</code> — KV v2 version metadata
|
|
@@ -816,6 +918,14 @@ the ones it was booted with, the client logs a warning rather than letting the d
|
|
|
816
918
|
silently. Use [`VaultClient.get(name)`](#VaultClient.get) to fetch an existing instance, or
|
|
817
919
|
[`VaultClient.clear(name)`](#VaultClient.clear) before booting to replace one.
|
|
818
920
|
|
|
921
|
+
Options are compared as hashes, so no credential is kept for the comparison. Plain objects and
|
|
922
|
+
arrays compare by content, in any key order. Functions and other objects compare by identity, and
|
|
923
|
+
a `Buffer`, `Date`, `URL` or `RegExp` by value. So a logger class instance, an undici `dispatcher`,
|
|
924
|
+
or a logger object built from new functions counts as different even when it would behave the
|
|
925
|
+
same; a plain object that reuses the same functions does not. The warning names the top-level
|
|
926
|
+
options that differ (for example `logger`) and is logged once per instance for each set of them.
|
|
927
|
+
Boot each name once and fetch it with `get()` elsewhere.
|
|
928
|
+
|
|
819
929
|
**Kind**: static method of [<code>VaultClient</code>](#VaultClient)
|
|
820
930
|
**Returns**: <code>VaultClient</code>
|
|
821
931
|
|
|
@@ -846,6 +956,18 @@ Clear named Vault instance
|
|
|
846
956
|
|
|
847
957
|
If no name passed all named instances will be cleared.
|
|
848
958
|
|
|
959
|
+
Only a string names one instance. Any other argument (a number, `null`, an object, a symbol)
|
|
960
|
+
clears every instance not named by a symbol, as it always has. That is rarely what was meant:
|
|
961
|
+
`clear(42)`, or `clear` passed as a callback that receives a code or a resolved value
|
|
962
|
+
(`promise.then(VaultClient.clear)`). So the first such call in a process also emits a
|
|
963
|
+
`DeprecationWarning` with code `NVC_DEP_CLEAR_NON_STRING`, naming the argument's type but not its
|
|
964
|
+
value. Call `clear()` with no argument to clear every instance.
|
|
965
|
+
|
|
966
|
+
Node delivers warnings on the next tick, so the warning is not shown when `clear` runs inside a
|
|
967
|
+
`process.on('exit')` listener, which still clears every instance. `--no-deprecation` silences the
|
|
968
|
+
warning. Under `--throw-deprecation` it becomes an uncaught exception on the next tick, not an
|
|
969
|
+
error thrown by `clear()`.
|
|
970
|
+
|
|
849
971
|
**Kind**: static method of [<code>VaultClient</code>](#VaultClient)
|
|
850
972
|
|
|
851
973
|
| Param | Type | Description |
|
|
@@ -1059,12 +1181,13 @@ try {
|
|
|
1059
1181
|
|
|
1060
1182
|
| Class | Extends | When thrown |
|
|
1061
1183
|
|---|---|---|
|
|
1062
|
-
| `VaultError` | `Error` | Base class for every error below. Raised directly when mount detection fails (e.g. permission denied) and no `api.engines` override was provided. |
|
|
1184
|
+
| `VaultError` | `Error` | Base class for every error below. Raised directly when mount detection fails (e.g. permission denied) and no `api.engines` override was provided; the failure is on `cause`, so Vault's HTTP status is `err.cause.statusCode`. Vault answers 403 both for a mount the token may not use and for one that does not exist. |
|
|
1063
1185
|
| `VaultHttpError` | `VaultError` | Vault answered with a non-2xx status. Carries the status as `statusCode` and the parsed body as `error`. `message` (`"<status> - <raw body text>"`) and `error` hold the full response body, which can include secret material (e.g. a partially failed transit batch), so log `statusCode` and `error.errors` rather than the error object itself. |
|
|
1064
|
-
| `InvalidArgumentsError` | `VaultError` | Bad arguments or configuration: `boot()` called without options, an unknown instance name passed to `get()`, an unsupported `auth.type`, invalid renewal options
|
|
1186
|
+
| `InvalidArgumentsError` | `VaultError` | Bad arguments or configuration: `boot()` called without options, an unknown instance name passed to `get()`, an unsupported `auth.type`, invalid renewal options or `api.timeout`, an invalid node-config substitution map, or a KV method path that fails [path validation](#path-validation). |
|
|
1065
1187
|
| `InvalidAWSCredentialsError` | `InvalidArgumentsError` | `auth.config.credentials` was supplied but is not a usable `accessKeyId` / `secretAccessKey` pair. |
|
|
1066
1188
|
| `AuthTokenExpiredError` | `VaultError` | The Vault token expired and the backend cannot obtain a new one. `token` auth can never re-authenticate and reaches this error at max TTL even with renewal enabled; other backends can also reach it when re-authentication is disabled or their credential can no longer be replayed. |
|
|
1067
1189
|
| `UnsupportedOperationError` | `VaultError` | A v2-only method (`deleteVersions`, `undeleteVersions`, `destroyVersions`, `readMetadata`, `deleteMetadata`) was called against a mount that did not resolve as KV v2. |
|
|
1190
|
+
| `RequestTimeoutError` | `VaultError` | A request to Vault did not finish within `api.timeout` milliseconds (see [Request timeouts](#request-timeouts)). Carries the limit as `timeout` and the `fetch()` rejection (a `TimeoutError` DOMException) as `cause`. |
|
|
1068
1191
|
|
|
1069
1192
|
## Contributing
|
|
1070
1193
|
|
package/index.d.ts
CHANGED
|
@@ -39,6 +39,8 @@ declare namespace VaultClient {
|
|
|
39
39
|
apiVersion?: string;
|
|
40
40
|
namespace?: string;
|
|
41
41
|
requestOptions?: Record<string, any>;
|
|
42
|
+
/** Milliseconds a request may take before it rejects with RequestTimeoutError; `0` turns it off. Default 120000. */
|
|
43
|
+
timeout?: number | null;
|
|
42
44
|
kv?: { autoDetect?: boolean };
|
|
43
45
|
engines?: Record<string, 1 | 2>;
|
|
44
46
|
}
|
|
@@ -119,7 +121,7 @@ declare namespace VaultClient {
|
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
interface Lease {
|
|
122
|
-
getValue<T = any>(key: string): T;
|
|
124
|
+
getValue<T = any>(key: string | number): T;
|
|
123
125
|
getData<T = Record<string, any>>(): T;
|
|
124
126
|
isRenewable(): boolean;
|
|
125
127
|
getMetadata(): Record<string, any> | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-vault-client",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.2.0",
|
|
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"
|
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
"test": "mocha --exit \"test/**/*.test.mjs\"",
|
|
19
19
|
"test:unit": "mocha \"test/*.test.mjs\"",
|
|
20
20
|
"test:types": "tsc -p types/tsconfig.json",
|
|
21
|
-
"test:e2e": "mocha --exit \"test/e2e/e2e.test.mjs\"",
|
|
22
|
-
"test:e2e:kv2": "mocha --exit \"test/e2e/e2e.kv2.test.mjs\"",
|
|
23
|
-
"test:e2e:jwt": "mocha --exit \"test/e2e/e2e.jwt.test.mjs\"",
|
|
21
|
+
"test:e2e": "mocha --exit --require test/e2e/renewal-timer-guard.mjs \"test/e2e/e2e.test.mjs\"",
|
|
22
|
+
"test:e2e:kv2": "mocha --exit --require test/e2e/renewal-timer-guard.mjs \"test/e2e/e2e.kv2.test.mjs\"",
|
|
23
|
+
"test:e2e:jwt": "mocha --exit --require test/e2e/renewal-timer-guard.mjs \"test/e2e/e2e.jwt.test.mjs\"",
|
|
24
|
+
"test:e2e:generic": "mocha --exit --require test/e2e/renewal-timer-guard.mjs \"test/e2e/e2e.generic.test.mjs\"",
|
|
24
25
|
"test:example": "node examples/jwt-auth/vault-jwt-demo.mjs",
|
|
25
26
|
"coverage": "c8 npm run test:unit",
|
|
26
27
|
"lint": "eslint src/ test/ examples/",
|
|
@@ -54,15 +55,17 @@
|
|
|
54
55
|
},
|
|
55
56
|
"overrides": {
|
|
56
57
|
"serialize-javascript": "^7.1.4",
|
|
57
|
-
"glob": "^
|
|
58
|
+
"glob": "^14.0.3",
|
|
58
59
|
"diff": "^8.0.3",
|
|
59
60
|
"brace-expansion": "^5.0.12",
|
|
60
|
-
"js-yaml": "^4.3.2"
|
|
61
|
+
"js-yaml": "^4.3.2",
|
|
62
|
+
"minimatch": "^11.0.4"
|
|
61
63
|
},
|
|
62
64
|
"devDependencies": {
|
|
63
65
|
"@eslint/js": "^10.0.1",
|
|
64
66
|
"c8": "^12.0.0",
|
|
65
67
|
"chai": "^6.2.2",
|
|
68
|
+
"config": "^4.4.2",
|
|
66
69
|
"deep-freeze": "^0.0.1",
|
|
67
70
|
"eslint": "^10.10.0",
|
|
68
71
|
"globals": "^17.8.0",
|
package/src/Lease.js
CHANGED
|
@@ -29,15 +29,21 @@ class Lease {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* @param {String} key
|
|
33
|
-
* @returns {
|
|
32
|
+
* @param {String|number} key - a field name, or an index when the data is an array (a list)
|
|
33
|
+
* @returns {*} the field's value as Vault returned it: a string for most secrets, but
|
|
34
|
+
* `null`, `false`, `0`, an object or an array are returned as they are
|
|
35
|
+
* @throws {Error} 'Requested key does not exist' unless `key` is an own property of the
|
|
36
|
+
* response data whose value is not `undefined`
|
|
34
37
|
*/
|
|
35
38
|
getValue(key) {
|
|
36
|
-
|
|
39
|
+
const data = this.__data;
|
|
40
|
+
// Own fields only: an inherited member such as `constructor` or `__proto__` is not
|
|
41
|
+
// something Vault returned.
|
|
42
|
+
if (data === null || typeof data !== 'object' || !Object.hasOwn(data, key) || data[key] === undefined) {
|
|
37
43
|
throw new Error('Requested key does not exist');
|
|
38
44
|
}
|
|
39
45
|
|
|
40
|
-
return
|
|
46
|
+
return data[key];
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
/**
|
package/src/MountResolver.js
CHANGED
|
@@ -36,6 +36,26 @@ function decodePath(path) {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* The error a caller gets when the detection it started or joined failed. It is built for each
|
|
41
|
+
* caller, so its message names that caller's own path rather than the first caller's. The
|
|
42
|
+
* failure stays on `cause`.
|
|
43
|
+
* safeMessage: this message is logged by the operation's catch handler, so it must not embed a
|
|
44
|
+
* raw response body.
|
|
45
|
+
*
|
|
46
|
+
* @param {Error} err - why the detection failed
|
|
47
|
+
* @param {string} interimKey - the mount the detection was keyed on
|
|
48
|
+
* @param {string} path - the caller's path
|
|
49
|
+
* @returns {VaultError}
|
|
50
|
+
*/
|
|
51
|
+
function detectionError(err, interimKey, path) {
|
|
52
|
+
return new VaultError(
|
|
53
|
+
`Failed to detect KV engine version for mount "${interimKey}" (path: ${path}): ${safeMessage(err)}. ` +
|
|
54
|
+
'Set api.engines or disable autoDetect to bypass detection.',
|
|
55
|
+
{ cause: err }
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
39
59
|
/**
|
|
40
60
|
* Resolves the KV engine version for a given secret path.
|
|
41
61
|
*
|
|
@@ -215,6 +235,11 @@ class MountResolver {
|
|
|
215
235
|
// The shared detection resolved to a different mount — start our own.
|
|
216
236
|
// Use the full path as key so it won't collide with the first-segment key.
|
|
217
237
|
return this.__detectWithCache(path, path);
|
|
238
|
+
}, (err) => {
|
|
239
|
+
// A failed detection is shared, as it always was: one request, whatever it
|
|
240
|
+
// answered, so a rate limit or a failed login is not repeated per caller. Only
|
|
241
|
+
// the error is built per caller, so it names that caller's own path.
|
|
242
|
+
throw detectionError(err, interimKey, path);
|
|
218
243
|
});
|
|
219
244
|
}
|
|
220
245
|
|
|
@@ -255,17 +280,14 @@ class MountResolver {
|
|
|
255
280
|
.catch((err) => {
|
|
256
281
|
// Clear in-flight so callers can retry
|
|
257
282
|
this.__inflight.delete(interimKey);
|
|
258
|
-
|
|
259
|
-
// must not embed a raw response body; the original error stays on `cause`.
|
|
260
|
-
throw new VaultError(
|
|
261
|
-
`Failed to detect KV engine version for mount "${interimKey}" (path: ${path}): ${safeMessage(err)}. ` +
|
|
262
|
-
'Set api.engines or disable autoDetect to bypass detection.',
|
|
263
|
-
{ cause: err }
|
|
264
|
-
);
|
|
283
|
+
throw err;
|
|
265
284
|
});
|
|
266
285
|
|
|
286
|
+
// Callers that join get the raw failure and build their own error (see above).
|
|
267
287
|
this.__inflight.set(interimKey, promise);
|
|
268
|
-
return promise
|
|
288
|
+
return promise.catch((err) => {
|
|
289
|
+
throw detectionError(err, interimKey, path);
|
|
290
|
+
});
|
|
269
291
|
}
|
|
270
292
|
|
|
271
293
|
/**
|