Skip to content

Node.js Vault Client

Build Status Download Status NPM Version License Dependency Status

A Vault Client implemented in pure javascript for HashiCorp Vault. It supports variety of Auth Backends and performs lease renewal for issued auth token.

Install

npm install --save node-vault-client

Requirements

Node.js >= 18 — the client uses the native fetch API.

TypeScript

Type declarations ship with the package, so no separate install is needed:

import VaultClient = require('node-vault-client');

const client = new VaultClient({
    api: { url: 'http://127.0.0.1:8200' },
    auth: { type: 'appRole', config: { role_id: 'roleId', secret_id: 'secretId' } },
});

const lease = await client.read('secret/app');
const password = lease.getValue<string>('password');

auth is a discriminated union on type, so each backend only accepts its own configuration, and the three mutually-exclusive JWT sources (jwt, jwtPath, jwtProvider) are enforced at compile time — as is the mutually-exclusive pair distributedClaimAccessToken / distributedClaimAccessTokenProvider.

Types that appear in signatures are exported in type space under the VaultClient namespace — VaultClient.Lease, VaultClient.VaultOptions, VaultClient.AuthToken and the per-backend config interfaces. They are types only; the classes behind them are not exported at runtime.

If you previously installed the third-party @types/node-vault-client, remove it — it stops at the 1.x API and its declarations take precedence in some setups:

npm uninstall @types/node-vault-client

Example

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' | 'jwt'
        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));

Supported Auth Backends

AWS IAM auth

const vaultClient = VaultClient.boot('main', {
    api: {
        url: 'https://vault.example.com:8200/',
        namespace: 'some_namespace',                   // Optional. X-Vault-Namespace header (canonical location; auth.config.namespace is honored as a legacy fallback)
    },
    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
            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,
            },
        },
    },
});

region

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.

AppRole auth

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
        },
    },
});

Token auth

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
        },
    },
});

Kubernetes auth

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
        },
    },
});

JWT auth

const vaultClient = VaultClient.boot('main', {
    api: { url: 'https://vault.example.com:8200/' },
    auth: {
        type: 'jwt',
        mount: 'jwt',                                  // Optional. Vault JWT auth mount point ("jwt" by default)
        config: {
            role: 'my-app',                                       // Optional. Role configured in Vault's JWT auth backend; omitted uses the mount's `default_role`
            jwt: process.env.CI_JOB_JWT,                          // Exactly one of `jwt` / `jwtPath` / `jwtProvider` is required (see below)
            distributedClaimAccessToken: process.env.GRAPH_TOKEN, // Optional. Azure/Entra ID group lookups only (see below); or `distributedClaimAccessTokenProvider`
        },
    },
});

Exactly one of three mutually exclusive config keys supplies the JWT:

  • jwt — a literal token string. Use it for CI jobs and other processes that are guaranteed to finish before the token expires. Caveat: the value is fixed at construction, so if the client re-authenticates (its Vault-issued token TTL runs out while the process is still up) it re-sends that exact same JWT — which works only until the IdP-issued token itself expires, after which Vault rejects every further login attempt. Do not use jwt in a long-running process.
  • jwtPath — path to a file containing the JWT, re-read on every login (mirrors Kubernetes auth's tokenPath). Use it for rotated projected tokens, such as a Kubernetes projected service-account token the kubelet refreshes on disk, so each login picks up whatever is currently on disk instead of a token captured once at startup.
  • jwtProvider — an (optionally async) function, called fresh at login time (never at construction, never cached), returning string | Promise<string>. Use it when the JWT has to be minted per login — GitHub Actions' core.getIDToken(), a cloud metadata endpoint, a SPIFFE/SPIRE workload API.
The Vault role needs bound_audiences when your JWT carries an aud claim

This is server-side configuration rather than a client option, but it is the most common reason a first login fails. Vault requires a jwt role to bind the audience your token carries, and it does not catch the omission when the role is created — as long as the role has some other bound constraint (bound_subject, bound_claims, ...) it is accepted, and the problem only surfaces at login:

VaultHttpError: 400 - {"errors":["audience claim found in JWT but no audiences bound to the role"]}

A wrong (rather than missing) audience fails with error validating token: invalid audience (aud) claim: audience claim does not match any expected audience. So bind the audience the token actually has:

vault write auth/jwt/role/my-app \
    role_type=jwt user_claim=sub bound_audiences=my-audience token_policies=my-policy

A role with no bound constraint at all is rejected when you create it (must have at least one bound constraint when creating/updating a role), so that case is self-correcting. An aud array is fine — Vault matches bound_audiences against any entry.

Vault also does not require an exp claim: a token minted without one is accepted and never expires. If you write your own jwtProvider, give the tokens it mints a short exp.

Azure / Entra ID group lookups need distributedClaimAccessToken

Skip this unless you log in with Microsoft Entra ID (Azure AD) tokens and the Vault role sets groups_claim. Vault's distributed_claim_access_token parameter "only applies to the Azure (Entra ID) provider"; on every other IdP these two keys do nothing at all.

Azure does not always put group membership in the token. Past 200 groups it sends OIDC distributed claims instead — _claim_names/_claim_sources pointing at the Microsoft Graph API rather than the group names themselves — and a mount configured with fetch_groups skips the claim entirely and always asks Graph. Either way Vault has to call Graph itself, and the JWT you logged in with is not a credential for that call: it needs a separate Graph access token, sent on the login request as the optional distributed_claim_access_token. Leave it out on a mount/role set up this way and the login fails at the group-fetch step — after the JWT has already validated, so the error names the group lookup rather than anything about your token.

Two optional, mutually exclusive config keys supply it. Passing both raises InvalidArgumentsError at construction; passing neither leaves the login request byte-for-byte what it was before these keys existed.

  • distributedClaimAccessToken — a literal Graph access token. It carries exactly the caveat the literal jwt above does, for the same reason: the value is fixed at construction and every re-login re-sends it, so it works until that token expires. Entra access tokens last about an hour, which is shorter than most processes — fine for a one-shot script, wrong for a service.
  • distributedClaimAccessTokenProvider — an (optionally async) function, called fresh at login time (never at construction, never cached), returning string | Promise<string>. Acquire the Graph token inside it — an MSAL client-credentials call, the Azure IMDS managed-identity endpoint — and every login gets a current one. Resolving to a non-string or an empty string raises InvalidArgumentsError without sending a login request.
const vaultClient = VaultClient.boot('main', {
    api: { url: 'https://vault.example.com:8200/' },
    auth: {
        type: 'jwt',
        config: {
            role: 'my-app',
            jwtProvider: () => getEntraIdToken(),
            distributedClaimAccessTokenProvider: () => getGraphAccessToken(),
        },
    },
});

On the Vault side, fetch_groups lives in provider_config on the mount's config, not on the role — which is where people tend to look for it:

vault write auth/jwt/config \
    oidc_discovery_url=https://login.microsoftonline.com/<tenant-id>/v2.0 \
    provider_config='{"provider":"azure","fetch_groups":true}'

vault write auth/jwt/role/my-app \
    role_type=jwt user_claim=sub bound_audiences=<application-client-id> \
    groups_claim=groups token_policies=my-policy

fetch_groups is optional — the distributed-claim path is taken without it whenever Azure omits the groups claim. groups_claim on the role is not: with it unset Vault never resolves groups at all, so a Graph token you pass is accepted and never used.

Authenticating from GitHub Actions

permissions:
  id-token: write
const core = require('@actions/core');
const VaultClient = require('node-vault-client');

const vaultClient = VaultClient.boot('ci', {
    api: { url: process.env.VAULT_ADDR },
    auth: {
        type: 'jwt',
        mount: 'gha',                                  // matches wherever the JWT method was mounted, e.g. `vault auth enable -path=gha jwt`
        config: { role: 'ci', jwtProvider: () => core.getIDToken('vault') },
    },
});

core.getIDToken('vault') mints a token whose aud is vault, so the role has to bind that audience or the login fails with audience claim found in JWT but no audiences bound to the role:

vault write auth/gha/role/ci \
    role_type=jwt user_claim=sub bound_audiences=vault \
    bound_claims='{"repository":"my-org/my-repo"}' token_policies=ci

Pass the same string to core.getIDToken() and to bound_audiences. Calling getIDToken() with no argument uses GitHub's default audience instead, which then will not match.

role is optional here too — omit it to use the mount's default_role. mount and api.namespace behave exactly as they do for the other four backends.

Vault's JWT method also offers an interactive oidc login (a browser redirect for a human user). This library implements only the non-interactive jwt flow: a service client doing headless background renewal has no browser to redirect to, so oidc is deliberately not supported.

Token renewal

Whenever Vault issues a renewable token, the client arms a background timer and renews it at half its remaining lifetime, for as long as the client lives. That is the default and suits long-running services.

Set renewal: false on the auth block (beside type, not inside config) to turn it off:

auth: {
    type: 'appRole',
    renewal: false,
    config: {
        role_id: '637c065f-...',
        secret_id: '...',
    },
}

These keys sit on auth rather than in auth.config on purpose: config is the backend's own credential bag and may already carry keys of your own, so reserving names inside it could break an existing caller. Same reasoning that put the namespace at api.namespace.

With renewal off, the client keeps using the token until it expires and then simply logs in again on the next call — no background timer at all. Two reasons to want that:

  • Short-lived processes. The renewal timer keeps the Node.js event loop alive, so a script that finishes its work does not exit on its own; you have to call close(). With renewal: false there is no timer to hold it open.
  • You would rather re-authenticate than renew — for example where the auth backend can always mint a fresh token (jwtProvider, Kubernetes, IAM) and you prefer a clean login over extending an existing lease.

Before you turn it off

Renewal off means the token is allowed to expire, and what happens next depends on whether your backend can obtain a fresh credential unaided:

backend on expiry with renewal: false
kubernetes, iam, jwt with jwtPath/jwtProvider clean re-login — the JWT or AWS credential is re-acquired, so this is the intended case
appRole re-login replays the same secret_id. Fine for a reusable one; with Vault's recommended hardening (secret_id_num_uses=1, or a short secret_id_ttl) the second login is rejected
jwt with a literal jwt replays the same JWT, so it works only until the IdP-issued token expires
token cannot re-authenticate at all. The client raises AuthTokenExpiredError from then on, permanently

Two consequences worth stating plainly:

  • For token auth this is a behaviour change, not a no-op. A renewable token handed to token auth is renewed indefinitely today; with renewal: false it expires and every later call rejects for the life of the process. close() does not reset it — recovery means VaultClient.clear(name) and booting again.
  • For appRole and literal-jwt, a login that can no longer succeed is retried on every subsequent call, since a failed login clears the cached token. That is a failing request per read(), with no backoff. Prefer jwtPath/jwtProvider, a reusable secret_id, or leaving renewal on.

Expiring a token also revokes its leases. Vault revokes every lease created by a token when that token expires, and this client does not renew secret leases — only the auth token. If you read dynamic credentials (database/creds/*, cloud credentials) whose lease outlives the auth token, leaving renewal on is what currently keeps them alive. KV reads are unaffected.

Tuning renewal instead of disabling it

Two further keys, also on auth, shape how renewal happens. Both are optional, and leaving them out reproduces the behaviour the client has always had.

key default meaning
renewalFraction 0.5 How much of the token's remaining lifetime to wait before renewing, as a fraction in (0, 1). 0.5 renews at the halfway point.
renewalIncrement (unset) Seconds of extra TTL to ask for, sent as increment to auth/token/renew-self. Unset means Vault applies the token's own period.
auth: {
    type: 'kubernetes',
    renewalFraction: 0.25,   // renew after a quarter of the remaining lifetime
    renewalIncrement: 3600,  // ask Vault for another hour each time
    config: { role: 'my-app' },
}

Lower renewalFraction values renew earlier and more often, which buys headroom if Vault is briefly unreachable. A failed renewal is retried on the same rule against the same token, so the waits shrink geometrically as the remaining lifetime does — for a 1-hour token that is roughly 12 attempts at 0.5 (1800s, 900s, 450s, …) versus 27 at 0.25, both bottoming out at a 1-second floor just before expiry. Higher values renew later and talk to Vault less.

renewalIncrement is a request, not a guarantee: Vault grants at most the token's max TTL and may return less. Both keys are validated at construction — a renewalFraction outside (0, 1) or a non-positive/non-integer renewalIncrement raises InvalidArgumentsError rather than failing later inside a background timer.

API

VaultClient

Return contract: read() and list() resolve to a Lease — use its accessors to extract the secret data. Every other data-plane method (write, delete, update, request and the KV v2 helpers) resolves to the raw parsed Vault response body, which may be empty/undefined for 204 No Content responses.

new VaultClient(options)

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.api.namespace] String Optional. Vault namespace, sent as the X-Vault-Namespace header on every request — login, token lookup/renewal, and all secret operations — for every auth type. This is the canonical location; auth.config.namespace is still honored for backward compatibility.
[options.api.kv.autoDetect] boolean false auto-detect the KV version of each mount on first use (see KV v2 & generic backends)
[options.api.engines] Object {} static mount-to-version map, e.g. { secret: 2, legacy: 1 } (see KV v2 & generic backends)
options.auth Object
options.auth.type String one of: 'appRole' | 'token' | 'iam' | 'kubernetes' | 'jwt'
[options.auth.mount] String Vault auth backend mount point; default varies per method (e.g. "aws" for iam, "approle", "token", "kubernetes", "jwt")
options.auth.config Object auth configuration variables
[options.auth.config.namespace] String Optional. Legacy location for the Vault namespace (see api.namespace). Sent as the X-Vault-Namespace header on every request for every auth type.
[options.auth.renewal] boolean true Set false to never renew the Vault token in the background; the token is used until it expires and the next call re-authenticates. See Token renewal.
[options.auth.renewalFraction] number 0.5 How much of the token's remaining lifetime to wait before renewing, as a fraction in (0, 1).
[options.auth.renewalIncrement] number Seconds of extra TTL to request on each renewal, sent as increment to auth/token/renew-self. Vault caps it at the token's max TTL.
[options.logger] Object | false Logger that must implement all five of "error", "warn", "info", "debug" and "trace" — an object missing any one of them is silently ignored and the default logger is used instead. The default logger writes to console, except debug, which is discarded so that sensitive data is never printed. Pass false to disable logging entirely.
Custom transport (proxy / self-signed TLS)

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.

The undici you install must be interface-compatible with the one Node bundles (process.versions.undici). A dispatcher from a mismatched major is rejected by the global fetch() before the request leaves the process — on Node 24, undici@8 fails immediately with TypeError: fetch failed and cause.code of invalid onRequestStart method. Node 22 and 24 bundle undici 7.x, so pin undici@^7.

Request timeouts

There is no default timeout. A Vault that accepts the connection and never answers will leave read() (and every other call) pending indefinitely. Set one through the dispatcher:

const { Agent } = require('undici');

const vaultClient = VaultClient.boot('main', {
    api: {
        url: 'https://vault.example.com:8200/',
        requestOptions: { dispatcher: new Agent({ headersTimeout: 5000, bodyTimeout: 5000 }) },
    },
    auth: { type: 'token', config: { token: '...' } },
});

A failed request rejects with TypeError: fetch failed, with cause.code set to UND_ERR_HEADERS_TIMEOUT or UND_ERR_BODY_TIMEOUT.

Do not put signal: AbortSignal.timeout(ms) in requestOptions. It is created once and shared by every request the client makes, so it works for the first call and then aborts all later ones with TimeoutError. Pass a fresh signal per call, or use the dispatcher above.

Redirects and the Vault token

Requests follow HTTP redirects, because Vault HA clusters answer standby nodes with a 307 to the active node. Node's fetch() strips only Authorization, Cookie and Proxy-Authorization when a redirect crosses origins — X-Vault-Token is not on that list and is forwarded. A host that can answer for api.url can therefore redirect the client and receive its Vault token, so treat api.url (and the DNS and any load balancer in front of it) as trusted infrastructure.

vaultClient.fillNodeConfig() ⇒ Promise

Populates Vault's values to NPM "config" module

Resolves once the npm config module has been populated from Vault. Note that setup failures are thrown synchronously, not returned as a rejected promise: a missing config peer dependency and an unreadable <NODE_CONFIG_DIR>/custom-vault-variables.js both throw VaultError before the promise is created, so use await or wrap the call in try/catch rather than relying on .catch() alone.

Kind: instance method of VaultClient

vaultClient.read(path) ⇒ Promise.<Lease>

Read secret from Vault

Kind: instance method of VaultClient

Param Type Description
path string path to the secret

vaultClient.list(path) ⇒ Promise.<Lease>

Retrieves secrets list

Kind: instance method of VaultClient

Param Type Description
path string path to the secret

vaultClient.write(path, data) ⇒ Promise.<Object>

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 string path used to write data
data object data to write

vaultClient.delete(path) ⇒ Promise.<Object>

Deletes a secret

On KV v2 mounts this sends DELETE to the data/ path, soft-deleting the latest version. On KV v1 / non-KV mounts this sends DELETE to the raw path. 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 string path to the secret

vaultClient.update(path, data) ⇒ Promise.<Object>

Updates (merge-patches) a KV v2 secret

Sends PATCH with Content-Type: application/merge-patch+json, merging data into the existing secret without overwriting keys that are not listed. KV v2 merge-patch operation — KV v1 mounts do not support PATCH and Vault returns 405 there. Resolves to the raw parsed Vault response body.

Kind: instance method of VaultClient

Param Type Description
path string path to the secret
data object keys to merge into the existing secret

vaultClient.request(method, path, [data]) ⇒ Promise.<Object>

Raw request — escape hatch for any Vault backend

Sends the literal API path with no KV path rewriting and no response unwrapping, and resolves to the parsed response body. Use it for non-KV backends (e.g. Transit) or when you have already constructed the complete Vault API path.

Kind: instance method of VaultClient

Param Type Description
method string HTTP method (e.g. GET, POST)
path string literal API path, sent as-is
[data] object request body

vaultClient.deleteVersions(path, versions) ⇒ Promise.<Object>

Soft-deletes specific versions of a KV v2 secret

KV v2 only — rejects with UnsupportedOperationError on non-v2 mounts. Resolves to the raw parsed Vault response body.

Kind: instance method of VaultClient

Param Type Description
path string path to the secret
versions Array.<number> version numbers to soft-delete

vaultClient.undeleteVersions(path, versions) ⇒ Promise.<Object>

Undeletes (restores) soft-deleted versions of a KV v2 secret

KV v2 only — rejects with UnsupportedOperationError on non-v2 mounts. Resolves to the raw parsed Vault response body.

Kind: instance method of VaultClient

Param Type Description
path string path to the secret
versions Array.<number> version numbers to restore

vaultClient.destroyVersions(path, versions) ⇒ Promise.<Object>

Permanently destroys specific versions of a KV v2 secret

The destroyed version data cannot be recovered. KV v2 only — rejects with UnsupportedOperationError on non-v2 mounts. Resolves to the raw parsed Vault response body.

Kind: instance method of VaultClient

Param Type Description
path string path to the secret
versions Array.<number> version numbers to destroy

vaultClient.readMetadata(path) ⇒ Promise.<Object>

Reads KV v2 metadata for a secret

Resolves to the raw parsed Vault response body; the metadata document (current_version, the versions map, timestamps, etc.) is under its data property, e.g. (await client.readMetadata('secret/foo')).data.current_version. KV v2 only — rejects with UnsupportedOperationError on non-v2 mounts.

Kind: instance method of VaultClient

Param Type Description
path string path to the secret

vaultClient.deleteMetadata(path) ⇒ Promise.<Object>

Deletes all metadata and version history for a KV v2 secret (permanent)

KV v2 only — rejects with UnsupportedOperationError on non-v2 mounts. 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 string path to the secret

vaultClient.close()

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 exit

Kind: instance method of VaultClient

close() also settles a renewal that is already in flight: a request that lands after the call is discarded rather than arming a fresh timer, so the client stops holding the event loop open. The same applies when a token is replaced by a new login — a renewal still running against the previous token cannot overwrite the newer one or cancel its timer.

Lease

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. Throws Requested key does not exist when the key is absent.
  • getData()Object — a deep-cloned copy of the whole secret data object.
  • isRenewable()boolean — whether the underlying lease is renewable.
  • getMetadata()Object | undefined — KV v2 version metadata (version, created_time, deletion_time, destroyed, custom_metadata), or undefined on KV v1 and non-KV mounts. See Lease.getMetadata().

VaultClient.boot(name, options) ⇒ VaultClient

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.

options are used only when the instance is first created. A later call for a name that already exists returns the existing instance and ignores the options passed to it; when those differ from the ones it was booted with, the client logs a warning rather than letting the difference pass silently. Use VaultClient.get(name) to fetch an existing instance, or VaultClient.clear(name) before booting to replace one.

Kind: static method of VaultClient
Returns: VaultClient

Param Type Description
name String Vault instance name
options Object options for Vault#constructor. Required on every call, including for a name that was already booted — use VaultClient.get(name) to fetch an existing instance.

VaultClient.get(name) ⇒ VaultClient

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

VaultClient.clear([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

KV v2 & generic backends

Overview

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.

Configuration options

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).

Auto-detect example

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)

Static engines override example

const client = VaultClient.boot('main', {
    api: {
        url: 'https://vault.example.com:8200/',
        engines: { secret: 2, legacy: 1 },
    },
    auth: { type: 'token', config: { token: '...' } },
});

KV v2-specific methods

These methods require a KV v2 mount and throw UnsupportedOperationError on v1 / non-KV mounts. They also require the mount to be resolved as v2: with neither api.kv.autoDetect: true nor an api.engines entry covering the mount, every path resolves as v1 passthrough with no detection call, and these methods fail with UnsupportedOperationError (Mount "secret" is not a KV v2 engine.) even against a genuine KV v2 mount.

// 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. The full Vault envelope is returned, so the metadata
// fields live under `.data` (meta.data.current_version, meta.data.versions, ...)
const meta = await client.readMetadata('secret/foo');

// Delete all metadata and version history (permanent)
await client.deleteMetadata('secret/foo');

update() — merge-patch

// 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

delete()

// Soft-delete the latest version on KV v2; DELETE on v1/passthrough
await client.delete('secret/foo');

request() — raw escape hatch

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);

Lease.getMetadata()

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 mounts

Path requirements when autoDetect / engines are active

When autoDetect: true or api.engines is set, the client rewrites logical paths to the correct KV v2 API paths automatically (e.g. secret/foosecret/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');

Mount detection caching

  • Each canonical mount is detected once and then cached for the life of the VaultClient instance. The cache is a bounded LRU with a fixed cap of 500 mounts (not configurable through client options); a long-lived client that touches more than 500 distinct mounts evicts the least-recently-used entries, and an evicted mount is detected again on its next use.
  • 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.engines to skip detection.

Error classes

Every error the client raises extends VaultError. The package entry point exports the VaultClient class only, so the classes themselves are imported from node-vault-client/src/errors:

const errors = require('node-vault-client/src/errors');

try {
    await client.readMetadata('secret/app');
} catch (err) {
    if (err instanceof errors.UnsupportedOperationError) {
        // the mount did not resolve as KV v2 - see "Path requirements" above
        return null;
    }
    throw err;
}
Class Extends When thrown
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.
VaultHttpError VaultError Vault answered with a non-2xx status. Carries the status as statusCode and the parsed body as error.
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 an invalid node-config substitution map.
InvalidAWSCredentialsError InvalidArgumentsError auth.config.credentials was supplied but is not a usable accessKeyId / secretAccessKey pair.
AuthTokenExpiredError VaultError The Vault token expired and the backend cannot obtain a new one. token auth can never re-authenticate; other backends reach this only with renewal: false and a credential they cannot replay.
UnsupportedOperationError VaultError A v2-only method (deleteVersions, undeleteVersions, destroyVersions, readMetadata, deleteMetadata) was called against a mount that did not resolve as KV v2.

Contributing

Contributions are welcome! Please read the contributing guide to get started, and note that this project requires a DCO sign-off on every commit.

Getting help

Not sure where to start? See SUPPORT.md.

Code of Conduct

This project adheres to the Contributor Covenant Code of Conduct.

Security

To report a security vulnerability, please follow our Security Policy.

License

Licensed under the Apache License 2.0.

Releases

Packages

Used by

Contributors

Languages