npm.io
0.74.46 • Published 1 week ago

@stacksjs/env

Licence
MIT
Version
0.74.46
Deps
1
Size
119 kB
Vulns
0
Weekly
0
Stars
628

@stacksjs/env

A secure .env file management package with built-in encryption support for Bun and Node.js.

Features

  • Automatic Encryption/Decryption - Secure your environment variables with public-key cryptography
  • Bun Plugin - Seamless integration with Bun's runtime
  • Versioned envelope - X25519 + HKDF-SHA-256 + AES-256-GCM
  • Variable Expansion - Support for ${VAR}, defaults, and alternates
  • Command Substitution - Execute commands with $(command)
  • Multi-Environment - Manage multiple .env files for different environments
  • CLI Tools - Full-featured CLI via buddy commands
  • Environment Detection - Native runtime, platform, and CI/CD detection utilities

Installation

bun add @stacksjs/env

Quick Start

1. Auto-load .env files
import { autoLoadEnv } from '@stacksjs/env'

// Automatically loads .env files based on NODE_ENV or DOTENV_ENV
autoLoadEnv()

// Now use your environment variables
console.log(process.env.MY_SECRET)

When loading a named environment, loadEnv and autoLoadEnv look for its DOTENV_PRIVATE_KEY_<ENV> key, then fall back to DOTENV_PRIVATE_KEY if the scoped key is absent. This applies both to .env.keys (including a custom keysFile) and to process environment variables. A standard single-key setup does not need a duplicate DOTENV_PRIVATE_KEY_DEVELOPMENT alias.

The existing loader priority is unchanged: an explicit privateKey, then the keys file, then process environment variables. Within each source the scoped key wins over the generic key. A present key that fails decryption is an error, not a reason to try a different key. Plaintext shell/CI value overrides are still preserved unless overload is enabled.

2. Programmatic Usage
import { loadEnv } from '@stacksjs/env'

// Load specific .env files
loadEnv({
  path: ['.env.local', '.env'],
  overload: false,
})
3. Bun Plugin

Add to your bunfig.toml:

preload = ["./storage/framework/core/env/plugin.ts"]

Or import in your preloader:

import '@stacksjs/env/plugin'

Encryption

Encrypting .env Files

Use the buddy CLI to encrypt your environment variables:

# Encrypt .env file
buddy env:encrypt

# Encrypt specific file
buddy env:encrypt --file .env.production

# Encrypt specific keys only
buddy env:encrypt -k "SECRET**"

# Exclude specific keys from encryption
buddy env:encrypt -ek "PUBLIC**"

This will:

  1. Generate a public/private keypair
  2. Store keys in .env.keys (keep this secure!)
  3. Encrypt values in your .env file
  4. Add DOTENV_PUBLIC_KEY to your .env file
Decrypting .env Files
# Decrypt .env file
buddy env:decrypt

# Decrypt specific file
buddy env:decrypt --file .env.production
How Encryption Works

New writes use the experimental version 2 envelope proposed in stacksjs/rfcs#6:

  1. A recipient keypair is generated using X25519.
  2. Every value gets a fresh ephemeral X25519 pair, 16-byte HKDF salt, and 12-byte AES-GCM nonce.
  3. X25519 and HKDF-SHA-256 derive a one-use AES-256-GCM key.
  4. Ciphertext and envelope metadata are authenticated; malformed, modified, and wrong-key inputs fail with the same non-secret error.

The pre-RFC encrypted:<base64> format can only be read for migration. New writes are encrypted:v2:<base64url>, and legacy public keys are rejected. Run buddy env:rotate to decrypt legacy values in memory, generate a version 2 key, and replace the encrypted file without writing plaintext to disk.

This feature is not a complete secret-management system. It has not yet passed the independent review required by Stacks issue #2058, so do not treat it as a production security boundary without your own review. See SECURITY.md for its threat model and non-guarantees.

Example encrypted .env:

# /-------------------[DOTENV_PUBLIC_KEY]--------------------/
# /            public-key encryption for .env files          /
# /       [how it works](https://stacksjs.com/encryption)   /
# /----------------------------------------------------------/
DOTENV_PUBLIC_KEY="x25519-public:<base64url-spki>"

# .env
API_KEY="encrypted:v2:<base64url-envelope>"
DB_PASSWORD="encrypted:v2:<base64url-envelope>"

CLI Commands

All commands are available through the buddy CLI:

Get Environment Variables
# Get a specific variable
buddy env:get API_KEY

# Get all variables as JSON
buddy env:get --all

# Get all variables in shell format
buddy env:get --all --format shell

# Pretty print JSON
buddy env:get --all --pretty
Set Environment Variables
# Set a variable (encrypted by default)
buddy env:set API_KEY "my-secret-value"

# Set without encryption
buddy env:set PUBLIC_URL "https://example.com" --plain

# Set in specific file
buddy env:set API_KEY "value" --file .env.production
Manage Keypairs
# View keypair
buddy env:keypair

# View keypair for specific environment
buddy env:keypair --file .env.production

# Get specific key
buddy env:keypair DOTENV_PRIVATE_KEY

# Output in shell format
buddy env:keypair --format shell
Rotate Keys
# Rotate keypair and re-encrypt all values
buddy env:rotate

# Rotate for specific environment
buddy env:rotate --file .env.production

Variable Expansion

The env parser supports advanced variable expansion:

Basic Expansion
USERNAME="john"
DATABASE_URL="postgres://${USERNAME}@localhost/mydb"
# Result: postgres://john@localhost/mydb
Default Values
# Use default if unset or empty
DATABASE_HOST=${DB_HOST:-localhost}
DATABASE_PORT=${DB_PORT:-5432}

# Use default only if unset (empty is ok)
API_URL=${API_BASE_URL-https://api.example.com}
Alternate Values
NODE_ENV=production

# Use alternate if set and non-empty
DEBUG_MODE=${NODE_ENV:+false}
LOG_LEVEL=${NODE_ENV:+error}

# Use alternate if set (empty is ok)
CACHE_ENABLED=${NODE_ENV+true}
Command Substitution
# Execute command and use output
CURRENT_USER=$(whoami)
BUILD_TIME=$(date +%s)
GIT_COMMIT=$(git rev-parse HEAD)

Environment Detection

The package includes native utilities to detect runtime, platform, and CI/CD environments:

import {
  // Runtime detection
  isBun,
  isNode,
  runtime,
  runtimeInfo,

  // Platform detection
  platform,
  isWindows,
  isMacOS,
  isLinux,

  // Environment detection
  hasTTY,
  hasWindow,
  isCI,
  isDebug,
  isMinimal,
  isColorSupported,

  // Provider detection
  provider,
  providerInfo,
} from '@stacksjs/env'

// Check runtime
console.log(runtime) // 'bun' | 'node' | 'unknown'
console.log(runtimeInfo) // { name: 'bun', version: '1.3.2' }

// Check platform
console.log(platform) // 'darwin' | 'linux' | 'win32' | etc.
console.log(isMacOS) // true/false

// Check CI environment
console.log(isCI) // true/false
console.log(provider) // 'github' | 'gitlab' | 'vercel' | etc.
console.log(providerInfo) // { name: 'GitHub Actions', detected: true }
Supported CI/CD Providers
  • GitHub Actions
  • GitLab CI
  • CircleCI
  • Travis CI
  • Jenkins
  • Vercel
  • Netlify
  • Heroku
  • AWS
  • Azure
  • Cloudflare Pages
  • Railway
  • Render

Multi-Environment Support

Load different .env files based on environment:

# .env.local (highest priority)
# .env.development
# .env.production
# .env (lowest priority)

The loader will automatically detect NODE_ENV or DOTENV_ENV and load the appropriate files.

Environment-Specific Keys

Keys are automatically namespaced by environment:

# .env.keys
DOTENV_PUBLIC_KEY="..."
DOTENV_PRIVATE_KEY="..."

DOTENV_PUBLIC_KEY_PRODUCTION="..."
DOTENV_PRIVATE_KEY_PRODUCTION="..."

DOTENV_PUBLIC_KEY_CI="..."
DOTENV_PRIVATE_KEY_CI="..."

Security Best Practices

  1. Never commit .env.keys - Add to .gitignore
  2. Commit encrypted .env files - They're safe to commit
  3. Store private keys securely - Use your CI/CD secrets manager
  4. Rotate keys regularly - Use buddy env:rotate
  5. Use environment-specific keys - Different keys for dev/staging/prod

API Reference

autoLoadEnv(options?)

Automatically load .env files based on environment.

import { autoLoadEnv } from '@stacksjs/env'

autoLoadEnv({
  env: 'production',  // Override environment detection
  overload: false,    // Don't override existing vars
  quiet: true,        // Suppress output
  cwd: '/path/to/project'
})
loadEnv(options)

Load specific .env files.

import { loadEnv } from '@stacksjs/env'

loadEnv({
  path: ['.env.local', '.env'],
  overload: false,
  privateKey: 'your-private-key',
  keysFile: '.env.keys',
  quiet: false
})
encryptEnv(options)

Encrypt a .env file.

import { encryptEnv } from '@stacksjs/env'

const result = encryptEnv({
  file: '.env',
  keysFile: '.env.keys',
  key: 'SECRET**',        // Only encrypt keys matching pattern
  excludeKey: 'PUBLIC**',  // Exclude keys matching pattern
  stdout: false
})
decryptEnv(options)

Decrypt a .env file.

import { decryptEnv } from '@stacksjs/env'

const result = decryptEnv({
  file: '.env',
  keysFile: '.env.keys',
  stdout: false
})
setEnv(key, value, options)

Set an environment variable.

import { setEnv } from '@stacksjs/env'

setEnv('API_KEY', 'my-secret', {
  file: '.env',
  keysFile: '.env.keys',
  plain: false  // Encrypt by default
})
getEnv(key?, options)

Get environment variable(s).

import { getEnv } from '@stacksjs/env'

// Get single value
const result = getEnv('API_KEY', {
  file: '.env',
  keysFile: '.env.keys'
})

// Get all values
const result = getEnv(undefined, {
  all: true,
  format: 'json',  // or 'shell' or 'eval'
  prettyPrint: true
})

Migration from dotenvx

This package replaces @dotenvx/dotenvx and bun-plugin-dotenvx with a native Bun implementation.

Breaking Changes

None! The API is designed to be compatible with dotenvx.

Migration Steps
  1. Update your bunfig.toml preload
  2. Update imports from @dotenvx/dotenvx to @stacksjs/env
  3. buddy commands remain the same

Changelog

Please see our releases page for more information on what has changed recently.

Contributing

Please review the Contributing Guide for details.

Community

For help, discussion about best practices, or any other conversation that would benefit from being searchable:

Discussions on GitHub

For casual chit-chat with others using this package:

Join the Stacks Discord Server

License

The MIT License (MIT). Please see LICENSE for more information.

Made with

Keywords