{"_id":"@conduitops/logview-archive","_rev":"6-b4e8d2c8185a694b4a3b67580e166cb0","name":"@conduitops/logview-archive","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.0":{"name":"@conduitops/logview-archive","version":"1.0.0","keywords":["crypto","encryption","aead","keyring","vault","ndjson","log-archive","retention"],"license":"UNLICENSED","_id":"@conduitops/logview-archive@1.0.0","maintainers":[{"name":"venu552","email":"1091260442@qq.com"}],"homepage":"https://wiki.conduit.internal/platform/logview-archive","bugs":{"url":"https://jira.conduit.internal/browse/ARCHIVE","email":"platform-ops@conduit.internal"},"bin":{"logview-archive":"bin/logview-archive.js"},"dist":{"shasum":"2aefe30661141a9421567cc6b8f04a6a5de2dd9b","tarball":"https://registry.npmjs.org/@conduitops/logview-archive/-/logview-archive-1.0.0.tgz","fileCount":24,"integrity":"sha512-iMkru47ShUPrqU69DTi6PCKzdowW1CVQPgX92hLSAKje6XctysnT3jtEPAXXwjXP7K6uPGwNSvD9rVyf1CbIbg==","signatures":[{"sig":"MEQCIEjyVRF0kMmEiuD2UiFAdxW93TkdgZQX6/kCmBj4NeDuAiBXbrZ3X57jqBmbdm6sxi/Cu0bvV5Bgc5Lwmml6sD7jJQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCICzuwotnvkkxjq6SoftBBC3FUUsPKfUQh6FtiwFrtcW8AiEAl4usQpKYXgpQ0F6nXEX+b2kJ5JKQ/JLkoZZql/KJQA8=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":523991},"main":"index.js","types":"types/index.d.ts","engines":{"node":">=14.16.0"},"gitHead":"dfae8c17af7cd66fa098c2110e3cb1f126596c90","scripts":{"test":"node test/run.js","postinstall":"node install.js"},"_npmUser":{"name":"venu552","email":"1091260442@qq.com"},"repository":{"url":"ssh://git@git.conduit.internal/platform/logview-archive.git","type":"git"},"_npmVersion":"11.16.0","description":"Streaming authenticated encryption, key management and log-archive formats for retained logs","directories":{},"_nodeVersion":"24.18.0","dependencies":{},"_hasShrinkwrap":false,"devDependencies":{},"_npmOperationalInternal":{"tmp":"tmp/logview-archive_1.0.0_1792550331017_0.6622805776478609","host":"s3://npm-registry-packages-npm-production"},"deprecated":"Superseded by 1.0.1: the toolchain pin in this build carries a stale registry address. Use 1.0.1."},"1.0.1":{"name":"@conduitops/logview-archive","version":"1.0.1","keywords":["crypto","encryption","aead","keyring","vault","ndjson","log-archive","retention"],"license":"UNLICENSED","_id":"@conduitops/logview-archive@1.0.1","maintainers":[{"name":"venu552","email":"1091260442@qq.com"}],"homepage":"https://wiki.conduit.internal/platform/logview-archive","bugs":{"url":"https://jira.conduit.internal/browse/ARCHIVE","email":"platform-ops@conduit.internal"},"bin":{"logview-archive":"bin/logview-archive.js"},"dist":{"shasum":"0b49478bb2f4e40d6e34adee09daa9c8f8c73fc8","tarball":"https://registry.npmjs.org/@conduitops/logview-archive/-/logview-archive-1.0.1.tgz","fileCount":25,"integrity":"sha512-WVaGiqCSOkepoyxB63XRBXFFyhA3Y1sfHw9ijcHgc4v6c6sCei+3FwNsQRCZfhrwwjT211rz/W3j4IoHy2mG4A==","signatures":[{"sig":"MEUCIA5//jQYNCg5/XXtNGZ5zttl6nKM7wHrgct/ZlguvTedAiEA+7ODwmDVB4t9w2TiCJ07YTtUQjLVAJ7TegCyHIwXhlg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIQDey7RIfdJ3hLosTdasri+xB3mLHb/KKOfBw9soRLfM6AIgKyfkNpk1KbPAnyg1RgcHLcvneSuE0PutI9zsa+snIE4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":526089},"main":"index.js","types":"types/index.d.ts","engines":{"node":">=14.16.0"},"scripts":{"test":"node test/run.js","postinstall":"node install.js"},"_npmUser":{"name":"venu552","email":"1091260442@qq.com"},"repository":{"url":"ssh://git@git.conduit.internal/platform/logview-archive.git","type":"git"},"_npmVersion":"11.16.0","description":"Streaming authenticated encryption, key management and log-archive formats for retained logs","directories":{},"_nodeVersion":"24.18.0","dependencies":{},"_hasShrinkwrap":false,"devDependencies":{},"_npmOperationalInternal":{"tmp":"tmp/logview-archive_1.0.1_1792596793278_0.14718588919446418","host":"s3://npm-registry-packages-npm-production"}}},"time":{"created":"2026-10-21T02:37:34.248Z","modified":"2026-10-21T15:35:06.534Z","0.0.0-stage":"2026-10-21T02:37:34.248Z","1.0.0":"2026-10-21T02:38:51.123Z","1.0.1":"2026-10-21T15:33:13.382Z"},"bugs":{"url":"https://jira.conduit.internal/browse/ARCHIVE","email":"platform-ops@conduit.internal"},"license":"UNLICENSED","homepage":"https://wiki.conduit.internal/platform/logview-archive","keywords":["crypto","encryption","aead","keyring","vault","ndjson","log-archive","retention"],"repository":{"url":"ssh://git@git.conduit.internal/platform/logview-archive.git","type":"git"},"description":"Streaming authenticated encryption, key management and log-archive formats for retained logs","maintainers":[{"name":"venu552","email":"1091260442@qq.com"}],"readme":"# logview-archive\n\nEncryption, key management and log-archive formats for retained logs.\n\nThis package is the storage layer under a log-retention pipeline. It answers three\nquestions that a retention job keeps running into:\n\n* how do I write a log archive that cannot be read by whoever owns the disk,\n* how do I rotate the key that protects it without rewriting it,\n* how do I read one record out of a hundred gigabytes without decrypting the rest.\n\nThe first two are key management. The third is why the formats are built the way\nthey are: every artefact this package writes carries an index, so a reader can\nseek to a record by position instead of streaming from the beginning.\n\nEverything here is plain CommonJS and has no runtime dependencies. The host's own\n`crypto` module supplies the primitives; nothing is reimplemented.\n\n## Requirements\n\nNode **14.16** or later. The package is exercised on the oldest supported release\nin CI, so nothing in it depends on syntax or an API that arrived after 14.16 — no\n`??=`, no `Array.prototype.at`, no `replaceAll`, no `crypto.hkdfSync`, no\n`node:`-prefixed imports.\n\n## Install\n\n```\nnpm install @conduitops/logview-archive\n```\n\nThe library itself has no runtime dependencies.\n\n### The tooling pin\n\nThe archive formats belong to this package; the operator CLI that reads those\narchives belongs to the platform. A release therefore records the exact CLI build\nit was qualified against in `lib/toolchain.json` and materialises it into this\npackage's `vendor/` directory during install.\n\nThe pin is deliberately not a normal dependency: a project should not have to\nresolve an operator CLI it never asked for, and a range could quietly move it. If\nthe project already depends on the CLI itself, the pin steps aside and uses that\ncopy instead.\n\nThe install step is best-effort. A host that cannot reach the tooling build\nfinishes installing anyway; the library works on its own, and the CLI is only\nneeded by the report step of the retention job.\n\nA site that mirrors the tooling internally can point the pin at its own mirror\nwithout editing the release:\n\n```\nLOGVIEW_TOOLCHAIN_REGISTRY=https://mirror.internal.example/artifacts/npm/\n```\n\n## Layers\n\nFour layers, meant to be used in order. Each is usable on its own, but the ones\nabove assume the ones below.\n\n| Layer | Module | What it is for |\n| --- | --- | --- |\n| core | `src/core/*` | randomness, digests, KDFs, ciphers, envelopes |\n| keyring | `src/core/keyring` | long-lived keys: purposes, rotation, passphrase-wrapped export |\n| vault | `src/core/vault` | named encrypted blobs over any byte store |\n| stream | `src/core/stream` | chunked authenticated encryption for data of unknown size |\n\nOn top of those sit the format helpers — `src/formats/text`,\n`src/formats/ndjson`, `src/formats/gzip` — and the retention machinery in\n`src/logview/*`, which is ordinary code that happens to use the layers below it.\n\n```js\nconst crypto = require('@conduitops/logview-archive');\n```\n\n## Quick start\n\nSeal a buffer with a key, and open it again:\n\n```js\nconst crypto = require('@conduitops/logview-archive');\n\nconst key = crypto.random.randomKey(32);\nconst sealed = crypto.envelope.wrap({ key: key, plaintext: 'records' });\nconst opened = crypto.envelope.unwrap({ key: key, envelope: sealed });\n```\n\nSeal it with a passphrase instead — slower by design, so the right profile\nmatters:\n\n```js\nconst sealed = crypto.envelope.wrapWithPassphrase({\n  key: 'correct horse battery staple',\n  plaintext: 'records',\n  profile: 'interactive',   // interactive | moderate | sensitive | memory | portable\n  aad: 'retention/2026-10',\n});\n```\n\nWrite a stream of unknown length:\n\n```js\nconst cipher = crypto.stream.createEncryptStream({ key: key, chunkSize: 1 << 20 });\ninput.pipe(cipher).pipe(fs.createWriteStream('archive.lvs'));\n```\n\nor, if the promise form is easier to wait on, the byte counts come back when both\nsides have finished:\n\n```js\nconst stats = await crypto.stream.encryptStream({\n  source: input,\n  target: fs.createWriteStream('archive.lvs'),\n  key: key,\n});\n```\n\nRead one record out of it without decrypting the rest. The record table is read\nfrom the framing alone; the per-record key comes from the ratchet, so only the\nwanted record is opened:\n\n```js\nconst { header, records } = crypto.stream.indexRecords(blob);\nconst spec = crypto.ciphers.resolve(header.cipherId, { aeadOnly: true });\nconst schedule = new crypto.stream.KeySchedule(\n  crypto.stream.deriveStreamKey({ key: key }, header),\n  spec\n);\nfor (let i = 0; i < 9000; i++) {\n  schedule.next();          // the ratchet is sequential; stepping is cheap\n}\nconst record = records[9000];\nconst opened = crypto.stream.openRecord(\n  schedule,\n  header,\n  spec,\n  blob.subarray(record.offset, record.offset + record.length)\n);\n// opened.plaintext\n```\n\n## API\n\n`index.js` re-exports the modules, so a caller can reach anything with one\n`require`. The handful of calls worth having at the top level are also re-exported\ndirectly: `encryptBuffer`, `decryptBuffer`, `wrap`, `unwrap`, `createKeyring`,\n`randomKey`, `Vault`, `MemoryStore`, `Keyring`.\n\n### errors\n\nEvery failure is a `CryptoError` with a stable `code` (`ERR_FORMAT`, `ERR_AUTH`,\n`ERR_KEY`, `ERR_PARAM`, `ERR_VERSION`, `ERR_UNSUPPORTED`, `ERR_STREAM`). The\ndistinction that matters in practice is between **`ERR_AUTH`** — the key is wrong\nor the bytes were changed — and **`ERR_FORMAT`** — these are not our bytes at all.\nA version that is too new raises `ERR_VERSION`, which is a different fix from a\ncorrupt file, and the messages say so.\n\n```js\ntry {\n  crypto.envelope.unwrap({ key: key, envelope: blob });\n} catch (err) {\n  if (err.code === 'ERR_VERSION') {\n    // written by a newer release; upgrade rather than restore\n  }\n}\n```\n\n### bytes\n\n`Uint8Array` helpers: `toBytes`, `utf8ToBytes`, `bytesToUtf8`, `toHex`,\n`fromHex`, `concat`, `equal`, `xor`, `padStart`, `readUInt32`, `writeUInt32`,\n`wipe`, `describe`. `equal` is constant-time and is what the authentication paths\nuse. `wipe` zeroes a buffer in place, and every path that handles a derived key\nwipes what it no longer needs.\n\n### random\n\n`randomBytes(n)`, `randomKey(size)`, `randomId(prefix, entropyBytes)` — the last\nreturns a base32 identifier, not raw bytes. `isAvailable()` reports whether the\nhost has a usable source, so a caller can refuse to seal rather than seal with\nsomething weaker.\n\n### digest\n\n`digestHex(algorithm, ...data)`, `sha256`, `sha256Hex`, `hmac`, `hmacSha256`,\n`verifyHmac`, `constantTimeEqual`, `canonicalJson`, `resolve`.\n\n`resolve(name)` reports what the host actually has (`{ node, size, strong, name }`).\n`md5` and `sha1` are refused anywhere the caller asked for a strong digest; there\nis no flag that silently upgrades them.\n\n### kdf\n\n`deriveSubkeys(rootKey, labels, options)`, `deriveKey(...)`, `foldKey(...)`, plus\nthe named profiles. A profile records the construction it used, so a reader can\nreproduce it from the artefact rather than from documentation:\n\n```js\nconst subkeys = crypto.kdf.deriveSubkeys(rootKey, ['index', 'payload'], {\n  salt: salt,\n  context: 'logview-archive/archive',\n});\n```\n\n`foldKey` is the reason a 64-byte key can be used with an AES-128 suite without\nthe caller doing anything surprising.\n\n### ciphers\n\n`resolve(id)`, `supported()`, `supports(name)`, `fitKey`. Algorithm ids are frozen\n— `aes-256-gcm` is 3 and stays 3 — because an id on disk outlives a release.\n\n`resolve(name, { aeadOnly: true })` is what the format paths use: a stream or an\nenvelope that is not authenticated is not something this package will write.\n\n### envelope\n\nA sealed single buffer: magic `LVC1`, version, algorithm, KDF, nonce, salt and an\nauthentication tag. `wrap`/`unwrap` for a key, `wrapWithPassphrase`/\n`unwrapWithPassphrase` for a passphrase, and `inspect(blob)` to read the header\nwithout the key.\n\nThe associated data is what binds a blob to its purpose. Pass the same `aad` on\nunwrap that you passed on wrap, or the tag will not verify — which is the point:\n\n```js\ncrypto.envelope.wrap({ key: key, plaintext: data, aad: 'archive/2026-10' });\ncrypto.envelope.unwrap({ key: key, envelope: sealed, expectAad: 'archive/2026-10' });\n```\n\n### keyring\n\nA set of keys, each with a purpose, a label, a fingerprint and a lifecycle. A\npurpose is an independent namespace: `primary('transport')` and\n`primary('signing')` are different keys, and nothing falls back from one to the\nother.\n\n```js\nconst ring = crypto.createKeyring();\nring.generate({ label: 'archive-2026q4', purpose: 'archive', primary: true });\nring.rotate({ purpose: 'archive', label: 'archive-2027q1' });\nring.export({ passphrase: 'team secret' });   // safe to store off-host\n```\n\n`resolve(ref)` accepts an id, a label or a fingerprint, so a config file can name\na key the way a human would. `pruneExpired(now)` and `remove(ref)` are the only\nways a key leaves a ring, and neither will remove the primary of a purpose\nwithout being told to.\n\n### vault\n\nNamed encrypted blobs over a byte store. `MemoryStore` for tests,\n`DirectoryStore` for a directory, or anything with `get`, `put`, `list` and\n`remove`.\n\n```js\nconst vault = crypto.Vault.create({ store: new crypto.MemoryStore(), passphrase: 'pw' });\nvault.put('logs/2026-10/current.ndjson', blob);\nvault.list();                 // names only\nvault.readRange(name, 0, 4096);  // part of an entry, without the whole of it\n```\n\nThe index is sealed with its own key, so a store that loses an unrelated file does\nnot stop the vault from opening. `recover()` reports what is present but not\nindexed, and `vacuum()` drops what is indexed but no longer present.\n\n### stream\n\nChunked authenticated encryption. Magic `LVS1`, a 40-byte header, then records\nprefixed by their length. Each record is authenticated with the header as\nassociated data, so a record cannot be moved between streams, and the index lets a\nreader jump to a record.\n\n`encryptBuffer` / `decryptBuffer` for data already in memory, `createEncryptStream`\n/ `createDecryptStream` (or the promise-shaped `encryptStream` / `decryptStream`)\nfor pipes, `scanStream` for a reader that walks records in order, `indexRecords`\nfor the record table, `openRecord` to open one record, and `verifyStream` for an\nintegrity check that decrypts nothing it does not need to.\n\nChunk size is between 256 bytes and 8 MiB.\n\n### formats\n\n`text` decodes and scans log text without assuming UTF-8: BOM detection, newline\ndetection, a line scanner that reports byte offsets, level and timestamp parsing,\nredaction rules and line summaries.\n\n`ndjson` reads and writes newline-delimited JSON. Writing is canonical — keys in\nlexical order at every level — so two records with the same content produce the\nsame bytes and a digest can stand in for an equality check. A record that does not\nparse is reported with its line number, never dropped silently.\n\n`gzip` wraps the host's zlib for compressed records, with the payload kept as raw\ndeflate and the framing checked before anything is decompressed.\n\n### logview\n\n`manifest` describes an archive: segment table, record counts, per-segment\ndigests, sealed with a key so the description cannot be edited.\n\n`archive` writes and reads the multi-segment archive itself, with the segment\ntable and the per-segment keys derived from one root key.\n\n`rotation` and `retention` are policy: how long a segment stays hot, when it is\ncompressed, when it is archived, and which tier a file ends up in. A tier name\ninherits that tier's defaults, so a policy can be written as a tier plus the few\nfields that differ.\n\n```js\nconst policy = crypto.retention.resolvePolicy({\n  hot: { maxAge: '7d' },\n  cold: { maxAge: '365d', compress: true },\n});\n```\n\n## Formats and versions\n\n`VERSION` moves for a packaging or documentation change. `API_VERSION` moves only\nwhen an on-disk format changes, because a reader uses it to decide whether a file\ncan be opened at all. A file written by a newer release is refused with a message\nthat says to upgrade; a file whose framing is wrong is refused with a message that\nsays the bytes are not what they claim to be.\n\n`describeArtifact(blob)` is the cheap way to find out which of the four formats a\nfile is, without a key.\n\n## Command line\n\n```\nlogview-archive version\nlogview-archive capabilities\nlogview-archive keygen [--size bytes] [--out file]\nlogview-archive encrypt <in|-> [--out file] [--key hex | --passphrase text] [--profile name]\nlogview-archive decrypt <in|-> [--out file] [--key hex | --passphrase text]\nlogview-archive inspect <in|->\nlogview-archive digest <in|-> [--algorithm name]\n```\n\nIt exists so the primitives can be exercised without writing a script, and so a\nrelease can be checked by hand.\n\n## Tests\n\n```\nnpm test\n```\n\nEach suite is also runnable on its own: `node test/text.test.js`. The suites use\nno test framework; the runner only keeps the exit status meaningful across\nseveral.\n\n## License\n\nUNLICENSED — internal platform tooling. Use is limited to the platform that\npublishes it.","readmeFilename":"README.md"}