# Capability Spec Delta: bitpacker (restore-crypto-keychain-packer) **Package**: `@suaveplan/bitpacker` **Status**: PROPOSED > Every requirement below carries a `**Verified by:**` line at authoring time. (See `openspec/AGENTS.md` Section 13.) ## ADDED Requirements ### Requirement: `bytes` field type for fixed-length byte arrays The package SHALL support a `"bytes"` `FieldType` in `defineBitSchema` field definitions, alongside the existing `uint | int | bool | fixed`. A `"bytes"` field carries a fixed-length `Uint8Array` payload of `byteLength` bytes (1 ≤ byteLength ≤ 65535). A bytes field is byte-aligned: the writer pads the bit cursor to the next byte boundary before writing the field's bytes, and pads again after writing if the next field is not also bytes-aligned. The reader matches: it consumes the alignment padding before and after. This field type is required by `@suaveplan/crypto` for key material (32-byte keypairs), domain tags (4-byte ASCII), and signatures (64-byte ed25519). Without it, those payloads must be encoded by interleaving raw `BitWriter.writeBytes` calls with `defineBitSchema`-driven structured fields, which loses the typed-schema benefits. #### Scenario: schema with a bytes field round-trips - **WHEN** `defineBitSchema<{ id: number; key: Uint8Array }>([{ name: "id", type: "uint", bits: 16 }, { name: "key", type: "bytes", byteLength: 32 }])` is called and the schema's `pack({ id: 7, key: <32 random bytes> })` is then `unpack()`ed - **THEN** the result is structurally equal to the input — `id === 7` and `key` is a `Uint8Array` with the same 32 bytes #### Scenario: bytes field aligns to byte boundary - **WHEN** a schema places a `bool` field (1 bit) immediately before a `bytes` field - **THEN** the encoder pads 7 bits of zero after the bool so the bytes field starts on a byte boundary; `byteSize` reports the aligned size #### Scenario: byteLength outside `[1, 65535]` is rejected at schema definition - **WHEN** `defineBitSchema([{ name: "x", type: "bytes", byteLength: 0 }])` is called - **THEN** the call throws `BitpackerError` with code `SCHEMA.INVALID_BYTE_LENGTH` and identifies the offending field #### Scenario: pack rejects payloads of the wrong length - **WHEN** the schema declares `byteLength: 32` and `pack({ key: <16 bytes> })` is called - **THEN** the call throws `BitpackerError` with code `PACK.BYTES_LENGTH_MISMATCH`, naming the field, expected length, and actual length #### Scenario: existing schemas using only uint/int/bool/fixed are unchanged - **WHEN** any schema defined before this change is packed and unpacked - **THEN** the byte-for-byte output is identical to the pre-change baseline (KAT-asserted) **Verified by:** `src/bit-schema/bit-schema.test.ts::"bytes field round-trips a Uint8Array"` AND `src/bit-schema/bit-schema.test.ts::"bytes field pads to byte boundary after a bool"` AND `src/bit-schema/bit-schema.test.ts::"byteLength of zero is rejected at schema-definition time"` AND `src/bit-schema/bit-schema.test.ts::"pack rejects payloads of the wrong length"` AND `tests/legacy-schema-kat.test.ts::"existing uint/int/bool/fixed schemas produce byte-identical output"` --- ### Requirement: `BitWriter.byteAlign()` and `BitReader.byteAlign()` are publicly exposed The package SHALL expose explicit byte-alignment methods on both `BitWriter` and `BitReader` for callers that interleave raw `writeBytes`/`readBytes` calls with bit-level operations. `byteAlign()` advances the cursor to the next byte boundary, padding with zero bits on write and skipping bits on read. Calling `byteAlign()` when already aligned is a no-op. The `bytes` schema field type uses these internally; exposing them lets advanced consumers (e.g., custom keychain layouts in `@suaveplan/crypto`) build composite formats without reaching into private state. #### Scenario: byteAlign on an already-aligned writer is a no-op - **WHEN** a writer is at bit position 0 (aligned) and `writer.byteAlign()` is called - **THEN** the bit position is unchanged and `writer.byteSize` is unchanged #### Scenario: byteAlign pads with zero bits - **WHEN** a writer has written 5 bits and `writer.byteAlign()` is called - **THEN** the writer pads 3 zero bits and the bit cursor is at position 8 **Verified by:** `src/bit-writer/bit-writer.test.ts::"byteAlign on aligned writer is no-op"` AND `src/bit-writer/bit-writer.test.ts::"byteAlign pads with zero bits"` AND `src/bit-reader/bit-reader.test.ts::"byteAlign skips trailing bits"`