@rytass/secret-adapter-vault
Version:
Rytass Secret Vault adapter
307 lines (302 loc) • 11.2 kB
JavaScript
'use strict';
var secret = require('@rytass/secret');
var axios = require('axios');
var events = require('events');
var VaultEvents = /*#__PURE__*/ function(VaultEvents) {
VaultEvents["INITED"] = "INITED";
VaultEvents["READY"] = "READY";
VaultEvents["TOKEN_RENEWED"] = "TOKEN_RENEWED";
VaultEvents["TERMINATED"] = "TERMINATED";
VaultEvents["ERROR"] = "ERROR";
return VaultEvents;
}({});
var VaultSecretState = /*#__PURE__*/ function(VaultSecretState) {
VaultSecretState["INIT"] = "INIT";
VaultSecretState["READY"] = "READY";
VaultSecretState["TERMINATED"] = "TERMINATED";
return VaultSecretState;
}({});
class VaultSecret extends secret.SecretManager {
_host;
_auth;
_tokenTTL = 2764724;
_sessionInterval;
_state = VaultSecretState.INIT;
emitter = new events.EventEmitter();
_tokenExpiredOn;
_token;
_online = false;
_cacheData;
_cacheVersion;
constructor(path, options){
super(path);
this._host = options.host;
this._auth = options.auth;
this._tokenTTL = options.tokenTTL || this._tokenTTL;
this._online = options.online || this._online;
this.retrieveToken()?.then(async ()=>{
if (!this._online) {
[this._cacheData, this._cacheVersion] = await this.getSecretVersionOnline();
}
this._state = VaultSecretState.READY;
this.emitter.emit(VaultEvents.READY);
}).catch((ex)=>{
this.emitter.emit(VaultEvents.ERROR, ex);
});
if (typeof options.onError === 'function') {
this.emitter.on(VaultEvents.ERROR, options.onError);
}
if (typeof options.onReady === 'function') {
this.emitter.on(VaultEvents.READY, options.onReady);
}
if (this._online) {
this._sessionInterval = setInterval(()=>this.checkRenew(), this._tokenTTL);
}
}
checkRenew() {
if (this._token) this.renewToken();
}
retrieveToken() {
if (this._auth.account) {
return this.retrieveTokenByUserPass(this._auth.account, this._auth.password);
}
}
async retrieveTokenByUserPass(account, password) {
const { data } = await axios.post(`${this._host}/v1/auth/userpass/login/${account}`, JSON.stringify({
password
}), {
headers: {
'Content-Type': 'application/json'
}
});
if (Array.isArray(data?.errors)) {
data?.errors.forEach((error)=>{
this.emitter.emit(VaultEvents.ERROR, error);
});
this.terminate();
return;
}
this._token = data.auth.client_token;
this._tokenExpiredOn = Date.now() + Math.max(data.auth.lease_duration - 300, 0); // Calculate safety expires time
}
async renewToken() {
if (this._tokenExpiredOn < Date.now()) {
return this.retrieveToken();
}
const { data } = await axios.post(`${this._host}/v1/auth/token/renew-self`, null, {
headers: {
'X-Vault-Token': this._token
}
});
if (Array.isArray(data?.errors)) {
data?.errors.forEach((error)=>{
this.emitter.emit(VaultEvents.ERROR, error);
});
this.terminate();
return;
}
this._token = data.auth.client_token;
this._tokenExpiredOn = Date.now() + data.auth.lease_duration - 300; // Calculate safety expires time
this.emitter.emit(VaultEvents.TOKEN_RENEWED);
}
async getSecretVersionOnline() {
try {
const { data } = await axios.get(`${this._host}/v1/secret/data/${this.project}`, {
headers: {
'X-Vault-Token': this._token
}
});
if (Array.isArray(data?.errors)) {
data?.errors.forEach((error)=>{
this.emitter.emit(VaultEvents.ERROR, error);
});
throw new Error(data?.errors.join(', '));
}
return [
data.data.data,
data.data.metadata.version
];
} catch (ex) {
if (axios.isAxiosError(ex)) {
const { response } = ex;
switch(response?.status){
case 404:
return [
{},
0
];
case 403:
default:
this.emitter.emit(VaultEvents.ERROR, ex);
throw ex;
}
}
throw ex;
}
}
getSecretValue(key) {
if (!this._online) {
return this._cacheData[key];
}
return this.getSecretVersionOnline().then(([currentVersion])=>currentVersion[key] || undefined);
}
async fullReplaceSecretValue(newData) {
await this.renewToken();
const { data } = await axios.post(`${this._host}/v1/secret/data/${this.project}`, JSON.stringify({
data: newData
}), {
headers: {
'X-Vault-Token': this._token
}
});
if (Array.isArray(data?.errors)) {
data?.errors.forEach((error)=>{
this.emitter.emit(VaultEvents.ERROR, error);
});
throw new Error(data?.errors.join(', '));
}
}
async setSecretValueOnline(key, value) {
const [currentValue, currentVersion] = await this.getSecretVersionOnline();
const { data } = await axios.post(`${this._host}/v1/secret/data/${this.project}`, JSON.stringify({
data: {
...currentValue,
[key]: value
},
options: {
cas: currentVersion
}
}), {
headers: {
'X-Vault-Token': this._token
}
});
if (Array.isArray(data?.errors)) {
data?.errors.forEach((error)=>{
this.emitter.emit(VaultEvents.ERROR, error);
});
throw new Error(data?.errors.join(', '));
}
if (!this._online) {
this._cacheData[key] = value;
}
}
async removeSecretKeyOnline(key) {
const [currentValue, currentVersion] = await this.getSecretVersionOnline();
if (!currentValue[key]) throw new Error('Secret key not found');
const { data } = await axios.post(`${this._host}/v1/secret/data/${this.project}`, JSON.stringify({
data: {
...Object.entries(currentValue).reduce((vars, [secretKey, secret])=>{
if (secretKey === key) return vars;
return {
...vars,
[secretKey]: secret
};
}, {})
},
options: {
cas: currentVersion
}
}), {
headers: {
'X-Vault-Token': this._token
}
});
if (Array.isArray(data?.errors)) {
data?.errors.forEach((error)=>{
this.emitter.emit(VaultEvents.ERROR, error);
});
throw new Error(data?.errors.join(', '));
}
if (!this._online) {
delete this._cacheData[key];
}
}
terminate() {
this._state = VaultSecretState.TERMINATED;
this._tokenExpiredOn = Date.now();
if (this._sessionInterval) {
clearInterval(this._sessionInterval);
}
this.emitter.emit(VaultEvents.TERMINATED);
}
get state() {
return this._state;
}
get(key) {
if (!this._online) {
if (this._state === VaultSecretState.INIT) {
throw new Error('Cache data not ready');
}
return this.getSecretValue(key);
}
if (this._token && this._tokenExpiredOn && this._tokenExpiredOn >= Date.now()) {
return this.getSecretValue(key);
}
return new Promise((resolve)=>{
const onTokenRetrieved = ()=>{
this.emitter.removeListener(VaultEvents.READY, onTokenRetrieved);
this.emitter.removeListener(VaultEvents.TOKEN_RENEWED, onTokenRetrieved);
resolve(this.getSecretValue(key));
};
this.emitter.on(VaultEvents.READY, onTokenRetrieved);
this.emitter.on(VaultEvents.TOKEN_RENEWED, onTokenRetrieved);
});
}
set(key, value, syncToOnline = false) {
if (!syncToOnline) {
if (this._state === VaultSecretState.INIT) {
throw new Error('Cache data not ready');
}
this._cacheData[key] = value;
return Promise.resolve();
}
if (this._token && this._tokenExpiredOn && this._tokenExpiredOn >= Date.now()) {
return this.setSecretValueOnline(key, value);
}
return new Promise((resolve)=>{
const onTokenRetrieved = async ()=>{
this.emitter.removeListener(VaultEvents.READY, onTokenRetrieved);
this.emitter.removeListener(VaultEvents.TOKEN_RENEWED, onTokenRetrieved);
resolve(this.setSecretValueOnline(key, value));
};
this.emitter.on(VaultEvents.READY, onTokenRetrieved);
this.emitter.on(VaultEvents.TOKEN_RENEWED, onTokenRetrieved);
});
}
delete(key, syncToOnline = false) {
if (!syncToOnline) {
if (this._state === VaultSecretState.INIT) {
throw new Error('Cache data not ready');
}
delete this._cacheData[key];
return Promise.resolve();
}
if (this._token && this._tokenExpiredOn && this._tokenExpiredOn >= Date.now()) {
return this.removeSecretKeyOnline(key);
}
return new Promise((resolve)=>{
const onTokenRetrieved = async ()=>{
this.emitter.removeListener(VaultEvents.READY, onTokenRetrieved);
this.emitter.removeListener(VaultEvents.TOKEN_RENEWED, onTokenRetrieved);
resolve(this.removeSecretKeyOnline(key));
};
this.emitter.on(VaultEvents.READY, onTokenRetrieved);
this.emitter.on(VaultEvents.TOKEN_RENEWED, onTokenRetrieved);
});
}
async sync(force = false) {
if (this._online) {
throw new Error('This feature only works for offline mode');
}
const [_data, version] = await this.getSecretVersionOnline();
if (version !== this._cacheVersion && !force) {
throw new Error('Online version is not match cached version, please use force mode instead');
}
await this.fullReplaceSecretValue(this._cacheData);
[this._cacheData, this._cacheVersion] = await this.getSecretVersionOnline();
}
}
exports.VaultEvents = VaultEvents;
exports.VaultSecret = VaultSecret;
exports.VaultSecretState = VaultSecretState;