# zipnative > Zero-dependency native ZIP engine in pure TypeScript: random-access reads, > secure-by-default extraction, streaming, deterministic archives (v0.2+) and > incremental modification (v0.4+). Node ≥22, browsers, Deno, Bun, Workers. Verified on: 2026-09-02 Machine-readable artefacts: docs/agent-brief.md (a compact briefing for coding agents — paste it into your context), docs/assets/api.json (the full export surface — mechanically extracted, never guessed), docs/data/errors.json (the frozen error-code and diagnostic registry — code, class, cause, remedy), docs/llms-full.txt (concatenated documentation), docs/llms-index.json (artefact/guide index with byte and approximate token sizes, so you can decide what to fetch before spending the tokens). Interactive playgrounds (toolkit, inspector, secure-extraction demo, incremental modify, forward streaming) run the real engine in the browser at docs/playgrounds/ — the published package from a version-pinned CDN, falling back to a committed copy of the library's own bundle. ## Essentials - Single entry point: `import { openZip, extractZip } from 'zipnative'` — everything public is exported there; nothing else is API. - No classes: factories return interfaces (`openZip(bytes) → ZipReader`). - Every error message starts `zipnative: ` and contains the remedy, and every thrown error carries a STABLE machine-readable `err.code` (v0.8+, frozen vocabulary — e.g. `ZIP_EOCD_NOT_FOUND`, `ZIP_PATH_TRAVERSAL`, `ZIP_LIMIT_EXCEEDED`): branch on the code, never on message text. Registry: docs/data/errors.json; guide: docs/guides/errors.md. - Extraction is safe by default: traversal, symlinks, duplicates and bombs are rejected unless explicitly configured otherwise via `limits` / `rejectTraversal` / `rejectSymlinks` / `onDuplicate`. - Encrypted entries: detected (`entry.isEncrypted`), reads throw `ZipUnsupportedError`. No encryption support in 1.x. - The engine never touches the filesystem — `extractZip` returns `{ path, data, entry }[]`; writing to disk is the caller's (or the future CLI's) job. `sanitizeEntryPath()` is exported for external sinks. ## Core writing API (v0.2) - `createZip(options?)` → `ZipWriter` with `add(name, data, opts?)`, `addDirectory(name)`, `addStream(name, source)` (a `ByteSource` — any `AsyncIterable` or a Web `ReadableStream` since 0.9; data-descriptor layout; ≤ 4 GiB), `setComment`, `toBytes()` (sync) and `stream({ chunkSize? })` (async, byte-identical to toBytes). - Reproducible archives: `createZip({ compression: { deterministic: true } })` → identical inputs give identical SHA-256 on every runtime (contract: docs/guides/determinism.md). Default timestamps are the DOS epoch — already reproducible; `defaultDate: 'now'` opts into wall-clock time. - The writer never emits traversal-capable names (validated at `add()`), entries are sorted canonically by raw name bytes (`order: 'insertion'` preserves call order — EPUB/JAR mimetype case), and Zip64 records appear exactly when a field overflows. ## Parallel creation (v0.5, subpath zipnative/worker) - `createParallelZip(options?)` — same surface as createZip but `toBytes(): Promise`; per-entry deflate runs across a worker pool (Node worker_threads / Web Workers). Byte-identical to createZip per compression tier; unconditional with `compression: { deterministic: true }`. Worker failures degrade gracefully — archives never fail for infra reasons. `workers: 0` forces main-thread mode. ## Forward streaming read (v0.5) - `iterateZipEntries(source)` — read a ZIP off an UNSEEKABLE stream (`ByteSource`: an `AsyncIterable` or, since 0.9, a Web `ReadableStream` — a `fetch` body, `File.stream()`, …): yields `{ header, data(), skip() }` per local entry; consume or skip each before advancing. Bounded memory; CRC verified; stops cleanly at the central directory. - TRUST CAVEAT for agents: forward iteration reads local headers ALONE (no central-directory cross-check) — prefer openZip() whenever the complete archive is available; it is the authoritative path. - Data-descriptor entries (flag bit 3) are readable since 0.6 for plain deflate — zipnative's own addStream() output and bsdtar-style archives included (the resumable pure-TS inflater delimits the stream and the trailing descriptor is validated against the measured CRC/sizes). Still refused: store+bit3 (not self-delimiting), encrypted+bit3, custom codecs+bit3. skip() on a bit-3 entry costs a full decompress-and-discard. ## Incremental modification API (v0.4) - `createZipModifier(openZip(bytes))` → `ZipModifier` with `addEntry`/`replaceEntry`/`removeEntry`/`renameEntry`/`setComment` and two save paths: `save()` (append-only — untouched entries never recompressed, original bytes verbatim, no-op returns the same reference) and `saveCompact()` (canonical rewrite, still no recompression, removed content truly deleted). - IMPORTANT for agents: `removeEntry` + `save()` does NOT erase content — removed/replaced payloads remain recoverable in the output (data remanence). Use `saveCompact()` when deletion matters. - Interop caveat (measured 2026-09-03): 7-Zip's CLI mis-reads append-only `save()` output — it extracts the stale replaced payload and misses appended entries (it does not honour the final central directory; unzip/bsdtar/python/jar/Expand-Archive all do). When the output must interoperate with 7-Zip, ship `saveCompact()` instead. - Renames never overwrite implicitly; duplicate-name archives are refused with a typed error; encrypted entries can be kept/renamed/copied but never read. ## Core reading API (v0.1) - `openZip(bytes, options?)` → `ZipReader` with `entries()`, `getEntry(name)`, `readEntry(entryOrName)`, `readEntryStream(entryOrName)`, `readEntryRaw(entryOrName)`, `verifyEntry(entryOrName)`. - `extractZip(bytes, options?)` → `ExtractedEntry[]` (in memory). - Options embed `ZipCommonOptions`: `strict` (throw on first diagnostic), `onDiagnostic` (handler), `limits` (Partial). - Entry-attribute helpers (v0.9): `isSymlinkEntry(entry)` and `getUnixMode(entry)` (`number | null` — null when the entry was not authored on a Unix host; 0 is a real mode, never a "no data" marker). ## One-call verification (v0.9) - `verifyZip(bytes, options?)` → `ZipVerificationReport` — the agent-facing archive inventory: `{ ok, error, entryCount, entries, diagnostics }`. NEVER throws for archive problems: a structural refusal lands in `report.error` (`{ code, message }` — the same frozen err.code vocabulary), per-entry results carry `crcMatch`/`sizeMatch`/`localHeaderMatch`, and encrypted or stream-only-codec entries are reported as `skipped` with a reason — never faked as corruption. Only caller bugs (invalid limits) throw. ## Conformance (v1.0) - Every archive zipnative writes conforms to ISO/IEC 21320-1:2015 (the ISO-standardised ZIP profile) — validated clause by clause by `npm run validate:zip`, the first open ISO 21320-1 validator (raw parser, independent of the engine), blocking in CI and before every publish, plus a six-parser differential extraction matrix (`npm run test:interop`). Guide: docs/guides/conformance.md. Note: spec-valid ≠ safe — zip-slip is ISO-conformant, which is why the extraction guards exist on top. ## Docs - README.md: positioning, quick start, comparison, limitations - SECURITY.md: threat model and every bound with CWE tags - ROADMAP.md: 0.1 read → 0.2 deterministic write → 0.4 incremental modify → 0.5 workers → 0.9 RC → 1.0 (frozen API, npm + provenance) → satellites - AGENTS.md: conventions and architecture for contributors and agents