npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@achs/azure-key-vault

v5.0.0

Published

Wrapper for @azure/keyvault-secrets for ease secrets handler in JSON files.

Readme

Azure Key Vault

Node library to handle Azure Key Vault secrets by project, environment and group (when a vault is shared), as nested JSON. Every failure is classified and logged at its level, and the library says who it signed in as, so a missing vault, an identity without access and a missing secret never look the same.

ESM only, Node.js 22.12 or later.

How To Use 💡

import { AzureKeyVault } from '@achs/azure-key-vault';

// DefaultAzureCredential: environment variables, workload or
// managed identity, Azure CLI ("az login")...
const keyVault = new AzureKeyVault({
	vaultUrl: 'https://my-key-vault.vault.azure.net',
	project: 'my-project',
	group: 'web',
	env: 'dev',
});

await keyVault.setAll({
	SECRET1: 'my secret 1',
	otherConfig: { SECRET2: 'my secret 2' },
});

const secrets = await keyVault.getFor({
	SECRET1: null,
	otherConfig: { SECRET2: null },
	SECRET3: 'default for secret 3',
});
// { SECRET1: 'my secret 1', otherConfig: { SECRET2: 'my secret 2' }, SECRET3: 'default for secret 3' }
// typed with the shape of the input: secrets.otherConfig.SECRET2

Options

| Option | Description | | ------------------- | ---------------------------------------------------------------------------------------- | | vaultUrl | (string) vault URL, i.e. https://my-key-vault.vault.azure.net | | project | (string) project the secrets belong to | | group | (string) secrets group | | env | (string) environment (dev, qa, prod...) | | credential | (TokenCredential) any @azure/identity credential, wins over credentials | | credentials | (object) service principal { clientId, clientSecret, tenantId }: all three, or none | | logger | (object) logger with trace, debug, info, warn and error (console, tslog...) | | logLevel | (string) lowest level written: trace, debug, info, warn, error or silent | | strict | (boolean, default false) a vault or access failure throws instead of being logged only | | concurrency | (number, default 8) requests at once in getFor, getAll and the *All functions | | timeoutMs | (number) time limit of each operation, retries included | | dnsCheck | (boolean, default true) resolve the vault host before the first request (see below) | | expiryWarningDays | (number, default 30) warn about secrets expiring within these days; 0 turns it off | | clientOptions | (SecretClientOptions) options of the secrets client: retries, proxy, user agent... | | client | (SecretClient) secrets client, for tests |

A service principal with only some of its values throws a TypeError, naming the missing ones, and so do invalid concurrency, timeoutMs or expiryWarningDays values. The library never writes process.env.

Secret names

A key maps to the secret project-group-env-key, lowercase, with _ and spaces turned into - (toSecretName(scope, key) gives it).

  • : (or --) nests a key: car:props:name is { car: { props: { name } } }.
  • A last segment starting with $ is shared by every group (app) of the project, in the same environment: parent:$global_var maps to project-env-parent--global-var. It is not shared with other projects. Without a group, KEY and $KEY map to the same secret.
  • A key that does not map to a valid secret name (letters, digits and dashes, up to 127) fails before any request; a scoped package name as group (@scope/name) does too, so set AZURE_GROUP.
  • The scope (project, group, environment) compares like names do, whatever case or spaces it was written with (normalizeScope): My App and my-app are one group, and tags are written normalized.

Tags

Every secret carries tags that rebuild its key and scope (project, group, env, name, path), plus:

| Tag | Value | | ------------ | ---------------------------------------------------------------------------------- | | managed-by | @achs/azure-key-vault/<version> that wrote it | | updated-by | who wrote it: a user's upn, service-principal:<appId>, managed-identity:<name> | | serialized | 1 for a JSON value (kept for versions before contentType) |

A JSON value (anything but a string) also has contentType: application/json, and is parsed when either says so. updated-by comes from the identity the library signed in with (it asks for a token before its first write); with an injected client it is left out.

Logging and traceability 🔎

Every failure is classified (AzureKeyVaultErrorKind) and logged at its level, naming the vault and the secrets it affects. Values are never logged, only secret names.

| Kind | Meaning | Level | | --------------------- | ------------------------------------------------------------------------- | ----- | | vault-not-found | the vault host does not resolve (wrong URL, or the DNS/VPN that has it) | error | | vault-unreachable | no answer from the vault (network, proxy, TLS) | error | | timeout | an operation took longer than timeoutMs | error | | unauthenticated | no usable identity (not signed in, wrong service principal, wrong tenant) | error | | forbidden | the identity lacks the permission, or the vault firewall rejects it | error | | throttled | the vault throttles requests (429) and retries are exhausted | error | | invalid-secret-name | a key that does not map to a valid secret name | error | | invalid-value | a secret tagged as JSON whose value does not parse, or an undefined value | error | | unknown | anything else | error | | secret-not-found | the secret does not exist | warn | | secret-disabled | the secret is disabled | warn |

What each level shows:

  • error / warn: one line per cause, with every secret it affects. A forbidden line names the identity the vault saw (appid/oid), the permission and role the operation needs (Key Vault Secrets User to read and list, Key Vault Secrets Officer to write) and who the library signed in as.
  • warn also: secrets read that are expired, not valid yet (Key Vault gives them anyway) or about to expire (expiryWarningDays).
  • info: who the library signed in as (authenticated as user [email protected] (tenant ...), from the first token: user, service principal or managed identity), the summary of every operation over many secrets (read 3 of 5 secrets, 1 not found or disabled, 1 failed), and the result of check().
  • debug: each secret read, written, deleted, purged or restored.
  • trace: the original Azure SDK error of each cause.

keyVault.identity gives the identity once known (only when the library builds the client).

Where the logs go

  • An injected logger receives every level and filters itself, so a host application keeps one log level for everything. @achs/env passes its own tslog logger this way. Set logLevel to filter it here too.
  • Without a logger, the library writes to the console with a [akv] prefix, from logLevel, else the AKV_LOG_LEVEL environment variable, else warn.
const keyVault = new AzureKeyVault({
	vaultUrl,
	project: 'my-project',
	env: 'dev',
	logger: parentLogger.getSubLogger({ name: 'akv' }),
});

Failures and strict

  • Reads and maintenance (get, getFor, getAll, keys, versions, delete, purge, restore and the *All functions) log their failures and go on: a value that could not be read keeps its default (or null). With strict: true, an error level failure throws an AzureKeyVaultError once everything is logged; a missing or disabled secret only warns.
  • Writes (set, setAll, rollback) and getInfo always throw an AzureKeyVaultError.
import { AzureKeyVaultError } from '@achs/azure-key-vault';

try {
	await keyVault.getFor(schema);
} catch (error) {
	if (error instanceof AzureKeyVaultError)
		console.error(error.kind, error.vaultUrl, error.secretNames);
}

classifyError(error) gives the kind of any error the Azure SDK throws.

check()

Diagnoses a vault with a single request for a secret that should not exist: it passes when the vault host resolves and answers, the identity authenticates and may read secrets, and it says who signed in. It never throws.

const { ok, kind, message, identity } = await keyVault.check();
// { ok: false, kind: 'forbidden', identity: { type: 'service-principal', appId: '...' }, message: 'https://...: the identity appid=... lacks the "get" secret permission (...), signed in as service principal appid=...' }

Network: DNS check, VPN and proxies

Before its first request, the library resolves the vault host once with the system DNS, the one every request uses (a VPN's DNS included): a host that does not resolve fails at once as vault-not-found, instead of after the retries of each request. It never contradicts the requests, unlike a check against public DNS servers, which a VPN may block. It is skipped when a proxy resolves the host instead (clientOptions.proxyOptions, or HTTPS_PROXY/HTTP_PROXY/ALL_PROXY), with an injected client, and with dnsCheck: false.

concurrency caps the requests at once (a shared vault throttles all of its callers), timeoutMs bounds each operation, and clientOptions reaches the Azure client (retries, proxy). The user agent carries @achs/azure-key-vault/<version>, so the vault's diagnostic logs tell who called.

Functions 🧰

| Function | Description | | ------------------------------- | ----------------------------------------------------------------------------------- | | get(key, { version }?) | secret value (parsed when stored as JSON), null when not read; a version if given | | getInfo(key) | secret with its properties; throws on failure | | set(key, value) | inserts or updates a secret; a value that is not a string is stored as JSON | | delete(key) | deletes a secret: the deleted secret, null on failure | | purge(key) | purges a deleted secret: whether it was purged | | restore(key) | restores a deleted secret: its properties, null on failure | | versions(key) | versions of a secret, newest first, without values; null on failure | | rollback(key, version) | writes a previous version (value and tags) as the current one | | keys() | keys of the project group, sorted, without values (needs list) | | getFor(secrets, override?) | reads the secrets of an object whose values are defaults, typed with its shape | | getAll() | every secret of the project group (lists the whole vault: needs list) | | setAll(secrets) | inserts or updates the secrets of an object, in order | | deleteAll(skipShared = true) | deletes every secret of the project group | | purgeAll(skipShared = true) | purges every deleted secret of the project group | | restoreAll(skipShared = true) | restores every deleted secret of the project group | | check() | diagnoses the vault (see above) | | audit({ allProjects }?) | audits the project (or the whole vault) without reading values (see below) |

getFor reads only the keys whose default is empty (null, '', 0, false), or every key with override; a secret stored as JSON (an array, an object, a number) comes back parsed.

const secrets = await keyVault.getFor({
	'$global-var': null,
	'my-secret': null,
	'my-secret-2': 'default value',
	'my-secret-group1': { 'my-secret-3': null },
	'my-array-secret': null,
});

Audit

audit() lists the vault once (names, tags and dates; never values) and reports, for the project or with allProjects for the whole vault, a summary (secrets, managed by the library, tagged, untagged, projects, groups, by environment) and these findings, logged at their level:

| Finding | Level | Meaning | | -------------------- | ----- | --------------------------------------------------------------------------- | | untagged | warn | secrets without the library tags (invisible to getAll and keys) | | shadowed | warn | a key both own and shared ($): @achs/env injects them as one variable | | scope-variants | warn | one scope written in different ways (case, spaces): its secrets share names | | name-mismatch | warn | a name that is not the one its tags give (renamed, or tagged by hand) | | expired | warn | expired secrets | | expiring | info | secrets expiring within expiryWarningDays | | not-yet-valid | info | secrets whose notBefore is in the future | | disabled | info | disabled secrets | | unnormalized-scope | info | scope tags with capitals or spaces (normalized on their next write) | | other-env | info | project audit with an environment: the project's secrets in other ones |

auditSecrets(properties, options) runs the same checks over secret properties listed elsewhere.

Testing with the mock

createAzureKeyVaultMock(options, { faults }) gives a handler over an in-memory vault shared by every mock, that fails like Azure does (a missing secret is a secret-not-found). faults injects failures by key, or for every request with '*'; resetAzureKeyVaultMock() empties the vault.

const vault = createAzureKeyVaultMock(
	{ project: 'app', env: 'dev' },
	{ faults: { DB_PASS: 'forbidden', '*': 'timeout' } },
);

Command 💻

The akv command runs the same operations. Logs go to stderr (at info unless --log says otherwise) and output to stdout.

| Option | Description | | ---------------- | --------------------------------------------------------------- | | --uri | (required) vault URL | | --project | (required) project name | | --group | secrets group | | --env | environment | | --spn | service principal (application) id | | --password | service principal secret | | --tenant | tenant id | | --log | trace, debug, info (default), warn, error or silent | | --strict | a vault or access failure fails the command | | --concurrency | requests at once | | --timeout | time limit of each operation, in milliseconds | | --no-dns-check | skips resolving the vault host before the first request | | --key | secret key (get, set, versions, rollback) | | --value | secret value of set (else read from stdin) | | --version | secret version (get, rollback) | | --file | input JSON file (relative to the working directory) | | --output | output JSON file (relative to the working directory) | | --override | getfor reads keys with a default value too | | --json | check and audit print their result as JSON | | --all | audit covers the whole vault (no --project needed) |

| Command | Description | | ---------- | ---------------------------------------------------------------------------------- | | check | diagnoses the vault and who signed in | | audit | audits the project, or the vault with --all; --strict fails on warnings | | get | prints a secret value (JSON when it is not a string) | | set | writes a secret; pipe the value (--value would stay in the shell history) | | list | prints the keys of the project group, one per line | | versions | prints the versions of a secret as JSON, newest first | | rollback | writes a previous version as the current one | | getfor | writes --output with the secrets of the --file structure (values are defaults) | | getall | writes --output with every secret of the project group | | publish | creates or updates the secrets of --file | | clear | deletes every secret of the project group | | restore | restores every deleted secret of the project group |

Exit codes: 0 success, 2 vault not found, unreachable or timed out, 3 no usable identity, 4 permission denied, 1 anything else. Reads only fail the command with --strict (or when get, list or versions read nothing).

akv check --uri=https://my-key-vault.vault.azure.net --project=my-project --env=dev --json
printf '%s' "$DB_PASS" | akv set --uri=... --project=my-project --env=dev --key=DB_PASS
akv getfor --uri=... --project=my-project --env=dev --file=structure.json --output=secrets.json --log=debug

runCli(argv) runs the same command from code, and exitCode(kind) maps a failure kind to its code.

Development 🧿

pnpm test:cov   # Vitest with coverage (100% thresholds)
pnpm lint       # ESLint
pnpm typecheck  # TypeScript
pnpm format     # Prettier
pnpm build      # Vite library build into dist/

Changelog 📄

For last changes see CHANGELOG.md.

Built with 🛠️