32 KiB
Capability Spec: cbor
Package: @suaveplan/cbor
Status: ACTIVE
Every requirement below carries a
**Verified by:**line at authoring time. (Seeopenspec/AGENTS.mdSection 13.)
Requirements
Requirement: RFC 8949 encoder for major types 0–7 with preferred-serialization shortest-form integers
The package SHALL export an Encoder class and an encode(value, options?): Uint8Array function that produce RFC 8949 §3.1 byte sequences for every major type: 0 unsigned integer, 1 negative integer, 2 byte string, 3 text string, 4 array, 5 map, 6 tagged item, 7 simple/float. Integers SHALL use the preferred-serialization shortest-form rule (RFC 8949 §4.2.1) — the smallest of 1 / 2 / 3 / 5 / 9 byte representations that fits the value, regardless of the input numeric type. Floats SHALL encode as float64 by default; float32 SHALL be emitted only when the value round-trips through float32 losslessly; float16 SHALL be emitted only when EncoderOptions.preferFloat16: true AND the value round-trips through binary16 losslessly.
Scenario: encodes unsigned integer 0 to single byte 0x00
- WHEN
encode(0)is called - THEN the result equals
Uint8Array([0x00])
Scenario: encodes unsigned integer 23 to single byte 0x17
- WHEN
encode(23)is called - THEN the result equals
Uint8Array([0x17])
Scenario: encodes unsigned integer 24 to two bytes 0x18 0x18
- WHEN
encode(24)is called - THEN the result equals
Uint8Array([0x18, 0x18])
Scenario: encodes negative integer -1 to single byte 0x20
- WHEN
encode(-1)is called - THEN the result equals
Uint8Array([0x20])
Scenario: encodes empty array to single byte 0x80
- WHEN
encode([])is called - THEN the result equals
Uint8Array([0x80])
Scenario: encodes empty map to single byte 0xa0
- WHEN
encode(new Map())is called - THEN the result equals
Uint8Array([0xa0])
Scenario: integer 1000000 encodes to 5-byte form not 9-byte
- WHEN
encode(1_000_000)is called - THEN the result is 5 bytes long with leading byte
0x1a
Verified by: packages/flow/cbor/src/encoder/encoder.test.ts::"encodes unsigned integer 0" AND packages/flow/cbor/src/encoder/encoder.test.ts::"encodes unsigned integer 23 in single byte" AND packages/flow/cbor/src/encoder/encoder.test.ts::"encodes unsigned integer 24 in two bytes" AND packages/flow/cbor/src/encoder/encoder.test.ts::"encodes negative integer -1" AND packages/flow/cbor/src/encoder/encoder.test.ts::"encodes empty array" AND packages/flow/cbor/src/encoder/encoder.test.ts::"encodes empty map" AND packages/flow/cbor/src/encoder/encoder.test.ts::"encodes 1000000 in shortest 5-byte form" AND packages/flow/cbor/src/encoder/encoder.spec-conformance.test.ts::"RFC 8949 Appendix A vectors round-trip"
Requirement: Indefinite-length encoding (opt-in)
The package SHALL emit indefinite-length arrays, maps, byte strings, and text strings (RFC 8949 §3.2) only when the caller passes EncoderOptions.indefiniteLength: true. With the option off (default), the encoder SHALL emit definite-length forms for every container. Under the deterministic profile (deterministic: true), indefinite-length encoding SHALL be rejected: callers passing both indefiniteLength: true and deterministic: true SHALL receive a CborError(CBOR.INDEFINITE_NOT_ALLOWED) at the boundary.
Scenario: indefinite-length emission opt-in
- WHEN
encode([1, 2, 3], { indefiniteLength: true })is called - THEN the first byte equals
0x9f(indefinite-length array start) and the last byte equals0xff(break)
Scenario: definite-length is the default
- WHEN
encode([1, 2, 3])is called - THEN the first byte equals
0x83(definite-length array of 3) and there is no break stop
Scenario: indefinite + deterministic is rejected
- WHEN
encode([1, 2, 3], { indefiniteLength: true, deterministic: true })is called - THEN the call throws
CborErrorwith code"CBOR.INDEFINITE_NOT_ALLOWED"
Verified by: packages/flow/cbor/src/encoder/indefinite.test.ts::"emits indefinite-length array when opt-in" AND packages/flow/cbor/src/encoder/indefinite.test.ts::"defaults to definite-length" AND packages/flow/cbor/src/encoder/indefinite.test.ts::"throws INDEFINITE_NOT_ALLOWED under deterministic"
Requirement: Tagged items with built-in registry
The package SHALL encode and decode tagged items (RFC 8949 §3.4 / major type 6) and SHALL ship a TagRegistry with built-in handlers for IANA-registered tags 0 (RFC 3339 date/time), 1 (epoch-based date/time), 2 (positive bignum), 3 (negative bignum), 21 (expected base64url), 22 (expected base64), 23 (expected base16), 24 (encoded CBOR — re-enters the decoder), 32 (URI), 35 (regex), and 55799 (self-describe CBOR). Consumers SHALL be able to register custom handlers via registry.register(tag, handler). Per RFC 8949 §3.4, an unknown tag SHALL NOT cause decode failure — the decoder SHALL return a Tagged<T> value object so the caller can decide.
Scenario: tag 0 round-trips an RFC 3339 date string
- WHEN
encode(new Date("2026-05-10T00:00:00Z"))is called and the result is decoded - THEN the decoded value is a
Datewhose ISO string equals"2026-05-10T00:00:00.000Z"
Scenario: tag 24 (encoded CBOR) re-enters the decoder
- GIVEN a tag 24 wrapping the byte string for
encode(42) - WHEN the wrapper is decoded
- THEN the result equals
42(the inner CBOR was decoded transparently)
Scenario: unknown tag in strict mode returns Tagged
- GIVEN a CBOR payload with tag number 9999 wrapping the string
"hi" - WHEN
decode(bytes)is called with defaultmode: "strict" - THEN the result is a
Taggedwith{ tag: 9999, value: "hi" }
Scenario: registering a handler intercepts decode
- GIVEN a
TagRegistrywithregister(9999, (v) => ({ wrapped: v })) - WHEN the same payload is decoded through that registry
- THEN the result equals
{ wrapped: "hi" }
Scenario: double-register throws
- WHEN
registry.register(0, handler)is called twice for the same tag - THEN the second call throws
CborErrorwith code"CBOR.UNEXPECTED_TYPE"
Verified by: packages/flow/cbor/src/tags/builtin.test.ts::"tag 0 round-trips RFC 3339 date" AND packages/flow/cbor/src/tags/builtin.test.ts::"tag 24 re-enters the decoder" AND packages/flow/cbor/src/tags/registry.test.ts::"unknown tag in strict mode returns Tagged" AND packages/flow/cbor/src/tags/registry.test.ts::"registered handler intercepts decode" AND packages/flow/cbor/src/tags/registry.test.ts::"double-register throws"
Requirement: Half-precision float encode (opt-in) and decode
The package SHALL decode IEEE-754 binary16 values (RFC 8949 §3.3, major type 7 / value 25) — always available, never gated. The package SHALL encode binary16 only when EncoderOptions.preferFloat16: true AND the value round-trips through binary16 losslessly. Subnormals, ±0, ±Inf, and quiet/signalling NaN payloads SHALL all decode correctly per RFC 8949 §D.
Scenario: decodes 0xf90000 to +0
- WHEN
decode(Uint8Array([0xf9, 0x00, 0x00]))is called - THEN the result equals
0andObject.is(result, 0)is true (positive zero)
Scenario: decodes 0xf98000 to -0
- WHEN
decode(Uint8Array([0xf9, 0x80, 0x00]))is called - THEN the result satisfies
Object.is(result, -0)
Scenario: decodes 0xf97c00 to +Infinity
- WHEN
decode(Uint8Array([0xf9, 0x7c, 0x00]))is called - THEN the result equals
Number.POSITIVE_INFINITY
Scenario: encodes 1.0 as binary16 when opt-in
- WHEN
encode(1.0, { preferFloat16: true })is called - THEN the result equals
Uint8Array([0xf9, 0x3c, 0x00])
Scenario: encoder skips binary16 when value would lose precision
- WHEN
encode(1.1, { preferFloat16: true })is called - THEN the result is NOT a 3-byte half-float — it is the float32 or float64 form that preserves
1.1
Verified by: packages/flow/cbor/src/decoder/half-float.test.ts::"decodes binary16 +0" AND packages/flow/cbor/src/decoder/half-float.test.ts::"decodes binary16 -0" AND packages/flow/cbor/src/decoder/half-float.test.ts::"decodes binary16 +Infinity" AND packages/flow/cbor/src/encoder/half-float.test.ts::"encodes 1.0 as binary16 when opt-in" AND packages/flow/cbor/src/encoder/half-float.test.ts::"skips binary16 when value would lose precision"
Requirement: Deterministic encoding profile (RFC 8949 §4.2)
The package SHALL provide a deterministic encoding profile gated by EncoderOptions.deterministic: true that enforces every rule in RFC 8949 §4.2.1 + §4.2.2: integers use shortest-form encoding, floats use the smallest losslessly-representable form (float64 → float32 → no half-float — half-float disabled in deterministic mode), maps use sorted keys via lexicographic comparison over canonical encoded key bytes, and indefinite-length encodings are forbidden. Two runs of the deterministic encoder over the same logical structure (with map-key ordering shuffled) SHALL produce byte-identical output.
Scenario: map keys are sorted lexicographically over encoded key bytes
- WHEN
encode(new Map([["b", 1], ["a", 2]]), { deterministic: true })is called - THEN the resulting bytes encode
"a"before"b"regardless of insertion order
Scenario: integer key 10 sorts after integer key 2 by encoded-byte length
- WHEN a map containing keys
2and10is encoded under deterministic mode - THEN key
2(single byte 0x02) precedes key10(two bytes 0x18 0x0a) per RFC 8949 §4.2.1 length-then-lexicographic ordering
Scenario: half-float disabled under deterministic profile
- WHEN
encode(1.0, { deterministic: true, preferFloat16: true })is called - THEN the result is the float32 form (5 bytes starting
0xfa), not the binary16 form (3 bytes starting0xf9)
Scenario: shuffled-key map produces byte-identical output across runs
- GIVEN a fast-check arbitrary that generates the same logical map with shuffled insertion order
- WHEN both shuffles are encoded under
deterministic: true - THEN the two
Uint8Arrayoutputs are byte-identical
Verified by: packages/flow/cbor/src/encoder/deterministic.test.ts::"sorts map keys lexicographically by encoded bytes" AND packages/flow/cbor/src/encoder/deterministic.test.ts::"sorts integer keys by length then value" AND packages/flow/cbor/src/encoder/deterministic.test.ts::"disables half-float under deterministic profile" AND packages/flow/cbor/src/encoder/deterministic-property.test.ts::"shuffled-key map produces byte-identical deterministic output"
Requirement: dCBOR profile (draft-mcnally-deterministic-cbor)
The package SHALL provide an opt-in dCBOR profile under the ./deterministic subpath that layers the dCBOR draft's reduction rules over the RFC 8949 §4.2 deterministic baseline: numeric values SHALL collapse to the smallest type that represents them losslessly (e.g. 1.0 collapses to integer 1), and undefined and unknown simple values SHALL be rejected with CborError(CBOR.NON_DETERMINISTIC). dCBOR output SHALL be a strict subset of RFC 8949 §4.2 deterministic output and SHALL satisfy every scenario of the prior requirement.
Scenario: numeric reduction collapses 1.0 to integer 1
- WHEN
encodeDcbor(1.0)is called - THEN the result equals
Uint8Array([0x01])(major type 0 unsigned integer 1)
Scenario: rejects undefined value
- WHEN
encodeDcbor(undefined)is called - THEN the call throws
CborErrorwith code"CBOR.NON_DETERMINISTIC"
Scenario: dCBOR output is RFC 8949 deterministic
- GIVEN any non-rejected input
- WHEN
encodeDcbor(value)is called and the bytes are also produced viaencode(value, { deterministic: true }) - THEN the dCBOR output is byte-identical to the deterministic output OR is a strict reduction of it (numeric collapse rule)
Verified by: packages/flow/cbor/src/encoder/dcbor.test.ts::"reduces 1.0 to integer 1" AND packages/flow/cbor/src/encoder/dcbor.test.ts::"rejects undefined as NON_DETERMINISTIC" AND packages/flow/cbor/src/encoder/dcbor.test.ts::"dCBOR output satisfies RFC 8949 deterministic invariants"
Requirement: RFC 8742 CBOR Sequences
The package SHALL encode and decode CBOR Sequences per RFC 8742 §2 and §3 — the concatenation of zero or more well-formed CBOR data items with no enclosing array. The package SHALL export encodeSequence(items, options?): Uint8Array, decodeSequence(bytes, options?): unknown[], and decodeSequenceStream(stream, options?): AsyncIterable<unknown> for incremental consumption. An empty input SHALL produce an empty Uint8Array on encode and an empty array (or empty stream) on decode.
Scenario: encodes mixed-major-type sequence
- WHEN
encodeSequence([1, "two", [3]])is called - THEN the result equals the concatenation of
encode(1),encode("two"), andencode([3])
Scenario: decodes the same bytes back to the same array
- GIVEN the bytes from the prior scenario
- WHEN
decodeSequence(bytes)is called - THEN the result deeply equals
[1, "two", [3]]
Scenario: empty sequence round-trips
- WHEN
encodeSequence([])is called and the result is decoded - THEN the round-trip yields
[]and the encoded byte length equals 0
Scenario: stream form yields one item at a time
- GIVEN an
AsyncIterable<Uint8Array>chunking the bytes from scenario 1 - WHEN
decodeSequenceStream(stream)is iterated - THEN three items are yielded in order:
1,"two",[3]
Verified by: packages/flow/cbor/src/sequences/sequences.test.ts::"encodes mixed-major-type sequence" AND packages/flow/cbor/src/sequences/sequences.test.ts::"decodes sequence back to source array" AND packages/flow/cbor/src/sequences/sequences.test.ts::"empty sequence round-trips" AND packages/flow/cbor/src/sequences/sequences.test.ts::"stream form yields one item at a time"
Requirement: Decoder strict mode and well-formedness checks (RFC 8949 §5.1)
The package SHALL export a Decoder class and decode(bytes, options?): unknown function. Strict mode (default) SHALL enforce every well-formedness rule from RFC 8949 §5.1: no extraneous trailing bytes after a complete item, every break stop matches an indefinite-length container start, every indefinite-length text/byte string contains only definite-length chunks of the same major type, integer / length encodings use a valid initial-byte form, and reserved simple values are rejected. Lax mode (options.mode: "lax") SHALL apply only RFC 8949 §5.2 basic validity. Strict-mode failures SHALL throw CborError(CBOR.MALFORMED).
Scenario: strict mode rejects extraneous trailing bytes
- GIVEN the bytes
[0x01, 0xff](valid integer 1 followed by stray break) - WHEN
decode(bytes)is called with default mode - THEN the call throws
CborErrorwith code"CBOR.MALFORMED"
Scenario: lax mode tolerates extraneous trailing bytes
- GIVEN the same bytes
- WHEN
decode(bytes, { mode: "lax" })is called - THEN the result equals
1and no error is thrown
Scenario: strict mode rejects indefinite-string with wrong-major-type chunk
- GIVEN an indefinite-length text string (
0x7f) containing a byte-string chunk - WHEN
decode(bytes)is called - THEN the call throws
CborErrorwith code"CBOR.MALFORMED"
Scenario: isWellFormed returns false for every §5.1 failure class
- WHEN
isWellFormedis invoked over a fixture array of malformed payloads (one per §5.1 rule) - THEN every fixture returns
false
Verified by: packages/flow/cbor/src/decoder/decoder.test.ts::"strict mode rejects extraneous trailing bytes" AND packages/flow/cbor/src/decoder/decoder.test.ts::"lax mode tolerates extraneous trailing bytes" AND packages/flow/cbor/src/decoder/decoder.spec-conformance.test.ts::"strict mode rejects indefinite text with wrong-major chunk" AND packages/flow/cbor/src/decoder/well-formed.test.ts::"isWellFormed returns false for every §5.1 failure class"
Requirement: Streaming pull-decoder over Uint8Array and ReadableStream
The package SHALL export a PullDecoder that consumes either a Uint8Array or an AsyncIterable<Uint8Array> / ReadableStream<Uint8Array> and exposes next(): Promise<IteratorResult<unknown>> so callers materialize one CBOR item at a time. Memory usage SHALL be bounded — the decoder SHALL NOT buffer the entire input before yielding. The decoder SHALL honor AbortSignal: when the signal aborts mid-item, the next next() call SHALL reject with CborError(CBOR.MALFORMED) and SHALL release any internal buffers.
Scenario: pulls one item per next() call
- GIVEN an input encoding
[1, 2, 3]as a CBOR Sequence - WHEN
next()is called three times - THEN the calls resolve to
{ value: 1, done: false },{ value: 2, done: false },{ value: 3, done: false }in order, followed by{ value: undefined, done: true }
Scenario: bounded memory under a 1 GiB sequence
- GIVEN a synthetic stream emitting one million 1 KiB CBOR items
- WHEN the consumer iterates with
next()and discards every result - THEN the peak heap delta during iteration is bounded by a fixed constant (asserted via
process.memoryUsage().heapUsedsnapshot) regardless of total stream length
Scenario: AbortSignal cancellation rejects next()
- GIVEN a
PullDecoderconstructed with anAbortSignal - WHEN the signal is aborted mid-stream and
next()is called - THEN the returned promise rejects with
CborErrorwhose code equals"CBOR.MALFORMED"
Verified by: packages/flow/cbor/src/streaming/pull-decoder.test.ts::"pulls one item per next() call" AND packages/flow/cbor/src/streaming/pull-decoder.test.ts::"bounded memory under 1 GiB sequence" AND packages/flow/cbor/src/streaming/pull-decoder.test.ts::"AbortSignal cancellation rejects next()"
Requirement: Bounded recursion and bounded item count
The encoder and decoder SHALL enforce caller-configurable security bounds per RFC 8949 §10: maxDepth (default 64) caps nested container depth and overflows SHALL throw CborError(CBOR.NESTED_TOO_DEEP); maxItems (default 1_000_000) caps the total number of items materialized in a single decode (across nested arrays / maps / sequences) and overflows SHALL throw CborError(CBOR.LENGTH_OVERFLOW). Both caps SHALL be configurable via EncoderOptions / DecoderOptions. The defaults SHALL apply when the caller passes no override.
Scenario: depth overflow throws NESTED_TOO_DEEP
- GIVEN an array nested 65 levels deep
- WHEN
encode(value)is called with defaultmaxDepth: 64 - THEN the call throws
CborErrorwith code"CBOR.NESTED_TOO_DEEP"
Scenario: item-count overflow throws LENGTH_OVERFLOW
- GIVEN a CBOR payload claiming an indefinite array of 1_000_001 small items
- WHEN
decode(bytes)is called with defaultmaxItems: 1_000_000 - THEN the call throws
CborErrorwith code"CBOR.LENGTH_OVERFLOW"
Scenario: configurable override increases the cap
- GIVEN a 100-deep nested array
- WHEN
encode(value, { maxDepth: 128 })is called - THEN the call succeeds with no error
Verified by: packages/flow/cbor/src/encoder/encoder.test.ts::"throws NESTED_TOO_DEEP at default depth 64" AND packages/flow/cbor/src/decoder/decoder.test.ts::"throws LENGTH_OVERFLOW at default cap" AND packages/flow/cbor/src/encoder/encoder.test.ts::"honors caller-supplied maxDepth override"
Requirement: COSE_Key decoder (RFC 9052 §7)
The package SHALL export decodeCoseKey(bytes): CoseKey from the ./cose subpath that decodes a COSE_Key map per RFC 9052 §7 into a discriminated union OkpKey | Ec2Key | RsaKey | SymmetricKey. The discriminator SHALL be the kty parameter (label 1): 1 → OKP (RFC 8037), 2 → EC2 (RFC 9052 §7.1), 3 → RSA (RFC 8230), 4 → Symmetric. Each variant SHALL include the structurally-typed parameters required by its key type (crv, x, y, d, n, e, k as appropriate). Malformed input SHALL throw CborError(CBOR.MALFORMED); unknown kty SHALL throw CborError(CBOR.UNEXPECTED_TYPE).
Scenario: decodes EC2 P-256 public key
- GIVEN the canonical RFC 9052 §7.1 sample EC2 P-256 public key bytes
- WHEN
decodeCoseKey(bytes)is called - THEN the result is
{ kty: "EC2", crv: "P-256", x: <Uint8Array>, y: <Uint8Array> }and TypeScript narrows the type toEc2Key
Scenario: decodes Ed25519 OKP public key
- GIVEN an OKP public key with
crv: Ed25519 - WHEN
decodeCoseKey(bytes)is called - THEN the result is
{ kty: "OKP", crv: "Ed25519", x: <Uint8Array> }
Scenario: rejects unknown kty
- GIVEN a COSE_Key map with
kty: 99 - WHEN
decodeCoseKey(bytes)is called - THEN the call throws
CborErrorwith code"CBOR.UNEXPECTED_TYPE"
Verified by: packages/flow/cbor/src/cose/cose-key.test.ts::"decodes EC2 P-256 public key" AND packages/flow/cbor/src/cose/cose-key.test.ts::"decodes Ed25519 OKP public key" AND packages/flow/cbor/src/cose/cose-key.test.ts::"rejects unknown kty with UNEXPECTED_TYPE"
Requirement: COSE ↔ JWK round-trip
The package SHALL export coseKeyToJwk(key: CoseKey): JsonWebKey and jwkToCoseKey(jwk: JsonWebKey): CoseKey round-trip helpers per RFC 9052 §7.1, RFC 8037 §2 (OKP), and RFC 7518 §6 (RSA / EC / symmetric). Round-trip SHALL be lossless for every supported key type — calling jwkToCoseKey(coseKeyToJwk(key)) SHALL produce a CoseKey deeply equal to the input, and the reverse SHALL hold for any JWK constructible from a supported COSE key. RSA keys SHALL serialize n and e as base64url strings per RFC 7518; EC2/OKP coordinate buffers SHALL be base64url-encoded.
Scenario: ES256 EC2 round-trip
- GIVEN an RFC 9052 §7.1 sample ES256 EC2 key
- WHEN
jwkToCoseKey(coseKeyToJwk(key))is computed - THEN the result deeply equals the input key
Scenario: Ed25519 OKP round-trip
- GIVEN an RFC 8037 sample Ed25519 OKP key
- WHEN the same round-trip is computed
- THEN the result deeply equals the input
Scenario: RS256 RSA round-trip via base64url n/e
- GIVEN an RSA public key
{ kty: "RSA", n: <Uint8Array>, e: <Uint8Array> } - WHEN
coseKeyToJwk(key)is called - THEN the resulting JWK has
nandeas base64url strings, and the round-trip back viajwkToCoseKeyrecovers the original buffers
Verified by: packages/flow/cbor/src/cose/jwk.test.ts::"ES256 EC2 round-trip" AND packages/flow/cbor/src/cose/jwk.test.ts::"Ed25519 OKP round-trip" AND packages/flow/cbor/src/cose/jwk.test.ts::"RS256 RSA round-trip via base64url n/e"
Requirement: COSE alg label → genesis crypto-curve algorithm ID mapping
The package SHALL export coseAlgToCryptoAlg(alg: number) mapping every IANA-registered COSE algorithm label this package supports to the corresponding @suaveplan/crypto-curve algorithm ID per RFC 9053 §2: -7 → ES256, -8 → EdDSA (Ed25519), -35 → ES384, -36 → ES512, -257 → RS256, -258 → RS384, -259 → RS512. Unknown labels SHALL throw CborError(CBOR.UNEXPECTED_TYPE). The mapping SHALL be one-to-one and SHALL NOT silently substitute a "close enough" algorithm.
Scenario: ES256 maps to crypto-curve ES256 ID
- WHEN
coseAlgToCryptoAlg(-7)is called - THEN the result is the
@suaveplan/crypto-curveES256 algorithm ID
Scenario: EdDSA maps to Ed25519
- WHEN
coseAlgToCryptoAlg(-8)is called - THEN the result is the Ed25519 algorithm ID
Scenario: unknown label throws UNEXPECTED_TYPE
- WHEN
coseAlgToCryptoAlg(99999)is called - THEN the call throws
CborErrorwith code"CBOR.UNEXPECTED_TYPE"
Verified by: packages/flow/cbor/src/cose/alg-map.test.ts::"ES256 (-7) maps to crypto-curve ES256" AND packages/flow/cbor/src/cose/alg-map.test.ts::"EdDSA (-8) maps to Ed25519" AND packages/flow/cbor/src/cose/alg-map.test.ts::"unknown label throws UNEXPECTED_TYPE"
Requirement: Diagnostic notation emitter (RFC 8949 §8)
The package SHALL export toDiagnostic(value): string from the ./diagnostic subpath that emits RFC 8949 §8 diagnostic notation for any decoded CBOR value. The emitter SHALL be debug-only — its sibling .md SHALL document that it is never to be invoked from encode/decode hot paths. Output SHALL match RFC 8949 §8 examples for the documented value forms (integers, floats with _h_, byte strings as h'' hex, text strings as quoted, arrays / maps / tags with the documented separators).
Scenario: emits RFC 8949 §8 example for {1: 2, 3: 4}
- WHEN
toDiagnostic(new Map([[1, 2], [3, 4]]))is called - THEN the result equals
"{1: 2, 3: 4}"
Scenario: emits byte-string as h''
- WHEN
toDiagnostic(new Uint8Array([0x01, 0x02]))is called - THEN the result equals
"h'0102'"
Scenario: emits tagged value as N(...)
- WHEN
toDiagnostic(new Tagged(0, "2026-05-10T00:00:00Z"))is called - THEN the result equals
"0(\"2026-05-10T00:00:00Z\")"
Verified by: packages/flow/cbor/src/diagnostic/diagnostic.test.ts::"emits RFC §8 example map form" AND packages/flow/cbor/src/diagnostic/diagnostic.test.ts::"emits byte string as h''" AND packages/flow/cbor/src/diagnostic/diagnostic.test.ts::"emits tagged value as N(...)"
Requirement: Error registry — every code thrown by ≥1 production path
The package SHALL register exactly seven error codes via defineCodes("CBOR", [...]): MALFORMED, UNEXPECTED_TYPE, UNKNOWN_TAG_STRICT, NESTED_TOO_DEEP, LENGTH_OVERFLOW, INDEFINITE_NOT_ALLOWED, NON_DETERMINISTIC. Every code SHALL be thrown by at least one production code path inside src/; the registry SHALL NOT contain unused entries. The package SHALL export CborError extends SuaveplanError<CborErrorCode> where CborErrorCode is the literal-union of the seven codes.
Scenario: every code is reachable from src/
- WHEN the test scans
src/for the throw site of each registered code - THEN every one of the seven codes is found in at least one production
.tsfile
Scenario: CborErrorCode literal union is the registry shape
- WHEN
expectTypeOf<CborErrorCode>().toEqualTypeOf<"CBOR.MALFORMED" | "CBOR.UNEXPECTED_TYPE" | "CBOR.UNKNOWN_TAG_STRICT" | "CBOR.NESTED_TOO_DEEP" | "CBOR.LENGTH_OVERFLOW" | "CBOR.INDEFINITE_NOT_ALLOWED" | "CBOR.NON_DETERMINISTIC">()runs - THEN the type-level assertion holds
Verified by: packages/flow/cbor/src/errors/error-codes.test.ts::"every registered code is thrown from production src" AND packages/flow/cbor/src/errors/error-codes.test.ts::"CborErrorCode literal union matches registry"
Requirement: Zod boundary schemas at every public option type
The package SHALL declare Zod schemas (EncoderOptionsSchema, DecoderOptionsSchema, TagHandlerSchema, CoseKeySchema — discriminated by kty — and JwkSchema) and SHALL parse caller-supplied options at every public entry point (encode, decode, decodeCoseKey, jwkToCoseKey, registry.register). Schema failures SHALL surface as the genesis Zod-wrapped boundary error and SHALL NOT silently coerce malformed options into valid encodes / decodes.
Scenario: malformed EncoderOptions rejected at boundary
- WHEN
encode(0, { maxDepth: -1 } as never)is called - THEN the call throws a Zod-validation error before any encoding takes place
Scenario: malformed DecoderOptions rejected at boundary
- WHEN
decode(bytes, { mode: "weird" as never })is called - THEN the call throws a Zod-validation error
Scenario: CoseKeySchema validates discriminated union
- WHEN
CoseKeySchema.parse({ kty: "EC2", crv: "P-256", x: new Uint8Array(32), y: new Uint8Array(32) })is called - THEN the result narrows to
Ec2Key
Verified by: packages/flow/cbor/src/schemas/schemas.test.ts::"EncoderOptionsSchema rejects negative maxDepth" AND packages/flow/cbor/src/schemas/schemas.test.ts::"DecoderOptionsSchema rejects unknown mode" AND packages/flow/cbor/src/schemas/schemas.test.ts::"CoseKeySchema discriminates by kty"
Requirement: OpenTelemetry instrumentation
The package SHALL emit cbor.encode_duration_ms and cbor.decode_duration_ms histograms (units: milliseconds) wrapping every encode / decode call, a cbor.tag_total{tag} counter incremented per decoded tagged item (where tag is the numeric tag label as a string), and a cbor.error_total{code} counter incremented on every thrown CborError (where code is one of the seven registry codes). Instruments SHALL be sourced via @opentelemetry/api and SHALL be assertable under a real in-memory MeterProvider.
Scenario: encode emits histogram observation
- GIVEN a test in-memory
MeterProviderregistered globally - WHEN
encode(0)is called - THEN the recorded histogram for
cbor.encode_duration_mshas at least one observation
Scenario: decoded tag increments tag_total counter
- GIVEN a payload encoding a tag-32 URI value
- WHEN
decode(bytes)is called under the test meter - THEN
cbor.tag_total{tag="32"}equals 1
Scenario: thrown CborError increments error_total counter
- GIVEN a malformed payload
- WHEN
decode(bytes)throwsCborError(CBOR.MALFORMED) - THEN
cbor.error_total{code="CBOR.MALFORMED"}equals 1
Verified by: packages/flow/cbor/src/observability/metrics.test.ts::"encode records encode_duration_ms histogram" AND packages/flow/cbor/src/observability/metrics.test.ts::"decoded tag increments tag_total counter" AND packages/flow/cbor/src/observability/metrics.test.ts::"thrown CborError increments error_total counter"
Requirement: Property-based round-trip and deterministic-mode commutativity
The package SHALL ship fast-check property tests that prove encoder + decoder agree on the wire format under arbitrary structures: for any randomly-generated CBOR-representable value v, decode(encode(v)) SHALL deeply equal v. The package SHALL also ship a property test proving deterministic-mode commutativity: for any logical map m, encoding m under deterministic: true SHALL produce byte-identical output regardless of the map's internal key ordering.
Scenario: round-trip property holds over arbitrary inputs
- GIVEN a fast-check arbitrary covering integers, floats, byte strings, text strings, arrays, maps, tagged items, and nested combinations
- WHEN
decode(encode(v))is computed for 1000 generated values - THEN every result deeply equals the input
Scenario: deterministic-mode commutativity holds over shuffled maps
- GIVEN a fast-check arbitrary that generates the same logical map with shuffled insertion order
- WHEN
encode(map, { deterministic: true })is computed for both shuffles across 1000 trials - THEN the two byte sequences are byte-identical for every trial
Verified by: packages/flow/cbor/src/encoder/property.test.ts::"round-trip holds for arbitrary CBOR values" AND packages/flow/cbor/src/encoder/deterministic-property.test.ts::"deterministic encoding is commutative over shuffled map keys"
Requirement: Subpath exports
The package SHALL declare subpath exports so consumers tree-shake to the minimum surface they need:
@suaveplan/cbor— root barrel@suaveplan/cbor/encoder—Encoder,encode, encoder types, half-float encode helper@suaveplan/cbor/decoder—Decoder,decode, decoder types, well-formedness checker@suaveplan/cbor/streaming—PullDecoder@suaveplan/cbor/sequences— RFC 8742encodeSequence,decodeSequence,decodeSequenceStream@suaveplan/cbor/tags—TagRegistry,Tagged, built-in tag handlers@suaveplan/cbor/deterministic— RFC 8949 §4.2 deterministic profile + dCBOR profile@suaveplan/cbor/cose—decodeCoseKey,coseKeyToJwk,jwkToCoseKey,coseAlgToCryptoAlg, COSE types@suaveplan/cbor/diagnostic—toDiagnostic@suaveplan/cbor/errors—CBOR_CODES,CborError,CborErrorCode
Scenario: every subpath resolves
- WHEN every documented subpath is imported in a consumer test fixture
- THEN every import resolves with no
Module not founderrors
Verified by: packages/flow/cbor/src/index.test.ts::"every documented subpath resolves"