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

@charcoalhq/lockbox

v0.2.0

Published

Typed configuration with encrypted secrets. JSON-based, per-environment overrides, libsodium sealed-box encryption, TypeScript codegen, and validation hooks.

Readme

lockbox

The last config and secrets manager your TypeScript app needs.

Define your config in JSON. Secrets get encrypted automatically. Everything is merged per-environment, typed end-to-end, and validated on every commit. No more .env juggling, no more runtime surprises.

  • Per-environment overrides — base defaults deep-merged with environment-specific config and secrets
  • Encrypted secrets — libsodium sealed boxes. Public key lives in your repo, private key stays in your secrets manager
  • Full type safety — validate and type your config with any Standard Schema-compatible library (Zod, Valibot, ArkType, etc.)
  • Git hooks — validates that secrets are encrypted and generated files are fresh before you push

Quick start

1. Install

pnpm add @charcoalhq/lockbox

2. Initialize

npx lockbox init --dir ./src/config --envs test,production

This creates the directory structure, generates a keypair, and prints your private key. Save the private key securely — it won't be shown again.

3. Add config values

Use the CLI to set values — it writes the JSON and regenerates TypeScript files automatically:

# Set defaults (shared across all environments)
npx lockbox set server.host 0.0.0.0
npx lockbox set server.port 3000
npx lockbox set db.host localhost
npx lockbox set db.port 5432
npx lockbox set db.password '**REQUIRED**'

# Override per environment
npx lockbox set db.host prod.db.example.com --env production

# Set secrets (encrypted automatically)
npx lockbox set-secret db.password hunter2 --env production

Or edit the JSON files directly if you prefer:

src/config/default.json — base values merged into every environment:

{
  "server": { "host": "0.0.0.0", "port": 3000 },
  "db": { "host": "localhost", "port": 5432, "password": "**REQUIRED**" }
}

src/config/production/clear.json — non-secret production overrides:

{
  "db": { "host": "prod.db.example.com" }
}

src/config/production/secret.json — secret values (add as plaintext, they'll be encrypted):

{
  "db": { "password": "hunter2" }
}

Then run npx lockbox generate to encrypt secrets and generate TypeScript files.

5. Use in your app

Define your schema using any Standard Schema-compatible library (Zod, Valibot, ArkType, etc.):

import { z } from 'zod';
import { createConfig } from '@charcoalhq/lockbox';
import testConfig from './config/test/generated.js';
import prodConfig from './config/production/generated.js';

const configSchema = z.object({
  server: z.object({
    host: z.string(),
    port: z.coerce.number(),
  }),
  db: z.object({
    host: z.string(),
    port: z.number().default(5432),
    password: z.string(),
  }),
});

export const { config, environment } = await createConfig({
  configs: { test: testConfig, production: prodConfig },
  environment: process.env.NODE_ENV ?? 'test',
  privateKey: process.env.MY_PRIVATE_KEY,
  schema: configSchema,
});
// config is fully typed as z.infer<typeof configSchema>

Validation runs after decryption, so your schema sees the final plaintext values. On failure, you get a clear error:

lockbox: Config validation failed:
  ✖ server.port: Expected number, received string
  ✖ db.password: Required

Access your config with full type safety:

import { config } from './config.js';

app.listen(config.server.port, config.server.host);

How it works

Directory structure

src/config/
├── lockbox.pub          # Public key (committed to repo)
├── default.json         # Base config merged into all environments
├── test/
│   ├── clear.json       # Non-secret overrides
│   ├── secret.json      # Encrypted secret values
│   └── generated.ts     # Auto-generated (do not edit)
└── production/
    ├── clear.json
    ├── secret.json
    └── generated.ts

Merge order

For each environment, configs are deep-merged in this order:

default.json < {env}/clear.json < {env}/secret.json

Later values override earlier ones. Objects are merged recursively; all other values (arrays, primitives, null) are replaced.

Encryption

Secrets use libsodium sealed boxes:

  • Anyone with the public key (committed to repo) can encrypt new secrets
  • Only the private key holder can decrypt
  • Format: ENC[base64_ciphertext]

Add secrets as plaintext to secret.json and run lockbox generate — they'll be encrypted automatically.

Required fields

Use the **REQUIRED** sentinel in default.json to mark fields that must be set in each environment:

{ "db": { "password": "**REQUIRED**" } }

lockbox validate will fail if any non-skipped environment still has **REQUIRED** values. Configure which environments skip this check in lockbox.json:

{ "skipRequiredFieldValidation": ["test"] }

CLI reference

All commands respect lockbox.json for defaults. Use --dir to override.

lockbox init

Scaffold a new config directory and generate a keypair.

lockbox init --dir ./src/config --envs test,staging,production

lockbox generate

Encrypt plaintext secrets and generate per-environment config files.

lockbox generate

lockbox validate

Check that secrets are encrypted, generated files are up-to-date, and required fields are present.

lockbox validate

Add to your git hooks:

{
  "simple-git-hooks": {
    "pre-commit": "npx lockbox validate",
    "pre-push": "npx lockbox validate"
  }
}

lockbox set

Set a plaintext config value. Supports dot-notation for nested keys. Auto-runs generate.

lockbox set server.port 8080 --env production
lockbox set db.host localhost                    # writes to default.json

lockbox set-secret

Set a secret value. Requires --env. Auto-runs generate (which encrypts and regenerates).

lockbox set-secret db.password s3cret --env production

lockbox keygen

Generate a new encryption keypair.

lockbox keygen

lockbox set-private-key

Store a private key locally for CLI operations (saved to .lockbox/private-key with 600 permissions, auto-added to .gitignore).

lockbox set-private-key <base64-key>

lockbox view

View the full decrypted config for an environment. Requires --env. Reads the private key from .lockbox/private-key.

lockbox view --env production

Configuration

lockbox.json

Created by lockbox init in your project root:

{
  "dir": "./src/config",
  "importSource": "@charcoalhq/lockbox",
  "skipRequiredFieldValidation": ["test"]
}

| Field | Default | Description | |---|---|---| | dir | ./config | Path to the environments directory | | importSource | @charcoalhq/lockbox | Package name used in generated import statements | | skipRequiredFieldValidation | [] | Environments that skip required field checks |

createConfig options

| Option | Default | Description | |---|---|---| | configs | (required) | Map of environment name to imported config object | | environment | (required) | The active environment. Must be a key in configs | | privateKey | — | Base64 private key, or () => string \| Promise<string> resolver (e.g. from KMS). Required if config contains encrypted values | | schema | (required) | A Standard Schema-compliant schema to validate (and optionally transform) the config after loading |

API

import { createConfig, deepFreeze } from '@charcoalhq/lockbox';
import type { CreateConfigOptions, CreateConfigResult, LockboxConfig } from '@charcoalhq/lockbox';
  • createConfig<T>(options) — load and decrypt config for the current environment
  • deepFreeze<T>(obj) — recursively freeze an object (used by generated files)

License

MIT