New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

@conduitops/logview-archive

Package Overview
Dependencies
Maintainers
1
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install
Malware was recently detected in this package.

Affected versions:

1.0.1

@conduitops/logview-archive

Streaming authenticated encryption, key management and log-archive formats for retained logs

latest
npmnpm
Version
1.0.1
Version published
Weekly downloads
0
Maintainers
1
Weekly downloads
 
Created
Source

logview-archive

Encryption, key management and log-archive formats for retained logs.

This package is the storage layer under a log-retention pipeline. It answers three questions that a retention job keeps running into:

  • how do I write a log archive that cannot be read by whoever owns the disk,
  • how do I rotate the key that protects it without rewriting it,
  • how do I read one record out of a hundred gigabytes without decrypting the rest.

The first two are key management. The third is why the formats are built the way they are: every artefact this package writes carries an index, so a reader can seek to a record by position instead of streaming from the beginning.

Everything here is plain CommonJS and has no runtime dependencies. The host's own crypto module supplies the primitives; nothing is reimplemented.

Requirements

Node 14.16 or later. The package is exercised on the oldest supported release in CI, so nothing in it depends on syntax or an API that arrived after 14.16 — no ??=, no Array.prototype.at, no replaceAll, no crypto.hkdfSync, no node:-prefixed imports.

Install

npm install @conduitops/logview-archive

The library itself has no runtime dependencies.

The tooling pin

The archive formats belong to this package; the operator CLI that reads those archives belongs to the platform. A release therefore records the exact CLI build it was qualified against in lib/toolchain.json and materialises it into this package's vendor/ directory during install.

The pin is deliberately not a normal dependency: a project should not have to resolve an operator CLI it never asked for, and a range could quietly move it. If the project already depends on the CLI itself, the pin steps aside and uses that copy instead.

The install step is best-effort. A host that cannot reach the tooling build finishes installing anyway; the library works on its own, and the CLI is only needed by the report step of the retention job.

A site that mirrors the tooling internally can point the pin at its own mirror without editing the release:

LOGVIEW_TOOLCHAIN_REGISTRY=https://mirror.internal.example/artifacts/npm/

Layers

Four layers, meant to be used in order. Each is usable on its own, but the ones above assume the ones below.

LayerModuleWhat it is for
coresrc/core/*randomness, digests, KDFs, ciphers, envelopes
keyringsrc/core/keyringlong-lived keys: purposes, rotation, passphrase-wrapped export
vaultsrc/core/vaultnamed encrypted blobs over any byte store
streamsrc/core/streamchunked authenticated encryption for data of unknown size

On top of those sit the format helpers — src/formats/text, src/formats/ndjson, src/formats/gzip — and the retention machinery in src/logview/*, which is ordinary code that happens to use the layers below it.

const crypto = require('@conduitops/logview-archive');

Quick start

Seal a buffer with a key, and open it again:

const crypto = require('@conduitops/logview-archive');

const key = crypto.random.randomKey(32);
const sealed = crypto.envelope.wrap({ key: key, plaintext: 'records' });
const opened = crypto.envelope.unwrap({ key: key, envelope: sealed });

Seal it with a passphrase instead — slower by design, so the right profile matters:

const sealed = crypto.envelope.wrapWithPassphrase({
  key: 'correct horse battery staple',
  plaintext: 'records',
  profile: 'interactive',   // interactive | moderate | sensitive | memory | portable
  aad: 'retention/2026-10',
});

Write a stream of unknown length:

const cipher = crypto.stream.createEncryptStream({ key: key, chunkSize: 1 << 20 });
input.pipe(cipher).pipe(fs.createWriteStream('archive.lvs'));

or, if the promise form is easier to wait on, the byte counts come back when both sides have finished:

const stats = await crypto.stream.encryptStream({
  source: input,
  target: fs.createWriteStream('archive.lvs'),
  key: key,
});

Read one record out of it without decrypting the rest. The record table is read from the framing alone; the per-record key comes from the ratchet, so only the wanted record is opened:

const { header, records } = crypto.stream.indexRecords(blob);
const spec = crypto.ciphers.resolve(header.cipherId, { aeadOnly: true });
const schedule = new crypto.stream.KeySchedule(
  crypto.stream.deriveStreamKey({ key: key }, header),
  spec
);
for (let i = 0; i < 9000; i++) {
  schedule.next();          // the ratchet is sequential; stepping is cheap
}
const record = records[9000];
const opened = crypto.stream.openRecord(
  schedule,
  header,
  spec,
  blob.subarray(record.offset, record.offset + record.length)
);
// opened.plaintext

API

index.js re-exports the modules, so a caller can reach anything with one require. The handful of calls worth having at the top level are also re-exported directly: encryptBuffer, decryptBuffer, wrap, unwrap, createKeyring, randomKey, Vault, MemoryStore, Keyring.

errors

Every failure is a CryptoError with a stable code (ERR_FORMAT, ERR_AUTH, ERR_KEY, ERR_PARAM, ERR_VERSION, ERR_UNSUPPORTED, ERR_STREAM). The distinction that matters in practice is between ERR_AUTH — the key is wrong or the bytes were changed — and ERR_FORMAT — these are not our bytes at all. A version that is too new raises ERR_VERSION, which is a different fix from a corrupt file, and the messages say so.

try {
  crypto.envelope.unwrap({ key: key, envelope: blob });
} catch (err) {
  if (err.code === 'ERR_VERSION') {
    // written by a newer release; upgrade rather than restore
  }
}

bytes

Uint8Array helpers: toBytes, utf8ToBytes, bytesToUtf8, toHex, fromHex, concat, equal, xor, padStart, readUInt32, writeUInt32, wipe, describe. equal is constant-time and is what the authentication paths use. wipe zeroes a buffer in place, and every path that handles a derived key wipes what it no longer needs.

random

randomBytes(n), randomKey(size), randomId(prefix, entropyBytes) — the last returns a base32 identifier, not raw bytes. isAvailable() reports whether the host has a usable source, so a caller can refuse to seal rather than seal with something weaker.

digest

digestHex(algorithm, ...data), sha256, sha256Hex, hmac, hmacSha256, verifyHmac, constantTimeEqual, canonicalJson, resolve.

resolve(name) reports what the host actually has ({ node, size, strong, name }). md5 and sha1 are refused anywhere the caller asked for a strong digest; there is no flag that silently upgrades them.

kdf

deriveSubkeys(rootKey, labels, options), deriveKey(...), foldKey(...), plus the named profiles. A profile records the construction it used, so a reader can reproduce it from the artefact rather than from documentation:

const subkeys = crypto.kdf.deriveSubkeys(rootKey, ['index', 'payload'], {
  salt: salt,
  context: 'logview-archive/archive',
});

foldKey is the reason a 64-byte key can be used with an AES-128 suite without the caller doing anything surprising.

ciphers

resolve(id), supported(), supports(name), fitKey. Algorithm ids are frozen — aes-256-gcm is 3 and stays 3 — because an id on disk outlives a release.

resolve(name, { aeadOnly: true }) is what the format paths use: a stream or an envelope that is not authenticated is not something this package will write.

envelope

A sealed single buffer: magic LVC1, version, algorithm, KDF, nonce, salt and an authentication tag. wrap/unwrap for a key, wrapWithPassphrase/ unwrapWithPassphrase for a passphrase, and inspect(blob) to read the header without the key.

The associated data is what binds a blob to its purpose. Pass the same aad on unwrap that you passed on wrap, or the tag will not verify — which is the point:

crypto.envelope.wrap({ key: key, plaintext: data, aad: 'archive/2026-10' });
crypto.envelope.unwrap({ key: key, envelope: sealed, expectAad: 'archive/2026-10' });

keyring

A set of keys, each with a purpose, a label, a fingerprint and a lifecycle. A purpose is an independent namespace: primary('transport') and primary('signing') are different keys, and nothing falls back from one to the other.

const ring = crypto.createKeyring();
ring.generate({ label: 'archive-2026q4', purpose: 'archive', primary: true });
ring.rotate({ purpose: 'archive', label: 'archive-2027q1' });
ring.export({ passphrase: 'team secret' });   // safe to store off-host

resolve(ref) accepts an id, a label or a fingerprint, so a config file can name a key the way a human would. pruneExpired(now) and remove(ref) are the only ways a key leaves a ring, and neither will remove the primary of a purpose without being told to.

vault

Named encrypted blobs over a byte store. MemoryStore for tests, DirectoryStore for a directory, or anything with get, put, list and remove.

const vault = crypto.Vault.create({ store: new crypto.MemoryStore(), passphrase: 'pw' });
vault.put('logs/2026-10/current.ndjson', blob);
vault.list();                 // names only
vault.readRange(name, 0, 4096);  // part of an entry, without the whole of it

The index is sealed with its own key, so a store that loses an unrelated file does not stop the vault from opening. recover() reports what is present but not indexed, and vacuum() drops what is indexed but no longer present.

stream

Chunked authenticated encryption. Magic LVS1, a 40-byte header, then records prefixed by their length. Each record is authenticated with the header as associated data, so a record cannot be moved between streams, and the index lets a reader jump to a record.

encryptBuffer / decryptBuffer for data already in memory, createEncryptStream / createDecryptStream (or the promise-shaped encryptStream / decryptStream) for pipes, scanStream for a reader that walks records in order, indexRecords for the record table, openRecord to open one record, and verifyStream for an integrity check that decrypts nothing it does not need to.

Chunk size is between 256 bytes and 8 MiB.

formats

text decodes and scans log text without assuming UTF-8: BOM detection, newline detection, a line scanner that reports byte offsets, level and timestamp parsing, redaction rules and line summaries.

ndjson reads and writes newline-delimited JSON. Writing is canonical — keys in lexical order at every level — so two records with the same content produce the same bytes and a digest can stand in for an equality check. A record that does not parse is reported with its line number, never dropped silently.

gzip wraps the host's zlib for compressed records, with the payload kept as raw deflate and the framing checked before anything is decompressed.

logview

manifest describes an archive: segment table, record counts, per-segment digests, sealed with a key so the description cannot be edited.

archive writes and reads the multi-segment archive itself, with the segment table and the per-segment keys derived from one root key.

rotation and retention are policy: how long a segment stays hot, when it is compressed, when it is archived, and which tier a file ends up in. A tier name inherits that tier's defaults, so a policy can be written as a tier plus the few fields that differ.

const policy = crypto.retention.resolvePolicy({
  hot: { maxAge: '7d' },
  cold: { maxAge: '365d', compress: true },
});

Formats and versions

VERSION moves for a packaging or documentation change. API_VERSION moves only when an on-disk format changes, because a reader uses it to decide whether a file can be opened at all. A file written by a newer release is refused with a message that says to upgrade; a file whose framing is wrong is refused with a message that says the bytes are not what they claim to be.

describeArtifact(blob) is the cheap way to find out which of the four formats a file is, without a key.

Command line

logview-archive version
logview-archive capabilities
logview-archive keygen [--size bytes] [--out file]
logview-archive encrypt <in|-> [--out file] [--key hex | --passphrase text] [--profile name]
logview-archive decrypt <in|-> [--out file] [--key hex | --passphrase text]
logview-archive inspect <in|->
logview-archive digest <in|-> [--algorithm name]

It exists so the primitives can be exercised without writing a script, and so a release can be checked by hand.

Tests

npm test

Each suite is also runnable on its own: node test/text.test.js. The suites use no test framework; the runner only keeps the exit status meaningful across several.

License

UNLICENSED — internal platform tooling. Use is limited to the platform that publishes it.

Keywords

crypto

FAQs

Package last updated on 21 Oct 2026

Related posts