
Security News
Cloudflare Introduces a Faster TypeScript Compiler Written in Rust
Cloudflare introduces tsrs, a Rust port of TypeScript’s compiler promising faster checks and lower memory use, with the TypeScript team’s help.
@conduitops/logview-archive
Advanced tools
Affected versions:
Streaming authenticated encryption, key management and log-archive formats for retained logs
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:
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.
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.
npm install @conduitops/logview-archive
The library itself has no runtime dependencies.
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/
Four layers, meant to be used in order. Each is usable on its own, but the ones above assume the ones below.
| Layer | Module | What it is for |
|---|---|---|
| core | src/core/* | randomness, digests, KDFs, ciphers, envelopes |
| keyring | src/core/keyring | long-lived keys: purposes, rotation, passphrase-wrapped export |
| vault | src/core/vault | named encrypted blobs over any byte store |
| stream | src/core/stream | chunked 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');
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
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.
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
}
}
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.
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.
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.
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.
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.
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' });
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.
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.
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.
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.
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 },
});
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.
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.
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.
UNLICENSED — internal platform tooling. Use is limited to the platform that publishes it.
FAQs
Streaming authenticated encryption, key management and log-archive formats for retained logs
The npm package @conduitops/logview-archive receives a total of 0 weekly downloads. As such, @conduitops/logview-archive popularity was classified as not popular.
We found that @conduitops/logview-archive demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
Cloudflare introduces tsrs, a Rust port of TypeScript’s compiler promising faster checks and lower memory use, with the TypeScript team’s help.

Security News
The PHP Foundation says Chainguard broke a security embargo ahead of critical RCE patches, as leaks disrupted the coordinated release.

Security News
PHP releases emergency patches for two critical RCE vulnerabilities as agencies urge organizations to take unpatched sites offline.