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

yamlock

v1.1.6

Published

Value-level encryption for YAML/JSON configuration files with CLI + Node.js APIs.

Readme

yamlock

npm version CI

Project website

Value-level encryption for YAML and JSON configuration files. The name yamlock combines "YAML" and "lock" while also sounding like "warlock", hinting at a little configuration magic.

Requirements

  • Node.js 22 or newer

The package works with npm or another Node.js package manager. This repository uses Yarn Classic 1.22.22 for development and release checks.

Installation

npm

npm install -g yamlock      # CLI usage
npm install yamlock         # project dependency

Yarn Classic

yarn global add yamlock     # CLI usage
yarn add yamlock            # project dependency

Features

  • Encrypt/decrypt individual values with authenticated field-path metadata.
  • CLI workflow that processes YAML or JSON files in place.
  • Safe CLI migration from legacy payloads to authenticated v2 payloads.
  • Recursively lock/unlock entire objects via processConfig.
  • Select repeated fields or subtrees with structural path patterns.
  • Public API exports that mirror CLI behavior for programmatic use.
  • Focus on Node.js 22+, ESM modules, and a lightweight dependency set (js-yaml).

Usage

CLI

# Encrypt values in a YAML file
YAMLOCK_KEY="super-secret" yamlock encrypt config.yaml

# Decrypt values in place; the payload determines its format and algorithm
yamlock decrypt settings.json --key "super-secret"

# Encrypt only selected fields into a new file
yamlock encrypt config.json --key "$YAMLOCK_KEY" --paths "db.password,api.token" --output config.secure.json

# Encrypt repeated tokens and an entire subtree
yamlock encrypt config.json --key "$YAMLOCK_KEY" --path-patterns 'users[*].token,services.**'

# Inspect CLI metadata
yamlock --help
yamlock version
yamlock algorithms

# Generate a random key for YAMLOCK_KEY
yamlock keygen --length 64 --format base64

# Preview changes without touching files
yamlock encrypt config.yml -o config.enc.yml -p db.password -k "my-secret-key" -d

# Preview a legacy-to-v2 migration without printing config contents
yamlock migrate config.yml -k "$YAMLOCK_KEY" -p db.password -d

# Explicitly write legacy v1 for a temporary compatibility requirement
yamlock encrypt config.yml -k "$YAMLOCK_KEY" --legacy --algorithm aes-256-cbc

The CLI detects YAML (.yaml/.yml) and JSON extensions automatically and writes the file back in the same format. Normal encrypt and decrypt writes use an fsynced temporary file plus an atomic rename. In-place writes preserve the source mode; a new --output inherits the source mode, while an existing regular output preserves its own mode. Mutating operations reject symbolic-link inputs and outputs rather than following or replacing their targets.

YAML writes preserve parsed values, not the original syntax tree. Comments, anchor/alias syntax, merge keys, explicit tags, quoting, and formatting may be removed or normalized. Review the YAML rewrite contract and use --dry-run or --output when presentation details matter.

Options of note:

  • --output <file> writes the result to a separate file instead of overwriting the input.
  • --paths <path1,path2> targets only the specified fields using the escaped path syntax.
  • --path-patterns <pattern1,pattern2> selects structural paths with whole-segment *, [*], and ** wildcards.
  • --dry-run previews an operation without modifying files; encrypt/decrypt print content changes, while migrate prints only counts and target paths.
  • migrate decrypts selected legacy payloads and re-encrypts them as authenticated v2 payloads.
  • migrate --allow-mixed additionally authenticates and preserves selected values that are already v2.
  • In-place migration creates <file>.yamlock.bak by default; --no-backup disables it explicitly.
  • encrypt --legacy writes the legacy v1 format; --algorithm is accepted for encryption only together with --legacy.
  • encrypt --error-on-encrypted fails when a selected value is already encrypted instead of preserving it.
  • encrypt --force-encrypt explicitly treats selected yl|... strings as plaintext and adds another encryption layer.
  • Command keygen produces a random key of 1–4096 whole bytes and shows how to store it (shell export or .env).
  • Command algorithms prints the fixed v2 profile, tested legacy presets, and additional legacy ciphers available from the runtime.
  • Command version prints the installed CLI version.

The CLI rejects unknown or duplicate options, missing option values, extra positional arguments, and options that do not apply to the selected command. Argument errors use structured [yamlock:ERR_*] codes and are reported before the CLI reads or modifies a configuration file.

Field path syntax

Default field paths use dots for object nesting and brackets for array indexes: db.password and users[0].token. Inside an object key, backslashes, dots, brackets, and commas are escaped with a backslash. For example, db\.primary.token selects token below the literal key db.primary, while db.primary.token selects three nested object keys. Quote escaped CLI paths so the shell passes each backslash unchanged:

yamlock encrypt config.json --key "$YAMLOCK_KEY" --paths 'db\.primary.token,labels\,primary'

Node.js callers can build the same canonical strings from unambiguous segments:

import { processConfig, serializePath } from 'yamlock';

const selectedPath = serializePath(['db.primary', 'token']);
const encrypted = processConfig(config, {
  mode: 'encrypt',
  key: process.env.YAMLOCK_KEY,
  paths: [selectedPath]
});

New payloads bind authentication to the escaped canonical path. Payloads written by older yamlock versions for keys containing reserved characters remain readable through the default serializer's compatibility path; selecting those keys now requires the canonical escaped spelling. A custom pathSerializer keeps its own contract and does not use the default compatibility fallback.

Path patterns

Patterns are separate from exact paths; using both forms a union. They match complete structural leaf paths:

  • services.*.token matches one object-key segment, such as services.api.token, but not services[0].token.
  • users[*].token matches array elements such as users[0].token.
  • db.** matches a leaf at db or any descendant below it, including arrays.

Wildcards must occupy a complete segment. Partial globs such as service-* are rejected with ERR_INVALID_PATH_PATTERNS before the CLI reads the input file. Literal reserved characters use the same escaping as exact paths, and \* selects a literal asterisk key:

yamlock encrypt config.json \
  --key "$YAMLOCK_KEY" \
  --paths 'root.literal' \
  --path-patterns 'services.*.token,users[*].token,labels\,primary'

Patterns only decide which leaves are selected. Encryption, decryption, and migration continue to bind every payload to its exact canonical leaf path. Node.js callers use pathPatterns: string[]; it cannot be combined with a custom pathSerializer.

Node.js API

import { encryptValue, decryptValue, processConfig, serializePath } from 'yamlock';

const encrypted = encryptValue('swordfish', process.env.YAMLOCK_KEY, 'db.password');
const decrypted = decryptValue(encrypted, process.env.YAMLOCK_KEY, 'db.password');

const config = { db: { password: 'swordfish' } };
const selectors = { pathPatterns: ['db.**'] };
const locked = processConfig(config, {
  mode: 'encrypt',
  key: process.env.YAMLOCK_KEY,
  ...selectors
});
const unlocked = processConfig(locked, {
  mode: 'decrypt',
  key: process.env.YAMLOCK_KEY,
  ...selectors
});

Expected Node.js API failures extend YamlockError and expose stable ERR_* codes. Specialized classes distinguish validation, payload, authentication, legacy decryption, and config-processing failures; YAMLOCK_ERROR_CODES avoids repeating code strings. See the Node.js error contract.

The package includes TypeScript declarations. The supported exports, option types, and 1.x compatibility guarantees are listed in the public Node.js API contract.

Repeated encryption is safe by default. processConfig authenticates selected existing payloads with the supplied key and field path, preserves them unchanged, and encrypts only selected plaintext values. Use existingPayloadPolicy: 'error' when existing encrypted values should fail the operation:

processConfig(config, {
  mode: 'encrypt',
  key: process.env.YAMLOCK_KEY,
  existingPayloadPolicy: 'error'
});

Malformed payloads, incorrect keys, and incorrect field paths are never silently skipped. Re-running yamlock encrypt on a fully encrypted input does not rewrite the source file. Use yamlock migrate rather than encrypt to convert preserved legacy values to v2.

If plaintext intentionally begins with yl|, use existingPayloadPolicy: 'encrypt' or CLI --force-encrypt. This also permits deliberate nested encryption, so it should not be enabled in routine workflows; each added layer requires a matching decrypt operation.

See examples/basic.js for a runnable end-to-end script (node examples/basic.js).

Legacy algorithm customization

V2 deliberately has no free-form cipher settings. For temporary legacy compatibility, select format version 1 and provide the old cipher options:

const encrypted = encryptValue('swordfish', KEY, 'db.password', {
  formatVersion: 1,
  algorithm: 'chacha20-poly1305',
  ivLength: 12 // override the IV size used during encryption
});

// When decrypting, the algorithm is inferred from the payload,
// but you can still override key/IV sizes if the cipher requires it.
const decrypted = decryptValue(encrypted, KEY, 'db.password', /* optional overrides */);

// processConfig propagates the same options through every nested field.
const processed = processConfig(
  { db: { password: 'swordfish' }, api: { token: 'secret' } },
  {
    mode: 'encrypt',
    key: KEY,
    formatVersion: 1,
    algorithm: { algorithm: 'aes-192-cbc', ivLength: 24 }
  }
);

// Later you can decrypt with the same options:
const restored = processConfig(processed, {
  mode: 'decrypt',
  key: KEY,
  formatVersion: 1,
  algorithm: { algorithm: 'aes-192-cbc', ivLength: 24 }
});

// Control what happens when encountering non-string values and customize path IDs
const mixedConfig = { db: { password: 'secret', retries: 3 } };
const lockedMixed = processConfig(mixedConfig, {
  mode: 'encrypt',
  key: KEY,
  nonStringPolicy: 'stringify', // stringifies finite numbers, booleans, and null
  pathSerializer: (segments) => segments.join('/') // custom path naming (db/password instead of dot notation)
});

// Example of a path serializer that includes array indexes explicitly
const lockedUsers = processConfig(
  { users: [{ tokens: ['abc'] }] },
  {
    mode: 'encrypt',
    key: KEY,
    pathSerializer: (segments) =>
      segments
        .map((segment, index) =>
          typeof segment === 'number' ? `[${segment}]` : index === 0 ? segment : `/${segment}`
        )
        .join('')
  }
);

processConfig recursively traverses arrays and plain objects. With the default nonStringPolicy: 'ignore', selected non-string and opaque values such as Date, undefined, or BigInt are preserved unchanged. Policy 'error' rejects a selected non-string value. Policy 'stringify' accepts only finite numbers, booleans, and null; values that native JSON conversion could omit or silently change are rejected instead of being encrypted with lost type information. Decrypted stringified primitives remain strings.

Circular arrays/objects are rejected. A custom pathSerializer must return a non-empty, unique string for every leaf and the same serializer must be used for encryption and decryption. Policies apply only to values selected by paths; unselected leaf values are preserved.

Direct processConfig calls preserve empty containers, sparse-array length and holes, null-prototype objects, and own keys such as __proto__. JSON and YAML files retain the values their parsers can represent, but those formats do not encode JavaScript sparse-array holes as a distinct portable value.

Authenticated payload v2 (default)

encryptValue, processConfig, and yamlock encrypt write authenticated v2 payloads by default. decryptValue and processConfig automatically read both v1 and v2.

const encrypted = encryptValue('swordfish', KEY, 'db.password');

const locked = processConfig(
  { db: { password: 'swordfish' } },
  { mode: 'encrypt', key: KEY }
);

// No format or algorithm option is required when decrypting.
const original = decryptValue(encrypted, KEY, 'db.password');
const unlocked = processConfig(locked, { mode: 'decrypt', key: KEY });

V2 uses a fixed AES-256-GCM profile, a random 12-byte nonce, a 16-byte authentication tag, and scrypt with a separate random KDF salt. The field path and security-critical metadata are authenticated. Free-form cipher and size overrides are intentionally unavailable for v2.

Existing legacy files can be migrated safely with the CLI:

# Preview counts and target paths; config contents are not printed.
yamlock migrate config.yaml --key "$YAMLOCK_KEY" --paths "db.password,api.token" --dry-run

# Migrate in place and create config.yaml.yamlock.bak.
yamlock migrate config.yaml --key "$YAMLOCK_KEY" --paths "db.password,api.token"

# Preserve the source and write a new file. Existing outputs are never replaced.
yamlock migrate config.yaml --key "$YAMLOCK_KEY" --paths "db.password,api.token" --output config.v2.yaml

Migration validates every selected value and builds the complete result before writing. Selected plaintext and non-string values are rejected, so use --paths or --path-patterns for partially encrypted configs. Selected v2 values are rejected unless --allow-mixed is set; with that flag they are authenticated and kept unchanged. In-place writes are atomic, preserve the source file mode, and do not replace an existing backup. To roll back, verify the backup and then copy config.yaml.yamlock.bak over config.yaml.

Legacy AES-CBC payloads have no authentication, so migration can only validate their structure, field path, and successful decryption. Backups use the source file permissions and match *.yamlock.bak in the repository .gitignore. See the payload v2 design for the format, threat model, limits, and staged migration plan. This design and implementation have not received a third-party security audit.

Advanced usage

  • Selective encryption: use exact --paths/paths selectors, structural --path-patterns/pathPatterns, or their union to encrypt only sensitive fields.
  • Repeated encryption: valid selected payloads are authenticated and preserved; add --error-on-encrypted or existingPayloadPolicy: 'error' for strict workflows.
  • Non-string handling: use nonStringPolicy: 'ignore' | 'stringify' | 'error' to preserve opaque leaves, stringify finite JSON primitives, or reject selected non-string values; use pathSerializer to change path representation (e.g., db/password instead of dot notation).
  • CI/CD flows: see examples/docs/ci-cd.md for a GitHub Actions job that decrypts configs for builds and re-encrypts them before publishing artifacts.
  • Key rotation: follow examples/docs/key-rotation.md for a step-by-step process, including scripting tips for large repos.

Supported algorithms

| Algorithm | Format | Notes | |-----------|--------|-------| | aes-256-gcm | v2 default | Fixed authenticated profile with scrypt, a 12-byte nonce, and a 16-byte tag. | | aes-128-cbc | legacy v1 | Compatibility only; ciphertext and metadata are not authenticated. | | aes-192-cbc | legacy v1 | Compatibility only; ciphertext and metadata are not authenticated. | | aes-256-cbc | legacy v1 | Compatibility default when --legacy is used without --algorithm. | | chacha20-poly1305 | legacy v1 | Authenticates ciphertext, but not all serialized metadata protected by v2. |

Additional algorithms exposed by crypto.getCiphers() are available only in explicit legacy mode and are not part of the supported v2 profile. Prefer the default v2 writer for new data.

Release information

  • The badges at the top show the latest npm version and the status of the full CI matrix.
  • See GitHub Releases and CHANGELOG.md for detailed release notes; install a specific version via npm install yamlock@<version>.
  • yamlock versions are bumped with my own release utility, Bumpster. It keeps the tracked VERSION file and package.json synchronized while publishing the dev, main, and vX.Y.Z Git refs atomically. Each stable tag must pass the full CI and package preflight before its GitHub Release is published.

Encrypted value formats

New values use the authenticated v2 envelope:

yl|2|aes-256-gcm|scrypt|32768|8|1|<kdf_salt>|<nonce>|<path>|<ciphertext>|<tag>

Legacy values remain readable and can still be written explicitly:

yl|<algorithm>|<salt_base64>|<iv_base64>|<data_base64>

The legacy name <salt_base64> is historical: that segment is only the Base64-encoded field path and is not a random salt or KDF input.

See the payload v2 design for the canonical field definitions, limits, compatibility rules, and legacy security limitations.

Key rotation

See the key rotation guide for a step-by-step workflow that re-encrypts values with v2 and a new YAMLOCK_KEY.

Inspiration and motivation

I have worked with Ruby on Rails apps for more than ten years and appreciated how its secret management evolved between 4.2 and 6.x. That flow influenced yamlock, but I also explored modern tools such as:

Each of those projects solves secure config storage differently, yet none fit my exact needs. yamlock is the bicycle I am building for my own projects to add an extra layer of encryption for sensitive YAML/JSON values while keeping the workflow lightweight.

Contributing

See CONTRIBUTING.md for development workflow, available scripts, and release instructions.

Exit codes

yamlock returns 0 when encryption/decryption completes successfully and 1 on validation or runtime errors (missing keys, malformed payloads, failed file reads). Use these exit codes to gate CI jobs or deployment steps.

Future work

  • An async encryption API with bounded scrypt concurrency for large configs.
  • Stricter file-format validation and preservation rules for advanced YAML features.

Author

Created and maintained by Pavel Tkachev (@phoenixweiss).

License

MIT © PAVEL TKACHEV