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

secure-library-loader

v0.1.3

Published

Encrypt a library's source into a single portable, password-protected file, then decrypt, install, and load its modules at runtime.

Downloads

1,394,729

Readme

secure-library-loader

Encrypt a library's source directory into a single portable file, and decrypt it back to disk with a password. Built on Node's native crypto module — zero runtime dependencies.

What it does

  • Encrypt: packs every file under a source directory into a single archive, compresses it, and encrypts it with a password using scrypt (key derivation) + AES-256-GCM (authenticated encryption).
  • Decrypt: given the encrypted file and the correct password, restores the original directory tree exactly.
  • Install & load: decrypts an encrypted library into your module's lib/ folder, then hands you loaded module exports via getModuleByName("name").

The output is a single self-describing file. It stores everything needed to decrypt it (KDF parameters, salt, IV, auth tag) except the password, which you must supply yourself.

What it does not do

This is encryption, not code obfuscation or DRM. If someone has both the encrypted file and the password, they can decrypt it and read the source — same as any password-protected zip. Use this to protect source code at rest or in transit (e.g. in a private artifact store, over email, in a CI artifact), not to prevent a licensed end user from ever seeing your code.

Install

npm install secure-library-loader

For global CLI use:

npm install -g secure-library-loader

Password handling

The password is never accepted as a command-line argument (that would leak into shell history and process listings). It's resolved in this order:

  1. An explicit password option passed to the JS API
  2. The SLL_PASSWORD environment variable
  3. An interactive, echo-disabled terminal prompt (async APIs only)

Synchronous APIs (encryptLibrarySync, decryptLibrarySync, installLibrarySync, loadEncryptedModuleSync, listModulesSync) skip the prompt: they require password or SLL_PASSWORD. The async names exist mainly so the CLI and JS API can prompt on a TTY; the encrypt / decrypt / install work itself is synchronous.

CLI usage

Encrypt a directory

secure-library-loader encrypt ./my-lib -o my-lib.sll

You'll be prompted for a password (or set SLL_PASSWORD beforehand).

Decrypt back to a directory

secure-library-loader decrypt my-lib.sll -o ./restored-lib

Add --force to overwrite a non-empty output directory:

secure-library-loader decrypt my-lib.sll -o ./restored-lib --force

Install into a lib/ subfolder

secure-library-loader install my-lib.sll --module-root . --lib-dir lib --name my-lib

This decrypts into <module-root>/lib/<name> and prints the modules it found inside that subfolder. All flags are optional:

  • --module-root defaults to secure-library-loader's own package directory
  • --lib-dir defaults to lib
  • --name defaults to my-lib.sll's basename (my-lib)

Using an environment variable instead of a prompt

export SLL_PASSWORD="correct-horse-battery-staple"
secure-library-loader encrypt ./my-lib -o my-lib.sll
secure-library-loader decrypt my-lib.sll -o ./restored-lib

Programmatic API

const { encryptLibrarySync, decryptLibrarySync } = require("secure-library-loader");

encryptLibrarySync({
  sourceDir: "./my-lib",
  outputFile: "./my-lib.sll",
  password: "correct-horse-battery-staple", // or set SLL_PASSWORD
});

decryptLibrarySync({
  inputFile: "./my-lib.sll",
  outDir: "./restored-lib",
  password: "correct-horse-battery-staple",
  force: false, // set true to overwrite a non-empty outDir
});

encryptLibrary / decryptLibrary are the same operations, but async so they can prompt for a password on a TTY when none is provided.

encryptLibrary(options)

| Option | Type | Required | Description | |---------------|--------|----------|----------------------------------------------------------------------| | sourceDir | string | yes | Directory whose files will be packed and encrypted | | outputFile | string | yes | Path to write the encrypted .sll file | | password | string | no | Falls back to SLL_PASSWORD env var, then an interactive prompt | | scryptCost | object | no | Override scrypt cost { N, r, p } (defaults to {16384, 8, 1}) |

Returns { outputFile, bytesWritten }.

encryptLibrarySync(options)

Synchronous counterpart of encryptLibrary(). Same options and return value. Interactive password prompt is not available — pass password or set SLL_PASSWORD.

decryptLibrary(options)

| Option | Type | Required | Description | |-------------|---------|----------|--------------------------------------------------------------------| | inputFile | string | yes | Path to the encrypted .sll file | | outDir | string | yes | Directory to restore files into | | password | string | no | Falls back to SLL_PASSWORD env var, then an interactive prompt | | force | boolean | no | Overwrite outDir even if it already exists and isn't empty |

Returns { outDir }.

Throws if the password is wrong, the file is corrupted/tampered with, or outDir already contains files and force isn't set. On any failure, no partial output is left behind — extraction happens in a temp directory and is only moved into place after it fully succeeds.

decryptLibrarySync(options)

Synchronous counterpart of decryptLibrary(). Same options and return value. Interactive password prompt is not available — pass password or set SLL_PASSWORD.

Installing and loading modules at runtime

Each encrypted library is installed into its own named subfolder inside lib/, so installing multiple libraries never overwrites each other:

lib/
├── my-lib/       <- from my-lib.sll
│   ├── greeter.js
│   └── sub/adder.js
└── other-lib/    <- from other-lib.sll
    └── index.js

The subfolder name comes from the name option, and defaults to the .sll file's basename (extension stripped) when omitted — e.g. my-lib.sll installs into lib/my-lib unless you pass a different name.

By default, moduleRoot is secure-library-loader's own package directory (exported as PACKAGE_ROOT) — not process.cwd() and not the consuming app's directory. That means with no moduleRoot override, decrypted files land in <node_modules>/secure-library-loader/lib/<name>, regardless of where your process is run from. Pass an explicit moduleRoot (e.g. __dirname of your own module) if you want the lib folder to live somewhere else instead.

const { installLibrarySync, getModuleByName } = require("secure-library-loader");

// 1. Decrypt ./my-lib.sll into <moduleRoot>/lib/my-lib
//    (name defaults to "my-lib" from the file's basename)
installLibrarySync({
  inputFile: "./my-lib.sll",
  password: "correct-horse-battery-staple",
  moduleRoot: __dirname, // defaults to secure-library-loader's own package root
  force: false,
});

// 2. Load a module from inside that installed library and use it.
//    Paths are "<name>/<module>", since each library lives in its own subfolder.
const greeter = getModuleByName("my-lib/greeter", { moduleRoot: __dirname });
console.log(greeter.greet("world"));

// Nested paths work too
const adder = getModuleByName("my-lib/sub/adder", { moduleRoot: __dirname });
console.log(adder(2, 3)); // 5

Or do both in one call:

const { loadEncryptedModuleSync } = require("secure-library-loader");

const greeter = loadEncryptedModuleSync({
  inputFile: "./my-lib.sll",
  modulePath: "greeter", // file to load, relative to the installed library
  name: "my-lib",        // optional; defaults to the .sll file's basename
  password: process.env.SLL_PASSWORD,
  moduleRoot: __dirname,
});

installLibrary(options)

| Option | Type | Required | Description | |--------------|---------|----------|---------------------------------------------------------------------| | inputFile | string | yes | Path to the encrypted .sll file | | password | string | no | Falls back to SLL_PASSWORD env var, then an interactive prompt | | moduleRoot | string | no | Module root the lib folder sits in (defaults to secure-library-loader's own package root) | | libDir | string | no | Lib folder name, or an absolute path (defaults to "lib") | | name | string | no | Subfolder name to install into, inside the lib folder (defaults to inputFile's basename, extension stripped) | | force | boolean | no | Overwrite that subfolder if it exists and isn't empty |

Returns { libDir, name, installDir, modules }:

  • libDir — the resolved base lib directory (e.g. <moduleRoot>/lib)
  • name — the library name actually used (useful when it was defaulted)
  • installDir — the specific subfolder decrypted into (<libDir>/<name>)
  • modules — the loadable module names inside installDir

installLibrarySync(options)

Synchronous counterpart of installLibrary(). Same options and return value, but it blocks while deriving the key and writing files. Interactive password prompt is not available — pass password or set SLL_PASSWORD.

const { installLibrarySync, getModuleByName } = require("secure-library-loader");

installLibrarySync({
  inputFile: "./my-lib.sll",
  password: "correct-horse-battery-staple",
  moduleRoot: __dirname,
});

const greeter = getModuleByName("my-lib/greeter", { moduleRoot: __dirname });

getModuleByName(modulePath, options)

Loads a module from the lib folder and returns its exports.

| Option | Type | Required | Description | |--------------|---------|----------|---------------------------------------------------------------------| | moduleRoot | string | no | Module root the lib folder sits in (defaults to secure-library-loader's own package root) | | libDir | string | no | Lib folder name, or an absolute path (defaults to "lib") | | reload | boolean | no | Bypass the require cache and re-execute the module |

modulePath is resolved relative to the lib folder using normal Node resolution, so "my-lib/greeter" finds my-lib/greeter.js, and accepts further nesting like "my-lib/sub/adder". It rejects absolute paths or anything containing ... Since installLibrary() puts each library in its own <name>/ subfolder, modulePath will typically start with that name.

loadEncryptedModule(options)

Convenience wrapper: installLibrary() followed by getModuleByName(). Takes all of installLibrary's options (including the optional name for the install subfolder) plus a required modulePath — the file to load, relative to that installed subfolder (e.g. "greeter", not "my-lib/greeter").

loadEncryptedModuleSync(options) is the synchronous counterpart. Interactive password prompt is not available — pass password or set SLL_PASSWORD.

listModules(options)

async function listModules({ moduleRoot, libDir, name } = {}): Promise<string[]>

Without name, lists the installed library names at the top of the lib folder (i.e. the name each was installed under). With name, lists the modules inside that specific installed library — i.e. what you can pass to getModuleByName() as "<name>/<module>".

| Option | Type | Required | Description | |--------------|--------|----------|--------------------------------------------------------------------| | moduleRoot | string | no | Module root the lib folder sits in (defaults to secure-library-loader's own package root) | | libDir | string | no | Lib folder name, or an absolute path (defaults to "lib") | | name | string | no | Scope the listing to this installed library's subfolder instead of the lib folder root |

Returns a sorted array of names: each directory, plus each .js / .cjs / .mjs / .json / .node file with its extension stripped. If the target directory doesn't exist yet, it resolves to [] rather than throwing.

const { installLibrarySync, listModulesSync } = require("secure-library-loader");

installLibrarySync({ inputFile: "./my-lib.sll", password: "pw", moduleRoot: __dirname });

console.log(listModulesSync({ moduleRoot: __dirname }));
// => ["my-lib"]

console.log(listModulesSync({ moduleRoot: __dirname, name: "my-lib" }));
// => ["adder", "config", "greeter"]

installLibrary() also returns the second list directly as result.modules, so you don't need a separate call right after installing.

listModulesSync(options) is the synchronous counterpart and returns the same array immediately.

unloadModule(name, options)

function unloadModule(name, { moduleRoot, libDir } = {}): boolean

Evicts a previously loaded module from Node's require cache, so the next getModuleByName() call for that name re-reads and re-executes the file from disk instead of returning the cached exports.

| Option | Type | Required | Description | |--------------|--------|----------|--------------------------------------------------------------------| | moduleRoot | string | no | Module root the lib folder sits in (defaults to secure-library-loader's own package root) | | libDir | string | no | Lib folder name, or an absolute path (defaults to "lib") |

Returns true if a cached entry was found and removed, false if the module was never loaded (or doesn't resolve) — it never throws for that case, so it's safe to call speculatively.

const { getModuleByName, unloadModule } = require("secure-library-loader");

const greeter = getModuleByName("my-lib/greeter", { moduleRoot: __dirname });

// ... later, e.g. after re-installing a newer encrypted library on disk ...
unloadModule("my-lib/greeter", { moduleRoot: __dirname }); // => true

const fresh = getModuleByName("my-lib/greeter", { moduleRoot: __dirname });
fresh !== greeter; // true — re-executed from the file on disk

Note this only clears Node's module cache; it does not delete the decrypted files from the lib folder. To remove the files too, re-run installLibrary() with { force: true }, or delete the lib directory yourself.

A note on executing decrypted code

getModuleByName() calls require() under the hood, which executes the decrypted module's code in your process. That is the point of the function, but it means the trust boundary is: anyone who can supply both an encrypted file and its password can run arbitrary code in your process.

This is safe when you encrypted the library yourself — the AES-GCM authentication tag guarantees nobody modified it in transit. Do not point it at encrypted bundles from untrusted sources.

Note also that this library deliberately has no postinstall hook. Decryption only ever happens when you explicitly call it. Packages that decrypt and execute code automatically at install time are a well-known malware pattern and get flagged by registry scanners.

How it works

Encrypted container format (produced by encryptLibrary):

magic        "SLL1"      4 bytes    format identifier
version      uint8       1 byte
kdf id       uint8       1 byte     1 = scrypt
N, r, p      uint32 x3   12 bytes   scrypt cost parameters
salt len     uint8       1 byte
salt         N bytes     random, unique per encryption
iv           12 bytes    random, unique per encryption
authTag      16 bytes    AES-GCM authentication tag
ciphertext   remainder   gzip-compressed archive, encrypted

Storing the KDF cost parameters in the header means future versions can raise the cost factor without breaking files encrypted with older defaults.

Archive format (the plaintext packed before encryption): a flat list of { path, mode, content } entries covering every regular file under sourceDir. Directory structure is preserved via /-separated relative paths; empty directories and symlinks are not preserved.

Security properties:

  • AES-256-GCM is authenticated — any tampering with the encrypted file is detected and decryption fails, rather than silently returning corrupted data.
  • A fresh random salt and IV are generated on every encryption, so encrypting the same content twice with the same password produces different ciphertext.
  • Every extracted file path is validated to resolve inside the target directory, rejecting archive entries that attempt path traversal (e.g. ../../.npmrc).
  • Decryption is atomic: files are extracted to a temporary directory first, then moved into place, so a crash mid-extraction never leaves a half-written library folder.

Development

npm test

Runs the test suite with Node's built-in test runner (node --test), covering encryption round-trips, wrong-password/tamper detection, path-traversal protection, and module install/load behaviour.