# `commands/files/packet-io` ## Purpose Shared on-disk format helpers for the file-transport commands. Holds the canonical packet envelope `{ data, signature, checksum, originalSize }` (all hex-encoded scalars in JSON), the canonical packet filename convention (`packet.<6-digit-index>.bin`), the manifest filename (`manifest.json`), and the read/write helpers used by both `prepareFileCommand` and `serveFileCommand`. Lives in its own module so the format is the contract: any tool that agrees on the envelope shape can interop with a server-keys-prepared directory without depending on the full file-command surface. Also keeps `files.ts` and `serve-file.ts` under the 300-line hard limit by lifting out the duplicated serialisation code. ## Features - `packetFilename(index)` — zero-padded 6-digit filename for deterministic sort order. - `bytesToHex` / `hexToBytes` — lowercase hex, strict input validation. - `serializePacket(packed, originalSize)` — JSON-encode a `PackedData` envelope. - `deserializePacket(buffer)` — parse a JSON envelope back into the shape `Unpacker.unpack` expects, with a typed `PACKET_INTEGRITY_FAILED` failure on malformed JSON. - `readManifest(dir)` / `writeManifest(dir, manifest)` — single-file JSON manifest I/O with directory creation. ## Usage: Basic ```ts import { packetFilename, serializePacket, writeManifest, } from "@suaveplan/server-keys/commands/files/packet-io"; const filename = packetFilename(7); // "packet.000007.bin" const text = serializePacket(packed, 1024); await fs.writeFile(`/tmp/prepared/${filename}`, text); await writeManifest("/tmp/prepared", manifest); ``` ## Usage: Advanced Round-trip a packet envelope without touching disk: ```ts import { deserializePacket, serializePacket, } from "@suaveplan/server-keys/commands/files/packet-io"; const text = serializePacket(packed, slice.byteLength); const buf = new TextEncoder().encode(text); const { packed: rehydrated, envelope } = deserializePacket(buf); expect(envelope.checksum).toBe(packed.metadata.checksum); expect(Array.from(rehydrated.data)).toEqual(Array.from(packed.data)); ``` ## API Reference - `MANIFEST_FILENAME = "manifest.json"` - `packetFilename(index: number) => string` - `bytesToHex(bytes: Uint8Array) => string` - `hexToBytes(hex: string) => Uint8Array` (throws on malformed input) - `interface SerializedPacket { data, signature, checksum, originalSize }` - `serializePacket(packed, originalSize) => string` - `deserializePacket(buffer) => { packed, envelope }` - `writeManifest(dir, manifest) => Promise` - `readManifest(dir) => Promise` ## Implementation Notes `readManifest` translates a missing file into `SERVER_KEYS_FILE_NOT_FOUND`; this is the closest match in the registry for "the directory exists but the manifest entry is absent" and aligns with how the keys commands report missing key files. `deserializePacket` raises `PACKET_INTEGRITY_FAILED` on malformed JSON rather than `INVALID_INPUT` because the input is a file the caller already trusted enough to read — the failure is on the storage side, not on a CLI option-bag boundary. The packet envelope deliberately hex-encodes both `data` and `signature` (rather than base64) to match the rest of the package's binary-on-disk convention and to keep the file readable by hand.