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/src/VaultClient.js
CHANGED
|
@@ -16,53 +16,113 @@ const safeMessage = require('./safeMessage');
|
|
|
16
16
|
const { trimTrailingSlashes } = require('./slashes');
|
|
17
17
|
|
|
18
18
|
const noop = () => {};
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
// Named instances. A Map, so a name such as "toString" or "constructor" is not found on
|
|
20
|
+
// Object.prototype; keyed exactly as an object property would be, so boot(1) and get('1') still
|
|
21
|
+
// meet and an object that converts to a symbol is keyed by that symbol.
|
|
22
|
+
const vaultInstances = new Map();
|
|
23
|
+
const instanceKey = (name) => Reflect.ownKeys({ [name]: undefined })[0];
|
|
24
|
+
// clear(name) with a non-string name warns once per process (#239); see VaultClient.clear().
|
|
25
|
+
let warnedClearNonString = false;
|
|
26
|
+
|
|
27
|
+
// Functions, and objects that are not plain data (an undici dispatcher, a logger class
|
|
28
|
+
// instance), compare by identity. Their own enumerable keys say nothing about what they do:
|
|
29
|
+
// structurally, any two dispatchers look alike, and so do two loggers built from different
|
|
30
|
+
// functions, so a boot() that passed a different one was ignored without a warning (#224).
|
|
31
|
+
// A Buffer, Date, URL or RegExp compares by value, so an equal one re-created per call still
|
|
32
|
+
// matches. No user code (getters, toJSON, Proxy traps) decides the comparison.
|
|
33
|
+
//
|
|
34
|
+
// Per-process ids, in a WeakMap so that fingerprinting never keeps an option (or the closure
|
|
35
|
+
// state it captures) alive.
|
|
36
|
+
const identityIds = new WeakMap();
|
|
37
|
+
let nextIdentityId = 0;
|
|
38
|
+
|
|
39
|
+
function identityId(value) {
|
|
40
|
+
if (!identityIds.has(value)) {
|
|
41
|
+
identityIds.set(value, ++nextIdentityId);
|
|
42
|
+
}
|
|
43
|
+
return identityIds.get(value);
|
|
44
|
+
}
|
|
25
45
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
46
|
+
/**
|
|
47
|
+
* How an object takes part in the fingerprint: 'array' and 'plain' by content, 'value' by
|
|
48
|
+
* {@link valueOf}, anything else by identity. Plain includes a null prototype and an
|
|
49
|
+
* Object.prototype from another realm (vm, a test runner's structuredClone). A Proxy whose
|
|
50
|
+
* traps throw compares by identity rather than making boot() throw.
|
|
51
|
+
*/
|
|
52
|
+
function kindOf(value) {
|
|
53
|
+
try {
|
|
54
|
+
if (Array.isArray(value)) {
|
|
55
|
+
return 'array';
|
|
56
|
+
}
|
|
57
|
+
if (Buffer.isBuffer(value) || value instanceof Date || value instanceof URL || value instanceof RegExp) {
|
|
58
|
+
return 'value';
|
|
59
|
+
}
|
|
60
|
+
const proto = Object.getPrototypeOf(value);
|
|
61
|
+
return proto === null || Object.getPrototypeOf(proto) === null ? 'plain' : 'identity';
|
|
62
|
+
} catch {
|
|
63
|
+
return 'identity';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
30
66
|
|
|
31
|
-
function
|
|
32
|
-
if (
|
|
33
|
-
|
|
67
|
+
function valueOf(value) {
|
|
68
|
+
if (Buffer.isBuffer(value)) {
|
|
69
|
+
return `buffer:${value.toString('base64')}`;
|
|
70
|
+
}
|
|
71
|
+
if (value instanceof Date) {
|
|
72
|
+
return `date:${value.getTime()}`;
|
|
34
73
|
}
|
|
35
|
-
return
|
|
74
|
+
return `${value instanceof URL ? 'url' : 'regexp'}:${JSON.stringify(String(value))}`;
|
|
36
75
|
}
|
|
37
76
|
|
|
38
77
|
/**
|
|
39
|
-
* Order-insensitive canonical form of
|
|
40
|
-
*
|
|
41
|
-
*
|
|
78
|
+
* Order-insensitive canonical form of a boot option, hashed so that no credential is retained
|
|
79
|
+
* just to compare two boot() calls. Plain objects and arrays compare by content, functions and
|
|
80
|
+
* other objects by identity, and a Buffer, Date, URL or RegExp by value.
|
|
42
81
|
*/
|
|
43
|
-
function fingerprintOptions(value, seen
|
|
82
|
+
function fingerprintOptions(value, seen) {
|
|
44
83
|
if (typeof value === 'function') {
|
|
45
|
-
return
|
|
84
|
+
return `fn#${identityId(value)}`;
|
|
85
|
+
}
|
|
86
|
+
if (typeof value === 'bigint') {
|
|
87
|
+
return `${value}n`;
|
|
46
88
|
}
|
|
47
89
|
if (value === null || typeof value !== 'object') {
|
|
48
90
|
return JSON.stringify(value) || String(value);
|
|
49
91
|
}
|
|
92
|
+
const kind = kindOf(value);
|
|
93
|
+
if (kind === 'value') {
|
|
94
|
+
return valueOf(value);
|
|
95
|
+
}
|
|
96
|
+
if (kind === 'identity') {
|
|
97
|
+
return `obj#${identityId(value)}`;
|
|
98
|
+
}
|
|
50
99
|
seen = seen || new Set();
|
|
51
100
|
if (seen.has(value)) {
|
|
52
101
|
return 'circular';
|
|
53
102
|
}
|
|
54
103
|
seen.add(value);
|
|
55
|
-
if (
|
|
104
|
+
if (kind === 'array') {
|
|
56
105
|
return `[${value.map((item) => fingerprintOptions(item, seen)).join(',')}]`;
|
|
57
106
|
}
|
|
58
107
|
const body = Object.keys(value).sort()
|
|
59
|
-
.map((key) => `${JSON.stringify(key)}:${fingerprintOptions(value[key], seen
|
|
108
|
+
.map((key) => `${JSON.stringify(key)}:${fingerprintOptions(value[key], seen)}`)
|
|
60
109
|
.join(',');
|
|
61
110
|
return `{${body}}`;
|
|
62
111
|
}
|
|
63
112
|
|
|
64
|
-
|
|
65
|
-
|
|
113
|
+
/** One digest per top-level option, so a mismatch can name the options that differ. */
|
|
114
|
+
function optionsDigests(options) {
|
|
115
|
+
const digests = new Map();
|
|
116
|
+
for (const key of Object.keys(options)) {
|
|
117
|
+
digests.set(key, crypto.createHash('sha256').update(fingerprintOptions(options[key])).digest('hex'));
|
|
118
|
+
}
|
|
119
|
+
return digests;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The top-level options whose digests differ, sorted; empty when the options match. */
|
|
123
|
+
function differingOptions(before, after) {
|
|
124
|
+
const keys = new Set([...before.keys(), ...after.keys()]);
|
|
125
|
+
return [...keys].filter((key) => before.get(key) !== after.get(key)).sort();
|
|
66
126
|
}
|
|
67
127
|
|
|
68
128
|
// fetch() parses the joined URL per the WHATWG URL Standard: '?' starts the query, '#' the
|
|
@@ -133,6 +193,32 @@ function assertSafeKvPath(path, allowTrailingSlash) {
|
|
|
133
193
|
return trimmed;
|
|
134
194
|
}
|
|
135
195
|
|
|
196
|
+
/**
|
|
197
|
+
* clear() with a name that is not a string closes every instance, which is rarely what the
|
|
198
|
+
* caller meant (#239): `clear(42)`, or `clear` passed as a callback that receives an exit code
|
|
199
|
+
* or a resolved value. The behaviour stays as it was; this makes it visible, once per process.
|
|
200
|
+
* It names only the argument's type, never its value, which could be anything (#239).
|
|
201
|
+
* Node delivers warnings on the next tick, so one emitted from an 'exit' listener is never shown;
|
|
202
|
+
* --no-deprecation silences it, as for any DeprecationWarning.
|
|
203
|
+
* @param {*} name
|
|
204
|
+
* @returns {void}
|
|
205
|
+
* @private
|
|
206
|
+
*/
|
|
207
|
+
function warnClearNonString(name) {
|
|
208
|
+
if (warnedClearNonString) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
warnedClearNonString = true;
|
|
212
|
+
const type = typeof name;
|
|
213
|
+
const what = name === null ? 'null' : (type === 'object' ? 'an object' : `a ${type}`);
|
|
214
|
+
process.emitWarning(
|
|
215
|
+
`VaultClient.clear() was called with ${what} ` +
|
|
216
|
+
'instead of a string name, which clears every instance not named by a symbol, as it always ' +
|
|
217
|
+
'has. Call clear() with no argument to clear them all, or pass the string name of one instance.',
|
|
218
|
+
{ type: 'DeprecationWarning', code: 'NVC_DEP_CLEAR_NON_STRING' }
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
136
222
|
class VaultClient {
|
|
137
223
|
|
|
138
224
|
/**
|
|
@@ -140,11 +226,16 @@ class VaultClient {
|
|
|
140
226
|
*
|
|
141
227
|
* @param {Object} options
|
|
142
228
|
* @param {Object} options.api
|
|
143
|
-
* @param {String} options.api.url - the url of the vault server
|
|
229
|
+
* @param {String} options.api.url - the url of the vault server. Plain `http://` to a host
|
|
230
|
+
* other than this machine logs a warning; see "Plain `http://` api.url" in the README
|
|
144
231
|
* @param {String} [options.api.apiVersion='v1']
|
|
145
232
|
* @param {Object} [options.api.requestOptions] - extra options merged into every HTTP
|
|
146
233
|
* request (e.g. an undici `dispatcher` for a proxy/SOCKS agent or custom TLS/CA trust).
|
|
234
|
+
* Redirects are followed only within the origin of `api.url` unless it sets `redirect`.
|
|
147
235
|
* See {@link VaultApiClient#constructor}.
|
|
236
|
+
* @param {number} [options.api.timeout=120000] - milliseconds a request may take, response
|
|
237
|
+
* headers, body and redirects included, before it rejects with RequestTimeoutError.
|
|
238
|
+
* `0` turns the limit off.
|
|
148
239
|
* @param {Object} [options.api.kv] - KV engine options
|
|
149
240
|
* @param {boolean} [options.api.kv.autoDetect=false] - When true, auto-detect KV version per mount
|
|
150
241
|
* @param {Object} [options.api.engines] - Mount-to-version overrides, e.g. { secret: 2, legacy: 1 }
|
|
@@ -227,8 +318,15 @@ class VaultClient {
|
|
|
227
318
|
* `options` are used only when the instance is first created. A later call for a name
|
|
228
319
|
* that already exists returns the existing instance and ignores the options it was
|
|
229
320
|
* given; when those options differ from the ones the instance was booted with, that is
|
|
230
|
-
* logged as a warning
|
|
231
|
-
*
|
|
321
|
+
* logged as a warning, naming the top-level options that differ, rather than passing
|
|
322
|
+
* silently. The warning is logged once per instance for each set of differing options.
|
|
323
|
+
* Use {@link VaultClient.get} to fetch an existing instance, or {@link VaultClient.clear}
|
|
324
|
+
* to replace one.
|
|
325
|
+
*
|
|
326
|
+
* Options are compared as hashes, so no credential is kept for the comparison. Plain
|
|
327
|
+
* objects and arrays compare by content, in any key order. Functions and other objects (a
|
|
328
|
+
* logger class instance, an undici dispatcher) compare by identity, and a Buffer, Date,
|
|
329
|
+
* URL or RegExp by value.
|
|
232
330
|
*
|
|
233
331
|
* @param {String} name - Vault instance name
|
|
234
332
|
* @param {Object} options - options for {@link VaultClient#constructor}. Required on
|
|
@@ -240,17 +338,26 @@ class VaultClient {
|
|
|
240
338
|
throw new errors.InvalidArgumentsError('Options should be provided');
|
|
241
339
|
}
|
|
242
340
|
|
|
243
|
-
let instance = vaultInstances
|
|
341
|
+
let instance = vaultInstances.get(instanceKey(name));
|
|
244
342
|
if (instance === undefined) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
343
|
+
instance = new VaultClient(options);
|
|
344
|
+
vaultInstances.set(instanceKey(name), instance);
|
|
345
|
+
instance.__bootDigests = optionsDigests(options);
|
|
346
|
+
instance.__bootWarned = new Set();
|
|
347
|
+
return instance;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Once per instance for each set of differing options: code that re-creates its logger
|
|
351
|
+
// for every boot() call would otherwise log this on every call.
|
|
352
|
+
const differing = differingOptions(instance.__bootDigests, optionsDigests(options)).join(', ');
|
|
353
|
+
if (differing !== '' && !instance.__bootWarned.has(differing)) {
|
|
354
|
+
instance.__bootWarned.add(differing);
|
|
248
355
|
instance.__log.warn(
|
|
249
356
|
'VaultClient.boot("%s") was called with different options than the existing ' +
|
|
250
357
|
'instance was booted with; the existing instance is returned and these options ' +
|
|
251
358
|
'are ignored. Use VaultClient.get("%s") to fetch it, or VaultClient.clear("%s") ' +
|
|
252
|
-
'first to replace it.',
|
|
253
|
-
name, name, name
|
|
359
|
+
'first to replace it. Differing options: %s.',
|
|
360
|
+
name, name, name, differing
|
|
254
361
|
);
|
|
255
362
|
}
|
|
256
363
|
return instance;
|
|
@@ -266,7 +373,7 @@ class VaultClient {
|
|
|
266
373
|
* @return {VaultClient}
|
|
267
374
|
*/
|
|
268
375
|
static get(name) {
|
|
269
|
-
let instance = vaultInstances
|
|
376
|
+
let instance = vaultInstances.get(instanceKey(name));
|
|
270
377
|
|
|
271
378
|
if (instance === undefined) {
|
|
272
379
|
throw new errors.InvalidArgumentsError('Invalid instance name');
|
|
@@ -280,22 +387,33 @@ class VaultClient {
|
|
|
280
387
|
*
|
|
281
388
|
* If no name passed all named instances will be cleared.
|
|
282
389
|
*
|
|
390
|
+
* Only a string names one instance. Any other argument (a number, `null`, an object) clears
|
|
391
|
+
* every instance, as it always has, and emits a one-time `DeprecationWarning`
|
|
392
|
+
* (`NVC_DEP_CLEAR_NON_STRING`): call `clear()` with no argument to clear them all.
|
|
393
|
+
*
|
|
283
394
|
* @param {String} [name] - Vault instance name, all instances will be cleared if no name were passed
|
|
284
395
|
*/
|
|
285
396
|
static clear(name) {
|
|
286
397
|
if (typeof name === 'string') {
|
|
287
|
-
const instance = vaultInstances
|
|
398
|
+
const instance = vaultInstances.get(name);
|
|
288
399
|
if (instance !== undefined) {
|
|
289
400
|
instance.close();
|
|
290
|
-
delete
|
|
401
|
+
vaultInstances.delete(name);
|
|
291
402
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (name !== undefined) {
|
|
406
|
+
warnClearNonString(name);
|
|
407
|
+
}
|
|
408
|
+
for (const [key, instance] of vaultInstances) {
|
|
409
|
+
// Kept for compatibility: the object this replaced was walked with for...in,
|
|
410
|
+
// which skips symbol keys, so a symbol-named instance has always stayed
|
|
411
|
+
// registered, with its renewal timer, through clear(), and callers may rely on it.
|
|
412
|
+
if (typeof key === 'symbol') {
|
|
413
|
+
continue;
|
|
298
414
|
}
|
|
415
|
+
instance.close();
|
|
416
|
+
vaultInstances.delete(key);
|
|
299
417
|
}
|
|
300
418
|
}
|
|
301
419
|
|
|
@@ -306,11 +424,17 @@ class VaultClient {
|
|
|
306
424
|
* alive for renewable tokens. Call this once you are done with the client so a
|
|
307
425
|
* short-lived script can exit on its own. Safe to call when no timer is armed and
|
|
308
426
|
* safe to call multiple times. After calling `close()` the client may still be used;
|
|
309
|
-
* the next operation that fetches a renewable token will arm a new refresh timer.
|
|
427
|
+
* the next operation that fetches a renewable token will arm a new refresh timer. A
|
|
428
|
+
* login already in flight when `close()` runs still resolves, but arms no timer, and
|
|
429
|
+
* neither does a call that joins that login after `close()`.
|
|
310
430
|
*
|
|
311
431
|
* @returns {void}
|
|
312
432
|
*/
|
|
313
433
|
close() {
|
|
434
|
+
if (this.__auth && typeof this.__auth.__markClosed === 'function') {
|
|
435
|
+
// Directly as well: a subclass may override cancelTokenRefresh() without super.
|
|
436
|
+
this.__auth.__markClosed();
|
|
437
|
+
}
|
|
314
438
|
if (this.__auth && typeof this.__auth.cancelTokenRefresh === 'function') {
|
|
315
439
|
this.__auth.cancelTokenRefresh();
|
|
316
440
|
}
|
|
@@ -396,11 +520,39 @@ class VaultClient {
|
|
|
396
520
|
* @private
|
|
397
521
|
*/
|
|
398
522
|
__detectMount(path) {
|
|
399
|
-
return this.
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
523
|
+
return this.__withAuthToken((token) => {
|
|
524
|
+
const detectPath = `sys/internal/ui/mounts/${path}`;
|
|
525
|
+
return this.__api.makeRequest('GET', detectPath, null, this.getHeaders(token));
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Calls `send` with the current auth token. When Vault answers 403 and the token turns out
|
|
531
|
+
* to be one Vault no longer accepts (revoked server-side), logs in again and calls `send`
|
|
532
|
+
* once more; see {@link VaultBaseAuth#checkRejectedToken}. A policy denial and a 403 under
|
|
533
|
+
* `token` auth surface that 403; otherwise the retry's own result or error is returned.
|
|
534
|
+
*
|
|
535
|
+
* @param {function(AuthToken): Promise<*>} send
|
|
536
|
+
* @returns {Promise<*>}
|
|
537
|
+
* @private
|
|
538
|
+
*/
|
|
539
|
+
__withAuthToken(send) {
|
|
540
|
+
return this.__auth.getAuthToken().then((token) => send(token).catch((err) => {
|
|
541
|
+
if (!(err instanceof errors.VaultHttpError) || err.statusCode !== 403
|
|
542
|
+
|| typeof this.__auth.checkRejectedToken !== 'function') {
|
|
543
|
+
throw err;
|
|
544
|
+
}
|
|
545
|
+
return this.__auth.checkRejectedToken(token).then((retry) => {
|
|
546
|
+
if (!retry) {
|
|
547
|
+
throw err;
|
|
548
|
+
}
|
|
549
|
+
// What the retry got, success or failure, is the request's outcome now: a login
|
|
550
|
+
// that fails, a 404 or a timeout says more than the 403 of a token already gone.
|
|
551
|
+
return this.__auth.getAuthToken().then(send);
|
|
552
|
+
}, () => {
|
|
553
|
+
throw err;
|
|
403
554
|
});
|
|
555
|
+
}));
|
|
404
556
|
}
|
|
405
557
|
|
|
406
558
|
// -------------------------------------------------------------------------
|
|
@@ -445,50 +597,49 @@ class VaultClient {
|
|
|
445
597
|
}
|
|
446
598
|
}
|
|
447
599
|
|
|
448
|
-
return this.
|
|
449
|
-
.
|
|
450
|
-
const headers = Object.assign({}, this.getHeaders(token), extraHeaders || {});
|
|
600
|
+
return this.__withAuthToken((token) => {
|
|
601
|
+
const headers = Object.assign({}, this.getHeaders(token), extraHeaders || {});
|
|
451
602
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
603
|
+
if (raw) {
|
|
604
|
+
return this.__api.makeRequest(method, path, data, headers)
|
|
605
|
+
.then((body) => ({ body, version: undefined, mount: undefined, apiPath: path }));
|
|
606
|
+
}
|
|
456
607
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
608
|
+
return this.__resolver.resolve(path)
|
|
609
|
+
.then(({ mount, version }) => {
|
|
610
|
+
if (v2Only && version !== 2) {
|
|
611
|
+
throw new errors.UnsupportedOperationError(
|
|
612
|
+
`Operation "${op}" is only supported on KV v2 mounts. ` +
|
|
613
|
+
`Mount "${mount}" is not a KV v2 engine.`
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
let apiPath;
|
|
618
|
+
if (version === 2) {
|
|
619
|
+
// Split the path into mount + logical sub-path and rewrite for KV v2
|
|
620
|
+
const logicalPath = this.__logicalPath(path, mount);
|
|
621
|
+
apiPath = rewritePath(version, op, mount, logicalPath);
|
|
622
|
+
} else {
|
|
623
|
+
// v1 / disabled: preserve the caller's literal path byte-for-byte,
|
|
624
|
+
// including any trailing slash, to maintain the old wire behavior.
|
|
625
|
+
apiPath = path;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Data envelope: wrapData forces { data } on every engine version
|
|
629
|
+
// (merge-patch update() semantics — v1 mounts reject PATCH anyway);
|
|
630
|
+
// otherwise only v2 write/update wrap non-null data.
|
|
631
|
+
let requestData = data;
|
|
632
|
+
if (wrapData) {
|
|
633
|
+
requestData = { data };
|
|
634
|
+
} else if (version === 2 && (op === 'write' || op === 'update')
|
|
635
|
+
&& data !== null && data !== undefined) {
|
|
636
|
+
requestData = { data };
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return this.__api.makeRequest(method, apiPath, requestData, headers)
|
|
640
|
+
.then((body) => ({ body, version, mount, apiPath }));
|
|
641
|
+
});
|
|
642
|
+
});
|
|
492
643
|
}
|
|
493
644
|
|
|
494
645
|
/**
|
package/src/auth/AuthToken.js
CHANGED
|
@@ -56,19 +56,25 @@ class AuthToken {
|
|
|
56
56
|
const hasPreciseExpiry = unadjustedExpiresAtMilliseconds !== null;
|
|
57
57
|
const expiresAtMilliseconds = AuthToken.__resolveExpiresAtMilliseconds(data, unadjustedExpiresAtMilliseconds);
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
const renewable = data.renewable !== undefined ? data.renewable : false;
|
|
60
|
+
const authToken = new AuthToken(
|
|
60
61
|
data.id,
|
|
61
62
|
data.accessor,
|
|
62
63
|
data.creation_time,
|
|
63
64
|
null,
|
|
64
65
|
data.explicit_max_ttl,
|
|
65
66
|
data.num_uses,
|
|
66
|
-
|
|
67
|
+
renewable,
|
|
67
68
|
expiresAtMilliseconds,
|
|
68
69
|
data.ttl,
|
|
69
70
|
hasPreciseExpiry,
|
|
70
71
|
unadjustedExpiresAtMilliseconds
|
|
71
72
|
);
|
|
73
|
+
// Without an expiry the token is not renewable. Remember that Vault said it was, so the
|
|
74
|
+
// renewal timer can say why it stops (VaultBaseAuth); a ttl of 0 means it never expires.
|
|
75
|
+
authToken.__renewableWithoutExpiry = Boolean(renewable) && data.ttl !== 0
|
|
76
|
+
&& expiresAtMilliseconds === null;
|
|
77
|
+
return authToken;
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
/**
|
|
@@ -85,7 +91,8 @@ class AuthToken {
|
|
|
85
91
|
* @private
|
|
86
92
|
* @param {Object} data - the `data` object of a token lookup response
|
|
87
93
|
* @param {number|null} expireTime - parsed Vault expiry in milliseconds
|
|
88
|
-
* @returns {int|null} client expiry in milliseconds, or null when
|
|
94
|
+
* @returns {int|null} client expiry in milliseconds, or null when there is none to use: ttl 0
|
|
95
|
+
* without expire_time, or neither expire_time nor a numeric ttl and creation time
|
|
89
96
|
*/
|
|
90
97
|
static __resolveExpiresAtMilliseconds(data, expireTime) {
|
|
91
98
|
const networkLatency = 60;
|
|
@@ -106,7 +113,9 @@ class AuthToken {
|
|
|
106
113
|
// Fallback for responses without a usable `expire_time`.
|
|
107
114
|
const creationTime = parseInt(data.last_renewal_time !== undefined ? data.last_renewal_time : data.creation_time, 10);
|
|
108
115
|
const ttl = data.ttl > networkLatency ? data.ttl - networkLatency : data.ttl;
|
|
109
|
-
|
|
116
|
+
const expiresAt = (creationTime + parseInt(ttl, 10)) * 1000;
|
|
117
|
+
// A missing ttl or creation time made this NaN, a deadline that never passes.
|
|
118
|
+
return Number.isFinite(expiresAt) ? expiresAt : null;
|
|
110
119
|
}
|
|
111
120
|
|
|
112
121
|
/**
|