# 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`: `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`. **Verified by:** `src/error-codes.spec-conformance.test.ts` (`PACKER-17`).