A Vault Client implemented in pure javascript for HashiCorp Vault. It supports variety of Auth Backends and performs lease renewal for issued auth token.
npm install --save node-vault-client
Node.js >= 18 — the client uses the native fetch API.
const VaultClient = require('node-vault-client');
const vaultClient = VaultClient.boot('main', {
api: { url: 'https://vault.example.com:8200/' },
auth: {
type: 'appRole', // one of: 'appRole' | 'token' | 'iam' | 'kubernetes'
config: { role_id: '637c065f-c644-5e12-d3d1-e9fa4363af61' }
},
});
vaultClient.read('secret/tst').then(lease => {
console.log(lease.getData()); // read() resolves to a Lease; use getData()/getValue(key)
}).catch(e => console.error(e));const vaultClient = VaultClient.boot('main', {
api: { url: 'https://vault.example.com:8200/' },
auth: {
type: 'iam',
mount: 'aws', // Optional. Vault AWS auth mount point ("aws" by default)
config: {
role: 'my_iam_role',
iam_server_id_header_value: 'https://vault.example.com:8200/', // Optional. X-Vault-AWS-IAM-Server-ID header
namespace: 'some_namespace', // Optional. X-Vault-Namespace header
region: 'eu-central-1', // Optional. AWS STS region (see below)
credentials: { // Optional. Resolved from the AWS provider chain when omitted
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
},
},
});By default the signed GetCallerIdentity request targets the global STS endpoint
sts.amazonaws.com and the SigV4 credential scope is bound to us-east-1. Set
config.region to sign against a regional STS endpoint instead — the request is then sent to
sts.<region>.amazonaws.com and the signature scope is bound to that region. This is required
when Vault's sts_region / sts_endpoint is configured for a non-us-east-1 region (e.g.
eu-central-1); otherwise STS rejects the replayed request with
SignatureDoesNotMatch — Credential should be scoped to a valid region. Omitting region
preserves the previous (global-endpoint) behavior.
const vaultClient = VaultClient.boot('main', {
api: { url: 'https://vault.example.com:8200/' },
auth: {
type: 'appRole',
mount: 'approle', // Optional. Vault AppRole auth mount point ("approle" by default)
config: {
role_id: '637c065f-c644-5e12-d3d1-e9fa4363af61', // Required. RoleID of the AppRole
secret_id: '...', // Optional. Required when bind_secret_id is enabled
},
},
});const vaultClient = VaultClient.boot('main', {
api: { url: 'https://vault.example.com:8200/' },
auth: {
type: 'token',
mount: 'token', // Optional. Vault token auth mount point ("token" by default)
config: {
token: 's.xxxxxxxxxxxxxxxxxxxxxxxx', // Required. Vault token
},
},
});const vaultClient = VaultClient.boot('main', {
api: { url: 'https://vault.example.com:8200/' },
auth: {
type: 'kubernetes',
mount: 'kubernetes', // Optional. Vault Kubernetes auth mount point ("kubernetes" by default)
config: {
role: 'my_k8s_role', // Required. Role configured in the Vault Kubernetes auth backend
tokenPath: '/var/run/secrets/kubernetes.io/serviceaccount/token', // Optional. Defaults to the in-pod service-account token path
},
},
});- VaultClient
- new VaultClient(options)
- instance
- .fillNodeConfig()
- .read(path) ⇒
Promise.<Lease> - .list(path) ⇒
Promise.<Lease> - .write(path, data) ⇒
Promise.<Object> - .close()
- static
- .boot(name, [options]) ⇒
VaultClient - .get(name) ⇒
VaultClient - .clear([name])
- .boot(name, [options]) ⇒
- Lease
Client constructor function.
| Param | Type | Default | Description |
|---|---|---|---|
| options | Object |
||
| options.api | Object |
||
| options.api.url | String |
the url of the vault server | |
| [options.api.apiVersion] | String |
v1 |
|
| [options.api.requestOptions] | Object |
extra options merged into every HTTP request (see Custom transport) | |
| options.auth | Object |
||
| options.auth.type | String |
||
| [options.auth.mount] | String |
Vault auth backend mount point; default varies per method (e.g. "aws" for iam, "approle", "token", "kubernetes") | |
| options.auth.config | Object |
auth configuration variables | |
| [options.auth.config.namespace] | String |
Optional. Vault namespace, sent as the X-Vault-Namespace header on all secret read/list/write requests. Applies to every auth type, not just IAM. |
|
| options.logger | Object |
false |
options.api.requestOptions is shallow-merged into every underlying fetch() call, so you
can route traffic through a proxy/SOCKS agent or trust a self-signed / internal-CA Vault.
Pass an undici dispatcher (request semantics like method
and body always win; headers are merged with per-request headers taking precedence):
const { Agent, ProxyAgent } = require('undici');
// Trust an internal/self-signed CA (preferred over disabling verification)
const vaultClient = VaultClient.boot('main', {
api: {
url: 'https://vault.internal:8200/',
requestOptions: {
dispatcher: new Agent({ connect: { ca: require('fs').readFileSync('/etc/ssl/internal-ca.pem') } }),
},
},
auth: { type: 'token', config: { token: '...' } },
});
// Route through an HTTP proxy / SOCKS agent
const proxied = VaultClient.boot('proxied', {
api: { url: 'https://vault.example.com:8200/', requestOptions: { dispatcher: new ProxyAgent('http://proxy:8080') } },
auth: { type: 'token', config: { token: '...' } },
});For the self-signed-CA case you can also use the process-wide NODE_EXTRA_CA_CERTS=/path/ca.pem
env var with no code change. Only disable verification
(new Agent({ connect: { rejectUnauthorized: false } })) in throwaway/dev setups — it removes
MITM protection.
Populates Vault's values to NPM "config" module
Resolves once the npm config module has been populated from Vault.
Kind: instance method of VaultClient
Read secret from Vault
Kind: instance method of VaultClient
| Param | Type | Description |
|---|---|---|
| path | string |
path to the secret |
Retrieves secrets list
Kind: instance method of VaultClient
| Param | Type | Description |
|---|---|---|
| path | string |
path to the secret |
Writes data to Vault
Resolves to the raw parsed Vault response body, which may be empty/undefined for
204 No Content responses.
Kind: instance method of VaultClient
| Param | Type | Description |
|---|---|---|
| path | path used to write data | |
| data | object |
data to write |
Release resources held by this client.
This client performs lease renewal for renewable auth tokens by arming a background timer.
That timer keeps the Node.js event loop alive, so a short-lived script (e.g. a one-off
read) never exits on its own. Call close() once you are done with the client to cancel
the timer and let the process exit. It is null-safe and safe to call multiple times. The
client may still be used afterwards — the next operation that fetches a renewable token
will arm a new refresh timer.
const vaultClient = VaultClient.boot('main', { /* ... */ });
const secret = await vaultClient.read('secret/tst');
console.log(secret);
vaultClient.close(); // process can now exitKind: instance method of VaultClient
The object returned by read() and list() (they resolve to Promise<Lease>). Use its
accessors to extract the secret data:
getValue(key)⇒String— value for a single key. ThrowsRequested key does not existwhen the key is absent.getData()⇒Object— a deep-cloned copy of the whole secret data object.isRenewable()⇒boolean— whether the underlying lease is renewable.
Boot an instance of Vault
The instance will be stored in a local hash. Calling Vault.boot multiple times with the same name will return the same instance.
Kind: static method of VaultClient
Returns: VaultClient
| Param | Type | Description |
|---|---|---|
| name | String |
Vault instance name |
| [options] | Object |
options for Vault#constructor. |
Get an instance of Vault
The instance will be stored in a local hash. Calling Vault.pop multiple times with the same name will return the same instance.
Kind: static method of VaultClient
Returns: VaultClient
| Param | Type | Description |
|---|---|---|
| name | String |
Vault instance name |
Clear named Vault instance
If no name passed all named instances will be cleared.
Kind: static method of VaultClient
| Param | Type | Description |
|---|---|---|
| [name] | String |
Vault instance name, all instances will be cleared if no name were passed |
By default the client behaves exactly as before (KV v1 / raw passthrough). To enable transparent
KV v2 support set api.kv.autoDetect: true or supply a static api.engines map. Either
option activates path-rewriting and response-unwrapping; callers do not need to know the engine
version.
| Option | Type | Default | Description |
|---|---|---|---|
api.kv.autoDetect |
boolean |
false |
Auto-detect the KV version of each mount on first use via GET sys/internal/ui/mounts/<path>. |
api.engines |
Object |
{} |
Static mount-to-version map, e.g. { secret: 2, legacy: 1 }. Overrides detection; use this when the token lacks permission on sys/internal/ui/mounts. |
Both options can be combined: engines acts as an override — matching mounts skip detection
while unmatched mounts are auto-detected (when autoDetect: true).
const client = VaultClient.boot('main', {
api: {
url: 'https://vault.example.com:8200/',
kv: { autoDetect: true },
},
auth: { type: 'token', config: { token: '...' } },
});
// Works transparently on both KV v1 and KV v2 mounts
const lease = await client.read('secret/my-app/config');
console.log(lease.getData()); // the secret object
console.log(lease.getMetadata()); // KV v2 version metadata (undefined on v1)const client = VaultClient.boot('main', {
api: {
url: 'https://vault.example.com:8200/',
engines: { secret: 2, legacy: 1 },
},
auth: { type: 'token', config: { token: '...' } },
});These methods require a KV v2 mount and throw UnsupportedOperationError on v1 / non-KV mounts.
// Soft-delete specific versions
await client.deleteVersions('secret/foo', [1, 2]);
// Restore soft-deleted versions
await client.undeleteVersions('secret/foo', [1]);
// Permanently destroy versions
await client.destroyVersions('secret/foo', [1, 2]);
// Read version metadata (current_version, versions map, etc.)
const meta = await client.readMetadata('secret/foo');
// Delete all metadata and version history (permanent)
await client.deleteMetadata('secret/foo');// PATCH a subset of keys without overwriting others (KV v2)
await client.update('secret/foo', { password: 'new-value' });
// Sends PATCH secret/data/foo with Content-Type: application/merge-patch+json// Soft-delete the latest version on KV v2; DELETE on v1/passthrough
await client.delete('secret/foo');For any Vault backend that does not benefit from KV path rewriting use request(). It sends the
literal path with no rewriting or response normalisation and returns the parsed body directly.
// Encrypt with Transit engine — path must not be rewritten
const result = await client.request('POST', 'transit/encrypt/my-key', {
plaintext: Buffer.from('hello').toString('base64'),
});
console.log(result.data.ciphertext);getMetadata() is additive — existing code is unaffected.
const lease = await client.read('secret/my-app/db');
lease.getData(); // the secret values
lease.getMetadata(); // { version, created_time, deletion_time, destroyed, custom_metadata }
// undefined on KV v1 / passthrough mountsWhen autoDetect: true or api.engines is set, the client rewrites logical paths to the
correct KV v2 API paths automatically (e.g. secret/foo → secret/data/foo for reads).
Callers must pass logical paths — do not include the internal KV v2 segments (data/,
metadata/, delete/, undelete/, destroy/) in the path argument:
// Correct — logical path only
await client.read('secret/my-app/config');
// Wrong — double-rewrite: 'secret/data/foo' becomes 'secret/data/data/foo' on the wire
await client.read('secret/data/foo');If you need to send a fully-literal Vault API path (e.g. when working with non-KV backends or
when you have already constructed the complete path), use request() which bypasses all path
rewriting:
// Literal path, no rewriting
await client.request('GET', 'secret/data/foo');- Each canonical mount is detected at most once per
VaultClientinstance. - Concurrent first-touch requests for the same mount share a single in-flight detection promise.
- The detection endpoint used is
GET sys/internal/ui/mounts/<path>(readable by any authenticated token). - When the token lacks permission on that endpoint, set
api.enginesto skip detection.
| Class | When thrown |
|---|---|
UnsupportedOperationError |
A v2-only method (deleteVersions, undeleteVersions, destroyVersions, readMetadata, deleteMetadata) is called against a non-v2 mount. |
VaultError |
Mount detection fails (e.g. permission denied) and no api.engines override was provided. |
Contributions are welcome! Please read the contributing guide to get started, and note that this project requires a DCO sign-off on every commit.
Not sure where to start? See SUPPORT.md.
This project adheres to the Contributor Covenant Code of Conduct.
To report a security vulnerability, please follow our Security Policy.
Licensed under the Apache License 2.0.