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

@vaultdotenv/cli

v0.5.3

Published

Drop-in dotenv replacement. One master key locally, everything else encrypted remotely.

Readme

vaultdotenv

Drop-in replacement for dotenv. One key in your .env, all your secrets encrypted in the cloud.

Your .env file (the only secret you deploy):
  VAULT_KEY=vk_abc123_def456...

That's it. All other env vars load automatically from the vault.

Why vaultdotenv?

Your .env file has 30 secrets. Every developer has a copy. Every server has a copy. When you rotate a key, you update it everywhere manually. If someone's laptop gets stolen, every secret is compromised.

vaultdotenv replaces all of that with a single key. Your secrets live encrypted on a remote server. The server never sees the decryption key. Clients decrypt locally. You rotate one secret instead of thirty.

| | dotenv | vaultdotenv | |---|---|---| | Secrets in .env | All of them | Just VAULT_KEY | | Laptop stolen | Everything exposed | One key to rotate | | Rotate an API key | Update every server | Push once, all servers get it | | Version history | None | Full version history + rollback | | Device control | None | Approve/revoke individual machines | | Code changes to migrate | - | Zero |


Common Questions

How do I add another machine or server? Copy the VAULT_KEY from your first machine's .env to the new machine, then register it as a device:

# On the new machine (with VAULT_KEY in .env)
vde register-device --name "my-server"

Then approve it from the dashboard (Project → Devices → Approve) or from the CLI:

vde approve-device --id <device-uuid>

I created a project on the dashboard — how do I connect locally?

vde login
vde init --link
# Pick your project from the list → generates VAULT_KEY and registers your device

How do I use this in CI/CD? Set two environment variables — no CLI or files needed:

VAULT_KEY=vk_...                  # From your .env
VAULT_DEVICE_SECRET=abc123...     # From ~/.vault/<projectId>.key

Then config() / load_vault() handles everything automatically.


Table of Contents


Quick Start

1. Install and log in

npm install -g @vaultdotenv/cli

vde login
# Opens browser → authorize → CLI is linked to your dashboard account

2. Create a project

vde init --name my-app

This creates the project, generates your VAULT_KEY, registers your machine as the first device, and links everything to your dashboard account automatically.

Already created a project on the dashboard? Link to it instead:

vde init --link
# Shows your projects → pick one → generates VAULT_KEY locally

Or pass the project ID directly:

vde init --link <project-id>

3. Push your secrets

vde push --env production

Reads your .env file, encrypts every key-value pair with AES-256-GCM, and uploads the encrypted blob. The server never sees the plaintext.

4. Replace dotenv

# Node.js
- require('dotenv').config()
+ require('@vaultdotenv/cli').config()

# Python
- from dotenv import load_dotenv
- load_dotenv()
+ from vaultdotenv import load_vault
+ load_vault()

Done. Your app now loads secrets from the vault. If there's no VAULT_KEY in the environment, it falls back to plain dotenv behavior — so the migration is completely safe.


How It Works

┌──────────────────────────────────┐
│          Your Application        │
│                                  │
│  require('@vaultdotenv/cli').config()   │
│         or load_vault()          │
└──────────────┬───────────────────┘
               │
               │ 1. Read VAULT_KEY from .env or environment
               │ 2. Derive auth key via HKDF
               │ 3. Sign request with HMAC-SHA256
               ▼
┌──────────────────────────────────┐
│     api.vaultdotenv.io         │
│     (Cloudflare Worker + D1)     │
│                                  │
│  • Validates HMAC signature      │
│  • Validates device hash         │
│  • Returns encrypted blob        │
│  • Logs access in audit table    │
│  • NEVER sees the decryption key │
└──────────────┬───────────────────┘
               │
               │ 4. Encrypted blob returned
               ▼
┌──────────────────────────────────┐
│         Client (local)           │
│                                  │
│  • Derives decryption key from   │
│    VAULT_KEY + device secret     │
│  • Decrypts with AES-256-GCM    │
│  • Injects into process.env     │
│    (or os.environ in Python)     │
│  • Caches encrypted blob locally │
└──────────────────────────────────┘

The server is a dumb encrypted storage layer. All cryptography happens on the client. A full database breach reveals nothing — the blobs are encrypted with keys the server has never seen.


Node.js Client

Installation

npm install @vaultdotenv/cli

Basic Usage

// Async (recommended) — pulls fresh secrets from the vault
await require('@vaultdotenv/cli').config();

// Options
await require('@vaultdotenv/cli').config({
  path: '.env',               // Path to .env file (default: .env)
  environment: 'production',  // Environment name (default: NODE_ENV or 'development')
  vaultUrl: 'https://...',    // Vault server URL (default: api.vaultdotenv.io)
  override: false,            // Override existing env vars (default: false)
  cache: true,                // Cache secrets locally for offline fallback (default: true)
});

Synchronous Mode

// Sync — reads from local encrypted cache only (no network call)
// Useful for scripts or tools that can't be async
require('@vaultdotenv/cli').configSync();

configSync() tries the local .vault-cache file first. If no cache exists, it falls back to the plain .env file. This is useful for development or situations where async isn't possible.

No VAULT_KEY? No problem.

If there's no VAULT_KEY in the environment or .env file, vaultdotenv behaves exactly like dotenv — reads the .env file and injects the values into process.env. This makes the migration completely backwards-compatible.


Python Client

Installation

Copy the clients/python/vaultdotenv/ directory into your project. Requires cryptography and httpx:

pip install cryptography httpx

Basic Usage

from vaultdotenv import load_vault

# Pulls secrets from vault and injects into os.environ
load_vault()

# With options
load_vault(
    path=".env",               # Path to .env file
    environment="production",  # Environment name (default: ENVIRONMENT or NODE_ENV or 'development')
    vault_url="https://...",   # Vault server URL
    override=False,            # Override existing env vars
    cache=True,                # Enable local cache fallback
)

Synchronous / Cache-Only Mode

from vaultdotenv import load_vault_sync

# Reads from local encrypted cache only — no network call
load_vault_sync()

Integration Example

Replace python-dotenv with a safe fallback:

# config.py
try:
    from vaultdotenv import load_vault
    load_vault()
except Exception:
    from dotenv import load_dotenv
    load_dotenv()

CLI Reference

All commands read VAULT_KEY from the VAULT_KEY environment variable or from the .env file in the current directory.

Initialize a Project

npx @vaultdotenv/cli init [--name my-project]

Creates the project on the server, generates the vault key, registers your machine as the first device (auto-approved), and writes VAULT_KEY to .env.

Link to an Existing Project

npx @vaultdotenv/cli init --link [project-id]

Connects your local directory to a project you already created on the dashboard. If you omit the project ID, you'll get an interactive picker showing all your projects. Requires vde login first.

Link Current Project to Dashboard

npx @vaultdotenv/cli link

Already ran vde init but weren't logged in? Run vde login then vde link to connect the current project to your dashboard account.

Push Secrets

npx @vaultdotenv/cli push [--env production] [--file .env.production]

Encrypts and uploads all key-value pairs from the specified file (default: .env). The VAULT_KEY itself is never pushed. Each push creates a new version.

Pull Secrets

npx @vaultdotenv/cli pull [--env staging] [--output .env.staging]

Pulls and decrypts secrets from the vault. Without --output, prints masked values to stdout. With --output, writes the full .env file.

List Versions

npx @vaultdotenv/cli versions [--env production]

Shows the version history for an environment, including timestamps and number of keys changed.

Rollback

npx @vaultdotenv/cli rollback --version 3 [--env production]

Creates a new version with the contents of the specified old version. Non-destructive — the rollback itself is a new version, so you can always roll forward again.

Device Management

npx @vaultdotenv/cli register-device [--name "CI Server"]
npx @vaultdotenv/cli approve-device --id <device-uuid>
npx @vaultdotenv/cli list-devices
npx @vaultdotenv/cli revoke-device --id <device-uuid>

See Device Management for details.

Global Options

| Flag | Description | Default | |---|---|---| | --env <name> | Environment name | NODE_ENV or development | | --url <url> | Vault server URL | https://api.vaultdotenv.io | | --file <path> | Source .env file (push only) | .env | | --output <path> | Output file (pull only) | stdout | | --name <name> | Project or device name | directory name / hostname | | --id <uuid> | Device ID (approve/revoke) | — |


Device Management

Devices add a second layer of security. Every machine that accesses your secrets must be registered and approved.

How It Works

  1. Register: A new machine runs vaultdotenv register-device. This generates a device secret (random 256-bit key), stores it locally at ~/.vault/<projectId>.key, and sends the SHA-256 hash to the server.
  2. Approve: The project owner runs vaultdotenv approve-device --id <uuid> to approve the new device.
  3. Access: On every pull/push, the client sends the device hash. The server checks it against the devices table. Unregistered or revoked devices get a 403.
  4. Revoke: If a machine is compromised, run vaultdotenv revoke-device --id <uuid>. That machine can no longer access secrets.

First Device = Owner

The first device registered for a project is automatically approved. All subsequent devices require explicit approval.

Device Secret Storage

| Location | Used by | |---|---| | ~/.vault/<projectId>.key | Local development machines (file permissions: 0600) | | VAULT_DEVICE_SECRET env var | CI/CD pipelines and servers |

Dual-Key Encryption

The device secret isn't just for authentication — it's used in the encryption itself. The encryption key is derived from both pieces:

key_material = HMAC-SHA256(vault_key, device_secret)
encryption_key = HKDF(key_material, salt="vault-encrypt-v1")

This means:

  • Stealing just the VAULT_KEY is not enough to decrypt secrets
  • Stealing just the device secret is not enough to decrypt secrets
  • Both are required — defense in depth

Important: Device-Bound Encryption

Secrets are encrypted with a specific device's secret at push time. Only the device whose secret was used to encrypt can decrypt those secrets. If you need multiple devices to pull the same secrets, you must push the secrets using each device's secret.

In practice for server deployments: register a device for the server, push secrets using that device's secret, and pass the device secret via the VAULT_DEVICE_SECRET environment variable.


Environments

Every project comes with three default environments: development, staging, and production. Custom environments are created automatically on first push.

# Push to different environments
npx @vaultdotenv/cli push --env development
npx @vaultdotenv/cli push --env staging
npx @vaultdotenv/cli push --env production

# Pull from a specific environment
npx @vaultdotenv/cli pull --env staging

Environment Resolution

The client determines the environment from (in order):

  1. The --env CLI flag
  2. NODE_ENV environment variable (Node.js)
  3. ENVIRONMENT environment variable (Python)
  4. Default: development

Hot Reload

Both clients support watching for secret changes and automatically updating the environment — no restart required.

Node.js

const vault = require('@vaultdotenv/cli');

// Load secrets first
await vault.config();

// Start watching (polls every 30s by default)
vault.watch({
  interval: 30000,          // Poll interval in ms
  environment: 'production',
  onChange(changed, allSecrets) {
    console.log('Secrets updated:', Object.keys(changed));
    // Reconnect services, refresh configs, etc.
  },
  onError(err) {
    console.error('Watch error:', err);
  },
});

// Stop watching when done
vault.unwatch();

Python

import vaultdotenv

vaultdotenv.load_vault()

vaultdotenv.watch(
    interval=30.0,  # seconds
    on_change=lambda changed, all_secrets: print("Updated:", list(changed.keys())),
    on_error=lambda err: print(f"Error: {err}"),
)

# Stop watching
vaultdotenv.unwatch()

How Watching Works

  1. Polls /api/v1/secrets/current-version at the configured interval (lightweight — no secrets transferred)
  2. If the version number changed, does a full pull
  3. Diffs the new secrets against process.env / os.environ
  4. Updates changed values in-place
  5. Calls onChange / on_change with the diff

The watcher runs on a background thread (Python) or unref'd timer (Node.js) — it won't keep your process alive.


CI/CD & Servers

For non-interactive environments (CI pipelines, Docker containers, production servers), pass the vault key and device secret as environment variables:

# Docker
docker run -e VAULT_KEY=vk_... -e VAULT_DEVICE_SECRET=abc123... -e ENVIRONMENT=production myapp

# GitHub Actions
env:
  VAULT_KEY: ${{ secrets.VAULT_KEY }}
  VAULT_DEVICE_SECRET: ${{ secrets.VAULT_DEVICE_SECRET }}

# Any CI/CD
export VAULT_KEY=vk_...
export VAULT_DEVICE_SECRET=abc123...
export ENVIRONMENT=production

Setting Up a Server

  1. Register a device for the server (from any machine with the vault key):

    npx @vaultdotenv/cli register-device --name "production-server"

    This outputs the device ID and saves the device secret to ~/.vault/.

  2. Approve the device:

    npx @vaultdotenv/cli approve-device --id <device-uuid>
  3. Push secrets using the server's device secret (it's now in your ~/.vault/ file):

    npx @vaultdotenv/cli push --env production
  4. Copy the device secret to the server as the VAULT_DEVICE_SECRET env var. The device secret is in ~/.vault/<projectId>.key.

  5. Deploy your app with just two env vars: VAULT_KEY and VAULT_DEVICE_SECRET.


Offline Fallback

When the vault server is unreachable, the client automatically falls back to a local encrypted cache:

  1. After every successful pull, secrets are cached to .vault-cache in the project directory
  2. The cache is encrypted with the same key (vault key + device secret) — it's not readable without both
  3. If the server is down on next startup, the client decrypts and uses the cache
  4. A warning is printed: [vaultdotenv] Remote fetch failed, using cached secrets

Add .vault-cache to your .gitignore:

.vault-cache

Security Model

What the server knows

  • Project metadata (name, UUID, created date)
  • Environment names
  • Encrypted blobs (opaque — server cannot decrypt)
  • Device hashes (SHA-256 of device secrets — server never sees the raw secrets)
  • Auth key hash (derived from vault key via HKDF — server never sees the vault key)
  • Audit log (who accessed what, from which IP, when)

What the server does NOT know

  • Your vault key
  • Your device secrets
  • Your decrypted secrets
  • The encryption key

Threat model

| Scenario | Impact | |---|---| | Server database breached | Attacker gets encrypted blobs. Useless without vault key + device secret. | | .env file leaked (with VAULT_KEY) | Attacker has vault key but not device secret. Cannot decrypt. Cannot pull (device hash check fails). | | ~/.vault/ directory leaked | Attacker has device secret but not vault key. Cannot decrypt. | | Both vault key AND device secret leaked | Attacker can pull and decrypt. Revoke the device, rotate the vault key. | | Network MITM | HMAC signatures prevent replay attacks. Secrets are encrypted end-to-end. HTTPS provides transport security. |

Key rotation

If a vault key is compromised, generate a new one and re-push secrets. The old key becomes useless because the server's auth key hash no longer matches.


Encryption Details

Key Derivation

Input Key Material (IKM):
  If device secret exists:  HMAC-SHA256(vault_key, device_secret)
  If no device secret:      vault_key (raw bytes)

Derived Keys:
  Encryption key = HKDF-SHA256(IKM, salt="vault-encrypt-v1", info="", length=32)
  Auth key       = HKDF-SHA256(IKM, salt="vault-auth-v1",    info="", length=32)
  Device hash    = SHA-256(device_secret)

Encryption (Push)

1. Derive 256-bit encryption key (see above)
2. Generate random 96-bit IV
3. Encrypt: AES-256-GCM(key, iv, plaintext_json)
4. Pack: base64(iv || auth_tag || ciphertext)
5. Send packed blob to server

Decryption (Pull)

1. Derive 256-bit encryption key (same derivation)
2. Unpack: base64 decode → iv (12 bytes) || auth_tag (16 bytes) || ciphertext
3. Decrypt: AES-256-GCM(key, iv, ciphertext, auth_tag)
4. Parse JSON → key-value pairs

Request Signing

1. Derive auth key: HKDF(vault_key, salt="vault-auth-v1")
2. Compute: HMAC-SHA256(auth_key, request_body + timestamp)
3. Header: X-Vault-Signature: v=<timestamp>,d=<hex_digest>
4. Server verifies against stored auth_key_hash (max age: 5 minutes)

API Reference

All endpoints are at https://api.vaultdotenv.io/api/v1/. All requests use POST with JSON bodies. Authenticated endpoints require the X-Vault-Signature header.

POST /project/create

Create a new project. No authentication required.

// Request
{ "project_name": "my-app" }

// Response
{ "project_id": "uuid", "environments": ["development", "staging", "production"] }

POST /project/set-key

Set the auth key hash for a project. One-time operation — cannot be overwritten.

// Request
{ "project_id": "uuid", "auth_key_hash": "hex" }

// Response
{ "ok": true }

POST /secrets/push (authenticated)

Push encrypted secrets.

// Request
{ "project_id": "uuid", "environment": "production", "secrets": "<base64_encrypted_blob>", "device_hash": "hex" }

// Response
{ "version": 1 }

POST /secrets/pull (authenticated)

Pull encrypted secrets.

// Request
{ "project_id": "uuid", "environment": "production", "device_hash": "hex" }

// Response
{ "secrets": "<base64_encrypted_blob>", "version": 1 }

POST /secrets/current-version (authenticated)

Lightweight version check — no secrets transferred.

// Request
{ "project_id": "uuid", "environment": "production" }

// Response
{ "version": 1, "updated_at": "2026-03-23T10:54:10.872Z" }

POST /secrets/versions (authenticated)

List version history.

// Request
{ "project_id": "uuid", "environment": "production" }

// Response
{ "versions": [{ "version": 1, "created_at": "...", "changed_keys": [...] }] }

POST /secrets/rollback (authenticated)

Rollback to a previous version (creates a new version with old content).

// Request
{ "project_id": "uuid", "environment": "production", "version": 1 }

// Response
{ "version": 3 }

POST /devices/register (authenticated)

Register a new device.

// Request
{ "project_id": "uuid", "device_name": "my-laptop", "device_hash": "hex" }

// Response
{ "device_id": "uuid", "status": "pending" }

POST /devices/approve (authenticated)

Approve a pending device.

// Request
{ "project_id": "uuid", "device_id": "uuid" }

// Response
{ "device_id": "uuid", "status": "approved" }

POST /devices/list (authenticated)

List all devices for a project.

// Request
{ "project_id": "uuid" }

// Response
{ "devices": [{ "id": "uuid", "device_name": "...", "status": "approved", "last_seen_at": "..." }] }

POST /devices/revoke (authenticated)

Revoke a device's access.

// Request
{ "project_id": "uuid", "device_id": "uuid" }

// Response
{ "device_id": "uuid", "status": "revoked" }

GET /health

Health check. No authentication.

{ "status": "ok", "ts": 1711187650872 }

Architecture

Server

  • Runtime: Cloudflare Worker (edge, globally distributed)
  • Database: Cloudflare D1 (SQLite at the edge)
  • Domain: api.vaultdotenv.io

Database Schema

projects
  id          TEXT (UUID, primary key)
  name        TEXT
  key_hash    TEXT (hex-encoded auth key hash)
  created_at  DATETIME

environments
  id          TEXT (UUID, primary key)
  project_id  TEXT (FK → projects)
  name        TEXT (development/staging/production/custom)
  created_at  DATETIME

secret_versions
  id              INTEGER (auto-increment)
  environment_id  TEXT (FK → environments)
  version         INTEGER
  encrypted_blob  TEXT (base64-encoded AES-256-GCM ciphertext)
  changed_keys    TEXT (JSON array of key names — not values)
  created_at      DATETIME

devices
  id            TEXT (UUID, primary key)
  project_id    TEXT (FK → projects)
  device_name   TEXT
  device_hash   TEXT (SHA-256 of device secret)
  status        TEXT (pending/approved/revoked)
  created_at    DATETIME
  approved_at   DATETIME
  last_seen_at  DATETIME

audit_log
  id              INTEGER (auto-increment)
  project_id      TEXT
  environment_id  TEXT
  action          TEXT (pull/push/rollback/device_register)
  ip              TEXT
  user_agent      TEXT
  created_at      DATETIME

Node.js Client

  • Zero external dependencies (uses Node.js built-in crypto)
  • CommonJS module (require('@vaultdotenv/cli'))
  • CLI built into the package (npx @vaultdotenv/cli)

Python Client

  • Dependencies: cryptography, httpx
  • Drop-in replacement for python-dotenv
  • Thread-based watcher for hot reload

Troubleshooting

"Device not registered. Run: vaultdotenv register-device"

Your machine isn't registered for this project. Run:

npx @vaultdotenv/cli register-device

Then ask the project owner to approve it.

"Device not yet approved"

Your device is registered but pending approval. The project owner needs to run:

npx @vaultdotenv/cli list-devices          # Find the device ID
npx @vaultdotenv/cli approve-device --id <uuid>

"Failed to fetch secrets and no cache available"

The vault server is unreachable and there's no local cache. This happens on first run with no network. Make sure you can reach api.vaultdotenv.io and run a successful pull first to populate the cache.

"VAULT_KEY not found in environment or .env file"

The CLI can't find your vault key. Either:

  • Set the VAULT_KEY environment variable, or
  • Make sure your .env file contains VAULT_KEY=vk_...

Decryption fails after registering a new device

Secrets are encrypted with a specific device's secret. If you registered a new device, the local ~/.vault/<projectId>.key file was overwritten with the new device's secret. You need to re-push secrets using the new device's secret for that device to decrypt them.

.vault-cache in .gitignore

Always add .vault-cache to your .gitignore. While the cache is encrypted, there's no reason to commit it.

# .gitignore
.vault-cache

License

MIT