Skip to content

Commit 7ad67a9

Browse files
authored
Merge pull request #292 from nodevault/copilot/add-dynamic-credentials-support
Add automatic token and lease renewal with event-based lifecycle management
2 parents e2b9625 + c392917 commit 7ad67a9

File tree

3 files changed

+444
-1
lines changed

3 files changed

+444
-1
lines changed

index.d.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,22 @@ declare namespace NodeVault {
4646
}
4747
}
4848

49+
interface TokenRenewalOptions {
50+
/** Initial TTL in seconds. If omitted, tokenLookupSelf is called. */
51+
ttl?: number;
52+
/** Increment to request when renewing. */
53+
increment?: number | string;
54+
/** Fraction of TTL at which to renew (default 0.8). */
55+
renewFraction?: number;
56+
}
57+
58+
interface LeaseRenewalOptions {
59+
/** Increment to request when renewing. */
60+
increment?: number;
61+
/** Fraction of TTL at which to renew (default 0.8). */
62+
renewFraction?: number;
63+
}
64+
4965
interface client {
5066
handleVaultResponse(res?: { statusCode: number, request: Option, body: any }): Promise<any>;
5167
apiVersion: string;
@@ -64,6 +80,26 @@ declare namespace NodeVault {
6480
generateFunction(name: string, conf: functionConf): void;
6581
commands: { [name: string]: functionConf };
6682

83+
// EventEmitter methods
84+
on(event: string | symbol, listener: (...args: any[]) => void): this;
85+
once(event: string | symbol, listener: (...args: any[]) => void): this;
86+
off(event: string | symbol, listener: (...args: any[]) => void): this;
87+
removeListener(event: string | symbol, listener: (...args: any[]) => void): this;
88+
removeAllListeners(event?: string | symbol): this;
89+
emit(event: string | symbol, ...args: any[]): boolean;
90+
listeners(event: string | symbol): Function[];
91+
listenerCount(event: string | symbol): number;
92+
eventNames(): Array<string | symbol>;
93+
94+
// Token renewal
95+
startTokenRenewal(options?: TokenRenewalOptions): Promise<any>;
96+
stopTokenRenewal(): void;
97+
98+
// Lease renewal
99+
startLeaseRenewal(leaseId: string, leaseDuration: number, options?: LeaseRenewalOptions): void;
100+
stopLeaseRenewal(leaseId: string): void;
101+
stopAllRenewals(): void;
102+
67103
status(options?: Option): Promise<any>;
68104
initialized(options?: Option): Promise<any>;
69105
init(options?: Option): Promise<any>;

src/index.js

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const originalCommands = require('./commands.js');
66
const originalMustache = require('mustache');
77
const axios = require('axios');
88
const https = require('https');
9+
const EventEmitter = require('events');
910

1011
class VaultError extends Error {}
1112

@@ -165,7 +166,7 @@ module.exports = (config = {}) => {
165166
});
166167
};
167168
})();
168-
const client = {};
169+
const client = new EventEmitter();
169170

170171
function handleVaultResponse(response) {
171172
if (!response) return Promise.reject(new VaultError('No response passed'));
@@ -378,5 +379,154 @@ module.exports = (config = {}) => {
378379
const assignFunctions = (commandName) => generateFunction(commandName, commands[commandName]);
379380
Object.keys(commands).forEach(assignFunctions);
380381

382+
// -- Token renewal management --
383+
let tokenRenewalTimer = null;
384+
385+
/**
386+
* Start automatic token renewal.
387+
* @param {Object} [opts={}] - Options
388+
* @param {number} [opts.ttl] - Initial TTL in seconds. If omitted,
389+
* tokenLookupSelf is called to determine the current TTL.
390+
* @param {number|string} [opts.increment] - Increment to request when
391+
* renewing (forwarded to tokenRenewSelf).
392+
* @param {number} [opts.renewFraction=0.8] - Fraction of TTL at which
393+
* to renew (0 < renewFraction < 1).
394+
* @returns {Promise} Resolves once the first renewal is scheduled.
395+
*
396+
* Events emitted:
397+
* 'token:renewed' – successful renewal, receives the response.
398+
* 'token:error:renew' – renewal failed, receives the error.
399+
* 'token:expired' – token is no longer renewable.
400+
*/
401+
client.startTokenRenewal = (opts = {}) => {
402+
const renewFraction = opts.renewFraction || 0.8;
403+
const increment = opts.increment;
404+
405+
client.stopTokenRenewal();
406+
407+
function scheduleRenewal(ttl) {
408+
const delay = Math.max(1, Math.floor(ttl * renewFraction)) * 1000;
409+
tokenRenewalTimer = setTimeout(() => {
410+
const renewArgs = increment != null ? { increment } : {};
411+
client.tokenRenewSelf(renewArgs)
412+
.then((result) => {
413+
client.emit('token:renewed', result);
414+
const newTtl = result.auth && result.auth.lease_duration;
415+
const renewable = result.auth && result.auth.renewable;
416+
if (newTtl > 0 && renewable !== false) {
417+
scheduleRenewal(newTtl);
418+
} else {
419+
tokenRenewalTimer = null;
420+
client.emit('token:expired');
421+
}
422+
})
423+
.catch((err) => {
424+
tokenRenewalTimer = null;
425+
client.emit('token:error:renew', err);
426+
});
427+
}, delay);
428+
if (tokenRenewalTimer.unref) tokenRenewalTimer.unref();
429+
}
430+
431+
const ttl = opts.ttl;
432+
if (ttl != null && ttl > 0) {
433+
scheduleRenewal(ttl);
434+
return Promise.resolve();
435+
}
436+
437+
return client.tokenLookupSelf().then((result) => {
438+
const currentTtl = result.data && result.data.ttl;
439+
if (currentTtl > 0) {
440+
scheduleRenewal(currentTtl);
441+
}
442+
return result;
443+
});
444+
};
445+
446+
/**
447+
* Stop automatic token renewal.
448+
*/
449+
client.stopTokenRenewal = () => {
450+
if (tokenRenewalTimer) {
451+
clearTimeout(tokenRenewalTimer);
452+
tokenRenewalTimer = null;
453+
}
454+
};
455+
456+
// -- Lease renewal management --
457+
const leaseTimers = {};
458+
459+
/**
460+
* Start automatic lease renewal.
461+
* @param {string} leaseId - The lease ID to renew.
462+
* @param {number} leaseDuration - Initial lease duration in seconds.
463+
* @param {Object} [opts={}] - Options
464+
* @param {number} [opts.increment] - Increment to request on renewal.
465+
* @param {number} [opts.renewFraction=0.8] - Fraction of TTL at which
466+
* to renew (0 < renewFraction < 1).
467+
*
468+
* Events emitted:
469+
* 'lease:renewed' – successful renewal, receives { leaseId, result }.
470+
* 'lease:error:renew' – renewal failed, receives { leaseId, error }.
471+
* 'lease:expired' – lease is no longer renewable, receives { leaseId }.
472+
*/
473+
client.startLeaseRenewal = (leaseId, leaseDuration, opts = {}) => {
474+
if (!leaseId) throw new VaultError('leaseId is required');
475+
if (!leaseDuration || leaseDuration <= 0) throw new VaultError('leaseDuration must be positive');
476+
477+
const renewFraction = opts.renewFraction || 0.8;
478+
const increment = opts.increment;
479+
480+
client.stopLeaseRenewal(leaseId);
481+
482+
function scheduleRenewal(duration) {
483+
const delay = Math.max(1, Math.floor(duration * renewFraction)) * 1000;
484+
const timer = setTimeout(() => {
485+
const renewArgs = { lease_id: leaseId };
486+
if (increment != null) renewArgs.increment = increment;
487+
client.renew(renewArgs)
488+
.then((result) => {
489+
client.emit('lease:renewed', { leaseId, result });
490+
if (result.lease_duration > 0 && result.renewable !== false) {
491+
scheduleRenewal(result.lease_duration);
492+
} else {
493+
delete leaseTimers[leaseId];
494+
client.emit('lease:expired', { leaseId });
495+
}
496+
})
497+
.catch((err) => {
498+
delete leaseTimers[leaseId];
499+
client.emit('lease:error:renew', { leaseId, error: err });
500+
});
501+
}, delay);
502+
if (timer.unref) timer.unref();
503+
leaseTimers[leaseId] = timer;
504+
}
505+
506+
scheduleRenewal(leaseDuration);
507+
};
508+
509+
/**
510+
* Stop automatic renewal for a specific lease.
511+
* @param {string} leaseId - The lease ID to stop renewing.
512+
*/
513+
client.stopLeaseRenewal = (leaseId) => {
514+
if (leaseTimers[leaseId]) {
515+
clearTimeout(leaseTimers[leaseId]);
516+
delete leaseTimers[leaseId];
517+
}
518+
};
519+
520+
/**
521+
* Stop all automatic renewals (token + all leases).
522+
*/
523+
client.stopAllRenewals = () => {
524+
client.stopTokenRenewal();
525+
Object.keys(leaseTimers).forEach((id) => {
526+
clearTimeout(leaseTimers[id]);
527+
delete leaseTimers[id];
528+
});
529+
};
530+
381531
return client;
382532
};

0 commit comments

Comments
 (0)