@molecule/api-encryption
Core interface · encryption · API (Node) · v1.1.1 · Apache-2.0
Field-level encryption core interface for molecule.dev — encrypt, decrypt, hash, verify, and rotate keys
npm install @molecule/api-encryptionHow it works
@molecule/api-encryption is the encryption core interface on the API (Node) side: the API your app calls, with no vendor inside.
Choose the implementation by bonding one of its 1 provider: @molecule/api-encryption-aes.
import { setProvider, encrypt, decrypt, hash, verify } from '@molecule/api-encryption'
import { provider } from '@molecule/api-encryption-aes'
// Wire the provider at startup
setProvider(provider)
// Field-level encryption for sensitive data at rest
const ciphertext = await encrypt(accessToken)
const plaintext = await decrypt(ciphertext)
// Integrity hashing (checksums, dedupe keys) — NOT for passwords
const checksum = await hash(documentBody)
const untampered = await verify(documentBody, checksum)
// Streaming encryption (optional capability) for large payloads such as
// backup archives — never whole in memory
import { createReadStream, createWriteStream } from 'node:fs'
import { pipeline } from 'node:stream/promises'
import { getProvider, hasStreamEncryption } from '@molecule/api-encryption'
const enc = getProvider()
if (!hasStreamEncryption(enc)) throw new Error('bonded encryption provider cannot stream')
await pipeline(
createReadStream('backup.tar'),
enc.encryptStream({ context: 'backup:2026-10-04' }),
createWriteStream('backup.tar.enc'),
)
await pipeline(
createReadStream('backup.tar.enc'),
enc.decryptStream({ context: 'backup:2026-10-04' }), // rejects on tampering/truncation
createWriteStream('restored.tar'),
)Providers (1): @molecule/api-encryption-aes
Works with: @molecule/api-bond, @molecule/api-i18n
Reference
Encryption core interface for molecule.dev.
Provides the EncryptionProvider interface for field-level encryption,
decryption, hashing, and key rotation. Bond a concrete provider
(e.g. @molecule/api-encryption-aes) at startup via setProvider().
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
import { setProvider, encrypt, decrypt, hash, verify } from '@molecule/api-encryption'
import { provider } from '@molecule/api-encryption-aes'
// Wire the provider at startup
setProvider(provider)
// Field-level encryption for sensitive data at rest
const ciphertext = await encrypt(accessToken)
const plaintext = await decrypt(ciphertext)
// Integrity hashing (checksums, dedupe keys) — NOT for passwords
const checksum = await hash(documentBody)
const untampered = await verify(documentBody, checksum)
// Streaming encryption (optional capability) for large payloads such as
// backup archives — never whole in memory
import { createReadStream, createWriteStream } from 'node:fs'
import { pipeline } from 'node:stream/promises'
import { getProvider, hasStreamEncryption } from '@molecule/api-encryption'
const enc = getProvider()
if (!hasStreamEncryption(enc)) throw new Error('bonded encryption provider cannot stream')
await pipeline(
createReadStream('backup.tar'),
enc.encryptStream({ context: 'backup:2026-10-04' }),
createWriteStream('backup.tar.enc'),
)
await pipeline(
createReadStream('backup.tar.enc'),
enc.decryptStream({ context: 'backup:2026-10-04' }), // rejects on tampering/truncation
createWriteStream('restored.tar'),
)
Type
core
Installation
npm install @molecule/api-encryption @molecule/api-bond @molecule/api-i18n
API
Interfaces
EncryptionConfig
Configuration options for an encryption provider.
interface EncryptionConfig {
/** The encryption key (or key identifier for KMS-backed providers). */
key: string
/** Optional algorithm identifier (provider-specific). */
algorithm?: string
/** Optional key version for rotation tracking. */
keyVersion?: number
}
EncryptStreamOptions
Options for EncryptionProvider.encryptStream and
EncryptionProvider.decryptStream.
interface EncryptStreamOptions {
/**
* Optional additional authenticated data bound to every chunk of the
* stream (e.g. a backup id). The identical context is required to
* decrypt; a mismatch fails authentication on the first chunk.
*/
context?: string
}
EncryptionProvider
Encryption provider interface.
All encryption providers must implement this interface to provide field-level encryption, decryption, hashing, and key rotation.
interface EncryptionProvider {
/**
* Encrypts a plaintext string.
*
* @param plaintext - The data to encrypt.
* @param context - Optional additional authenticated data (AAD) for
* authenticated encryption schemes.
* @returns The encrypted ciphertext string (typically base64-encoded).
*/
encrypt(plaintext: string, context?: string): Promise<string>
/**
* Decrypts a ciphertext string.
*
* @param ciphertext - The encrypted data to decrypt.
* @param context - Optional additional authenticated data (AAD) that was
* used during encryption.
* @returns The decrypted plaintext string.
*/
decrypt(ciphertext: string, context?: string): Promise<string>
/**
* Produces a one-way hash of the given data.
*
* @param data - The data to hash.
* @returns The hash string (hex or base64 encoded).
*/
hash(data: string): Promise<string>
/**
* Verifies that data matches a previously computed hash.
*
* @param data - The original data to verify.
* @param hash - The hash to verify against.
* @returns `true` if the data matches the hash.
*/
verify(data: string, hash: string): Promise<boolean>
/**
* Rotates the encryption key. Re-encrypts internal state or markers
* so that future operations use the new key while previously encrypted
* data can still be decrypted during a transition period.
*
* @param oldKey - The current encryption key.
* @param newKey - The new encryption key to rotate to.
*/
rotateKey(oldKey: string, newKey: string): Promise<void>
/**
* OPTIONAL. Creates a streaming encryptor: plaintext bytes in,
* authenticated ciphertext bytes out, never holding more than one chunk
* in memory. Providers that implement it use the
* `ENCRYPTED_STREAM_FORMAT` framing. Check with
* `hasStreamEncryption(provider)` before calling.
*
* @param options - Optional stream options (AAD context).
* @returns A Node `Transform` stream.
*/
encryptStream?(options?: EncryptStreamOptions): Transform
/**
* OPTIONAL. Creates a streaming decryptor, the reverse of
* `EncryptionProvider.encryptStream`. It emits a chunk's plaintext
* only after that chunk's authentication tag has verified, and destroys
* itself with an `EncryptionStreamError` whose `code` names the cause: a
* bad header (`bad-header`), an unknown key version (`unknown-key-version`),
* a key that is not the one that wrote the stream (`unknown-key`), a failed
* tag — tampering, chunks out of order or a wrong context (`auth`) — or
* input that ends before the final chunk (`truncated`).
*
* @param options - Optional stream options; `context` must match the one
* used to encrypt.
* @returns A Node `Transform` stream.
*/
decryptStream?(options?: EncryptStreamOptions): Transform
}
Types
EncryptionStreamErrorCode
Why a stream encryptor/decryptor failed. Three groups matter to a caller:
The object is damaged, or is not this key's/context's
'auth'— a chunk failed authentication: a flipped bit, reordered or duplicated chunks, an edited or swapped header, a final chunk cut part way, or acontextthat does not match the one used to encrypt.'bad-header'— the input does not start with a valid header (wrong magic, out-of-range chunk size, or fewer header bytes than required). It is not an encrypted stream in this format, or its first bytes are lost.'overflow'— the chunk counter would exceed 2^32 (the stream is longer than the format allows).
The environment, not the object
'unknown-key-version'— the header names a key version the keyring does not hold (never seeded, or pruned).'unknown-key'— the keyring holds that version, but it is a different key from the one that wrote the stream (a mistyped or rotated key under the same version number). Fix the configuration; the object may be fine.
The stream ended early — may be transport
'truncated'— the input ended at a chunk boundary without its final chunk. A dropped connection produces this as readily as an attacker; a retry of the read can succeed.
A bug or misuse
'internal'— the caller or the bond did something invalid (e.g. an out-of-range chunk size or key version at encrypt), or an unexpected error inside the transform. Never a property of the ciphertext.
type EncryptionStreamErrorCode =
| 'bad-header'
| 'unknown-key-version'
| 'unknown-key'
| 'auth'
| 'truncated'
| 'overflow'
| 'internal'
Classes
EncryptionStreamError
The error a stream encryptor/decryptor destroys itself with when the input
cannot be authenticated or parsed. name is always 'EncryptionStreamError';
code says why (see EncryptionStreamErrorCode).
class EncryptionStreamError extends Error {
/** Why the stream failed. */
readonly code: EncryptionStreamErrorCode
/**
* Creates the error.
*
* @param code - Why the stream failed.
* @param message - Plain-English description of what was wrong.
* @param options - Optional `cause`.
*/
constructor(code: EncryptionStreamErrorCode, message: string, options?: { cause?: unknown }) {
super(message, options)
this.name = ENCRYPTION_STREAM_ERROR_NAME
this.code = code
}
}
Functions
setProvider(provider)
Registers an encryption provider as the active singleton. Called by bond packages during application startup.
function setProvider(provider: EncryptionProvider): void
provider— The encryption provider implementation to bond.
getProvider()
Retrieves the bonded encryption provider, throwing if none is configured.
function getProvider(): EncryptionProvider
Returns: The bonded encryption provider.
hasProvider()
Checks whether an encryption provider is currently bonded.
function hasProvider(): boolean
Returns: true if an encryption provider is bonded.
encrypt(plaintext, context)
Encrypts a plaintext string using the bonded provider.
function encrypt(plaintext: string, context?: string): Promise<string>
plaintext— The data to encrypt.context— Optional additional authenticated data (AAD).
Returns: The encrypted ciphertext string.
decrypt(ciphertext, context)
Decrypts a ciphertext string using the bonded provider.
function decrypt(ciphertext: string, context?: string): Promise<string>
ciphertext— The encrypted data to decrypt.context— Optional additional authenticated data (AAD).
Returns: The decrypted plaintext string.
hash(data)
Produces a one-way hash of the given data using the bonded provider.
function hash(data: string): Promise<string>
data— The data to hash.
Returns: The hash string.
verify(data, hashed)
Verifies that data matches a previously computed hash.
function verify(data: string, hashed: string): Promise<boolean>
data— The original data to verify.hashed— The hash to verify against.
Returns: true if the data matches the hash.
rotateKey(oldKey, newKey)
Rotates the encryption key using the bonded provider.
function rotateKey(oldKey: string, newKey: string): Promise<void>
oldKey— The current encryption key.newKey— The new encryption key to rotate to.
Returns: Resolves when rotation completes successfully.
isEncryptionStreamError(error)
Recognizes an EncryptionStreamError, including one thrown by a
second copy of this package (checks name and code, not instanceof).
function isEncryptionStreamError(error: unknown): boolean
error— Any thrown value.
Returns: true when it is a stream error with a known code.
hasStreamEncryption(provider)
Narrows a provider to one that implements both streaming methods. Streaming is optional in the contract, so check before calling.
function hasStreamEncryption(provider: EncryptionProvider): boolean
provider— The encryption provider to inspect.
Returns: true when both encryptStream and decryptStream are functions.
Constants
ENCRYPTED_STREAM_FORMAT
Identifier of the chunked-AEAD stream framing (mol-aead-chunked-v2).
Layout (all integers big-endian):
- Header, 54 bytes: ASCII magic
MOLAEAD2(8) · key version uint16 (2) · chunk size in plaintext bytes uint32 (4, 4 KiB to 16 MiB) · random salt (32) · key id (8). - Key id: the first 8 bytes of HMAC-SHA256(key = the raw key for that
version, data = UTF-8
mol-aead-chunked-v2/kid). It lets a reader tell "this stream was written under a different key" from tampering. - Per-stream material: HKDF-SHA256(ikm = the raw key, salt = the
header's salt, info = UTF-8
mol-aead-chunked-v2, length 40). Bytes 0–31 are the stream's own AEAD key, bytes 32–39 its nonce prefix. Every stream therefore runs under a fresh subkey, domain-separated from any other use of the same long-lived key. - Body: a sequence of chunks, each the ciphertext of up to
chunkSizeplaintext bytes followed by its 16-byte AEAD tag. Every chunk except the last holds exactlychunkSizeplaintext bytes. - Per-chunk nonce, 12 bytes: nonce prefix (8) || chunk counter uint32 (4), the counter starting at 0 and increasing by one per chunk.
- Per-chunk AAD (every chunk): the 54 header bytes || UTF-8 bytes of the
caller's
context(empty when absent) || counter uint32 || final flag (1 byte:0x01on the last chunk,0x00otherwise). Binding the header means an edited chunk size or key version, or another stream's header, fails authentication.
(description truncated — read the full text in node_modules/@molecule/api-encryption/dist/stream.d.ts.)
const ENCRYPTED_STREAM_FORMAT: 'mol-aead-chunked-v2'
ENCRYPTION_STREAM_ERROR_NAME
Error name every stream decryptor uses when it rejects its input.
const ENCRYPTION_STREAM_ERROR_NAME: 'EncryptionStreamError'
Available Providers
| Provider | Package |
|---|---|
| AES | @molecule/api-encryption-aes |
Injection Notes
Requirements
Peer dependencies:
@molecule/api-bond^1.0.1@molecule/api-i18n^1.0.1
Runtime Dependencies
-
@molecule/api-bond -
@molecule/api-i18n -
NEVER hash passwords with
hash(). It is a fast, unsalted integrity hash (e.g. SHA-256 in the AES bond) — fine for checksums and dedupe keys, catastrophic for credentials. Passwords go through@molecule/api-password(salted, slow KDF). -
The key is a server-side secret (e.g. the AES bond's
ENCRYPTION_KEY, auto-generated at scaffold). Never hardcode, log, or expose it — and a lost key makes every ciphertext permanently unreadable, so treat key changes as a rotation (rotateKey), never an edit. -
Ciphertext is opaque. An encrypted DB column cannot be filtered, sorted, or
like-searched on its plaintext. Encrypt narrow sensitive fields (tokens, PII), not fields you query by. -
context(AAD) must match at decrypt. If you pass a context toencrypt(value, context), the identical context is required to decrypt. Binding a ciphertext to e.g. its record id stops cross-row copy-paste — but then the id can never change. -
decrypt()throws on a wrong key or tampered ciphertext — treat that as corruption/misconfiguration to surface, not a condition to retry. -
Stream encryption is OPTIONAL in the contract.
encryptStream()/decryptStream()return NodeTransformstreams for payloads too large to hold in memory (backup archives, exports). Not every bond implements them: callhasStreamEncryption(provider)first and fail clearly if it is false — never fall back to buffering the whole payload throughencrypt()(strings only, and it would load gigabytes into memory). -
Stream framing is
ENCRYPTED_STREAM_FORMAT(mol-aead-chunked-v2), shared by every bond: a 54-byte header (magicMOLAEAD2, key version uint16, chunk size uint32, 32-byte random salt, 8-byte key id), then chunks of ciphertext + 16-byte tag. Each stream encrypts under its own HKDF-SHA256 subkey and nonce prefix derived from the key and the salt (infomol-aead-chunked-v2), so the long-lived key is never used directly and is domain-separated from its other uses. Each chunk's nonce isnoncePrefix || counterand its AAD isheader || context || counter || finalFlag, so a flipped bit, an edited or swapped header, reordered/duplicated/dropped chunks or a wrongcontextall fail authentication. -
decryptStream()releases plaintext only after each chunk's tag verifies, and buffers at most one chunk. A failure destroys the stream with anEncryptionStreamError(err.name === 'EncryptionStreamError';isEncryptionStreamError(err)narrows it), sopipeline()rejects. Chunks that verified before a later failure were already written downstream — write to a temp file and only rename or use it once the pipeline resolves. -
Branch on
err.code, not the message:'auth'(and'bad-header','overflow') — the ciphertext is damaged, or it is not this key's/context's (a wrongcontextreads as'auth'). Treat the object as corrupt; do not retry.'unknown-key'/'unknown-key-version'— the ENVIRONMENT, not the object: the header's key id does not match the configured key for that version (a mistyped or rotated key), or the keyring lacks the version. Fix the key configuration; the object is probably intact.'truncated'— the stream ended at a chunk boundary before its final chunk. A dropped connection looks exactly like this; re-reading may succeed.'internal'— misuse or a bug (e.g. an out-of-range chunk size at encrypt), never a property of the ciphertext.
-
Truncation is detected. The encryptor always closes with a flagged final chunk (possibly holding zero bytes), so a stream cut at a chunk boundary still fails at the end instead of passing as a shorter file. A stream cut part way through a chunk fails that chunk's tag (
'auth').