Files
2026-05-27 23:29:53 +01:00

6.5 KiB
Raw Permalink Blame History

Packer — Spec Delta (packer-port)

ADDED Requirements

PACKER-01 — Whole-file pack/unpack round-trip

Packer.pack(data, filename?) followed by Unpacker.unpack(packed, key, pubKey?) MUST yield bytes equal to the original data. Optional MIME detection MUST surface in the returned metadata.

Verified by: src/packer/packer.spec-conformance.test.ts (PACKER-01).

PACKER-02 — Whole-file gzip compression

When compressionLevel > 0, pack MUST gzip the payload before AEAD; unpack MUST gunzip after AEAD. Any compression failure MUST throw PackerError(PACKER.COMPRESSION_FAILED).

Verified by: src/packer/packer.spec-conformance.test.ts (PACKER-02).

PACKER-03 — Whole-file signing

When a SignKey is provided to pack, the produced PackedData MUST include an ed25519 signature over the canonical packed bytes. unpack with a corresponding PublicKey MUST verify the signature and throw PackerError(PACKER.VERIFICATION_FAILED) on mismatch.

Verified by: src/packer/packer.spec-conformance.test.ts (PACKER-03).

PACKER-04 — Whole-file event lifecycle

Packer MUST emit typed events via EventBus<PackerEventMap>: pack:start, pack:complete, pack:error, with corresponding unpack:* events on the unpacker. Events MUST fire in deterministic FIFO order on completion.

Verified by: src/packer/packer.spec-conformance.test.ts (PACKER-04).

PACKER-05 — Chunked v1 byte layout

A v1 chunk produced by ChunkedPacker MUST be the byte concatenation metadata(100) ‖ sig(64) ‖ ciphertext(payloadLen), total 164 + payloadLen bytes, big-endian. Field offsets within metadata MUST be exactly: version 0–2, fileId 2–34, chunkId 34–38, totalChunks 38–42, fileSize 42–50, chunkSize 50–54, payloadLen 54–58, flags 58–60, nonce 60–84, aeadTag 84–100. Sig occupies 100–164. Ciphertext starts at 164.

Verified by: src/chunked/codec/chunk.spec-conformance.test.ts (PACKER-05).

PACKER-06 — flags reserved in v1

flags MUST be 0x0000 in v1. parseHeader MUST reject non-zero flags with PackerError(PACKER.FLAGS_RESERVED_NONZERO) before any further parse work.

Verified by: src/chunked/codec/chunk.spec-conformance.test.ts (PACKER-06).

PACKER-07 — payloadLen invariants

For non-final chunks (chunkId < totalChunks - 1), payloadLen MUST equal chunkSize - 164. For the final chunk, payloadLen MUST equal fileSize - chunkId * (chunkSize - 164). Mismatch MUST be rejected with PackerError(PACKER.PAYLOAD_LEN_INVALID) before sig verification.

Verified by: src/chunked/codec/chunk.spec-conformance.test.ts (PACKER-07).

PACKER-08 — Streaming pipeline produces v1 chunks

ChunkedPacker.pack(source, fileKey, signKey, options) MUST stream-process the input via normalizeSource → hash → aead-encrypt-stream → chop → per-chunk sign → emit. The output MUST be an AsyncIterable<{ chunkId, bytes }> where each bytes is a v1-format chunk. The full source MUST NOT be buffered in memory during pack.

Verified by: src/chunked/packer/packer.spec-conformance.test.ts (PACKER-08).

PACKER-09 — Non-final chunks are exactly chunkSize bytes

Every emitted chunk with chunkId < totalChunks - 1 MUST be exactly chunkSize bytes on the wire (default chunkSize = 1 MiB → payloadLen = 1048412). No zero padding is added.

Verified by: src/chunked/packer/packer.spec-conformance.test.ts (PACKER-09).

PACKER-10 — Final chunk is naturally short

The final chunk MUST be 164 + lastPayloadLen bytes, where lastPayloadLen = fileSize - (totalChunks - 1) * (chunkSize - 164). The final chunk MUST NOT be zero-padded to chunkSize.

Verified by: src/chunked/packer/packer.spec-conformance.test.ts (PACKER-10).

PACKER-11 — Round-trip parity

ChunkedUnpacker.unpack(chunks, fileKey, serverPubKey) over the chunks emitted by ChunkedPacker.pack(...) MUST yield a plaintext stream byte-equal to the original input. Order MUST be chunkId ascending; out-of-order delivery is the consumer's responsibility to reorder before passing to unpacker.

Verified by: src/chunked/unpacker/unpacker.spec-conformance.test.ts (PACKER-11).

PACKER-12 — Mutation detection

Any mutation to any signed byte (metadata field or any byte of ciphertext) MUST cause ChunkedUnpacker.unpack to throw PackerError(PACKER.VERIFICATION_FAILED) from the per-chunk sig-verify step before AEAD decryption is attempted.

Verified by: src/chunked/unpacker/unpacker.spec-conformance.test.ts (PACKER-12).

PACKER-13 — Verifier without file key

ChunkVerifier.verify(chunkBytes, serverPubKey) MUST validate the structural invariants and the ed25519 signature without the file decryption key. It MUST return { ok: true, metadata } for valid chunks and { ok: false, code } for invalid chunks. It MUST NOT attempt AEAD decryption.

Verified by: src/chunked/verifier/chunk-verifier.spec-conformance.test.ts (PACKER-13).

PACKER-14 — Verifier rejects forged sig

ChunkVerifier.verify MUST return { ok: false, code: "PACKER.VERIFICATION_FAILED" } for a chunk whose signature does not validate under serverPubKey. Verifier MUST NOT consult the chunks plugin or any persistence layer.

Verified by: src/chunked/verifier/chunk-verifier.spec-conformance.test.ts (PACKER-14).

PACKER-15 — Manifest signature

The manifest produced by buildManifest(...) MUST be ed25519-signed via signManifest(manifest, signKey). verifyManifest(manifest, serverPubKey) MUST return false for any manifest whose signature does not validate; mutation of any signed field MUST cause verification failure.

Verified by: src/chunked/manifest/manifest.spec-conformance.test.ts (PACKER-15).

PACKER-16 — Manifest matches chunk metadata

The manifest produced by a successful ChunkedPacker.pack(...) MUST satisfy: manifest.fileId == chunkMeta[i].fileId for all i, manifest.totalChunks == chunkCount, manifest.chunkSize == options.chunkSize, manifest.fileSize == sum(payloadLen), and manifest.chunks[i].payloadLen == chunkMeta[i].payloadLen for all i.

Verified by: src/chunked/manifest/manifest.spec-conformance.test.ts (PACKER-16).

PACKER-17 — Error registry namespaced and frozen

PACKER_CODES MUST be declared via defineCodes("PACKER", [...]) from @suaveplan/error. Object.isFrozen(PACKER_CODES) MUST be true. Every code value MUST start with PACKER.. PackerError MUST extend SuaveplanError<PackerErrorCode>.

Verified by: src/error-codes.spec-conformance.test.ts (PACKER-17).