7.2 KiB
Enterprise hardening — @suaveplan/packer resilience cluster
Package: @suaveplan/packer
Status: PROPOSED
Type: feature
Design: (inline below — small scope)
Spec delta: ./specs/packer/spec.md
Why
Cluster #1 (enterprise-packer-integrity, archived 2026-05-19) landed BLAKE3
content-addressable digests, the bundle-level Merkle root, mandatory
integrity verification on unpack, full OpenTelemetry instrumentation, and
i18n-keyed error messages. The enterprise/packer.md audit (L43, L46–L47)
calls out three remaining resilience gaps that block the ENTERPRISE label:
- No retry primitives on IO-backed sources (
ReadableStream.read()calls throw straight up the stack on transient errors). - No idempotency layer — repeat packs over identical inputs re-run the full gzip + AEAD + ed25519 pipeline every time.
- The whole-file
pack/unpackpaths do not acceptAbortSignal; only the chunked path honours cancellation today. Date.now()andcrypto.getRandomValues()are called at production code paths insrc/, violating the audit's "noMath.random()/Date.now()at module scope; injectable clock and RNG" line.
This is cluster #2 of a 3-cluster enterprise hardening of @suaveplan/packer
(integrity → resilience → extensibility). Cluster #3 is deliberately out of
scope here — see "Out of scope" below.
What changes
- ADDED:
src/retry/retryable-read.ts—retryRead(reader, opts)helper that wrapsReadableStreamreads in@suaveplan/retry'swithRetry, honoursAbortSignal, and returns the same{ value, done }shape. Defaults:createExponentialBackoff({ initial: 25, base: 2, max: 1000, jitter: "equal" })maxAttempts: 4.
- ADDED:
RetryPolicyoption bundle onPackerOptions,PackChunkedOptions, andUnpackChunkedOptions(retryConfig?,signal?). Threaded through tonormalizeSourceand the abstractcollectpath so everyreader.read()in production code is retry-aware. - ADDED:
src/idempotency/idempotency.ts—PackIdempotencyGuardthat computes a content-derived key (blake3(payload) || compressionLevel || encryptionAlgo || signed-bit) and short-circuits to a cachedPackedDatawhen the same input is packed again. Backed by an injectableIdempotencyStoreAdapter(defaultMemoryIdempotencyStore). - ADDED:
PackerOptions.idempotency?: { store, ttlMs?, enabled? }.Packer.packconsults the guard before any work; a hit returns the cached bundle bytes. Misses populate the store after the pack completes. - ADDED:
signal?: AbortSignalonUnpackOptions.Unpacker.unpacknow checkssignal.abortedat every yield point (before integrity verify, before signature verify, before AEAD decrypt, before gunzip, before checksum verify, before the final emit). - ADDED:
PackOptionsinterface ({ filename?, signal? }) and an additionalPacker.pack(data, opts: PackOptions)overload. The originalPacker.pack(data, filename?)overload remains for back-compat. - ADDED:
signal?: AbortSignalonPackChunkedOptions— already honoured on the unpacker; now enforced on the chunked packer between every emitted chunk and the source pull loop. - ADDED:
ClockandRnginjection onPackerOptions,PackChunkedOptions,UnpackChunkedOptions. Defaults wire to real platform implementations (() => Date.now()forClock, a wrapper aroundcrypto.getRandomValuesforRng). EveryDate.now()/crypto.getRandomValues()/performance.now()call in productionsrc/is replaced with a call through the injectedClock/Rng. - MODIFIED:
AbstractPacker.buildMetadatareadstimestampfrom the resolvedClockinstead ofDate.now(). - MODIFIED:
packer/packer.tsreads nonce bytes via the resolvedRnginstead of callingcrypto.getRandomValuesdirectly. - MODIFIED: every
nowMs()helper acrosspacker/,unpacker/,chunked/packer/,chunked/unpacker/now reads through the resolvedmonotonicClock(defaults toperformance.now()for backward parity).
Out of scope
@suaveplan/schema-registryintegration → cluster #3.@suaveplan/feature-flagsfor format-version rollouts → cluster #3.- Plugin / extension interface → cluster #3.
fast-checkproperty tests for round-trip invariants → cluster #3.- mitata benchmarks → cluster #3.
Source mapping
This change derives from ../suaveplan/enterprise/packer.md.
| Requirement | Source lines | Notes |
|---|---|---|
PACKER-22 (@suaveplan/retry on IO) |
L47 | Verbatim — "retry primitives for IO-backed sources". |
| PACKER-23 (idempotency cache) | L47 | Verbatim — "Idempotency — repeat packs over the same inputs are a no-op". |
| PACKER-24 (whole-file AbortSignal) | L46 | Verbatim — "Backpressure & cancellation — AbortSignal everywhere async". |
PACKER-25 (injected Clock + Rng) |
L43 | Verbatim — "no Math.random() / Date.now() at module scope; injectable clock and RNG". |
Requirements with no source line
None — every requirement in this cluster maps directly to an audit bullet.
Verification approach
src/retry/retryable-read.test.ts+.spec-conformance.test.ts— pinsretryReadagainst an injectable failing reader, asserts the retry policy is consulted, assertsAbortSignalcancels mid-backoff.src/idempotency/idempotency.test.ts+.spec-conformance.test.ts— pins identical-input dedup, cache-miss population, custom-store injection.src/packer/packer.spec-conformance.test.ts(extended) — pinspackabort, idempotency hit,Clock-driven timestamp,Rng-driven nonce.src/unpacker/unpacker.spec-conformance.test.ts(extended) — pins pre-flight abort, mid-pipeline abort.src/chunked/packer/packer.spec-conformance.test.ts(extended) — pins signal-driven mid-stream abort.
Migration impact
Public-surface additions only. No existing signature changes shape — the new
Packer.pack(data, opts: PackOptions) overload sits beside the existing
Packer.pack(data, filename?). The Clock / Rng / idempotency /
retryConfig / signal options are all optional with platform defaults.
Two consumer packages (@suaveplan/drive-plugin-chunks,
@suaveplan/drive-webrtc) are audited and the sweep is part of the gates;
no existing call site changes shape.
Impact
Affected dependents
Run bunx turbo ls --filter=...@suaveplan/packer to enumerate.
@suaveplan/drive-plugin-chunks— [x] tests run / [x] docs reviewed / [ ] no action needed@suaveplan/drive-webrtc— [x] tests run / [x] docs reviewed / [ ] no action needed
If tests run is unchecked for any dependent, the change MUST NOT be folded
into the canonical spec.
Migration
No behavioral change for callers that did not pass the new options. Existing calls keep their previous semantics:
- No
signalpassed: pack / unpack behaviour is identical to cluster #1. - No
idempotencypassed: no cache layer is consulted. - No
retryConfigpassed: a single read is attempted (no retries) for parity with cluster #1 — the default policy ismaxAttempts: 1so the retry layer is a no-op unless opted in. - No
clock/rngpassed: defaults callDate.now()/crypto.getRandomValuesexactly as before, so wire output is identical.