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
Maintainers
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 viagetModuleByName("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-loaderFor global CLI use:
npm install -g secure-library-loaderPassword 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:
- An explicit
passwordoption passed to the JS API - The
SLL_PASSWORDenvironment variable - 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.sllYou'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-libAdd --force to overwrite a non-empty output directory:
secure-library-loader decrypt my-lib.sll -o ./restored-lib --forceInstall into a lib/ subfolder
secure-library-loader install my-lib.sll --module-root . --lib-dir lib --name my-libThis decrypts into <module-root>/lib/<name> and prints the modules it found
inside that subfolder. All flags are optional:
--module-rootdefaults to secure-library-loader's own package directory--lib-dirdefaults tolib--namedefaults tomy-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-libProgrammatic 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.jsThe 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)); // 5Or 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 insideinstallDir
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 } = {}): booleanEvicts 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 diskNote 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, encryptedStoring 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 testRuns 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.
