Skip to content

Releases: Nizoka/zipnative

v1.1.0 — range access, reproducible-for-any-archive, issues #9–#12, supply-chain parity

Choose a tag to compare

@Nizoka Nizoka released this 08 Oct 20:55
1e2ab42

zipnative v1.1.0

Released 2026-10-08

The first minor since the freeze, and a statement about what a minor is: zero breaking changes. The public surface grew from 77 to 106 exports with none removed, the 39 error codes are unchanged, every new behaviour is opt-in, and the deterministic: true bytes are unchanged — proven by a byte-level baseline of the 33 pre-existing sample archives and by the previous release's own test suite running against the new sources in CI. Existing inputs produce byte-identical output, with one deliberate exception listed in the Upgrade section: dates outside the DOS range, whose previous output was wrong. The four issues reported by the satellites are closed at the source; the Zip64 streaming opt-in designed in 0.9 ships; five adoption features arrive — random access over remote and huge archives, cancellation and progress, raw transplant, canonicalisation of any archive, legacy name decoding — and the repository reaches hardening parity with pdfnative 1.8.0.

Highlights

  • Open a 50 GB archive with the memory of its central directory — openZipRange() reads an archive served by ranges (HTTP Range, Blob.slice(), a file handle, object storage) through a ByteRangeSource you inject; the engine never fetches, every limit and cross-check of openZip() applies, and a counting source shows one tail window, the central directory, then only the entry you read. Guide · playground.
  • Make any archive reproducible — analyzeDeterminism() names what would drift (timestamps, order, extra fields…); canonicalizeZip() and saveCompact({ canonical: true }) rewrite a Gradle, Maven, zip or Explorer archive into the canonical form without recompressing a payload, idempotently, under the frozen byte contract (golden SHA-256 hashes over two committed foreign fixtures); addRaw() / addFromReader() assemble artefacts from existing parts in O(bytes copied). Guide.
  • Issues #9–#12 closed at the source — UTC DOS timestamps and honest clamping (#9), one error classification for corrupt deflate on every tier (#10), custom codecs streamed by the forward reader through a new createDecompressor() contract (#11), verifyEntry().skipped with a real localHeaderMatch (#12). The satellites' workarounds become unnecessary.
  • Zip64 streaming opt-in — addStream(name, source, { zip64: true }) for entries that may exceed 4 GiB: the APPNOTE 4.3.9.2 layout, validated by the ISO/IEC 21320-1 gate and extracted byte-for-byte by the six-tool interop matrix; a real 4 GiB + 1 stream is exercised in the conformance workflow.
  • Supply-chain parity with pdfnative 1.8.0 — scripts/gate.ts profiles, harden-runner and --ignore-scripts on every job, OIDC publishing behind the npm-publish environment with SLSA Build L2 provenance and SBOM attestation, dependency review, weekly audit, committed rulesets, a byte baseline of every sample, 25 new verify-docs rules, release-prepare, and a compat-previous job that runs the 1.0.0 suite against 1.1.0.

Security

  • fix(security): verifyZip() / verifyEntry() no longer fabricate localHeaderMatch for encrypted entries (it was hard-coded true in one path and false in the other); the local header is cross-checked even when the payload cannot be verified, and an encrypted entry whose local header contradicts the central directory now fails the archive (CWE-1288, issue #12). Verdicts never loosened: unknown methods still fail the archive.
  • fix(security): corrupt deflate payloads surface as ZipFormatError ZIP_DEFLATE_CORRUPT / ZIP_DEFLATE_TRUNCATED on every inflate tier — node:zlib, DecompressionStream, pure TS — instead of a runtime-dependent class (CWE-703, issue #10); ZIP_DECOMPRESSION_FAILED is reserved for registered or injected codecs.
  • feat(security): encrypted entries are labelled by scheme — feature: 'aes' for WinZip AES (method 99 or the 0x9901 extra), 'zipcrypto', 'strong-encryption' — and still never decrypted in 1.x (CWE-327 context; AES is a 2.0 candidate).
  • feat(security): one new named bound, maxEntryCompressedSize (1 GiB + 1 MiB, CWE-770), bounds the byte-range reader's whole-payload fetches; the in-memory reader uses zero-copy views and never consults it, so no 1.0.0 archive is newly refused. A nameDecoder runs before path sanitisation, never around it (CWE-22); a lying byte-range source is ZIP_RECORD_TRUNCATED (CWE-20).
  • feat(ci): every workflow starts with step-security/harden-runner and installs with npm ci --ignore-scripts (.npmrc makes that the local default); actions stay SHA-pinned; dependency-review.yml (high severity, licence allow-list) and a weekly audit.yml; publish.yml is three jobs with one artifact between them: verify (no publishing token; fetches the previous tag, runs the full publish gate, packs the tarball once with its SBOM and SHA256SUMS) → publish (the npm-publish environment, id-token: write, a pinned npm ≥ 11.5.1, a sparse checkout of .nvmrc only, the handed-on tarball digest-checked then published through OIDC with provenance) → attest (the same tarball compared byte for byte with what the registry serves, then SLSA Build L2 provenance for it and the CycloneDX SBOM through actions/attest-build-provenance, attached to the GitHub Release); no release job restores a dependency cache; a workflow_dispatch dry run stops at npm publish --dry-run; branch and tag rulesets committed under .github/rulesets/.
  • fix(deps): npm audit --audit-level=high is green again — adm-zip 0.6.1, brace-expansion 1.1.21 / 5.0.12 and source-map-js 1.2.2 (dev dependencies only; the engine has none).

Added

  • feat(parser): openZipRange(source, options?), ByteRangeSource, ZipRangeReader, rangeSourceFromBytes() — random access over an injected byte-range source; shared per-entry defences with openZip() (src/parser/zip-entry-checks.ts); 256 KiB ranged streaming through createDecompressor(); the cached tail window serves the central directory and small archives without a round trip, and local headers are prefetched with 1 KiB of slack.
  • feat(core,parser): signal?: AbortSignal and onProgress?: ZipProgressHandler on every asynchronous call — on the options of extractZipStream, iterateZipEntries, openZip / openZipRange (every read of the reader), createZip / createParallelZip (every stream() and toBytes()), and per call on readEntryStream(entry, options) and stream(options); abort rejects with signal.reason after resources are released (stream locks released without cancel, worker pools closed); ZipProgress is monotonic, a skipped entry counts as done, the parallel toBytes() reports per settled entry.
  • feat(core): ZipWriter.addRaw(name, payload, meta) and addFromReader(reader, entry, options?) — transplant without recompression; addFromReader verifies the source entry first unless verify: false; RawEntryMeta, RawEntryReader, AddFromReaderOptions; the modifier's raw-copy plan moved to core/zip-segments.ts so both writers share one implementation.
  • feat(parser): canonicalizeZip(bytes, options?), CanonicalizeOptions; ZipModifier.saveCompact({ canonical }) with CanonicalOptions / CompactOptions; analyzeDeterminism(bytes) with DeterminismReport, DeterminismOffender, DeterminismConcern — the verdict the satellites re-derived.
  • feat(core,parser): nameDecoder option on every read path (applied when flag bit 11 is clear, and as the fallback when a UTF-8-flagged name is not valid UTF-8; entry.nameEncoding becomes 'custom'); getExtendedTimestamps(entry) (UT 0x5455, NTFS 0x000a, DOS fallback — ExtendedTimestamps) and getUnixIds(entry) (ux 0x7875 — UnixIds); externalAttributesFromUnixMode(mode, options?), the write-side mirror of getUnixMode.
  • feat(core): dosTimeMode: 'local' | 'utc' (DosTimeMode) on every option bag that writes or reads lastModified; full DOS-date clamping with the new ZIP_TIMESTAMP_CLAMPED diagnostic (the twelfth; issue #9).
  • feat(codecs): ZipCodec.createDecompressor?(maxOutput) → ZipDecompressor { push, end } — the incremental contract the forward reader streams in O(chunk); the built-in store and deflate codecs implement it (issue #11).
  • feat(core): AddEntryOptions.zip64 — the per-entry Zip64 streaming opt-in for addStream(): version 45 in both records, sentinel sizes and a (0, 0) Zip64 extra in the local header (lfhZip64Fields moved to core/zip-extra-fields.ts), 24-byte data descriptor, both sizes in the central record; assertStreamSizesInRange names the option in its refusal.
  • feat(parser): EntryVerification.skipped?: EntrySkipReason ('encrypted' | 'stream-only-codec' | 'unsupported-method'); verifyZip() delegates every entry to verifyEntry() (issue #12).
  • feat(samples): five samples — zip64/zip64-streamed, incremental/merged-from-two, names-encoding/shift-jis-legacy (an expected ISO non-conformant: non-ASCII names without bit 11), attributes/unix-ids, attributes/ntfs-timestamps — 38 in the corpus, 33/5 in the ISO canary; two interop write cases (zip64-streamed, transplanted) — 19 validation cases (6 producer reads + 13 write cases, each run against every extractor present).
  • feat(tooling): scripts/gate.ts (--fast / --ci / --publish, --require-all, --only, --from, --json, dist probes), scripts/verify-samples.ts + tests/regression/samples.test.ts + tests/regression/baselines/samples.sha256.json (SHA-256 of every sample with a since chain), scripts/release-prepare.ts (the bump in one pass), scripts/run-compat.ts (compat:previous), scripts/build-claude-rules.ts (agents:rules), scripts/install-git-hooks.mjs (opt-in pre-commit / pre-push), `scri...
Read more

v1.0.0 — first stable npm release

Choose a tag to compare

@Nizoka Nizoka released this 02 Sep 22:48

zipnative v1.0.0

Released 2026-09-02

The first stable release — and the first npm publication. zipnative 1.0.0 adds no engine behavior over v0.9.0: it is the freeze itself. Everything shipped across 0.1 → 0.9 — random-access reads, secure-by-default extraction, deterministic write, incremental modification, workers, forward streaming, the resumable inflater, one-call verification — becomes a semver commitment: the 77-export API surface, the 39-code error vocabulary and the deterministic: true output bytes are frozen (removing, renaming or changing any of them is semver-major; the promise is written down in SECURITY.md). 100% backward-compatible with v0.9.0. Zero runtime dependencies, 385+ tests, 93.9% statement coverage, published from CI with provenance.

Highlights

  • On npm at last — published via Trusted Publishing (OIDC): no long-lived token, the entire quality gate re-runs in the publish workflow, the package carries a provenance attestation (verify with npm audit signatures), and a CycloneDX SBOM is attached to the GitHub Release. A laptop npm publish fails by design.
  • The conformance authority gate ("veraZIP") — ZIP had no veraPDF (JHOVE never shipped a ZIP module; zip -T is an alias of unzip -tqq), so zipnative ships the first open clause-by-clause ISO/IEC 21320-1:2015 validator (npm run validate:zip — an independent raw parser, never the engine's own) plus the existing Archivematica-grade six-parser extraction matrix. Every archive zipnative writes conforms to the ISO profile, verified on Linux and Windows in CI and again before every publish; hostile-but-spec-valid archives (zip-slip et al.) are called out for what they are — conformance is not safety. Full story: the new conformance guide.
  • The freeze, made checkable — every one of the 77 exports carries TSDoc (a new verify rule fails the build on an undocumented export); the API registry (api.json), the error registry (errors.json) and the determinism contract are the machine-readable form of the promise.
  • A new face — the "zipper-cut Z" logo: the Z monogram's diagonal carries interlocking zipper teeth in negative space (reads "Z" at a glance, "zip" up close), declined across favicon, og-image and a new GitHub social preview.

Security

  • No parser or writer change — the security posture is exactly v0.9.0's.
  • feat(ci): supply-chain hardening around the release: OIDC Trusted Publishing (no NPM_TOKEN secret to steal), provenance attestations (SLSA-aligned, Sigstore), CycloneDX SBOM per release, SHA-pinned actions, and the full gate (audit included) re-run between the Release click and npm publish.
  • docs(security): SECURITY.md now states the supported-versions policy (1.0.x), how to verify release integrity, and the three-part compatibility promise.

Added

  • feat(conformance): validate:zip — the ISO/IEC 21320-1:2015 validator (the Table-1 structural clauses + APPNOTE well-formedness cross-checks, two-sided expectations, declared.iso21320 coverage canary, foreign integrity pass) — blocking in conformance.yml and publish.yml; new conformance guide.
  • feat(agents): docs/agent-brief.md (paste-ready coding-agent briefing + "Copy as prompt" on the landing) and the completed token budget in llms-index.json (errors.json + agent-brief now listed with byte/approx-token sizes); .github/prompts/quality-gate.prompt.md.
  • feat(ci): .github/workflows/publish.yml — Release-triggered, OIDC, full gate re-run, SBOM, npm publish --provenance --access public.
  • feat(docs): the tsdoc-complete verify rule (22 rules total); TSDoc for the five previously-undocumented exports.
  • feat(docs): the zipper-cut Z identity — logo.svg, favicon.svg, regenerated og-image, new social-preview.svg/png (1280×640).
  • feat(meta): release-notes/draft/PR-v1.0.0.md — the release PR with the measured verification table and the human go-live runbook.

Changed

  • docs(meta): documentation flipped to the stable-era stance — "through 1.0"/"pre-1.0" → "in 1.x" everywhere; README status is the semver freeze statement; satellites read "planned".
  • docs(meta): badges harmonised on the pdfnative set (CI, CodeQL, npm, bundle size, provenance, website; Scorecard on the landing — the npm-fed ones self-heal at first publication); persist-credentials: false on every CI checkout.
  • chore(meta): the npm package now ships CHANGELOG.md, SECURITY.md and THIRD-PARTY-NOTICES.md (21-file tarball, inspected); sourcemaps deliberately included; ecosystem.json records the freeze contracts.

Install

npm install zipnative@1.0.0

Upgrade

No breaking changes. Drop-in replacement for v0.9.0 — and for every 0.x tag: the API at 1.0.0 is the 0.9.0 API, frozen.

Known limitations

  • store+bit3, encrypted+bit3 and custom-codec+bit3 remain refused in forward reading — structurally undelimitable without the central directory.
  • Zip64 streaming (addStream > 4 GiB) remains a typed refusal — designed (roadmap ADR), implemented in a 1.x minor.
  • No encryption in 1.x by policy (ZipCrypto is broken); encrypted entries are detected and refused with a typed error.
  • A worker playground is deliberately absent (build-tooling changes; a 1.x docs item).

Downstream integration notes

The satellites can now exist: zipnative-cli and zipnative-mcp pin zipnative ^1.0.0. Their contract inputs are all machine-readable — docs/assets/api.json (surface), docs/data/errors.json (the --json envelope's err.code source of truth), and verifyZip's report shape for a future verify --json.

Links