first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,183 @@
# @suaveplan/cbor - RFC 8949 CBOR encoder/decoder + RFC 8742 sequences + RFC 9052 COSE bridge + RFC 8392 CWT + RFC 8610 CDDL — zero browser/Node deps
## Summary
RFC 8949 (CBOR) encoder + decoder, RFC 8742 (CBOR Sequences), pull-style
streaming decoder (`AsyncIterable<Uint8Array>` / `ReadableStream<Uint8Array>`),
RFC 9052 §7 `COSE_Key` ↔ JWK round-trip + IANA algorithm bridge,
deterministic-encoding shortcut, RFC 8949 §8 diagnostic notation, tag
registry with IANA built-in handlers, and a strict §5.1 well-formedness
predicate. Universal — runs unmodified in Bun, Node, Deno, and the
browser. Zero browser/Node deps.
> **Wave 1 status.** This package is the foundational codec layer. Larger
> capabilities authored in the design proposal —
> COSE structures (RFC 9052 §4–§6), CWT (RFC 8392), CDDL (RFC 8610), and the
> `ChunkDecoder` partial-item streaming surface — are scoped IN and queued
> as follow-up waves on top of this codec.
<!-- begin:auto-docs -->
## Documentation tree
- [ast](./src/cddl/ast.md) — Typed AST shapes per RFC 8610 §3 — the contract between the parser and the validator (and any future code generator). E…
- [lexer](./src/cddl/lexer.md) — CDDL tokenizer per RFC 8610 §3 ABNF grammar. Reads a CDDL source string and emits a sequence of `Token` objects termina…
- [parser](./src/cddl/parser.md) — CDDL parser per RFC 8610 §3 grammar. Consumes the token stream from the `tokenize` function and produces a typed AST (`…
- [validate](./src/cddl/validate.md) — Walk a decoded CBOR value against a parsed CDDL schema and report the first mismatch (with a JSON-pointer-like path). T…
- [alg-map](./src/cose/alg-map.md) — Bridge between IANA COSE Algorithm identifiers (`CoseAlg` literal strings or the equivalent numeric labels from RFC 905…
- [cose-key](./src/cose/cose-key.md) — Decodes and encodes RFC 9052 §7 `COSE_Key` structures. The decoder maps an RFC-encoded CBOR map onto a typed discrimina…
- [encrypt](./src/cose/encrypt.md) — `COSE_Encrypt` (RFC 9052 §5.1) — multi-recipient AEAD-encrypted COSE message in CBOR tag 96. Each recipient holds its o…
- [encrypt0](./src/cose/encrypt0.md) — `COSE_Encrypt0` (RFC 9052 §5.2) — single-recipient AEAD-encrypted COSE message in CBOR tag 16. Wraps AES-256-GCM and Ch…
- [headers](./src/cose/headers.md) — Typed `CoseHeader` map shape covering RFC 9052 §3.1 + IANA COSE Header Parameters (alg, crit, content type, kid, IV, Pa…
- [jwk](./src/cose/jwk.md) — Round-trip converter between RFC 9052 `COSE_Key` instances and RFC 7517 JWK shapes. WebAuthn attestation flows often ne…
- [mac](./src/cose/mac.md) — `COSE_Mac` (RFC 9052 §6.1) — multi-recipient MAC message in CBOR tag 97. Each recipient gets a wrapped copy of the cont…
- [mac0](./src/cose/mac0.md) — `COSE_Mac0` (RFC 9052 §6.2) — single-recipient MAC message in CBOR tag 17. Used for symmetric-key authentication of CWT…
- [sig-structure](./src/cose/sig-structure.md) — Builders for the canonical input to COSE signature, MAC, and AEAD computations per RFC 9052 §4.4 / §6.3 / §5.3. Every s…
- [sign](./src/cose/sign.md) — `COSE_Sign` (RFC 9052 §4.1) — multi-signer COSE message in CBOR tag 98. Use when a payload needs to be co-signed by mul…
- [sign1](./src/cose/sign1.md) — `COSE_Sign1` (RFC 9052 §4.2) — single-signer COSE message in CBOR tag 18. WebAuthn attestation, COSE-signed firmware, a…
- [claims](./src/cwt/claims.md) — Encode and decode RFC 8392 CBOR Web Token claim sets. The seven IANA- registered claims (`iss`, `sub`, `aud`, `exp`, `n…
- [verify](./src/cwt/verify.md) — End-to-end CWT verification: parse the COSE wrapper, verify the signature or MAC, decode the claim set, and check the s…
- [decoder](./src/decoder/decoder.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - half-float — Decodes 2 big-endian bytes representin…
- [decoder](./src/decoder/decoder.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - half-float — Decodes 2 big-endian bytes representin…
- [half-float](./src/decoder/half-float.md) — Decodes 2 big-endian bytes representing IEEE 754 binary16 to a JavaScript `number`. The decoder is always available — n…
- [well-formed](./src/decoder/well-formed.md) — `isWellFormed(bytes)` returns `true` iff `bytes` represent a single well-formed CBOR item per RFC 8949 §5.1. The functi…
- [deterministic](./src/deterministic/deterministic.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [deterministic](./src/deterministic/deterministic.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [diagnostic](./src/diagnostic/diagnostic.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [diagnostic](./src/diagnostic/diagnostic.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [encoder](./src/encoder/encoder.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - half-float — Encodes a JavaScript `number` as IEEE……
- [encoder](./src/encoder/encoder.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - half-float — Encodes a JavaScript `number` as IEEE……
- [half-float](./src/encoder/half-float.md) — Encodes a JavaScript `number` as IEEE 754 binary16 (CBOR major 7 / value 25) when the round-trip is exact, and otherwis…
- [errors](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [error-codes](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [sequences](./src/sequences/sequences.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [sequences](./src/sequences/sequences.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [chunk-decoder](./src/streaming/chunk-decoder.md) — Partial-item streaming decoder. While `PullDecoder` yields one whole CBOR item at a time (waiting until each item is fu…
- [pull-decoder](./src/streaming/pull-decoder.md) — `PullDecoder` is a pull-style streaming decoder over an `AsyncIterable<Uint8Array>` or `ReadableStream<Uint8Array>`. Ea…
- [builtin](./src/tags/builtin.md) — Provides `defaultTagHandlers()`, a `TagRegistry` populated with handlers for the most-used IANA-registered CBOR tags. C…
- [registry](./src/tags/registry.md) — `TagRegistry` is the in-memory mutable container for CBOR tag handlers. The decoder consults a snapshot of the registry…
- [tagged](./src/tags/tagged.md) — Provides the `Tagged<T>` value object used by `@suaveplan/cbor` to materialise RFC 8949 §3.4 tagged items. The decoder…
<!-- end:auto-docs --> - half-float — Decodes 2 big-endian bytes representin…
- [decoder](./src/decoder/decoder.md) — <!-- begin:auto-docs --> _(no submodules)_- half-float — Decodes 2 big-endian bytes representin…
- [half-float](./src/decoder/half-float.md) — Decodes 2 big-endian bytes representing IEEE 754 binary16 to a JavaScript `number`. The decoder is always available — n…
- [well-formed](./src/decoder/well-formed.md) — `isWellFormed(bytes)` returns `true` iff `bytes` represent a single well-formed CBOR item per RFC 8949 §5.1. The functi…
- [deterministic](./src/deterministic/deterministic.md) — <!-- begin:auto-docs --> _(no submodules)_- [deterministic](./src/deterministic/deterministic.md) — <!-- begin:auto-docs --> _(no submodules)_- [diagnostic](./src/diagnostic/diagnostic.md) — <!-- begin:auto-docs --> _(no submodules)_- [diagnostic](./src/diagnostic/diagnostic.md) — <!-- begin:auto-docs --> _(no submodules)_- [encoder](./src/encoder/encoder.md) — <!-- begin:auto-docs --> _(no submodules)_- half-float — Encodes a JavaScript `number` as IEEE……
- [encoder](./src/encoder/encoder.md) — <!-- begin:auto-docs --> _(no submodules)_- half-float — Encodes a JavaScript `number` as IEEE……
- [half-float](./src/encoder/half-float.md) — Encodes a JavaScript `number` as IEEE 754 binary16 (CBOR major 7 / value 25) when the round-trip is exact, and otherwis…
- [errors](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [sequences](./src/sequences/sequences.md) — <!-- begin:auto-docs --> _(no submodules)_- [sequences](./src/sequences/sequences.md) — <!-- begin:auto-docs --> _(no submodules)_- [chunk-decoder](./src/streaming/chunk-decoder.md) — Partial-item streaming decoder. While `PullDecoder` yields one whole CBOR item at a time (waiting until each item is fu…
- [pull-decoder](./src/streaming/pull-decoder.md) — `PullDecoder` is a pull-style streaming decoder over an `AsyncIterable<Uint8Array>` or `ReadableStream<Uint8Array>`. Ea…
- [builtin](./src/tags/builtin.md) — Provides `defaultTagHandlers()`, a `TagRegistry` populated with handlers for the most-used IANA-registered CBOR tags. C…
- [registry](./src/tags/registry.md) — `TagRegistry` is the in-memory mutable container for CBOR tag handlers. The decoder consults a snapshot of the registry…
- [tagged](./src/tags/tagged.md) — Provides the `Tagged<T>` value object used by `@suaveplan/cbor` to materialise RFC 8949 §3.4 tagged items. The decoder…
- half-float — Decodes 2 big-endian bytes representin…
- [decoder](./src/decoder/decoder.md) — <!-- begin:auto-docs --> _(no submodules)_- half-float — Decodes 2 big-endian bytes representin…
- [half-float](./src/decoder/half-float.md) — Decodes 2 big-endian bytes representing IEEE 754 binary16 to a JavaScript `number`. The decoder is always available — n…
- [well-formed](./src/decoder/well-formed.md) — `isWellFormed(bytes)` returns `true` iff `bytes` represent a single well-formed CBOR item per RFC 8949 §5.1. The functi…
- [deterministic](./src/deterministic/deterministic.md) — <!-- begin:auto-docs --> _(no submodules)_- [deterministic](./src/deterministic/deterministic.md) — <!-- begin:auto-docs --> _(no submodules)_- [diagnostic](./src/diagnostic/diagnostic.md) — <!-- begin:auto-docs --> _(no submodules)_- [diagnostic](./src/diagnostic/diagnostic.md) — <!-- begin:auto-docs --> _(no submodules)_- [encoder](./src/encoder/encoder.md) — <!-- begin:auto-docs --> _(no submodules)_- half-float — Encodes a JavaScript `number` as IEEE……
- [encoder](./src/encoder/encoder.md) — <!-- begin:auto-docs --> _(no submodules)_- half-float — Encodes a JavaScript `number` as IEEE……
- [half-float](./src/encoder/half-float.md) — Encodes a JavaScript `number` as IEEE 754 binary16 (CBOR major 7 / value 25) when the round-trip is exact, and otherwis…
- [errors](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [sequences](./src/sequences/sequences.md) — <!-- begin:auto-docs --> _(no submodules)_- [sequences](./src/sequences/sequences.md) — <!-- begin:auto-docs --> _(no submodules)_- [chunk-decoder](./src/streaming/chunk-decoder.md) — Partial-item streaming decoder. While `PullDecoder` yields one whole CBOR item at a time (waiting until each item is fu…
- [pull-decoder](./src/streaming/pull-decoder.md) — `PullDecoder` is a pull-style streaming decoder over an `AsyncIterable<Uint8Array>` or `ReadableStream<Uint8Array>`. Ea…
- [builtin](./src/tags/builtin.md) — Provides `defaultTagHandlers()`, a `TagRegistry` populated with handlers for the most-used IANA-registered CBOR tags. C…
- [registry](./src/tags/registry.md) — `TagRegistry` is the in-memory mutable container for CBOR tag handlers. The decoder consults a snapshot of the registry…
- [tagged](./src/tags/tagged.md) — Provides the `Tagged<T>` value object used by `@suaveplan/cbor` to materialise RFC 8949 §3.4 tagged items. The decoder…
- half-float — Decodes 2 big-endian bytes representin…
- [decoder](./src/decoder/decoder.md) — <!-- begin:auto-docs --> _(no submodules)_- half-float — Decodes 2 big-endian bytes representin…
- [half-float](./src/decoder/half-float.md) — Decodes 2 big-endian bytes representing IEEE 754 binary16 to a JavaScript `number`. The decoder is always available — n…
- [well-formed](./src/decoder/well-formed.md) — `isWellFormed(bytes)` returns `true` iff `bytes` represent a single well-formed CBOR item per RFC 8949 §5.1. The functi…
- [deterministic](./src/deterministic/deterministic.md) — <!-- begin:auto-docs --> _(no submodules)_- [deterministic](./src/deterministic/deterministic.md) — <!-- begin:auto-docs --> _(no submodules)_- [diagnostic](./src/diagnostic/diagnostic.md) — <!-- begin:auto-docs --> _(no submodules)_- [diagnostic](./src/diagnostic/diagnostic.md) — <!-- begin:auto-docs --> _(no submodules)_- [encoder](./src/encoder/encoder.md) — <!-- begin:auto-docs --> _(no submodules)_- half-float — Encodes a JavaScript `number` as IEEE…
- [encoder](./src/encoder/encoder.md) — <!-- begin:auto-docs --> _(no submodules)_- half-float — Encodes a JavaScript `number` as IEEE…
- [half-float](./src/encoder/half-float.md) — Encodes a JavaScript `number` as IEEE 754 binary16 (CBOR major 7 / value 25) when the round-trip is exact, and otherwis…
- [errors](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [sequences](./src/sequences/sequences.md) — <!-- begin:auto-docs --> _(no submodules)_- [sequences](./src/sequences/sequences.md) — <!-- begin:auto-docs --> _(no submodules)_- [chunk-decoder](./src/streaming/chunk-decoder.md) — Partial-item streaming decoder. While `PullDecoder` yields one whole CBOR item at a time (waiting until each item is fu…
- [pull-decoder](./src/streaming/pull-decoder.md) — `PullDecoder` is a pull-style streaming decoder over an `AsyncIterable<Uint8Array>` or `ReadableStream<Uint8Array>`. Ea…
- [builtin](./src/tags/builtin.md) — Provides `defaultTagHandlers()`, a `TagRegistry` populated with handlers for the most-used IANA-registered CBOR tags. C…
- [registry](./src/tags/registry.md) — `TagRegistry` is the in-memory mutable container for CBOR tag handlers. The decoder consults a snapshot of the registry…
- [tagged](./src/tags/tagged.md) — Provides the `Tagged<T>` value object used by `@suaveplan/cbor` to materialise RFC 8949 §3.4 tagged items. The decoder…
- [decoder](./src/decoder/decoder.md) — <!-- begin:auto-docs --> _(no submodules)_- [half-float](./src/decoder/half-float.md) — Decodes 2 big-endian bytes representing IEEE 754 binary16 to a JavaScript `number`. The decoder is always available — n…
- [well-formed](./src/decoder/well-formed.md) — `isWellFormed(bytes)` returns `true` iff `bytes` represent a single well-formed CBOR item per RFC 8949 §5.1. The functi…
- [deterministic](./src/deterministic/deterministic.md) — <!-- begin:auto-docs --> _(no submodules)_ - [deterministic](./src/deterministic/deterministic.md) — <!-- begin:auto-docs --> _(no submodules)_- [diagnostic](./src/diagnostic/diagnostic.md) — <!-- begin:auto-docs --> _(no submodules)_ - [diagnostic](./src/diagnostic/diagnostic.md) — <!-- begin:auto-docs --> _(no submodules)_- [encoder](./src/encoder/encoder.md) — <!-- begin:auto-docs --> _(no submodules)_ - [encoder](./src/encoder/encoder.md) — <!-- begin:auto-docs --> _(no submodules)_ - [half-float](./src/encoder/half-float.md) — Encodes a JavaScript `number` as IEEE 754 binary16 (CBOR major 7 / value 25) when the round-trip is exact, and otherwis…
- [errors](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_ - [error-codes](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [sequences](./src/sequences/sequences.md) — <!-- begin:auto-docs --> _(no submodules)_ - [sequences](./src/sequences/sequences.md) — <!-- begin:auto-docs --> _(no submodules)_- [chunk-decoder](./src/streaming/chunk-decoder.md) — Partial-item streaming decoder. While `PullDecoder` yields one whole CBOR item at a time (waiting until each item is fu…
- [pull-decoder](./src/streaming/pull-decoder.md) — `PullDecoder` is a pull-style streaming decoder over an `AsyncIterable<Uint8Array>` or `ReadableStream<Uint8Array>`. Ea…
- [builtin](./src/tags/builtin.md) — Provides `defaultTagHandlers()`, a `TagRegistry` populated with handlers for the most-used IANA-registered CBOR tags. C…
- [registry](./src/tags/registry.md) — `TagRegistry` is the in-memory mutable container for CBOR tag handlers. The decoder consults a snapshot of the registry…
- [tagged](./src/tags/tagged.md) — Provides the `Tagged<T>` value object used by `@suaveplan/cbor` to materialise RFC 8949 §3.4 tagged items. The decoder…
## Install
```bash
bun add @suaveplan/cbor
```
## Quick Start
```ts
import { encode, decode } from "@suaveplan/cbor";
const bytes = encode({ a: 1, b: [true, "two"] });
const value = decode(bytes);
```
## Modules
| Module | Doc |
|---|---|
| `encoder` | [`src/encoder/encoder.md`](src/encoder/encoder.md) |
| `encoder/half-float` | [`src/encoder/half-float.md`](src/encoder/half-float.md) |
| `decoder` | [`src/decoder/decoder.md`](src/decoder/decoder.md) |
| `decoder/half-float` | [`src/decoder/half-float.md`](src/decoder/half-float.md) |
| `decoder/well-formed` | [`src/decoder/well-formed.md`](src/decoder/well-formed.md) |
| `streaming/pull-decoder` | [`src/streaming/pull-decoder.md`](src/streaming/pull-decoder.md) |
| `deterministic` | [`src/deterministic/deterministic.md`](src/deterministic/deterministic.md) |
| `diagnostic` | [`src/diagnostic/diagnostic.md`](src/diagnostic/diagnostic.md) |
| `cose/cose-key` | [`src/cose/cose-key.md`](src/cose/cose-key.md) |
| `cose/jwk` | [`src/cose/jwk.md`](src/cose/jwk.md) |
| `cose/alg-map` | [`src/cose/alg-map.md`](src/cose/alg-map.md) |
| `cose/headers` | [`src/cose/headers.md`](src/cose/headers.md) |
| `cose/sig-structure` | [`src/cose/sig-structure.md`](src/cose/sig-structure.md) |
| `cose/sign1` | [`src/cose/sign1.md`](src/cose/sign1.md) |
| `cose/mac0` | [`src/cose/mac0.md`](src/cose/mac0.md) |
| `cwt/claims` | [`src/cwt/claims.md`](src/cwt/claims.md) |
| `cwt/verify` | [`src/cwt/verify.md`](src/cwt/verify.md) |
| `streaming/chunk-decoder` | [`src/streaming/chunk-decoder.md`](src/streaming/chunk-decoder.md) |
| `cose/sign` | [`src/cose/sign.md`](src/cose/sign.md) |
| `cose/mac` | [`src/cose/mac.md`](src/cose/mac.md) |
| `cose/encrypt0` | [`src/cose/encrypt0.md`](src/cose/encrypt0.md) |
| `cose/encrypt` | [`src/cose/encrypt.md`](src/cose/encrypt.md) |
| `tags/registry` | [`src/tags/registry.md`](src/tags/registry.md) |
| `tags/tagged` | [`src/tags/tagged.md`](src/tags/tagged.md) |
| `tags/builtin` | [`src/tags/builtin.md`](src/tags/builtin.md) |
| `sequences` | [`src/sequences/sequences.md`](src/sequences/sequences.md) |
| `errors` | [`src/errors/error-codes.md`](src/errors/error-codes.md) |
## Subpath exports
- `@suaveplan/cbor` — root barrel: `encode`, `decode`, `Tagged`, error codes
- `@suaveplan/cbor/encoder` — encoder + half-float
- `@suaveplan/cbor/decoder` — decoder + half-float + `isWellFormed`
- `@suaveplan/cbor/streaming` — `PullDecoder` over async-iterable / WHATWG streams
- `@suaveplan/cbor/sequences` — RFC 8742
- `@suaveplan/cbor/deterministic` — `encodeDeterministic` shortcut for §4.2
- `@suaveplan/cbor/diagnostic` — RFC 8949 §8 notation emitter
- `@suaveplan/cbor/cose` — `COSE_Key` ↔ JWK + IANA algorithm bridge (RFC 9052 §7), Sig_structure builders (§4.4 / §6.3 / §5.3), `COSE_Sign1` encode/decode/verify (§4.2, ES256/EdDSA), `COSE_Mac0` create/encode/decode/verify (§6.2, HS256/384/512)
- `@suaveplan/cbor/cwt` — RFC 8392 CBOR Web Token claim encode/decode + `verifyCwtSign1` / `verifyCwtMac0` with time-window + audience + issuer validation
- `@suaveplan/cbor/cddl` — RFC 8610 CDDL lexer + parser + AST + validator (primitive prelude, ranges, choices, arrays, maps, occurrence indicators, `.size`/`.regexp`/`.cbor` control operators, generics, named references)
- `@suaveplan/cbor/tags` — `Tagged`, `TagRegistry`, `defaultTagHandlers`
- `@suaveplan/cbor/errors` — `CBOR_CODES`, `CborError`, `CborErrorCode`
## Conformance
- RFC 8949 §3.1 — major types 0–7 encode + decode
- RFC 8949 §3.2 — indefinite-length text/byte strings, arrays, maps (decoder)
- RFC 8949 §3.3 — IEEE 754 binary16 encode (opt-in) + decode (always-on)
- RFC 8949 §3.4 — tagged-item registry
- RFC 8949 §4.1 — preferred serialization (shortest-form integers always-on,
shortest-exact float widths)
- RFC 8949 §4.2 — deterministic-encoding profile (sorted map keys, no
indefinite-length, half-float disabled in deterministic mode)
- RFC 8949 §5.1 — strict-mode well-formedness; every malformed-input class
raises `CborError(MALFORMED)`
- RFC 8742 §2 + §3 — CBOR Sequences encode + decode + async-iterator stream

View File

@@ -0,0 +1,58 @@
# `ast`
## Purpose
Typed AST shapes per RFC 8610 §3 — the contract between the parser and
the validator (and any future code generator). Every CDDL syntactic
construct lands as a discriminated-union node so consumers can pattern-match
exhaustively.
## Features
- `CddlAst` — root: `{ rules: CddlRule[] }`.
- `CddlRule` — `{ name, generics, type }`.
- `CddlType` — discriminated union over: primitives, named refs (with
generic args), literal values (text / byte / int / uint / bool / null /
undefined), ranges, choice, arrays, maps, tagged items, control operators.
- `CddlGroupEntry` — an array/map entry with `occurrence`, optional `key`,
and a `type`.
- `CddlGroupKey` — text / uint / int. (Typed keys are reserved for a
future parser extension.)
- `CddlOccurrence` — `exact` / `optional` / `zero-or-more` / `one-or-more` /
`range`.
- `PrimitiveName` + `PRIMITIVE_NAMES` — frozen set of every recognised
prelude type from RFC 8610 §3.8.
## Usage: Basic Example
```ts
import type { CddlAst, CddlRule } from "@suaveplan/cbor/cddl";
function ruleNames(ast: CddlAst): string[] {
return ast.rules.map((r: CddlRule) => r.name);
}
```
## Usage: Advanced Example
```ts
import type { CddlType } from "@suaveplan/cbor/cddl";
function isOptionalString(t: CddlType): boolean {
return t.kind === "primitive" && t.name === "tstr";
}
```
## API Reference
See the type definitions in `ast.ts`. The AST is intentionally pure data
— no methods, no mutable state — so it round-trips through serialisation
without surprises.
## Implementation Notes
The AST is the public contract for downstream consumers (validators, code
generators, schema-based fuzzers). Adding a new node kind requires updating
both the parser (to produce it) and every walker (to consume it). The
`PRIMITIVE_NAMES` set is exported so consumers writing their own walkers
can recognise the prelude types without duplicating the literal union.

View File

@@ -0,0 +1,69 @@
# `lexer`
## Purpose
CDDL tokenizer per RFC 8610 §3 ABNF grammar. Reads a CDDL source string and
emits a sequence of `Token` objects terminated by an `eof` sentinel. Throws
`CborError(CDDL_PARSE_FAILED)` with `{ line, col }` context on lexical
errors so callers can surface precise diagnostics.
## Features
- Full token coverage for the subset the parser supports:
- Identifiers (with `_`/`@`/`-` continuation)
- Integer literals (`42`, `-7`, `0`)
- Quoted-string literals with `\n` / `\r` / `\t` / `\\` / `\"` escapes
- Hex byte-string literals `h'deadbeef'` (whitespace-tolerant)
- Punctuation: `=`, `/`, `//`, `*`, `+`, `?`, `..`, `...`, `[`, `]`,
`{`, `}`, `(`, `)`, `<`, `>`, `,`, `:`, `=>`, `#`, `.`
- Control operators (`.size`, `.regexp`, `.cbor`, `.within`, `.and`,
`.lt`, `.le`, `.gt`, `.ge`, `.eq`, `.ne`, `.default`, `.bits`)
- Line-comment skip via `;`
- Line + column tracking — every token carries its source position for
precise error messages downstream.
- CDDL float literals are deliberately not lexed: they would conflict with
the tagged-item syntax `#6.<n>(T)` (where `6.1` would otherwise lex as a
float). Authors needing float values use the `float` / `float32` / `float64`
primitive types instead.
## Usage: Basic Example
```ts
import { tokenize } from "@suaveplan/cbor/cddl";
const tokens = tokenize("foo = uint");
// [{ kind: "identifier", value: "foo", line: 1, col: 1 },
// { kind: "=", ... }, { kind: "identifier", value: "uint", ... },
// { kind: "eof", ... }]
```
## Usage: Advanced Example
```ts
import { tokenize } from "@suaveplan/cbor/cddl";
try {
tokenize("foo % bar");
} catch (e) {
// CborError(CDDL_PARSE_FAILED) with line/col context
}
```
## API Reference
- `tokenize(source: string): Token[]`
- `interface Token { kind: TokenKind; value: string | number | Uint8Array; line: number; col: number }`
- `type TokenKind` — literal union of every emitted token kind.
Throws `CborError(CDDL_PARSE_FAILED)` for: unterminated string / byte string,
unknown escape sequence, invalid hex byte string, odd-length hex literal,
unexpected character.
## Implementation Notes
The lexer is a single-pass recursive-descent character scanner. Multi-char
punctuation (`//`, `=>`, `..`, `...`) is matched ahead of single-char so
`/` versus `//` and `.` versus `..` versus `...` discriminate cleanly.
Identifiers never include `.` — this keeps `0..255` and `tstr .size`
parsing correctly without backtracking. Comments are skipped at the top
level of the loop.

View File

@@ -0,0 +1,71 @@
# `parser`
## Purpose
CDDL parser per RFC 8610 §3 grammar. Consumes the token stream from the
`tokenize` function and produces a typed AST (`CddlAst`) that the validator
walks. Supports the working subset of CDDL needed by real consumers (CWT,
COSE_Sign1, WebAuthn attestation, vault manifests, drive templates).
## Features
- Rule definitions: `name = T`, `name<T> = ...` (generic params).
- Type expressions: primitive prelude, named references, generic instantiation.
- Type composition: choice (`/`), inclusive (`..`) + exclusive (`...`) ranges,
parenthesised grouping.
- Group composition: arrays (`[ ... ]`), maps (`{ ... }`), occurrence
indicators (`?`, `*`, `+`, `*N`, `M*N`, `M*`), group choice (`//`).
- Map keys: identifier-`:`-T (text key), `"quoted":` T (text key with quoted
syntax), `<int> => T` (numeric key), `"quoted" => T`.
- Tagged items: `#6.<tag>(T)` produces `{ kind: "tagged", tag, inner }`.
- Literal values: text strings, byte strings (`h'…'`), integers (positive and
negative), `true`/`false`, `null`/`nil`, `undefined`.
- Control operators applied to types: `tstr .size 1..32`, `tstr .regexp "pattern"`,
`bstr .cbor T`. Both literal and range right-hand sides supported on `.size`.
## Usage: Basic Example
```ts
import { parseCddl } from "@suaveplan/cbor/cddl";
const ast = parseCddl(`
cwt = { iss: tstr, ? exp: int }
`);
```
## Usage: Advanced Example
```ts
import { parseCddl } from "@suaveplan/cbor/cddl";
const ast = parseCddl(`
attested-key = #6.18(cose-sign1)
cose-sign1 = [ protected: bstr, unprotected: { * any => any }, payload: bstr / nil, signature: bstr ]
protected = bstr .size 0..1024
pair<K, V> = [ K, V ]
`);
```
## API Reference
- `parseCddl(source: string): CddlAst`
Throws `CborError(CDDL_PARSE_FAILED)` with `{ line, col }` for: empty
source (no rules), missing `=`, unexpected token, non-integer range bound,
`#N` with N != 6.
## Implementation Notes
The parser is a hand-written recursive descent. Operator precedence
mirrors RFC 8610 §3:
- `type` (top): handles `/` choice
- `type1`: handles `..` / `...` ranges and control operators (`.size`,
`.regexp`, etc.); the `.size` case can take a range right-hand side
- `type2` (atom): primitives, references, literals, parenthesised groups,
arrays, maps, tagged items
Group choice (`//`) is implemented at the `[`/`{` body level — when the
parser encounters `//` between two sets of entries, it returns a
`{ kind: "choice", alternatives: [arrayA, arrayB] }` so the validator
can short-circuit on the first matching alternative.

View File

@@ -0,0 +1,84 @@
# `validate`
## Purpose
Walk a decoded CBOR value against a parsed CDDL schema and report the first
mismatch (with a JSON-pointer-like path). The validator is the run-time
counterpart to the parser — given a typed AST and a value, return
`{ valid, errors }` describing whether the value conforms.
## Features
- All primitive prelude types: `uint`, `nint`, `int`, `bstr`, `tstr`, `bool`,
`nil`, `null`, `undefined`, `any`, `number`, `text`, `bytes`, `float` /
`float16` / `float32` / `float64`, `tdate` (RFC 3339 string), `time`
(number or bigint epoch).
- Literal-value matching: text, byte, integer (signed and unsigned),
bool, null, undefined.
- Range types: inclusive (`..`) and exclusive (`...`).
- Choice: tries each alternative in isolation; first match wins.
- Arrays: positional `exact`-occurrence entries, optional, one-or-more,
zero-or-more, and `M*N` ranges. Extra entries past the schema are rejected.
- Maps: text and integer keys; required (`exact`) and optional (`?`) keys;
reports the missing key in error messages.
- Tagged items via `#6.<n>(T)` parse syntax → typed `{ kind: "tagged" }` AST.
- Named references: walks `name = T` rules transparently; entry rule is
selectable via `options.rule`.
- Control operators: `.size <int|range>` (string or bytes length),
`.regexp "pattern"` (RegExp match on text), `.cbor T` (left side is bytes).
Unknown control operators pass through (treated as informational
rather than fail-closed).
## Usage: Basic Example
```ts
import { validateCbor } from "@suaveplan/cbor/cddl";
const r = validateCbor({ iss: "alice", exp: 1700000000 }, `
cwt = { iss: tstr, ? exp: int }
`);
console.log(r.valid); // true
```
## Usage: Advanced Example
```ts
import { parseCddl, validateCbor } from "@suaveplan/cbor/cddl";
const schema = parseCddl(`
message = #6.18(cose-sign1)
cose-sign1 = [ bstr, { * any => any }, bstr / nil, bstr ]
`);
import { decode } from "@suaveplan/cbor";
import { Tagged } from "@suaveplan/cbor/tags";
const value = decode(wireBytes, { tagHandling: "raw" });
const r = validateCbor(value, schema, { rule: "message" });
if (!r.valid) {
for (const err of r.errors) {
console.error(`at ${err.path.join(".")}: expected ${err.expected}, got ${err.got}`);
}
}
```
## API Reference
- `validateCbor(value: unknown, schema: CddlAst | string, options?: { rule?: string }): CddlValidationResult`
- `interface CddlValidationResult { valid: boolean; errors: CddlValidationError[] }`
- `interface CddlValidationError { path: (string | number)[]; expected: string; got: string }`
Throws `CborError(CDDL_RULE_NOT_FOUND)` if `options.rule` names a rule
that doesn't exist; throws `CborError(CDDL_UNDEFINED_REFERENCE)` if a rule
body references an undefined name.
## Implementation Notes
The walk is a recursive AST switch dispatching on `type.kind`. Choice
validation runs each alternative against a fresh sub-context — the first
alternative whose walk produces zero errors wins. Map validation accepts
both `Map` instances and plain objects; the latter is wrapped via
`Object.entries` for uniform key lookup. Array entries are matched
greedily within their occurrence-range bound — no backtracking — which
is correct for the schemas this package targets but a future extension
could add limited backtracking for more permissive schemas.

View File

@@ -0,0 +1,67 @@
# `alg-map`
## Purpose
Bridge between IANA COSE Algorithm identifiers (`CoseAlg` literal strings or
the equivalent numeric labels from RFC 9053 §2) and the suaveplan-internal
algorithm identifiers consumed by `@suaveplan/crypto-curve`,
`@suaveplan/crypto-cipher`, and `@suaveplan/crypto-hash`. The bridge is the
single point where COSE / CWT / WebAuthn flows decide *which* suaveplan
primitive will run for a given algorithm label.
## Features
- Full IANA registry coverage for the algorithms the crypto packages
ship: ES256, ES384, ES512, EdDSA (Ed25519), RS256, RS384, RS512, HS256,
HS384, HS512, A128GCM, A192GCM, A256GCM, ChaCha20Poly1305.
- Accepts both the typed `CoseAlg` string and the raw IANA numeric label.
- Reverse mapping (`cryptoAlgToCoseAlg`) for outbound encoding.
- Throws `CborError(UNEXPECTED_TYPE)` for any algorithm the bridge does
not implement — this is the intentional point of failure when a caller
tries to use a primitive suaveplan does not (yet) ship.
## Usage: Basic Example
```ts
import { coseAlgToCryptoAlg } from "@suaveplan/cbor/cose";
const alg = coseAlgToCryptoAlg("ES256"); // "ecdsa-p256-sha256"
```
## Usage: Advanced Example
```ts
import { coseAlgToCryptoAlg, decodeCoseKey } from "@suaveplan/cbor/cose";
// CWT verification dispatch
const key = decodeCoseKey(keyBytes);
const cryptoAlg = coseAlgToCryptoAlg(key.alg ?? "ES256");
switch (cryptoAlg) {
case "ecdsa-p256-sha256":
// hand off to @suaveplan/crypto-curve.p256.verify
break;
case "ed25519":
// hand off to @suaveplan/crypto-curve.ed25519.verify
break;
// ...
}
```
## API Reference
- `coseAlgToCryptoAlg(alg: CoseAlg | number): suaveplanCryptoAlg`
- `cryptoAlgToCoseAlg(alg: suaveplanCryptoAlg): CoseAlg`
- `type suaveplanCryptoAlg` — literal-typed union of the suaveplan-internal
algorithm IDs the bridge resolves to.
Throws `CborError(UNEXPECTED_TYPE)` for unknown numeric labels and for
algorithm strings outside the supported set.
## Implementation Notes
The bridge is a static lookup table — adding a new algorithm requires a
single new entry in both the `CoseAlg` literal type and the `ALG_BRIDGE`
record, plus a corresponding entry in the IANA-driven `COSE_ALG_LABELS`.
Keeping the conversion in a separate module (rather than threading raw
strings through the codec) ensures every inbound algorithm label crosses
exactly one validation boundary before reaching the cryptographic core.

View File

@@ -0,0 +1,71 @@
# `cose-key`
## Purpose
Decodes and encodes RFC 9052 §7 `COSE_Key` structures. The decoder maps an
RFC-encoded CBOR map onto a typed discriminated union (`Ec2Key | OkpKey |
RsaKey | SymmetricKey`); the encoder reverses the mapping with deterministic
encoding so two keys with identical fields always produce byte-identical
output. WebAuthn attestation public keys, COSE_Sign1 signing keys, and CWT
verification keys all flow through this module.
## Features
- Discriminated union by `kty` — TypeScript narrows on `key.kty === "EC2"`
etc. without explicit casts.
- Per-key-type field validation: EC2 (`crv` + `x` + `y` + optional `d`),
OKP (`crv` + `x` + optional `d`), RSA (`n` + `e` + optional `d`/`p`/`q`/
`dP`/`dQ`/`qInv`), Symmetric (`k`).
- Common fields (`alg`, `kid`, `key_ops`, `baseIV`) parsed once and applied
uniformly across all key types.
- Throws `CborError(UNEXPECTED_TYPE)` for malformed maps, unknown labels,
wrong inner types, and missing required fields.
- Encoder uses RFC 8949 §4.2 deterministic encoding so the byte output is
stable across runs — required for COSE structures that include the key
bytes in signed/verified payloads.
## Usage: Basic Example
```ts
import { decodeCoseKey, encodeCoseKey } from "@suaveplan/cbor/cose";
const key = decodeCoseKey(bytes);
if (key.kty === "EC2") {
console.log("ES256 public key", key.x, key.y);
}
const wire = encodeCoseKey(key);
```
## Usage: Advanced Example
```ts
import { decodeCoseKey } from "@suaveplan/cbor/cose";
import { coseAlgToCryptoAlg } from "@suaveplan/cbor/cose";
// WebAuthn attestation flow
const attestationKey = decodeCoseKey(authData.credentialPublicKey);
if (attestationKey.kty !== "EC2") {
throw new Error("expected EC2 attestation key");
}
const cryptoAlg = coseAlgToCryptoAlg(attestationKey.alg ?? "ES256");
// Hand off to @suaveplan/crypto-curve to verify the signature.
```
## API Reference
- `decodeCoseKey(bytes: Uint8Array): CoseKey` — decode a CBOR-encoded
`COSE_Key` byte string into the typed union. Throws `CborError` for any
structural defect.
- `encodeCoseKey(key: CoseKey): Uint8Array` — deterministic-encode the key.
- See `cose.types.ts` for `CoseKey`, `Ec2Key`, `OkpKey`, `RsaKey`,
`SymmetricKey`, `CoseAlg`, `CoseCurve`, `CoseKeyOp`.
## Implementation Notes
The decoder tolerates extra map entries — RFC 9052 §7.1 explicitly permits
forward-compatible additions, and rejecting unknown labels would break
forward compatibility. The encoder, by contrast, emits only the fields
defined in the typed shape so it cannot accidentally leak extension data
that was never modelled. Common fields are read once into a small `CommonOut`
record and spread into every key shape — this avoids drift between the four
per-kty readers when a new common field is added in the future.

View File

@@ -0,0 +1,55 @@
# `encrypt`
## Purpose
`COSE_Encrypt` (RFC 9052 §5.1) — multi-recipient AEAD-encrypted COSE
message in CBOR tag 96. Each recipient holds its own wrapped copy of the
content-encryption key (CEK); the body carries one ciphertext encrypted
under the CEK. This module ships encode/decode of the wire shape plus a
kid-based recipient lookup.
## Features
- `encodeCoseEncrypt` builds the 4-tuple `[bodyProtected, bodyUnprotected,
ciphertext, recipients]`. Per-recipient triples
`[recipientProtected, recipientUnprotected, encryptedKey]` carry the
wrapped-CEK bytes verbatim — KEK wrap is delegated to whatever scheme
the recipient declares.
- `decodeCoseEncrypt` recovers the typed view including each recipient's
`protectedRaw` for any per-recipient verification flow.
- `findEncryptRecipientByKid` does a `kid` byte compare across all
recipients (looking in protected then unprotected headers).
- Empty recipient arrays are rejected per §5.1.
## Usage
```ts
import { encodeCoseEncrypt, decodeCoseEncrypt, findEncryptRecipientByKid } from "@suaveplan/cbor/cose";
const wire = encodeCoseEncrypt({
protectedHeader: { alg: "A256GCM" },
unprotectedHeader: { iv },
ciphertext: aeadOutput,
recipients: [
{ unprotectedHeader: { kid: kidA }, encryptedKey: wrappedCekA },
{ unprotectedHeader: { kid: kidB }, encryptedKey: wrappedCekB },
],
});
const msg = decodeCoseEncrypt(wire);
const mine = findEncryptRecipientByKid(msg, kidA);
```
## API
- `encodeCoseEncrypt(input)` / `decodeCoseEncrypt(bytes)`.
- `findEncryptRecipientByKid(msg, kid): CoseRecipient | undefined`.
- `COSE_ENCRYPT_TAG = 96`.
## Notes
The CEK wrapping is handled by the caller — this module is the wire-shape
codec. Common KEK schemes (`A128KW`, `A256KW`, `direct`, `ECDH-ES`,
`ECDH-ES+A256KW`) all serialise into the recipient bucket the same way:
the wrapped key bytes go in slot 2, the algorithm declaration goes in the
recipient's protected header.

View File

@@ -0,0 +1,54 @@
# `encrypt0`
## Purpose
`COSE_Encrypt0` (RFC 9052 §5.2) — single-recipient AEAD-encrypted COSE
message in CBOR tag 16. Wraps AES-256-GCM and ChaCha20-Poly1305 from
`@suaveplan/crypto-cipher` and binds the ciphertext to the canonical
`Enc_structure` (§5.3) so external AAD propagates through the AEAD
authenticator.
## Features
- `encryptCoseEncrypt0` performs the AEAD computation and returns a fully-
formed COSE message; the IV automatically lands in the unprotected header
unless the caller already placed it there.
- `decryptCoseEncrypt0` resolves the IV from either the unprotected or
protected header, recomputes `Enc_structure`, and authenticates plus
decrypts in one call. Tampered ciphertext / wrong key / wrong AAD all
raise `CborError(COSE_AEAD_FAILED)`.
- Tagged + tagless wire forms supported.
- AES-256-GCM and ChaCha20-Poly1305 covered; ES384/ES512/AES-128-GCM/etc.
are explicit "not supported" errors so the gap is visible.
## Usage
```ts
import { encryptCoseEncrypt0, decryptCoseEncrypt0, decodeCoseEncrypt0 } from "@suaveplan/cbor/cose";
const wire = encryptCoseEncrypt0({
protectedHeader: { alg: "A256GCM" },
plaintext,
key,
iv,
});
const msg = decodeCoseEncrypt0(wire);
const back = decryptCoseEncrypt0(msg, key);
```
## API
- `encryptCoseEncrypt0(input)` / `encodeCoseEncrypt0(input)`.
- `decodeCoseEncrypt0(bytes): CoseEncrypt0`.
- `decryptCoseEncrypt0(msg, key, options?): Uint8Array` — throws
`CborError(COSE_AEAD_FAILED)` on auth failure.
- `COSE_ENCRYPT0_TAG = 16`.
## Notes
The AEAD primitive's AAD argument receives the canonical `Enc_structure`
bytes — RFC 9052 §5.3 dictates this binding so an attacker cannot replay
ciphertext under a different protected header. External AAD adds a second
layer of context (e.g. transaction-id or session-id) and is mixed in as the
third element of the structure before encoding.

View File

@@ -0,0 +1,43 @@
# `headers`
## Purpose
Typed `CoseHeader` map shape covering RFC 9052 §3.1 + IANA COSE Header
Parameters (alg, crit, content type, kid, IV, Partial IV, counter signature).
Two helpers — `mapToHeader` and `headerToMap` — round-trip between the typed
shape and the plain CBOR map representation that lands on the wire.
## Features
- `alg` is typed as `CoseAlg` so the algorithm union narrows downstream.
- `crit` rejects empty arrays and non-numeric labels per §3.1.
- `contentType` accepts both numeric (IANA CoAP Content-Format) and string
(RFC 6838) forms.
- All byte-string fields validated at the boundary; non-bytes throws
`COSE_HEADER_INVALID`.
- Round-trip is total: every field in the typed shape lands on the wire and
vice versa, with no implicit drops.
## Usage
```ts
import { headerToMap, mapToHeader } from "@suaveplan/cbor/cose";
import { encode, decode } from "@suaveplan/cbor";
const m = headerToMap({ alg: "ES256", kid: new Uint8Array([0xab]) });
const wire = encode(m, { deterministic: true });
const back = mapToHeader(decode(wire) as Map<unknown, unknown>);
```
## API
- `interface CoseHeader` — typed shape.
- `mapToHeader(m: Map<unknown, unknown>): CoseHeader`.
- `headerToMap(h: CoseHeader): Map<number, unknown>`.
## Notes
`crit` must be non-empty per §3.1: an empty list is malformed because it
implies "every header is unsupported", which contradicts the receiver's
ability to verify the message. The guard catches encoder bugs that would
otherwise emit `[]` for a header set with no critical entries.

View File

@@ -0,0 +1,70 @@
# `jwk`
## Purpose
Round-trip converter between RFC 9052 `COSE_Key` instances and RFC 7517 JWK
shapes. WebAuthn attestation flows often need to hand a public key to a
WebCrypto-style verifier that consumes JWK; conversely, server-issued
JWK-formatted keys may need to land in a CBOR-encoded COSE structure. The
two conversions preserve every field that has a defined cross-format
mapping.
## Features
- `coseKeyToJwk(key)` — accepts any `CoseKey` discriminant (EC2, OKP, RSA,
Symmetric) and emits the matching JWK (`EC`, `OKP`, `RSA`, `oct`).
- `jwkToCoseKey(jwk)` — reverse mapping with full byte-string round-trip.
- Byte-string fields use RFC 4648 §5 base64url encoding without padding.
- `kid` is treated as UTF-8 bytes on the COSE side and a string on the JWK
side; conversion is lossy only if a non-UTF-8 kid is in play (rare, and
RFC 9052 §3.1 documents the convention).
- `alg` round-trips through a fixed COSE↔JWK label table so the typed
algorithm lands consistently on either side.
## Usage: Basic Example
```ts
import { coseKeyToJwk, jwkToCoseKey } from "@suaveplan/cbor/cose";
const jwk = coseKeyToJwk(coseKey);
const back = jwkToCoseKey(jwk);
```
## Usage: Advanced Example
```ts
import { decodeCoseKey, coseKeyToJwk } from "@suaveplan/cbor/cose";
// Attestation key from CTAP2
const cose = decodeCoseKey(attestationKeyBytes);
const jwk = coseKeyToJwk(cose);
// Hand off to WebCrypto
const cryptoKey = await crypto.subtle.importKey(
"jwk",
jwk,
{ name: "ECDSA", namedCurve: "P-256" },
false,
["verify"],
);
```
## API Reference
- `coseKeyToJwk(key: CoseKey): Jwk` — discriminator-aware conversion to JWK.
- `jwkToCoseKey(jwk: Jwk): CoseKey` — reverse conversion.
- `Jwk = Ec2Jwk | OkpJwk | RsaJwk | SymmetricJwk` (typed union).
Throws `CborError(UNEXPECTED_TYPE)` for incompatible curves (EC2 with a
non-P curve, OKP with a non-Ed/X curve), unknown JWK `alg` strings, and
malformed base64url input via the underlying `atob`.
## Implementation Notes
base64url encoding is implemented inline with `btoa`/`atob` rather than
pulling in `@suaveplan/crypto-utils` so this module stays free of crypto
deps. Standard base64 padding (`=`) is stripped on output and re-padded on
input — the RFC 4648 §5 spec allows omission of padding and major JWK
producers do strip it. RSA JWKs use lowercase param names (`dp`, `dq`, `qi`)
per RFC 7518 §6.3.2; COSE_Key uses the labels from RFC 8230 §4 (`-6`, `-7`,
`-8`). The bridge preserves the field-by-field correspondence.

View File

@@ -0,0 +1,55 @@
# `mac`
## Purpose
`COSE_Mac` (RFC 9052 §6.1) — multi-recipient MAC message in CBOR tag 97.
Each recipient gets a wrapped copy of the content-MAC key (CMK); the body
itself carries one MAC computed over the payload using the CMK. This module
ships encode/decode of the wire shape plus a kid-based recipient lookup.
## Features
- `encodeCoseMac` builds the 5-tuple `[bodyProtected, bodyUnprotected,
payload, tag, recipients]`. Per-recipient triples
`[recipientProtected, recipientUnprotected, encryptedKey]` carry the
wrapped-key bytes verbatim — wrap/unwrap is delegated to whatever scheme
the recipient header declares (direct, KEK, ECDH-ES, etc.).
- `decodeCoseMac` recovers the typed `CoseMac` view. Each recipient lands
in `msg.recipients[]` with its own `protectedRaw` byte string ready for
any per-recipient verification flow that needs it.
- `findRecipientByKid` does a constant-time-ish byte compare over the
recipient's `kid` (consulted in protected then unprotected headers).
- Empty recipient arrays are rejected — RFC 9052 §6.1 requires at least one.
## Usage
```ts
import { encodeCoseMac, decodeCoseMac, findRecipientByKid } from "@suaveplan/cbor/cose";
const wire = encodeCoseMac({
protectedHeader: { alg: "HS256" },
payload,
tag: macTag,
recipients: [
{ unprotectedHeader: { kid: kidA }, encryptedKey: wrapped },
{ unprotectedHeader: { kid: kidB }, encryptedKey: wrappedB },
],
});
const msg = decodeCoseMac(wire);
const mine = findRecipientByKid(msg, kidA);
```
## API
- `encodeCoseMac(input)` / `decodeCoseMac(bytes)`.
- `findRecipientByKid(msg, kid): CoseRecipient | undefined`.
- `COSE_MAC_TAG = 97`.
## Notes
The body's `tag` is computed over the canonical `MAC_structure` (RFC 9052
§6.3) using the CMK. This module does not implement the wrapping/unwrapping
of the CMK to each recipient — that lives one layer up, where the recipient-
specific algorithm (`A128KW`, `direct`, `ECDH-ES + A256KW`, …) is dispatched.
The helper here gives a typed shape for callers to plug those wraps into.

View File

@@ -0,0 +1,50 @@
# `mac0`
## Purpose
`COSE_Mac0` (RFC 9052 §6.2) — single-recipient MAC message in CBOR tag 17.
Used for symmetric-key authentication of CWT claim sets, vault attachment
manifests, and any payload where the producer and verifier share a key.
## Features
- `createCoseMac0` computes the HMAC tag in one call (HMAC-SHA-{256,384,512}
via `@suaveplan/crypto-hash`) and returns a fully-formed `COSE_Mac0` byte
sequence.
- `encodeCoseMac0` emits a pre-computed tag (useful when the MAC is generated
outside the call site, e.g. by an HSM).
- `decodeCoseMac0` parses both tagged and tagless wire forms.
- `verifyCoseMac0` re-computes the tag in constant time and rejects on any
mismatch.
- External AAD propagated through the MAC computation — required for any
CWT claim set carrying authenticated context.
## Usage
```ts
import { createCoseMac0, decodeCoseMac0, verifyCoseMac0 } from "@suaveplan/cbor/cose";
const wire = createCoseMac0({
protectedHeader: { alg: "HS256" },
payload: new TextEncoder().encode("hello"),
key,
});
const msg = decodeCoseMac0(wire);
const ok = verifyCoseMac0(msg, key);
```
## API
- `createCoseMac0(input): Uint8Array` — sign-and-encode in one call.
- `encodeCoseMac0(input): Uint8Array` — wire encode for a pre-computed tag.
- `decodeCoseMac0(bytes): CoseMac0` — parse tagged or tagless input.
- `verifyCoseMac0(msg, key, options?): boolean` — constant-time tag compare.
- `COSE_MAC0_TAG = 17`
## Notes
Constant-time comparison is implemented inline rather than reaching for
`@suaveplan/crypto-utils` to keep the dep graph minimal. The XOR-accumulator
returns `false` for any byte difference and for length mismatches — the
shape every Web-Crypto-style timing-safe equality function uses.

View File

@@ -0,0 +1,48 @@
# `sig-structure`
## Purpose
Builders for the canonical input to COSE signature, MAC, and AEAD
computations per RFC 9052 §4.4 / §6.3 / §5.3. Every signed/verified COSE
message commits to a deterministically-encoded array whose first element is
a context discriminator — `Signature1`, `Signature`, `CounterSignature`,
`MAC`, `MAC0`, `Encrypt`, `Encrypt0`, `Enc_Recipient`. The discriminator
prevents confusion attacks where a signature crafted in one context could
be replayed in another.
## Features
- `buildSign1Structure(protectedRaw, externalAad, payload)` — RFC 9052 §4.4
Sig_structure1.
- `buildSignStructure(context, bodyProtected, signProtected, externalAad, payload)`
— RFC 9052 §4.4 Sig_structure (Signature + CounterSignature).
- `buildMacStructure(context, protectedRaw, externalAad, payload)` —
RFC 9052 §6.3 MAC_structure (MAC + MAC0).
- `buildEncStructure(context, protectedRaw, externalAad)` — RFC 9052 §5.3
Enc_structure (Encrypt + Encrypt0 + Enc_Recipient).
- All output is deterministically CBOR-encoded so two callers with the same
inputs produce byte-identical bytes.
## Usage
```ts
import { buildSign1Structure } from "@suaveplan/cbor/cose";
import * as ed25519 from "@suaveplan/crypto-curve/ed25519";
const tbs = buildSign1Structure(protectedRaw, new Uint8Array(0), payload);
const sig = ed25519.sign(tbs, privateKey);
```
## API
- `buildSign1Structure(protectedRaw: Uint8Array, externalAad: Uint8Array, payload: Uint8Array): Uint8Array`
- `buildSignStructure(context: "Signature" | "CounterSignature", bodyProtected, signProtected, externalAad, payload): Uint8Array`
- `buildMacStructure(context: "MAC" | "MAC0", protectedRaw, externalAad, payload): Uint8Array`
- `buildEncStructure(context: "Encrypt" | "Encrypt0" | "Enc_Recipient", protectedRaw, externalAad): Uint8Array`
## Notes
The protected-bucket arguments are the SERIALISED byte string per §3 — not
the inner map. An empty protected bucket is a zero-length byte string; the
encoder helpers in `mac0.ts` and `sign1.ts` produce this form when the
caller's `protectedHeader` is empty.

View File

@@ -0,0 +1,58 @@
# `sign`
## Purpose
`COSE_Sign` (RFC 9052 §4.1) — multi-signer COSE message in CBOR tag 98.
Use when a payload needs to be co-signed by multiple parties (e.g. multi-
party attestation, escrowed signing). Each signer carries its own protected
header (typically with its own `alg`) and produces an independent signature
over the canonical `Sig_structure` for context `"Signature"`.
## Features
- `encodeCoseSign` builds the 4-tuple `[bodyProtected, bodyUnprotected,
payload, signatures]` with deterministic encoding and per-signer
`[sigProtected, sigUnprotected, signature]` triples.
- `decodeCoseSign` recovers the typed `CoseSign` view, including
`protectedRaw` byte strings on every level needed to recompute the
five-element `Sig_structure`.
- `verifyCoseSignAny` walks the signers in order against a 1:1 candidate-
key array. Returns the first index whose signature verifies, or `-1` on
no match. Caller decides whether one valid signer is enough or whether all
signers must verify (loop the array).
- Currently dispatches to ES256 + EdDSA via `@suaveplan/crypto-curve`;
unsupported algs return `false` (signer is skipped) rather than throwing
so the helper can scan a heterogeneous array without aborting.
## Usage
```ts
import { encodeCoseSign, decodeCoseSign, verifyCoseSignAny } from "@suaveplan/cbor/cose";
const wire = encodeCoseSign({
payload,
signatures: [
{ protectedHeader: { alg: "ES256" }, signature: sig1 },
{ protectedHeader: { alg: "EdDSA" }, signature: sig2 },
],
});
const msg = decodeCoseSign(wire);
const matchIndex = verifyCoseSignAny(msg, [keyA, keyB]);
```
## API
- `encodeCoseSign(input)` / `decodeCoseSign(bytes)`.
- `verifyCoseSignAny(msg, keys, options?): number` — returns matching index
or `-1`. Length mismatch between `keys` and `msg.signatures` throws
`UNEXPECTED_TYPE`.
- `COSE_SIGN_TAG = 98`.
## Notes
Sig_structure for multi-signer is 5-element:
`[context, body_protected, sign_protected, external_aad, payload]` — the
extra `sign_protected` discriminates the signer's own protected header from
the message-wide one. Both must be the SERIALISED byte strings, not the
inner maps.

View File

@@ -0,0 +1,60 @@
# `sign1`
## Purpose
`COSE_Sign1` (RFC 9052 §4.2) — single-signer COSE message in CBOR tag 18.
WebAuthn attestation, COSE-signed firmware, and CWT signature flows all
land here. This module ships the encode/decode/verify triple plus a typed
`CoseSign1` view.
## Features
- `encodeCoseSign1` produces both tagged (default) and tagless wire forms.
- `decodeCoseSign1` accepts either form transparently.
- `verifyCoseSign1` wires `@suaveplan/crypto-curve` (ES256, EdDSA) and uses
the canonical `Sig_structure1` from `sig-structure.ts`.
- Public-key argument is polymorphic: typed `Ec2Key` / `OkpKey` (the helper
builds the wire-level point form internally) OR a raw byte-string key for
callers that already have a primitive shape.
- `null` payload (detached signature) decodes correctly; verify rejects with
a clear "not yet supported" error.
## Usage
```ts
import { decodeCoseSign1, encodeCoseSign1, verifyCoseSign1 } from "@suaveplan/cbor/cose";
import { buildSign1Structure } from "@suaveplan/cbor/cose";
import * as ed25519 from "@suaveplan/crypto-curve/ed25519";
// Sign
const draft = decodeCoseSign1(
encodeCoseSign1({ protectedHeader: { alg: "EdDSA" }, payload, signature: new Uint8Array(64) }),
);
const tbs = buildSign1Structure(draft.protectedRaw, new Uint8Array(0), payload);
const sig = ed25519.sign(tbs, privateKey);
const wire = encodeCoseSign1({ protectedHeader: { alg: "EdDSA" }, payload, signature: sig });
// Verify
const msg = decodeCoseSign1(wire);
const ok = verifyCoseSign1(msg, publicKey);
```
## API
- `encodeCoseSign1(input: EncodeCoseSign1Input): Uint8Array`
- `decodeCoseSign1(bytes: Uint8Array): CoseSign1`
- `verifyCoseSign1(msg: CoseSign1, key: Ec2Key | OkpKey | Uint8Array, options?): boolean`
- `COSE_SIGN1_TAG = 18`
`CoseSign1` carries `protectedRaw` (the serialised inner-map bytes) so
callers can recompute Sig_structure1 verbatim — critical when verifying a
message they did not produce.
## Notes
ES256 uses noble-p256's compact `(r||s)` signature form and consumes a
SHA-256 digest of the TBS bytes (matches RFC 9053 §2.1). EdDSA verifies
the raw TBS bytes directly per RFC 8032 §5.1.7. ES384 and ES512 throw
"not implemented" today — `@suaveplan/crypto-curve` does not ship the P-384
or P-521 primitives yet, and the bridge surfaces the gap rather than
silently falling back.

View File

@@ -0,0 +1,53 @@
# `claims`
## Purpose
Encode and decode RFC 8392 CBOR Web Token claim sets. The seven IANA-
registered claims (`iss`, `sub`, `aud`, `exp`, `nbf`, `iat`, `cti`) are
typed; private claims keyed by IANA-numeric label survive the round-trip
unchanged.
## Features
- `encodeCwt(claims)` — deterministic claim-set encoding so token bytes are
byte-stable across runs.
- `decodeCwt(bytes)` — typed `CwtClaims` output with full validation at the
field boundaries.
- Time fields (`exp`, `nbf`, `iat`) accept `Date` on the input side; the
decoder produces `Date` from raw integers, bigints, or values wrapped in
CBOR tag 1 (epoch).
- `aud` accepts both `string` and `string[]` per RFC 8392 §3.1.
- Private claims threaded through `claims.private` keyed by integer label
number — opaque to the typed claims but preserved on round-trip.
## Usage
```ts
import { decodeCwt, encodeCwt } from "@suaveplan/cbor/cwt";
const wire = encodeCwt({
iss: "https://issuer.example",
exp: new Date("2030-01-01"),
cti: new Uint8Array([0x01, 0x02]),
private: { 100: "tenant-a" },
});
const claims = decodeCwt(wire);
```
## API
- `encodeCwt(claims: CwtClaims): Uint8Array`
- `decodeCwt(bytes: Uint8Array): CwtClaims`
Throws `CborError(UNEXPECTED_TYPE)` for malformed claim labels, wrong inner
types, non-string `iss`/`sub`, malformed `aud`, etc.
## Notes
`exp`/`nbf`/`iat` always serialise as integer epoch seconds rather than
`Tagged(1, …)` — both forms are valid on the wire per RFC 8392, but the
integer form is shorter and the deterministic profile prefers shortest-form.
Private claims must use integer labels per the CWT registry; non-integer
keys are silently dropped on encode (the `Object.entries` walk filters
them) — the typed `private` shape is keyed by `number`.

View File

@@ -0,0 +1,51 @@
# `verify`
## Purpose
End-to-end CWT verification: parse the COSE wrapper, verify the signature
or MAC, decode the claim set, and check the standard time-window plus
audience plus issuer claims. Two entry points cover the two RFC 8392
profiles — `verifyCwtSign1` for COSE_Sign1-wrapped tokens and
`verifyCwtMac0` for COSE_Mac0-wrapped tokens.
## Features
- Time-window enforcement honours `exp` (with optional clock-skew tolerance)
and `nbf` per RFC 8392 §7.
- Audience checks accept both `string` and `string[]` audience claims.
- Issuer check is exact-match.
- External AAD propagates through to the underlying COSE verify.
- Distinct error codes per failure (`CWT_SIGNATURE_INVALID`, `CWT_EXPIRED`,
`CWT_NOT_YET_VALID`, `CWT_AUDIENCE_MISMATCH`, `CWT_ISSUER_MISMATCH`) so
callers can branch on the root cause.
## Usage
```ts
import { verifyCwtSign1 } from "@suaveplan/cbor/cwt";
const result = verifyCwtSign1(tokenBytes, publicKey, {
audience: "my-service",
issuer: "trusted-issuer",
clockSkewMs: 30_000,
});
console.log(result.claims.sub);
```
## API
- `verifyCwtSign1(bytes, key, options?): CwtVerifyResult`
- `verifyCwtMac0(bytes, key, options?): CwtVerifyResult`
- `CwtVerifyOptions`: `externalAad`, `audience`, `issuer`, `nowMs`,
`clockSkewMs`.
- `CwtVerifyResult`: `{ valid: true; claims: CwtClaims }` — the function
throws rather than returning `valid: false`, so the type carries no
failure variant. Callers `try/catch` on the typed `CborErrorCode`.
## Notes
Detached payloads (a `null` payload field on the COSE wrapper) are
explicitly rejected — RFC 8392 §1 says "The CWT structure is a CBOR
representation of a JWT-style claims set", which presumes the claim bytes
are carried inline. A future enhancement could accept a caller-provided
detached payload buffer; for now the failure mode is loud.

View File

@@ -0,0 +1,91 @@
# `decoder`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs --> - half-float — Decodes 2 big-endian bytes representin…
- [half-float](./half-float.md) — Decodes 2 big-endian bytes representing IEEE 754 binary16 to a JavaScript `number`. The decoder is always available — n…
- [well-formed](./well-formed.md) — `isWellFormed(bytes)` returns `true` iff `bytes` represent a single well-formed CBOR item per RFC 8949 §5.1. The functi…
- half-float — Decodes 2 big-endian bytes representin…
- [half-float](./half-float.md) — Decodes 2 big-endian bytes representing IEEE 754 binary16 to a JavaScript `number`. The decoder is always available — n…
- [well-formed](./well-formed.md) — `isWellFormed(bytes)` returns `true` iff `bytes` represent a single well-formed CBOR item per RFC 8949 §5.1. The functi…
- half-float — Decodes 2 big-endian bytes representin…
- [half-float](./half-float.md) — Decodes 2 big-endian bytes representing IEEE 754 binary16 to a JavaScript `number`. The decoder is always available — n…
- [well-formed](./well-formed.md) — `isWellFormed(bytes)` returns `true` iff `bytes` represent a single well-formed CBOR item per RFC 8949 §5.1. The functi…
- half-float — Decodes 2 big-endian bytes representin…
- [half-float](./half-float.md) — Decodes 2 big-endian bytes representing IEEE 754 binary16 to a JavaScript `number`. The decoder is always available — n…
- [well-formed](./well-formed.md) — `isWellFormed(bytes)` returns `true` iff `bytes` represent a single well-formed CBOR item per RFC 8949 §5.1. The functi…
- [half-float](./half-float.md) — Decodes 2 big-endian bytes representing IEEE 754 binary16 to a JavaScript `number`. The decoder is always available — n…
- [well-formed](./well-formed.md) — `isWellFormed(bytes)` returns `true` iff `bytes` represent a single well-formed CBOR item per RFC 8949 §5.1. The functi…
## Purpose
The CBOR decoder materialises an RFC 8949 byte sequence into a JavaScript
value. It defaults to strict mode — every §5.1 well-formedness rule is
enforced, including rejection of trailing bytes after the top-level item,
truncation, malformed UTF-8, unmatched break stops, and reserved minor-type
encodings. Lax mode applies §5.2 only and is provided for interop with
producers that emit looser CBOR.
## Features
- All seven major types decode end-to-end, including indefinite-length
byte/text strings and indefinite-length arrays/maps.
- Half-float (binary16) decode — always available; never gated by an option.
- BigInt fallback: `u64` values exceeding `Number.MAX_SAFE_INTEGER` decode to
`bigint`. Negative `u64` values likewise.
- Pluggable tag handling: callers pass a `Map<number, TagHandler>` and the
decoder substitutes handler output for the inner value. Unknown tags
return as `Tagged` per RFC 8949 §3.4.
- Bounded recursion + bounded item count — `maxDepth` (default 64) and
`maxItems` (default 1_000_000) caps prevent stack and memory blowups on
adversarial inputs.
- `decodeOne` returns the consumed byte count, enabling streaming and
sequence decoders to advance their cursors precisely.
## Usage: Basic Example
```ts
import { decode } from "@suaveplan/cbor/decoder";
const value = decode(bytes); // strict mode by default
```
## Usage: Advanced Example
```ts
import { decode } from "@suaveplan/cbor/decoder";
import { defaultTagHandlers } from "@suaveplan/cbor/tags";
// Decode with the IANA built-in tag handlers (Date, URL, RegExp, ...)
const tags = defaultTagHandlers().snapshot();
const value = decode(bytes, { mode: "strict" }, tags);
```
## API Reference
- `decode(bytes: Uint8Array, options?: DecoderOptions, tags?: ReadonlyMap<number, TagHandler>): DecodedValue`
- `decodeOne(bytes, options?, tags?): { value: DecodedValue; consumed: number }`
- `DecoderOptions`:
- `mode?: "strict" | "lax"` — default `"strict"`.
- `tagHandling?: "registered" | "raw"` — default `"registered"`. `"raw"`
yields `Tagged` instances even when a handler exists.
- `maxDepth?: number`, `maxItems?: number`, `allowTrailingBytes?: boolean`.
Throws `CborError` with codes `MALFORMED`, `UNEXPECTED_TYPE`,
`NESTED_TOO_DEEP`, `LENGTH_OVERFLOW`, `INDEFINITE_NOT_ALLOWED`.
## Implementation Notes
The decoder is single-pass and zero-copy where possible — `subarray` views
into the input buffer back returned `Uint8Array` byte strings. Indefinite
text/byte strings are reassembled chunk by chunk; nested indefinite chunks
are explicitly rejected per §3.2. UTF-8 decoding uses a fatal-mode
`TextDecoder` so invalid sequences raise `MALFORMED` rather than silently
substituting U+FFFD. The `decodeOne` exit point exposes the cursor position so
upstream sequence + streaming decoders can advance the input without
re-tokenising.

View File

@@ -0,0 +1,52 @@
# `half-float` (decoder side)
## Purpose
Decodes 2 big-endian bytes representing IEEE 754 binary16 to a JavaScript
`number`. The decoder is always available — never gated by an option —
because well-formed CBOR producers may emit `f9 xx xx` for any float value
that fits binary16 exactly, and the decoder MUST accept it per RFC 8949 §3.3.
## Features
- Full IEEE 754 binary16 coverage: sign, 5-bit exponent, 10-bit mantissa.
- ±0, ±Infinity, NaN, subnormals, normal values all decode correctly.
- Sub-normals decode via `mant * 2^-24`; normals via `(1 + mant/1024) * 2^(exp-15)`.
- NaN payloads are not preserved — JavaScript collapses NaN payloads, which
RFC 8949 §3.3 explicitly permits.
## Usage: Basic Example
```ts
import { decodeFloat16 } from "@suaveplan/cbor/decoder";
const value = decodeFloat16(0x3e, 0x00); // 1.5
```
## Usage: Advanced Example
```ts
import { decode, decodeFloat16 } from "@suaveplan/cbor/decoder";
// Manual half-float decode for an embedded protocol that prepends a length
function decodeFrame(frame: Uint8Array): number {
if (frame[0] !== 0xf9) throw new Error("expected binary16 head");
return decodeFloat16(frame[1] ?? 0, frame[2] ?? 0);
}
```
## API Reference
- `decodeFloat16(hi: number, lo: number): number` — accepts the two big-endian
bytes (high then low). Output is a JavaScript `number`. No errors thrown —
every 16-bit input represents a valid IEEE 754 binary16 value.
## Implementation Notes
The decoder uses bitfield extraction rather than a `DataView` because half-
float is not a primitive `Float16` type in any current JavaScript runtime,
and constructing a 16-bit view would require a dedicated buffer for each
call. Two integer arguments side-step that allocation cost on the hot path.
The function is intentionally branch-light — a single `if (exp === 0x1f)`
chooses between specials and finite values, and `exp === 0` selects the
subnormal scaling path.

View File

@@ -0,0 +1,57 @@
# `well-formed`
## Purpose
`isWellFormed(bytes)` returns `true` iff `bytes` represent a single
well-formed CBOR item per RFC 8949 §5.1. The function is the canonical
predicate that callers use BEFORE attempting a real decode — for example
gateways that need to reject malformed payloads with a specific error code
before threading them into business logic, or stream framers that need to
identify the boundary between two items without committing to materialising
either one.
## Features
- Full §5.1 coverage: trailing-byte rejection, truncation rejection, malformed
UTF-8 rejection, unmatched break-stop rejection, indefinite-length-chunk
type-mismatch rejection, length-overflow rejection.
- Pure boolean output — never throws. Every internal `CborError` is caught
and translated to `false`.
- Defers to the strict-mode decoder so the well-formedness criterion is
always exactly the criterion that the decoder enforces — no drift.
## Usage: Basic Example
```ts
import { isWellFormed } from "@suaveplan/cbor/decoder";
if (!isWellFormed(payload)) {
return new Response("malformed CBOR", { status: 400 });
}
```
## Usage: Advanced Example
```ts
import { decode, isWellFormed } from "@suaveplan/cbor/decoder";
function safeDecode(bytes: Uint8Array): unknown | null {
if (!isWellFormed(bytes)) return null;
return decode(bytes);
}
```
## API Reference
- `isWellFormed(bytes: Uint8Array): boolean` — never throws.
## Implementation Notes
The implementation is deliberately a thin wrapper over `decode(bytes, { mode:
"strict" })` rather than a hand-rolled scanner. This guarantees the predicate
agrees with the decoder on every malformed-input branch and removes a class
of bugs where the predicate accepts inputs the decoder rejects (or vice
versa). The cost is one full decode per call — for security-critical paths
where the decoded value is also needed, callers should call `decode`
directly and treat any thrown `CborError(MALFORMED)` as the not-well-formed
verdict.

View File

@@ -0,0 +1,65 @@
# `deterministic`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Convenience entry point for RFC 8949 §4.2 deterministic encoding. The
underlying `encode` accepts a `deterministic: true` option; this module
exposes a tiny `encodeDeterministic(value, options?)` wrapper that pins the
flag at `true` and forbids the caller from overriding it. Use this when the
encoded bytes must be byte-stable across runs — content-addressable storage,
hash digests, signature payloads, COSE structures, and CWT claim sets are
the canonical consumers.
## Features
- Sorted map keys — lexicographic byte order over the canonical CBOR-encoded
key bytes, NOT the JavaScript Map iteration order.
- Shortest-form integers — always-on regardless of mode.
- No indefinite-length items — caller cannot opt in.
- Half-float disabled in deterministic mode; floats serialise as the
shortest exact width among `{float32, float64}`.
- Rejects `undefined` with `CborError(NON_DETERMINISTIC)` — `undefined` has
no canonical CBOR representation, so it cannot be deterministically encoded.
## Usage: Basic Example
```ts
import { encodeDeterministic } from "@suaveplan/cbor/deterministic";
const m = new Map<string, number>([["b", 2], ["a", 1]]);
const bytes = encodeDeterministic(m);
// bytes are sorted by canonical key order
```
## Usage: Advanced Example
```ts
import { encodeDeterministic } from "@suaveplan/cbor/deterministic";
const a = encodeDeterministic({ z: 1, a: 2 });
const b = encodeDeterministic({ a: 2, z: 1 });
// a and b are byte-identical because key order is canonical, not insertion-order.
import { sha256 } from "@suaveplan/crypto-hash/sha256";
const digest = await sha256(a);
```
## API Reference
- `encodeDeterministic(value: unknown, options?: Omit<EncoderOptions, "deterministic">): Uint8Array`
The omitted `deterministic` field guarantees the flag stays pinned. Other
options (`maxDepth`, `maxItems`, `preferFloat16`) are honoured but
`preferFloat16` has no effect when `deterministic` is on.
## Implementation Notes
This module exists primarily for ergonomics — direct `encode(v, { deterministic: true })`
is identical at runtime. The wrapper guards against accidental
`deterministic: false` overrides and gives downstream readers a clear signal
about intent at the call site. Consumers who need to assert byte-stability
across runs should pair this with property tests over arbitrary
`JsonValue`-like inputs.

View File

@@ -0,0 +1,68 @@
# `diagnostic`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Emits RFC 8949 §8 diagnostic notation for any decoded CBOR value. Diagnostic
notation is the JSON-like text surface CBOR uses for log lines, error
messages, and human inspection. The format is lossless for booleans, integers,
floats, strings, byte strings (encoded as `h'...'` hex), arrays, maps, and
tagged values; objects are serialised with quoted string keys; `Date` values
are emitted as the canonical RFC 3339 tag-0 form.
This is a debug surface — never invoke from encode/decode hot paths. The
implementation makes a fresh string per call and walks the value recursively.
## Features
- Full primitive coverage: `null`, `undefined`, `boolean`, `number` (incl.
±0, ±Inf, NaN), `bigint`, `string`, `Uint8Array`, arrays, `Map`, objects.
- Tag rendering: `Tagged{tag, value}` → `tag(value-diagnostic)`.
- Byte strings rendered as `h'<hex>'` per RFC 8949 §8.1.
- String escapes for backslash, double quote, and ASCII control characters
(matches JSON-string escapes).
- `bytesToDiagnostic(bytes)` convenience: decode + format in one call. Tags
are decoded raw (no IANA-handler substitution) so the diagnostic output
reflects the exact wire-level structure.
## Usage: Basic Example
```ts
import { toDiagnostic } from "@suaveplan/cbor/diagnostic";
const repr = toDiagnostic({ name: "Alice", age: 30 });
// '{"name": "Alice", "age": 30}'
```
## Usage: Advanced Example
```ts
import { encode } from "@suaveplan/cbor/encoder";
import { Tagged } from "@suaveplan/cbor/tags";
import { bytesToDiagnostic } from "@suaveplan/cbor/diagnostic";
const wire = encode([new Tagged(0, "2026-05-10T12:00:00Z"), { id: 1 }]);
console.log(bytesToDiagnostic(wire));
// '[0("2026-05-10T12:00:00Z"), {"id": 1}]'
```
## API Reference
- `toDiagnostic(value: unknown): string` — recursively formats `value`.
- `bytesToDiagnostic(bytes: Uint8Array): string` — decode + format in one
pass; tag handling is `"raw"` so handlers do not substitute the inner
representation.
Neither function throws on a CBOR-decoded value — the input domain is
total. The byte-input variant inherits any decoder error.
## Implementation Notes
The output format follows the JSON-like surface from RFC 8949 §8 while
incorporating CBOR-specific extensions (the `h'...'` byte-string syntax, the
`tag(...)` tagged-item syntax). Numeric formatting preserves `-0` and the
three special float values; otherwise the standard `Number.toString()`
representation is used. The function is intentionally allocation-heavy:
debug paths can pay for clarity.

View File

@@ -0,0 +1,89 @@
# `encoder`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs --> - half-float — Encodes a JavaScript `number` as IEEE……
- [half-float](./half-float.md) — Encodes a JavaScript `number` as IEEE 754 binary16 (CBOR major 7 / value 25) when the round-trip is exact, and otherwis…
- half-float — Encodes a JavaScript `number` as IEEE……
- [half-float](./half-float.md) — Encodes a JavaScript `number` as IEEE 754 binary16 (CBOR major 7 / value 25) when the round-trip is exact, and otherwis…
- half-float — Encodes a JavaScript `number` as IEEE……
- [half-float](./half-float.md) — Encodes a JavaScript `number` as IEEE 754 binary16 (CBOR major 7 / value 25) when the round-trip is exact, and otherwis…
- half-float — Encodes a JavaScript `number` as IEEE…
- [half-float](./half-float.md) — Encodes a JavaScript `number` as IEEE 754 binary16 (CBOR major 7 / value 25) when the round-trip is exact, and otherwis…
- [half-float](./half-float.md) — Encodes a JavaScript `number` as IEEE 754 binary16 (CBOR major 7 / value 25) when the round-trip is exact, and otherwis…
## Purpose
The CBOR encoder serialises an arbitrary JavaScript value into RFC 8949 binary
form. It supports every CBOR major type (0–7) and applies preferred
serialisation rules — shortest-form integers always-on, shortest-exact float
width by default. It also exposes a deterministic-encoding mode (RFC 8949
§4.2) for callers that need byte-stable output across runs (e.g. signature
generation, content addressing).
## Features
- All seven major types: unsigned, negative, byte string, text string, array,
map, tag, and major-7 (simple values + floats).
- Shortest-form integers per RFC 8949 §4.2.1 — always-on, regardless of mode.
- Shortest-exact float selection among `{float16 (opt-in), float32, float64}`.
- Deterministic mode: sorted map keys (lexicographic over canonical encoded
key bytes), no indefinite-length items, no half-float, rejects `undefined`.
- BigInt support: ranges within `±u64` use major 0/1; outside that range, tag
2 (positive) or tag 3 (negative) bignum byte-string.
- `Tagged<T>` round-trip — encode emits tag-then-inner per §3.4.
- `Map<K,V>` and plain object literals both serialise to major 5 maps.
- Configurable `maxDepth` (default 64) and `maxItems` (default 1_000_000)
guard against pathological inputs.
## Usage: Basic Example
```ts
import { encode } from "@suaveplan/cbor/encoder";
const bytes = encode({ a: 1, b: [true, "two"] });
```
## Usage: Advanced Example
```ts
import { encode } from "@suaveplan/cbor/encoder";
import { Tagged } from "@suaveplan/cbor/tags";
// Deterministic encoding for signing
const claims = new Map<number, unknown>([
[1, "issuer"],
[4, new Tagged(1, 1700000000)], // exp claim with epoch tag
]);
const signedBytes = encode(claims, { deterministic: true });
```
## API Reference
- `encode(value: unknown, options?: EncoderOptions): Uint8Array`
- `EncoderOptions`:
- `deterministic?: boolean` — default `false`. Enforces RFC 8949 §4.2.
- `preferFloat16?: boolean` — default `false`. Try binary16 when exact.
Ignored when `deterministic` is `true`.
- `maxDepth?: number` — default 64. Throws `CBOR.NESTED_TOO_DEEP` on overflow.
- `maxItems?: number` — default 1_000_000. Throws `CBOR.LENGTH_OVERFLOW`.
Throws `CborError` with codes `UNEXPECTED_TYPE` (functions, symbols),
`NON_DETERMINISTIC` (e.g. `undefined` under deterministic), `NESTED_TOO_DEEP`,
and `LENGTH_OVERFLOW`.
## Implementation Notes
The encoder accumulates output as an array of `Uint8Array` chunks and
concatenates at the end. This avoids the O(n²) cost of repeatedly grow-and-
copying a single buffer. Map-key sorting in deterministic mode encodes each
key first (yielding the canonical bytes), then sorts the entry list by
lexicographic byte order before emitting headers and bodies. BigInt encoding
uses native `DataView.setBigUint64` for the in-range case and a hand-rolled
big-endian byte builder for tag 2/3 bignums to avoid pulling in any external
dependency.

View File

@@ -0,0 +1,58 @@
# `half-float` (encoder side)
## Purpose
Encodes a JavaScript `number` as IEEE 754 binary16 (CBOR major 7 / value 25)
when the round-trip is exact, and otherwise indicates non-representability so
the caller can pick a wider format. Also exports `selectFloatWidth`, which
chooses between float32 and float64 based on exact-representability — the
encoder's deterministic path uses this to skip half-float without giving up
shortest-exact behaviour at the float32 / float64 boundary.
## Features
- Full IEEE 754 binary16 coverage: ±0, ±Infinity, NaN, normal values,
subnormals, smallest-normal / largest-normal boundaries.
- `tryEncodeFloat16(value)` returns the 2-byte big-endian sequence on exact
round-trip; returns `undefined` on overflow / underflow / inexact-precision.
- `selectFloatWidth(value)` picks float32 (4 bytes) when `getFloat32` round-
trips the value exactly, otherwise float64 (8 bytes). NaN is treated as
float32-exact since both float32 and float64 represent NaN.
## Usage: Basic Example
```ts
import { tryEncodeFloat16 } from "@suaveplan/cbor/encoder";
const bytes = tryEncodeFloat16(1.5); // Uint8Array(2) [0x3e, 0x00]
```
## Usage: Advanced Example
```ts
import { selectFloatWidth, tryEncodeFloat16 } from "@suaveplan/cbor/encoder";
function pickFloat(value: number): { tag: number; bytes: Uint8Array } {
const f16 = tryEncodeFloat16(value);
if (f16) return { tag: 0xf9, bytes: f16 };
const { width, bytes } = selectFloatWidth(value);
return { tag: width === 4 ? 0xfa : 0xfb, bytes };
}
```
## API Reference
- `tryEncodeFloat16(value: number): Uint8Array | undefined` — exact-only.
- `selectFloatWidth(value: number): { width: 4 | 8; bytes: Uint8Array }`.
- `type FloatWidth = 4 | 8`.
## Implementation Notes
The exact-only contract is critical for deterministic encoding: a lossy
half-float emission would produce different bytes for inputs that look
"close" but differ in the last bit of the mantissa. The implementation
verifies round-trip via reconstruction (`(1 + mant/1024) * 2^(exp-15)` for
normals, `mant * 2^-24` for subnormals) and rejects when the rebuild does
not equal the input under `===`. NaN is hard-coded to `7e00` (the canonical
quiet-NaN form) — RFC 8949 §3.3 explicitly permits losing NaN payload bits,
which JavaScript would do anyway since `Number` collapses payloads.

View File

@@ -0,0 +1,77 @@
# `error-codes`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the stable, registered error-code surface for `@suaveplan/cbor`. The
codes form the discriminator that every public entry point uses to signal a
specific failure class. Call sites do not parse messages; they branch on
`error.code`. This module also exports the `CborError` class extending the
`SuaveplanError` base, plus a `cborError(code, message, options)`
helper that centralises the construction pattern so codes are never typo'd at
call sites.
## Features
- 21 stable codes spanning RFC 8949 codec, RFC 8742 sequences, RFC 9052 COSE,
RFC 8392 CWT, and RFC 8610 CDDL.
- `defineCodes` registry — codes carry the `CBOR.` prefix and are typed as a
literal union for exhaustive `switch` checks.
- `CborError` extends `SuaveplanError<CborErrorCode>` so the suaveplan
observability surface (OTel span events, logger redaction, i18n hook)
flows through without per-package adapters.
- `cborError()` helper preserves an underlying `Error` cause when supplied
and silently drops non-Error causes (per `SuaveplanError` contract).
## Usage: Basic Example
```ts
import { cborError, CBOR_CODES, CborError } from "@suaveplan/cbor/errors";
if (bytes.length === 0) {
throw cborError(CBOR_CODES.MALFORMED, "empty CBOR input");
}
```
## Usage: Advanced Example
```ts
import { CBOR_CODES, type CborErrorCode, cborError } from "@suaveplan/cbor/errors";
function classify(err: unknown): "retryable" | "fatal" | "unknown" {
if (!(err instanceof CborError)) return "unknown";
const retry: ReadonlySet<CborErrorCode> = new Set([
CBOR_CODES.STREAM_INTERRUPTED,
]);
if (retry.has(err.code)) return "retryable";
return "fatal";
}
try {
/* decode */
} catch (e) {
const decision = classify(e);
// route to retry queue or dead-letter accordingly
}
```
## API Reference
- `CBOR_CODES: Readonly<Record<string, \`CBOR.${string}\`>>` — frozen registry.
- `type CborErrorCode` — literal union of every value in `CBOR_CODES`.
- `class CborError extends SuaveplanError<CborErrorCode>` — base error type.
- `cborError(code, message, { cause? }): CborError` — terse constructor.
Throws nothing; non-`Error` `cause` is dropped per `SuaveplanError` contract.
## Implementation Notes
The `defineCodes` call freezes the registry at module-load time, so the literal
type derives from the const tuple without ambient declaration drift. The seven
RFC 8949 codes (`MALFORMED`, `UNEXPECTED_TYPE`, `UNKNOWN_TAG_STRICT`,
`NESTED_TOO_DEEP`, `LENGTH_OVERFLOW`, `INDEFINITE_NOT_ALLOWED`,
`NON_DETERMINISTIC`) each map to a distinct §5.1 / §10 failure class — no
overlap, no fallthrough. Adding a new code requires adding it to the `as const`
tuple AND wiring it from at least one production path; the `error-codes.test.ts`
gate enforces this so dead codes never accumulate.

View File

@@ -0,0 +1,66 @@
# `sequences`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`@suaveplan/cbor/sequences` implements RFC 8742 — the CBOR Sequence format,
which is just the concatenation of zero-or-more well-formed CBOR data items
with no enclosing array, tag, or framing byte. Sequences are the natural
encoding for log streams, event journals, and any wire format where items
arrive incrementally and the consumer does not know the count ahead of time.
## Features
- `encodeSequence(items, options?)` produces the byte-concatenated form of an
arbitrary item list. Empty input → empty output (zero bytes).
- `decodeSequence(bytes, options?, tags?)` walks the input from offset 0,
pulling one item at a time via `decodeOne` until the buffer is exhausted.
- `decodeSequenceStream(stream, options?, tags?)` — async generator over an
`AsyncIterable<Uint8Array>`. Yields each complete item as it becomes
available; buffers only what is needed for the next pending item.
- All three preserve the strict / lax mode and tag-handling semantics of the
underlying decoder — sequence framing is a pure outer concern.
## Usage: Basic Example
```ts
import { decodeSequence, encodeSequence } from "@suaveplan/cbor/sequences";
const wire = encodeSequence([{ id: 1, ok: true }, { id: 2, ok: false }]);
const items = decodeSequence(wire);
```
## Usage: Advanced Example
```ts
import { decodeSequenceStream } from "@suaveplan/cbor/sequences";
async function consumeLog(stream: AsyncIterable<Uint8Array>) {
for await (const event of decodeSequenceStream(stream)) {
handle(event);
}
}
```
## API Reference
- `encodeSequence(items: readonly unknown[], options?: EncoderOptions): Uint8Array`
- `decodeSequence(bytes: Uint8Array, options?: DecoderOptions, tags?: ReadonlyMap<number, TagHandler>): DecodedValue[]`
- `decodeSequenceStream(stream: AsyncIterable<Uint8Array>, options?, tags?): AsyncGenerator<DecodedValue>`
Throws `CborError(MALFORMED)` for sequence-internal incompleteness (e.g.
trailing bytes that don't form a complete item at end-of-stream).
## Implementation Notes
The streaming form coalesces partial chunks via a growing `Uint8Array`
buffer. After each new chunk is appended, the loop attempts as many full
`decodeOne` calls as the buffer allows; on the first failure (which signals
"need more bytes"), the loop breaks and waits for the next chunk. This
strategy is correct as long as `decodeOne` either succeeds and reports
exactly `consumed` bytes OR throws — both invariants are guaranteed by the
strict-mode decoder. The buffer is reallocated rather than `subarray`d after
each successful decode so backing-buffer ownership stays bound to the
generator and does not leak into yielded values.

View File

@@ -0,0 +1,98 @@
# `chunk-decoder`
## Purpose
Partial-item streaming decoder. While `PullDecoder` yields one whole CBOR
item at a time (waiting until each item is fully buffered), `ChunkDecoder`
emits structural events as bytes flow in — perfect for processing CBOR
items larger than memory. A multi-gigabyte indefinite-length byte string
can stream through with a fixed memory footprint, surfacing bytes-chunk
events as they arrive rather than buffering the whole body.
## Features
- Event stream: `value`, `string-chunk` / `string-end`, `bytes-chunk` /
`bytes-end`, `array-start` / `array-end`, `map-start` / `map-end`, `tag`,
`break`. Definite-length containers don't emit explicit ends — consumers
count entries via the `length` on the start event. Indefinite-length
containers do emit `array-end` / `map-end` when the break stop appears.
- Bounded memory: the internal buffer holds at most one item's head plus
the current chunk slice. Decoded values are NOT retained — consumers
materialise their own state from the event sequence.
- AbortSignal supported — `next()` rejects with `STREAM_INTERRUPTED`.
- Both `AsyncIterable<Uint8Array>` and `ReadableStream<Uint8Array>` sources;
the `Symbol.asyncIterator` async-iterator protocol is supported so
`for await (const event of decoder)` works directly.
## Usage: Basic Example
```ts
import { ChunkDecoder } from "@suaveplan/cbor/streaming";
const decoder = new ChunkDecoder(stream);
for await (const event of decoder) {
switch (event.kind) {
case "array-start":
pushContainer("array", event.length);
break;
case "value":
writeValue(event.value);
break;
case "bytes-chunk":
pipeToFile(event.chunk);
break;
case "bytes-end":
finalizeBlob();
break;
// ...
}
}
```
## Usage: Advanced Example
```ts
import { ChunkDecoder } from "@suaveplan/cbor/streaming";
// Stream a multi-GB indefinite byte string into a hash without buffering.
import { sha256 } from "@suaveplan/crypto-hash";
const decoder = new ChunkDecoder(httpStream);
const hashState = sha256.create();
for await (const event of decoder) {
if (event.kind === "bytes-chunk") hashState.update(event.chunk);
if (event.kind === "bytes-end") break;
}
const digest = hashState.finish();
```
## API Reference
- `class ChunkDecoder`
- `constructor(source, options?)` — `AsyncIterable<Uint8Array>` or
`ReadableStream<Uint8Array>` source; options accept `signal?: AbortSignal`.
- `next(): Promise<IteratorResult<ChunkDecoderEvent, void>>`.
- `[Symbol.asyncIterator]()`.
- `type ChunkDecoderEvent` — discriminated union of all 11 event kinds.
Throws `CborError` with codes `MALFORMED` (incomplete trailing bytes,
malformed UTF-8, mismatched chunk type, nested indefinite chunk, unexpected
break, indefinite-length on disallowed major type), `STREAM_INTERRUPTED`
(AbortSignal fired), `LENGTH_OVERFLOW` (length above MAX_SAFE_INTEGER).
## Implementation Notes
The decoder maintains a stack of pending container / string-context entries
plus a small head buffer. The `tryEmit` driver routes to a sub-decoder based
on the top of stack:
- `indef-string` / `indef-bytes` — drains chunk headers + bodies, emitting
`string-chunk` / `bytes-chunk` events incrementally.
- `definite-string` / `definite-bytes` — drains the body in chunks of
whatever's currently buffered, emitting one event per available slice.
- otherwise — reads a primitive head and dispatches by major type.
Empty definite-length containers don't push a stack entry; they call
`afterChild` directly to unwind the parent. UTF-8 decoding uses a fatal-mode
`TextDecoder` with `{ stream: true }` so multi-byte runes split across
chunks are reassembled correctly.

View File

@@ -0,0 +1,79 @@
# `pull-decoder`
## Purpose
`PullDecoder` is a pull-style streaming decoder over an
`AsyncIterable<Uint8Array>` or `ReadableStream<Uint8Array>`. Each call to
`next()` resolves once a complete CBOR item is available in the internal
buffer; until then, `PullDecoder` keeps reading chunks from the source.
Bounded memory — the internal buffer never holds more than one in-progress
item plus the current chunk's tail.
## Features
- Accepts both `AsyncIterable<Uint8Array>` (e.g. an async generator) and
`ReadableStream<Uint8Array>` (Web Streams) — `getReader()` is wrapped on
the fly when needed.
- Exposes `Symbol.asyncIterator` so consumers can `for await` directly.
- Honours an `AbortSignal` — `next()` rejects with
`CborError(STREAM_INTERRUPTED)` when the signal fires.
- Bounded memory: buffers only what is required for the next pending item;
reallocates after each successful decode so backing buffers do not leak.
- Reuses the production `decodeOne` cursor API so well-formedness is
identical to the synchronous decoder.
## Usage: Basic Example
```ts
import { PullDecoder } from "@suaveplan/cbor/streaming";
const decoder = new PullDecoder(stream);
for await (const item of decoder) {
handle(item);
}
```
## Usage: Advanced Example
```ts
import { PullDecoder } from "@suaveplan/cbor/streaming";
import { defaultTagHandlers } from "@suaveplan/cbor/tags";
const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 5_000);
const tags = defaultTagHandlers().snapshot();
const decoder = new PullDecoder(
fetchStream,
{ signal: ctrl.signal, mode: "strict" },
tags,
);
while (true) {
const r = await decoder.next();
if (r.done) break;
handle(r.value);
}
```
## API Reference
- `class PullDecoder`
- `constructor(source, options?, tags?)` — `source` is `AsyncIterable<Uint8Array>` or `ReadableStream<Uint8Array>`. Options extend `DecoderOptions` with `signal?: AbortSignal`.
- `next(): Promise<IteratorResult<DecodedValue, void>>` — resolves to the next item or `{ done: true }`.
- `[Symbol.asyncIterator]()` — async-iterator protocol; lets `for await` consume the decoder directly.
Throws `CborError` with codes `MALFORMED` (incomplete trailing bytes after
end-of-stream), `STREAM_INTERRUPTED` (AbortSignal fired), and any code the
underlying `decodeOne` would raise.
## Implementation Notes
The decoder maintains a single growing `Uint8Array` buffer. After each
successful `decodeOne`, the consumed prefix is dropped via a fresh allocation
rather than a `subarray` view so the backing `ArrayBuffer` is released as
soon as the previous chunk leaves scope. The "need more bytes" path is
expressed by catching the synchronous decoder's error and looping to fetch
the next chunk — this is correct because `decodeOne` only throws on either
genuine malformation OR truncation, and on truncation a subsequent attempt
with more bytes will succeed without regret.

View File

@@ -0,0 +1,64 @@
# `builtin`
## Purpose
Provides `defaultTagHandlers()`, a `TagRegistry` populated with handlers for
the most-used IANA-registered CBOR tags. Callers who want sensible
materialisation of standard tags (date-times, URIs, regular expressions,
positive/negative bignums, encoded-CBOR pointers, self-describe) can opt in
with a single call rather than registering each handler individually.
## Features
- Tag 0 (RFC 3339 date/time string) → `Date`. Throws `MALFORMED` on
unparseable input.
- Tag 1 (epoch seconds, integer or float) → `Date`.
- Tag 2 (positive bignum byte string) → `bigint`.
- Tag 3 (negative bignum byte string) → `bigint` of value `-1n - n`.
- Tag 21 / 22 / 23 (base64url / base64 / base16 expected encoding hints) →
identity. The hint indicates the *expected* encoding when serialising the
inner byte string to a text format; at decode time, the bytes themselves
are unchanged.
- Tag 24 (encoded CBOR data item) → re-decode the inner byte string.
- Tag 32 (URI string) → `URL`. Throws `MALFORMED` on invalid URIs.
- Tag 35 (regex string) → `RegExp`. Throws `MALFORMED` on invalid patterns.
- Tag 55799 (self-describe) → identity. The tag is a sentinel that says "the
following item is CBOR" — useful when CBOR is embedded in a multi-format
channel — and the inner item passes through unchanged.
## Usage: Basic Example
```ts
import { decode } from "@suaveplan/cbor/decoder";
import { defaultTagHandlers } from "@suaveplan/cbor/tags";
const tags = defaultTagHandlers().snapshot();
const value = decode(bytes, {}, tags);
```
## Usage: Advanced Example
```ts
import { decode } from "@suaveplan/cbor/decoder";
import { defaultTagHandlers } from "@suaveplan/cbor/tags";
// Layer a project-specific tag on top of the IANA defaults.
const reg = defaultTagHandlers();
reg.register(1700, (inner) => ({ kind: "tenant-id", inner }));
const value = decode(bytes, {}, reg.snapshot());
```
## API Reference
- `defaultTagHandlers(): TagRegistry` — returns a freshly constructed
registry populated with the tags listed above.
## Implementation Notes
Each handler validates the inner type before transforming. The tag-2/3
bignum handlers reconstruct the integer big-endian byte-by-byte rather than
delegating to a third-party big-int parser to keep the dependency surface
empty. The tag-32 handler uses `new URL(value)` so URI normalisation
(percent-encoding, lowercasing of host) follows whatever the runtime's URL
implementation does — callers needing strict round-trip preservation should
register a custom handler that returns the raw string instead.

View File

@@ -0,0 +1,65 @@
# `registry`
## Purpose
`TagRegistry` is the in-memory mutable container for CBOR tag handlers. The
decoder consults a snapshot of the registry on every encountered tag and
substitutes the handler's output for the inner value. The registry is the
extension point downstream packages use to add domain-specific tags
(WebAuthn AAGUIDs, vault-attachment manifests, custom timestamp formats)
without modifying `@suaveplan/cbor` itself.
## Features
- `register(tag, handler)` — adds a handler. Throws `CborError(UNEXPECTED_TYPE)`
on negative or non-integer tags AND on double-registration. Double-register
is treated as a programming error rather than silent override so two
libraries cannot quietly fight over the same tag value.
- `unregister(tag)` — removes a handler. Returns `true` if a handler was
removed, `false` otherwise.
- `lookup(tag)` — returns the handler or `undefined`.
- `snapshot()` — returns a detached `ReadonlyMap` for use with `decode()`.
Subsequent `register` / `unregister` calls do not mutate the snapshot.
- `size` — number of registered handlers.
## Usage: Basic Example
```ts
import { TagRegistry } from "@suaveplan/cbor/tags";
const reg = new TagRegistry();
reg.register(1001, (v) => `[domain]${String(v)}`);
```
## Usage: Advanced Example
```ts
import { decode } from "@suaveplan/cbor/decoder";
import { defaultTagHandlers, TagRegistry } from "@suaveplan/cbor/tags";
// Start from the IANA built-ins, layer one custom tag.
const reg = defaultTagHandlers();
reg.register(1234, (inner) => ({ kind: "custom", inner }));
const value = decode(bytes, {}, reg.snapshot());
```
## API Reference
- `class TagRegistry`
- `register(tag: number, handler: TagHandler): void` — throws on invalid
tag or duplicate.
- `unregister(tag: number): boolean`.
- `lookup(tag: number): TagHandler | undefined`.
- `snapshot(): ReadonlyMap<number, TagHandler>`.
- `get size: number`.
## Implementation Notes
The registry is a thin wrapper over `Map<number, TagHandler>`. The double-
register-throws contract is intentional — silent override would let a
late-loaded library shadow a legitimate built-in handler, and the resulting
behaviour change would be invisible at decode time. Callers who genuinely
want to replace a handler should call `unregister` first and then `register`
again. `snapshot()` returns a fresh `Map` (not a `ReadonlyMap` view of the
internal map) so external code cannot mutate the registry through a
captured snapshot.

View File

@@ -0,0 +1,59 @@
# `tagged`
## Purpose
Provides the `Tagged<T>` value object used by `@suaveplan/cbor` to materialise
RFC 8949 §3.4 tagged items. The decoder returns `Tagged` instances for any tag
without a registered handler (or always, when the caller requests raw-tag
decoding). Encoders accept `Tagged` instances on the input side to round-trip
arbitrary tags through the codec without modifying the registry. Holding the
tag alongside its value as a single object keeps tag semantics inspectable
even when callers do not pre-register a handler.
## Features
- Generic `Tagged<T>` so registered handlers can narrow the inner type.
- Constructor validates the tag — non-negative safe integer per RFC 8949 §3.4.
- `isTagged()` type guard for runtime branching.
- Plain class — no symbols, no proxies — so structured cloning, equality
testing, and JSON inspection all behave intuitively.
## Usage: Basic Example
```ts
import { Tagged } from "@suaveplan/cbor/tags";
const stamped = new Tagged(0, "2026-05-10T12:00:00Z");
```
## Usage: Advanced Example
```ts
import { decode } from "@suaveplan/cbor";
import { Tagged, isTagged } from "@suaveplan/cbor/tags";
const value = decode(bytes);
if (isTagged(value) && value.tag === 32) {
// RFC 8949 §3.4.5.3 — URI tag
const url = new URL(value.value as string);
// …
}
```
## API Reference
- `class Tagged<T = unknown>` — fields `tag: number` (non-negative safe
integer) and `value: T`. Constructor throws `RangeError` on a non-integer or
out-of-range tag.
- `isTagged(v: unknown): v is Tagged` — instanceof guard.
## Implementation Notes
`Tagged` is intentionally a plain class rather than a frozen object literal so
that subclassing remains an option for downstream consumers needing to attach
extra metadata (e.g. provenance for an attestation tag). The constructor
performs the validation eagerly because decoding a malformed tag is rare and
the synchronous throw beats deferring the check to encode time. `MAX_SAFE_INTEGER`
is the upper bound — RFC 8949 permits 64-bit unsigned tags but values above
`2^53 − 1` cannot round-trip through a JavaScript number; encoders that need
the full 64-bit range should accept `bigint` tags via a follow-up extension.