@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-encryption

npm · Source on GitHub

How 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.ts JSDoc, 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 a context that 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 chunkSize plaintext bytes followed by its 16-byte AEAD tag. Every chunk except the last holds exactly chunkSize plaintext 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: 0x01 on the last chunk, 0x00 otherwise). 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

ProviderPackage
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 to encrypt(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 Node Transform streams for payloads too large to hold in memory (backup archives, exports). Not every bond implements them: call hasStreamEncryption(provider) first and fail clearly if it is false — never fall back to buffering the whole payload through encrypt() (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 (magic MOLAEAD2, 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 (info mol-aead-chunked-v2), so the long-lived key is never used directly and is domain-separated from its other uses. Each chunk's nonce is noncePrefix || counter and its AAD is header || context || counter || finalFlag, so a flipped bit, an edited or swapped header, reordered/duplicated/dropped chunks or a wrong context all fail authentication.

  • decryptStream() releases plaintext only after each chunk's tag verifies, and buffers at most one chunk. A failure destroys the stream with an EncryptionStreamError (err.name === 'EncryptionStreamError'; isEncryptionStreamError(err) narrows it), so pipeline() 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 wrong context reads 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').