This repository has moved.
archive-codecnow lives atpackages/archive-codecin theExaDev/documents.jsmonorepo. This repository is archived and will receive no further commits, releases, issues, or pull requests — file issues and send pull requests against the monorepo instead. The npm package itself is unaffected:archive-codeckeeps publishing from its new home under the same name.
Recursive archive (ZIP-in-ZIP) detection and walking with depth and cumulative decompressed-size guards — zero document-format knowledge, the archive utility package for the documents.js family. Worker-isomorphic: the same code runs under Node and inside a Cloudflare Workers isolate.
Created for documents.js#564: nothing in the ecosystem recursed into a nested archive. Most concretely, OOXML's embedded-object model — a docx/pptx carrying a genuinely separate ZIP blob at word/embeddings/oleObject1.xlsx — had no safe handling anywhere, and no package guarded against recursive-archive inputs at all (byte-codec's 512 MiB per-stream inflate cap does not compose across recursion). A new sibling was chosen over extending byte-codec (whose charter is byte/image primitives, zero container-format knowledge) or doing it inline in documents.js (which would repeat the duplication byte-codec's own extraction was meant to avoid). No family package depends on it yet; documents.js consuming it for OOXML embedded objects is planned follow-up work.
Scope for v1: ZIP containers only — read and write over fflate, recursive walking of ZIP-in-ZIP entries, and archive-format detection (ZIP vs not-ZIP). tar and gzip are explicitly out of scope.
Requires Node.js >=20 and pnpm 11.6.0.
pnpm install
pnpm build # tsdown -> dist/ (ESM + CJS + .d.ts)
pnpm typecheck # tsc -p tsconfig.json && tsc -p tsconfig.node.json (dual tsconfig)
pnpm lint # eslint . --fix --cache --max-warnings 0
pnpm test # vitest run
pnpm test:watch # vitest
pnpm test:workers # vitest run --config vitest.workers.config.ts, inside a real Cloudflare Workers (workerd) isolateTo run a single test file, pass its path to vitest directly, e.g. pnpm exec vitest run src/zip/walk.test.ts.
| Module | Exports |
|---|---|
zip/container |
zipPackage (ordered-entries ZIP write with stored-uncompressed support), unzipPackage, ZipEntry |
zip/detect |
detectArchiveFormat ('zip' | 'unknown'), isZipArchive, ArchiveFormat |
zip/walk |
walkArchive (recursive ZIP-in-ZIP walking), ArchiveWalkEntry, ArchiveWalkLimitError, MAX_WALK_DEPTH, MAX_WALK_TOTAL_BYTES, WalkArchiveOptions |
import { walkArchive } from 'archive-codec';
// Every entry of every nested ZIP, flattened. Throws ArchiveWalkLimitError if
// the walk exceeds the depth cap or the cumulative decompressed-bytes budget.
for (const entry of walkArchive(docxBytes)) {
entry.path; // e.g. 'xl/workbook.xml', the path within its own archive
entry.ancestors; // e.g. ['word/embeddings/oleObject1.xlsx'] -- the nested
// ZIP entries descended through to reach this one
entry.bytes; // decompressed content
}Both guards throw rather than truncate: an input outside the contract must fail loudly, never return a partial listing that looks complete. The defaults are MAX_WALK_DEPTH (8 — real producers bottom out around depth 3; the motivating OOXML embedded-object case is depth 2) and MAX_WALK_TOTAL_BYTES (512 MiB cumulative across every nesting level — the same figure byte-codec grants a single stream, re-purposed as one shared budget so a bomb's multiplicative nesting leverage becomes bounded addition). Both are overridable per call via walkArchive(bytes, { maxDepth, maxTotalBytes }), and each constant's derivation is stated in its source comment.
zipPackage takes an ordered array of [path, entry] tuples, not a Record, so the caller controls the exact emission order deterministically (the property formats with a fixed-offset first entry — ODF's mimetype — depend on), and any entry can be written stored-uncompressed via stored: true. unzipPackage is the read side; the returned Record makes no ordering promise and collapses duplicate paths.
- Worker-isomorphic (see the family-wide convention): runtime
src/must not importnode:*, a bare Node builtin, or use theBufferglobal — enforced by ano-restricted-imports/no-restricted-globalsESLint rule and exercised in CI by running the test suite inside an actualworkerdisolate (pnpm test:workers). Test files undersrc/**/*.test.tsandsrc/test-support/are exempt and may use Node APIs for fixtures. - Only
src/index.tsmay be namedindex.*— a custom ESLint rule (local/no-non-barrel-index) rejects any other module using anindexbasename, since that would be a hidden entry point theexportsmap inpackage.jsondoesn't advertise. - Zero document-format knowledge: this package knows bytes and ZIP structure, never that any entry is a document. It depends only on
fflate— not onbyte-codec,ooxml.js, orodf.js(whose ZIP wrappers it deliberately mirrors rather than imports, keeping their branding and release cadences decoupled). - Releases are fully automated: a push to
mainrunssemantic-releasein CI, which determines the version from Conventional Commit messages and publishes to npm via OIDC trusted publishing (no localNPM_TOKENneeded). There is no manual publish step.
pnpm add archive-codec
# or
npm install archive-codecMIT