
Security News
Lovable’s OJ Rewrites Vite’s Dev Server in Rust as AI Lowers the Cost of Forking Open Source
Lovable’s OJ rewrites Vite’s dev server in Rust, reducing memory use and preview times as AI lowers the cost of open source reimplementation.
Zero-dependency native ZIP engine: random-access reads without extracting, secure-by-default extraction (zip-slip, zip-bomb and header-ambiguity guards), true streaming, deterministic reproducible archives, and incremental modification without recompressi
A safe, deterministic, streaming ZIP engine for modern apps — and for the agents that operate them.
Zero runtime dependencies. 100% TypeScript. One API across Node.js ≥ 22, browsers, Deno, Bun and Workers. Built for the archives that actually matter in 2026 — OOXML, EPUB, JAR/VSIX, and multi-gigabyte data drops that must never be buffered whole — under the same engineering doctrine as pdfnative.
Status: 1.0 — stable. The public API surface, the 39-code error vocabulary and the
deterministic: trueoutput bytes are frozen under semantic versioning — removals and byte changes are semver-major (the full promise is in SECURITY.md). Built up through read (v0.1), deterministic write (v0.2), incremental modification (v0.4), workers + forward streaming (v0.5), the resumable inflater (v0.6), the interop gate (v0.7), the frozen error codes (v0.8) and one-call verification (v0.9). Documentation: zipnative.dev (site sources in docs/, interactive playgrounds included).
Most ZIP libraries make you choose between speed, safety and capability. zipnative's positioning is different:
save() without recompressing the untouched 99% of the archive — the append-only overlay model proven in pdfnative's PDF incremental updates. saveCompact() is the true-deletion path (removed content is otherwise still recoverable — documented loudly).err.code from a frozen 39-code vocabulary (v0.8+ — registry in docs/data/errors.json, guide in docs/guides/errors.md): branch on the code, never on message text. Plus verifyZip() — one call, a machine-readable verification report that never throws for archive problems (v0.9), remedy-bearing messages, a structured diagnostics channel, executable recipes, four documented production use cases, llms.txt, and a human-in-the-loop AI governance policy.| zipnative | fflate | jszip | yauzl/yazl | adm-zip | |
|---|---|---|---|---|---|
| Zero runtime dependencies | ✅ | ✅ | ❌ | ❌ | ❌ |
| Random access (1 entry without full parse) | ✅ | ❌ | ❌ | yauzl ✅ | ❌ |
| Streaming read + write | ✅ | partial (low-level) | ❌ (memory-bound) | read or write per lib | ❌ |
| Safe-extract defaults (slip/bomb/ambiguity) | ✅ | DIY | DIY | DIY | historical CVEs |
| Deterministic output (documented contract) | ✅ | DIY | ❌ | ❌ | ❌ |
| Modify in place, no recompression | ✅ | ❌ | rewrite-all | ❌ | partial |
| Browser + Node + Deno + Bun + Workers | ✅ | ✅ | ✅ | Node-only | Node-only |
| Raw deflate throughput | good (platform zlib) | best | slow | good | poor |
fflate keeps the raw-deflate-speed crown and we do not chase it: zipnative uses the platform's native codecs (node:zlib, CompressionStream) behind a pluggable seam, and wins on scenarios — random access, bounded-memory streaming, in-place updates — not drag races.
npm install zipnative
import { openZip, extractZip } from 'zipnative';
// Open an archive — lazy: only the central directory is located, nothing decompressed.
const zip = openZip(bytes);
console.log(zip.entryCount);
for (const entry of zip.entries()) {
console.log(entry.name, entry.uncompressedSize);
}
// Random access: decompress exactly one entry, CRC-verified.
const manifest = zip.readEntry('manifest.json');
// Stream a large entry with bounded memory.
for await (const chunk of zip.readEntryStream('video.mp4')) {
// ...
}
// Secure extraction (in memory — filesystem sinks belong to zipnative-cli).
const files = extractZip(bytes, {
limits: { maxEntries: 10_000, maxTotalUncompressedSize: 1024 * 1024 * 1024 },
// rejectTraversal: true and rejectSymlinks: true are the DEFAULTS.
});
Creating archives (v0.2):
import { createZip } from 'zipnative';
const zip = createZip({
// Pin the pure-TS encoder: identical inputs → identical SHA-256,
// on every runtime. See docs/guides/determinism.md.
compression: { deterministic: true },
});
zip.add('manifest.json', JSON.stringify(manifest));
zip.add('assets/logo.png', logoBytes, { compression: { method: 'store' } });
zip.addDirectory('assets');
const bytes = zip.toBytes(); // sync, buffered
// — or, with bounded memory (serverless/Workers), byte-identical output:
for await (const chunk of zip.stream({ chunkSize: 64 * 1024 })) {
// send chunk...
}
// Large content from an async source (data-descriptor layout):
zip.addStream('video.bin', chunkSource);
Modifying an existing archive (v0.4):
import { createZipModifier, openZip } from 'zipnative';
const modifier = createZipModifier(openZip(bytes));
modifier.replaceEntry('word/document.xml', newDocumentXml);
modifier.addEntry('docProps/custom.xml', customProps);
modifier.removeEntry('word/obsolete.xml');
// Append-only: untouched entries are never recompressed; the original
// bytes are preserved verbatim (removed content stays recoverable!).
const updated = modifier.save();
// True deletion + compact canonical layout, still no recompression:
const compacted = modifier.saveCompact();
Parallel creation across worker threads (v0.5, zipnative/worker):
import { createParallelZip } from 'zipnative/worker';
const zip = createParallelZip(); // pool sized from your cores, capped at 8
zip.add('a.bin', bigBufferA); // entries deflate concurrently
zip.add('b.bin', bigBufferB);
const bytes = await zip.toBytes(); // async — the one signature difference
// Byte-identical to createZip() for the same inputs (per compression
// tier; unconditional with compression: { deterministic: true }).
// Worker failures degrade gracefully — the archive never fails for
// infrastructure reasons.
Reading an unseekable stream (v0.5 — pipes, uploads, serverless bodies; a web ReadableStream<Uint8Array> or any AsyncIterable<Uint8Array> is accepted since v0.9):
import { iterateZipEntries } from 'zipnative';
for await (const entry of iterateZipEntries(request.body)) {
console.log(entry.header.name, entry.header.uncompressedSize);
if (wanted(entry.header.name)) {
for await (const chunk of entry.data()) { /* bounded memory */ }
} else if (entry.header.compressedSize > 0) {
await entry.skip();
}
}
// TRUST CAVEAT: forward iteration reads local headers alone — no central
// directory cross-check. Use openZip() whenever the full archive is
// available; it is the authoritative path.
Verifying an archive in one call (v0.9):
import { verifyZip } from 'zipnative';
const report = verifyZip(bytes); // never throws for archive problems
if (!report.ok) {
// Structural refusal (report.error.code) or a failed entry —
// machine-readable either way, built on the frozen err.code vocabulary.
console.log(report.error?.code, report.entries.filter((e) => !e.crcMatch));
}
// Encrypted / stream-only-codec entries are reported as skipped with a
// reason — never faked as corruption.
Bundler notes for zipnative/worker: the worker script is resolved as new URL('./zip-worker.js', import.meta.url), which Vite and webpack 5 detect and bundle automatically. If your bundler cannot (or your CSP restricts worker sources), pass workerUrl explicitly — e.g. createParallelZip({ workerUrl: new URL('zip-worker.js', yourAssetBase) }) — pointing at a copy of the script served from your origin (locate it with import.meta.resolve('zipnative/worker/zip-worker.js') — a dedicated subpath export since 0.8). On runtimes without workers the same code runs entirely on the calling thread.
Everything public is exported from the two entry points — zipnative and zipnative/worker; if it is not exported there, it is private.
zipnative treats every archive as untrusted input. The guards, their defaults and their CWE mappings are documented in SECURITY.md. Highlights:
.. segments, absolute paths, drive letters, backslashes, NUL bytes, NTFS alternate data streams (CWE-22);ZIP has no veraPDF — JHOVE never shipped a ZIP module, and no ISO/IEC 21320-1 validator existed. So zipnative ships both halves of the answer: the first open clause-by-clause ISO/IEC 21320-1:2015 conformance validator (npm run validate:zip — an independent raw parser, never the engine's own, checking the ISO-standardised ZIP profile the Library of Congress recognises), and an Archivematica-grade differential extraction matrix (npm run test:interop — six independent parsers extract and byte-compare zipnative's archives on Linux and Windows). Both gates are blocking in CI and re-run before every npm publish. Every archive zipnative writes conforms to the ISO profile; the full story — including why spec-valid ≠ safe — is in the conformance guide.
iterateZipEntries reads data-descriptor entries (flag bit 3) for plain deflate since v0.6 — including zipnative's own addStream() output and bsdtar-style archives. Still refused: store+bit3 (not self-delimiting), encrypted+bit3, and custom-codec+bit3; skip() on a bit-3 entry costs a full decompress-and-discard.setDeflateImpl, registerCodec) on the main entry does not propagate to the zipnative/worker bundle (separate module state); parallel/sequential byte-identity is promised for the built-in tiers.save() keeps every original byte: removed/replaced content remains recoverable in the output (use saveCompact() for true deletion); saveCompact() drops SFX prefixes; archives with duplicate entry names cannot be modified incrementally.addStream entries beyond 4 GiB are rejected with a typed error — buffer via add(); the per-entry Zip64-streaming opt-in is designed (0.9 decision record in ROADMAP.md) and lands post-1.0. Buffered entries, entry counts and archive offsets are fully Zip64.CON, NUL, COM1…LPT9 — CWE-67) or that collapse to nothing (., ./). Archives authored on POSIX systems containing files like aux.h therefore throw by default on every platform; pass rejectTraversal: false to skip such entries instead.CompressionStream on the runtime (or when deterministic: true is requested), stream-entry compression buffers the entry before compressing — a documented memory caveat.Number.MAX_SAFE_INTEGER (≈ 9 PB) are rejected; the public API uses number, not bigint.compression: { deterministic: true } for cross-runtime identity — the full contract lives in docs/guides/determinism.md.entry.isEncrypted) and reads fail with a typed ZipUnsupportedError.| Package | Purpose | Status |
|---|---|---|
zipnative | core engine (this repo) | active |
zipnative-cli | command-line tool, agent-grade JSON contract | planned |
zipnative-mcp | MCP server for AI agents | planned |
The core stays dependency-free by exiling every dependency-bearing integration to a satellite repo — the pdfnative ecosystem pattern.
npm ci
npm run typecheck:all # src + tests + scripts
npm run lint
npm run test:coverage
npm run build
npm run test:interop # validate generated archives with unzip/7z/Expand-Archive/jar
Conventions live in AGENTS.md and .github/instructions/. Contributions welcome — see CONTRIBUTING.md.
zipnative is the second library in the native family, applying the architecture proven by pdfnative: zero dependencies, closure factories instead of classes, append-only incremental modification, a shared segment generator guaranteeing buffered and streaming output are byte-identical, determinism as a product feature, and CWE-tagged bounds on every untrusted-input loop.
FAQs
Zero-dependency native ZIP engine: random-access reads without extracting, secure-by-default extraction (zip-slip, zip-bomb and header-ambiguity guards), true streaming, deterministic reproducible archives, and incremental modification without recompressi
The npm package zipnative receives a total of 34 weekly downloads. As such, zipnative popularity was classified as not popular.
We found that zipnative 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
Lovable’s OJ rewrites Vite’s dev server in Rust, reducing memory use and preview times as AI lowers the cost of open source reimplementation.

Security News
It has been one year since Shai-Hulud made its first appearance on npm.

Research
/Security News
Operators behind PolinRider used a compromised GitHub account to plant malware in four development versions of a Packagist package with 700,000+ downloads.