node-vault-client 3.2.0 → 3.3.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 +39 -2
- package/index.d.ts +2 -1
- package/package.json +1 -1
- package/src/VaultApiClient.js +4 -1
- package/src/VaultClient.js +26 -1
- package/src/auth/VaultBaseAuth.js +114 -0
- package/src/errors.d.ts +8 -1
- package/src/errors.js +39 -1
package/README.md
CHANGED
|
@@ -459,6 +459,38 @@ So revoking a client's token no longer locks it out: it logs straight back in wi
|
|
|
459
459
|
credential. To cut a client off, also revoke or rotate what it logs in with (the AppRole
|
|
460
460
|
`secret_id` or role, the Kubernetes or JWT role binding, the IAM role).
|
|
461
461
|
|
|
462
|
+
#### Revoking the token on close
|
|
463
|
+
|
|
464
|
+
By default [`close()`](#VaultClient+close) only cancels the renewal timer: the token stays
|
|
465
|
+
valid on the Vault server until its TTL runs out. Set `revokeOnClose: true` on the `auth` block
|
|
466
|
+
(beside `type`, like the renewal keys) and `close()` also revokes the held token with
|
|
467
|
+
`auth/token/revoke-self`:
|
|
468
|
+
|
|
469
|
+
```javascript
|
|
470
|
+
auth: {
|
|
471
|
+
type: 'appRole',
|
|
472
|
+
revokeOnClose: true,
|
|
473
|
+
config: { /* ... */ },
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
The revocation covers the token held when `close()` runs and the one a login still in flight is
|
|
478
|
+
about to produce — that login still resolves after a close, and its token would otherwise
|
|
479
|
+
outlive the client by its whole TTL. `close()` returns the revocation as a promise, so a caller
|
|
480
|
+
that wants certainty can await it; ignoring the return value is fine too, since the request
|
|
481
|
+
itself keeps the process open until it lands. The promise never rejects: a failed revocation is
|
|
482
|
+
logged (`revokeSelf on close failed`) and the token is left live on the Vault server.
|
|
483
|
+
|
|
484
|
+
Two cautions:
|
|
485
|
+
|
|
486
|
+
* Under `token` auth the token revoked is the one from your configuration, not one the client
|
|
487
|
+
issued for itself. The client warns about this combination at construction; only enable it
|
|
488
|
+
when that token is not shared with anything else.
|
|
489
|
+
* A revoked token cannot be renewed back to life. Calling `close()` and then using the client
|
|
490
|
+
again logs in once more — with backends that cannot mint a fresh credential (`token`), the
|
|
491
|
+
next call fails with Vault's 403 (`VaultHttpError`) from `lookup-self`. A request still in
|
|
492
|
+
flight when the revocation lands surfaces that 403 instead of logging in again.
|
|
493
|
+
|
|
462
494
|
#### Tuning renewal instead of disabling it
|
|
463
495
|
|
|
464
496
|
Two further keys, also on `auth`, shape *how* renewal happens. Both are optional, and leaving them
|
|
@@ -874,6 +906,11 @@ console.log(secret);
|
|
|
874
906
|
vaultClient.close(); // process can now exit
|
|
875
907
|
```
|
|
876
908
|
|
|
909
|
+
With `auth.revokeOnClose: true` the held token is also revoked on the Vault server
|
|
910
|
+
(`auth/token/revoke-self`), and `close()` returns the revocation as a never-rejecting promise;
|
|
911
|
+
see [Revoking the token on close](#revoking-the-token-on-close). Ignored, the request still
|
|
912
|
+
completes: it keeps the process open until it lands.
|
|
913
|
+
|
|
877
914
|
**Kind**: instance method of [<code>VaultClient</code>](#VaultClient)
|
|
878
915
|
|
|
879
916
|
|
|
@@ -1181,8 +1218,8 @@ try {
|
|
|
1181
1218
|
|
|
1182
1219
|
| Class | Extends | When thrown |
|
|
1183
1220
|
|---|---|---|
|
|
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. |
|
|
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. |
|
|
1221
|
+
| `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`, and a rate-limited detection's `Retry-After` is `err.cause.retryAfter` / `err.cause.retryAfterMs`. Vault answers 403 both for a mount the token may not use and for one that does not exist. |
|
|
1222
|
+
| `VaultHttpError` | `VaultError` | Vault answered with a non-2xx status. Carries the status as `statusCode` and the parsed body as `error`. When the response sent a `Retry-After` header (Vault's own rate-limit quotas answer 429 with it, in seconds; a proxy in front of Vault may send it with 429 or 503), it is on `retryAfter` as sent and on `retryAfterMs` in milliseconds: delay-seconds and HTTP-dates are both parsed, the result is capped at 2147483647 (the longest `setTimeout()` delay), and `retryAfterMs` is absent when the value is in neither format. Prefer `retryAfterMs` to parsing `retryAfter` yourself, and clamp it to what your caller can afford to wait. The client does not wait and retry on `Retry-After` itself (token renewal retries do not honour it yet). `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. |
|
|
1186
1223
|
| `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). |
|
|
1187
1224
|
| `InvalidAWSCredentialsError` | `InvalidArgumentsError` | `auth.config.credentials` was supplied but is not a usable `accessKeyId` / `secretAccessKey` pair. |
|
|
1188
1225
|
| `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. |
|
package/index.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ declare class VaultClient {
|
|
|
22
22
|
|
|
23
23
|
fillNodeConfig(): Promise<any>;
|
|
24
24
|
getHeaders(token: VaultClient.AuthToken): Record<string, string>;
|
|
25
|
-
close(): void;
|
|
25
|
+
close(): Promise<void> | null | undefined;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
declare namespace VaultClient {
|
|
@@ -103,6 +103,7 @@ declare namespace VaultClient {
|
|
|
103
103
|
renewal?: boolean;
|
|
104
104
|
renewalFraction?: number;
|
|
105
105
|
renewalIncrement?: number;
|
|
106
|
+
revokeOnClose?: boolean;
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
type AuthOptions = AuthOptionsCommon &
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-vault-client",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.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"
|
package/src/VaultApiClient.js
CHANGED
|
@@ -390,7 +390,10 @@ class VaultApiClient {
|
|
|
390
390
|
}
|
|
391
391
|
|
|
392
392
|
if (!response.ok) {
|
|
393
|
-
|
|
393
|
+
// headers?.: fetch test doubles in callers' suites often have no headers
|
|
394
|
+
throw new errors.VaultHttpError(response.status, text, body, {
|
|
395
|
+
retryAfter: response.headers?.get('retry-after'),
|
|
396
|
+
});
|
|
394
397
|
}
|
|
395
398
|
|
|
396
399
|
// Do not log the response body: Vault responses carry secret
|
package/src/VaultClient.js
CHANGED
|
@@ -254,6 +254,10 @@ class VaultClient {
|
|
|
254
254
|
* @param {number} [options.auth.renewalIncrement] - Seconds of extra TTL to request on each
|
|
255
255
|
* renewal, sent as `increment` to `auth/token/renew-self`. Vault caps it at the token's
|
|
256
256
|
* max TTL. Omitted by default, which lets Vault apply the token's own period.
|
|
257
|
+
* @param {boolean} [options.auth.revokeOnClose=false] - Set `true` to revoke the held token
|
|
258
|
+
* (`auth/token/revoke-self`) when {@link VaultClient#close} runs. The token the
|
|
259
|
+
* `token` backend was configured with is revoked too, so only enable it when that
|
|
260
|
+
* token is not shared with anything else.
|
|
257
261
|
* @param {Object|false} options.logger - Logger that supports "error", "info", "warn", "trace", "debug" methods. Uses `console` by default. Pass `false` to disable logging.
|
|
258
262
|
*/
|
|
259
263
|
constructor(options) {
|
|
@@ -288,6 +292,13 @@ class VaultClient {
|
|
|
288
292
|
// `namespace` to `api.namespace`.
|
|
289
293
|
this.__auth.configureRenewal(options.auth);
|
|
290
294
|
|
|
295
|
+
if (options.auth && options.auth.revokeOnClose === true && options.auth.type === 'token') {
|
|
296
|
+
this.__log.warn(
|
|
297
|
+
'auth.revokeOnClose revokes the token from the configuration itself on close(); ' +
|
|
298
|
+
'enable it only when that token is not shared with anything else'
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
291
302
|
// KV v2 support
|
|
292
303
|
const kvOpts = (options.api && options.api.kv) || {};
|
|
293
304
|
const autoDetect = kvOpts.autoDetect === true;
|
|
@@ -428,7 +439,15 @@ class VaultClient {
|
|
|
428
439
|
* login already in flight when `close()` runs still resolves, but arms no timer, and
|
|
429
440
|
* neither does a call that joins that login after `close()`.
|
|
430
441
|
*
|
|
431
|
-
*
|
|
442
|
+
* With `auth.revokeOnClose: true` the held token is also revoked on the Vault server
|
|
443
|
+
* (`auth/token/revoke-self`): the one held now, or the one a login still in flight is
|
|
444
|
+
* about to produce. The revocation request keeps the event loop open until it lands, so
|
|
445
|
+
* a short-lived script that ignores the return value still completes it.
|
|
446
|
+
*
|
|
447
|
+
* @returns {Promise<void>|null|undefined} `null` (or `undefined` for an auth provider
|
|
448
|
+
* without revocation support) when there is nothing to revoke, otherwise a
|
|
449
|
+
* promise for the revocation. It never rejects: a failed revocation is logged and
|
|
450
|
+
* the token is left live on the Vault server.
|
|
432
451
|
*/
|
|
433
452
|
close() {
|
|
434
453
|
if (this.__auth && typeof this.__auth.__markClosed === 'function') {
|
|
@@ -438,6 +457,12 @@ class VaultClient {
|
|
|
438
457
|
if (this.__auth && typeof this.__auth.cancelTokenRefresh === 'function') {
|
|
439
458
|
this.__auth.cancelTokenRefresh();
|
|
440
459
|
}
|
|
460
|
+
if (this.__auth && typeof this.__auth.revokeSelfOnClose === 'function') {
|
|
461
|
+
// null unless auth.revokeOnClose started a revocation. Fire-and-forget when
|
|
462
|
+
// ignored (clear() does that): the request holds the event loop open until it
|
|
463
|
+
// lands. Returned so a caller that wants certainty can await it.
|
|
464
|
+
return this.__auth.revokeSelfOnClose();
|
|
465
|
+
}
|
|
441
466
|
}
|
|
442
467
|
|
|
443
468
|
/**
|
|
@@ -162,6 +162,19 @@ class VaultBaseAuth {
|
|
|
162
162
|
* @type {{id: string, promise: Promise<boolean>}|null}
|
|
163
163
|
*/
|
|
164
164
|
this.__rejectionCheck = null;
|
|
165
|
+
/**
|
|
166
|
+
* Whether {@link VaultBaseAuth#revokeSelfOnClose} revokes the held token on the Vault
|
|
167
|
+
* server (`auth/token/revoke-self`). Off by default; set `auth.revokeOnClose` (see
|
|
168
|
+
* {@link VaultBaseAuth#configureRenewal}).
|
|
169
|
+
* @type {boolean}
|
|
170
|
+
*/
|
|
171
|
+
this.__revokeOnClose = false;
|
|
172
|
+
/**
|
|
173
|
+
* The id of the token whose revocation on close has already been started, so a
|
|
174
|
+
* repeated close() does not revoke it twice.
|
|
175
|
+
* @type {String|null}
|
|
176
|
+
*/
|
|
177
|
+
this.__revokedOnCloseId = null;
|
|
165
178
|
}
|
|
166
179
|
|
|
167
180
|
/**
|
|
@@ -173,12 +186,15 @@ class VaultBaseAuth {
|
|
|
173
186
|
* @param {boolean} [authConfig.renewal=true]
|
|
174
187
|
* @param {number} [authConfig.renewalFraction=0.5]
|
|
175
188
|
* @param {number} [authConfig.renewalIncrement]
|
|
189
|
+
* @param {boolean} [authConfig.revokeOnClose=false] - revoke the held token
|
|
190
|
+
* (`auth/token/revoke-self`) when the client is closed
|
|
176
191
|
* @returns {void}
|
|
177
192
|
*/
|
|
178
193
|
configureRenewal(authConfig) {
|
|
179
194
|
this.__renewalEnabled = VaultBaseAuth.__validateRenewal(authConfig);
|
|
180
195
|
this.__renewalFraction = VaultBaseAuth.__validateFraction(authConfig);
|
|
181
196
|
this.__renewalIncrement = VaultBaseAuth.__validateIncrement(authConfig);
|
|
197
|
+
this.__revokeOnClose = VaultBaseAuth.__validateRevokeOnClose(authConfig);
|
|
182
198
|
}
|
|
183
199
|
|
|
184
200
|
/**
|
|
@@ -204,6 +220,29 @@ class VaultBaseAuth {
|
|
|
204
220
|
return config.renewal;
|
|
205
221
|
}
|
|
206
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Strict for the same reason as "renewal": node-config's `custom-environment-variables`
|
|
225
|
+
* yields strings, so a loose check would read `VAULT_REVOKE_ON_CLOSE=true` as `'true'`
|
|
226
|
+
* and silently leave revocation off -- the exact behaviour the flag exists to request.
|
|
227
|
+
*
|
|
228
|
+
* @param {Object} [authConfig]
|
|
229
|
+
* @returns {boolean}
|
|
230
|
+
* @private
|
|
231
|
+
*/
|
|
232
|
+
static __validateRevokeOnClose(config) {
|
|
233
|
+
if (!config || config.revokeOnClose === undefined) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (typeof config.revokeOnClose !== 'boolean') {
|
|
238
|
+
throw new errors.InvalidArgumentsError(
|
|
239
|
+
`"revokeOnClose" should be a boolean, got ${String(config.revokeOnClose)}`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return config.revokeOnClose;
|
|
244
|
+
}
|
|
245
|
+
|
|
207
246
|
/**
|
|
208
247
|
* @param {Object} [authConfig]
|
|
209
248
|
* @returns {number}
|
|
@@ -346,6 +385,75 @@ class VaultBaseAuth {
|
|
|
346
385
|
this.__cancelCount++;
|
|
347
386
|
}
|
|
348
387
|
|
|
388
|
+
/**
|
|
389
|
+
* Revoke the held token on the Vault server (`POST auth/token/revoke-self`), when
|
|
390
|
+
* `auth.revokeOnClose` is enabled. Called by `VaultClient#close()`.
|
|
391
|
+
*
|
|
392
|
+
* The token to revoke is the one held now, or the one an in-flight login is about to
|
|
393
|
+
* produce: that login still resolves after a close(), and its token would otherwise
|
|
394
|
+
* outlive the client by its whole TTL. A login that fails issued no token. A token that
|
|
395
|
+
* was never obtained, and one whose revocation an earlier close() already started, cost
|
|
396
|
+
* nothing to leave alone.
|
|
397
|
+
*
|
|
398
|
+
* @returns {Promise<void>|null} the revocation, so a caller may await it, or `null` when
|
|
399
|
+
* there is nothing to do: the option is off, or no token was ever obtained. The
|
|
400
|
+
* returned promise never rejects: `VaultClient.clear()` fires it without awaiting,
|
|
401
|
+
* and a failed revocation is logged (`revokeSelf on close failed`) with the token
|
|
402
|
+
* left live on the Vault server instead.
|
|
403
|
+
*/
|
|
404
|
+
revokeSelfOnClose() {
|
|
405
|
+
if (!this.__revokeOnClose) {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
if (this.__pendingLogin === null && this.__authToken === null) {
|
|
409
|
+
// No token was ever obtained: nothing was issued, so there is nothing to revoke.
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// The token to revoke: the one held now, or the one the in-flight login resolves
|
|
414
|
+
// with. The login's own bookkeeping (the token cache, timer decisions) has run by
|
|
415
|
+
// the time this `then` sees it.
|
|
416
|
+
const held = this.__pendingLogin !== null
|
|
417
|
+
? this.__pendingLogin.then((authToken) => authToken, () => null)
|
|
418
|
+
: Promise.resolve(this.__authToken);
|
|
419
|
+
|
|
420
|
+
return held.then((authToken) => {
|
|
421
|
+
// An expired token is already dead server-side: revoke-self would answer 403 and log
|
|
422
|
+
// a failure claiming the token was left live.
|
|
423
|
+
if (authToken === null || authToken.isExpired() || authToken.getId() === this.__revokedOnCloseId) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const id = authToken.getId();
|
|
427
|
+
this.__revokedOnCloseId = id;
|
|
428
|
+
try {
|
|
429
|
+
this._log.info('revoking the auth token on close (accessor=%s)', authToken.getAccessor());
|
|
430
|
+
} catch {
|
|
431
|
+
// a throwing logger must not cancel the revocation
|
|
432
|
+
}
|
|
433
|
+
return this.__apiClient
|
|
434
|
+
.makeRequest('POST', '/auth/token/revoke-self', null, { 'X-Vault-Token': authToken.getId() })
|
|
435
|
+
.then(() => {
|
|
436
|
+
// Gone server-side: do not hand it to a call that keeps using the client.
|
|
437
|
+
if (this.__authToken === authToken) {
|
|
438
|
+
this.__authToken = null;
|
|
439
|
+
}
|
|
440
|
+
}, (err) => {
|
|
441
|
+
// Still live: let a later close() try again.
|
|
442
|
+
if (this.__revokedOnCloseId === id) {
|
|
443
|
+
this.__revokedOnCloseId = null;
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
this._log.error(
|
|
447
|
+
'revokeSelf on close failed (accessor=%s): %s',
|
|
448
|
+
authToken.getAccessor(), safeMessage(err)
|
|
449
|
+
);
|
|
450
|
+
} catch {
|
|
451
|
+
// a throwing logger changes nothing: the failure is already lost to the caller
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
349
457
|
/**
|
|
350
458
|
* Called by VaultClient after Vault answered 403 to a request made with `authToken`.
|
|
351
459
|
* Vault answers 403 both to a token it no longer accepts (revoked, or expired before the
|
|
@@ -363,6 +471,12 @@ class VaultBaseAuth {
|
|
|
363
471
|
return Promise.resolve(false);
|
|
364
472
|
}
|
|
365
473
|
const id = authToken.getId();
|
|
474
|
+
if (id === this.__revokedOnCloseId) {
|
|
475
|
+
// close() revoked this token on purpose (auth.revokeOnClose): a request still in
|
|
476
|
+
// flight that it rejects must not log in again after close() and leave a new,
|
|
477
|
+
// unrevoked token behind.
|
|
478
|
+
return Promise.resolve(false);
|
|
479
|
+
}
|
|
366
480
|
if (this.__authToken === null || this.__authToken.getId() !== id) {
|
|
367
481
|
return Promise.resolve(true);
|
|
368
482
|
}
|
package/src/errors.d.ts
CHANGED
|
@@ -12,9 +12,16 @@ export declare class AuthTokenExpiredError extends VaultError {}
|
|
|
12
12
|
export declare class UnsupportedOperationError extends VaultError {}
|
|
13
13
|
|
|
14
14
|
export declare class VaultHttpError extends VaultError {
|
|
15
|
-
constructor(statusCode: number, text: string, body?: unknown);
|
|
15
|
+
constructor(statusCode: number, text: string, body?: unknown, headers?: { retryAfter?: string | null });
|
|
16
16
|
statusCode: number;
|
|
17
17
|
error: unknown;
|
|
18
|
+
/** Raw `Retry-After` response header; absent when the response did not send one or it was empty. */
|
|
19
|
+
retryAfter?: string;
|
|
20
|
+
/**
|
|
21
|
+
* `Retry-After` in milliseconds (delay-seconds or HTTP-date), capped at 2147483647;
|
|
22
|
+
* absent when the header was not sent or cannot be parsed.
|
|
23
|
+
*/
|
|
24
|
+
retryAfterMs?: number;
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
export declare class RequestTimeoutError extends VaultError {
|
package/src/errors.js
CHANGED
|
@@ -19,6 +19,29 @@ class InvalidAWSCredentialsError extends InvalidArgumentsError {}
|
|
|
19
19
|
class AuthTokenExpiredError extends VaultError {}
|
|
20
20
|
class UnsupportedOperationError extends VaultError {}
|
|
21
21
|
|
|
22
|
+
// The longest delay setTimeout() honours; a longer one fires after 1 ms (same limit as api.timeout).
|
|
23
|
+
const MAX_DELAY_MS = 2147483647;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parses a `Retry-After` header value (RFC 9110 §10.2.3): delay-seconds or an HTTP-date.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} value - the raw header value, trimmed and not empty
|
|
29
|
+
* @param {number} now - current time in milliseconds
|
|
30
|
+
* @returns {number|undefined} milliseconds to wait, at most MAX_DELAY_MS; `undefined` for a
|
|
31
|
+
* value in neither format (e.g. `-5`, or `30, 120` from two headers)
|
|
32
|
+
*/
|
|
33
|
+
function parseRetryAfter(value, now) {
|
|
34
|
+
if (/^\d+$/.test(value)) {
|
|
35
|
+
return Math.min(Number(value) * 1000, MAX_DELAY_MS);
|
|
36
|
+
}
|
|
37
|
+
// An HTTP-date always names the day or the month; Date.parse() alone also accepts e.g. "-5".
|
|
38
|
+
const date = /[a-z]/i.test(value) ? Date.parse(value) : NaN;
|
|
39
|
+
if (Number.isNaN(date)) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return Math.min(Math.max(0, date - now), MAX_DELAY_MS);
|
|
43
|
+
}
|
|
44
|
+
|
|
22
45
|
/**
|
|
23
46
|
* A non-2xx HTTP response from the Vault server.
|
|
24
47
|
*
|
|
@@ -33,11 +56,26 @@ class VaultHttpError extends VaultError {
|
|
|
33
56
|
* @param {string} text - raw response body text, used in the message.
|
|
34
57
|
* @param {*} [body] - parsed JSON body when the response was valid JSON, the raw text
|
|
35
58
|
* otherwise, or `undefined` for an empty body. Exposed as `error.error`.
|
|
59
|
+
* @param {Object} [headers] - response headers the error exposes:
|
|
60
|
+
* @param {string|null} [headers.retryAfter] - the raw `Retry-After` header, `null` or
|
|
61
|
+
* `undefined` when it was not sent. Vault's rate-limit quotas answer 429 with it, in
|
|
62
|
+
* seconds; proxies in front of Vault send it with 429 and 503. Exposed as
|
|
63
|
+
* `error.retryAfter` (raw) and `error.retryAfterMs` (parsed); both are absent when the
|
|
64
|
+
* header was not sent or is empty, and `retryAfterMs` also when it cannot be parsed.
|
|
36
65
|
*/
|
|
37
|
-
constructor(statusCode, text, body) {
|
|
66
|
+
constructor(statusCode, text, body, { retryAfter } = {}) {
|
|
38
67
|
super(`${statusCode} - ${text}`);
|
|
39
68
|
this.statusCode = statusCode;
|
|
40
69
|
this.error = body;
|
|
70
|
+
|
|
71
|
+
const value = typeof retryAfter === 'string' ? retryAfter.trim() : '';
|
|
72
|
+
if (value !== '') {
|
|
73
|
+
this.retryAfter = value;
|
|
74
|
+
const ms = parseRetryAfter(value, Date.now());
|
|
75
|
+
if (ms !== undefined) {
|
|
76
|
+
this.retryAfterMs = ms;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
41
79
|
}
|
|
42
80
|
}
|
|
43
81
|
|