first commit
This commit is contained in:
183
docs/foundation/flow/cbor/README.md
Normal file
183
docs/foundation/flow/cbor/README.md
Normal 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
|
||||
|
||||
58
docs/foundation/flow/cbor/src/cddl/ast.md
Normal file
58
docs/foundation/flow/cbor/src/cddl/ast.md
Normal 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.
|
||||
69
docs/foundation/flow/cbor/src/cddl/lexer.md
Normal file
69
docs/foundation/flow/cbor/src/cddl/lexer.md
Normal 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.
|
||||
71
docs/foundation/flow/cbor/src/cddl/parser.md
Normal file
71
docs/foundation/flow/cbor/src/cddl/parser.md
Normal 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.
|
||||
84
docs/foundation/flow/cbor/src/cddl/validate.md
Normal file
84
docs/foundation/flow/cbor/src/cddl/validate.md
Normal 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.
|
||||
67
docs/foundation/flow/cbor/src/cose/alg-map.md
Normal file
67
docs/foundation/flow/cbor/src/cose/alg-map.md
Normal 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.
|
||||
71
docs/foundation/flow/cbor/src/cose/cose-key.md
Normal file
71
docs/foundation/flow/cbor/src/cose/cose-key.md
Normal 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.
|
||||
55
docs/foundation/flow/cbor/src/cose/encrypt.md
Normal file
55
docs/foundation/flow/cbor/src/cose/encrypt.md
Normal 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.
|
||||
54
docs/foundation/flow/cbor/src/cose/encrypt0.md
Normal file
54
docs/foundation/flow/cbor/src/cose/encrypt0.md
Normal 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.
|
||||
43
docs/foundation/flow/cbor/src/cose/headers.md
Normal file
43
docs/foundation/flow/cbor/src/cose/headers.md
Normal 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.
|
||||
70
docs/foundation/flow/cbor/src/cose/jwk.md
Normal file
70
docs/foundation/flow/cbor/src/cose/jwk.md
Normal 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.
|
||||
55
docs/foundation/flow/cbor/src/cose/mac.md
Normal file
55
docs/foundation/flow/cbor/src/cose/mac.md
Normal 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.
|
||||
50
docs/foundation/flow/cbor/src/cose/mac0.md
Normal file
50
docs/foundation/flow/cbor/src/cose/mac0.md
Normal 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.
|
||||
48
docs/foundation/flow/cbor/src/cose/sig-structure.md
Normal file
48
docs/foundation/flow/cbor/src/cose/sig-structure.md
Normal 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.
|
||||
58
docs/foundation/flow/cbor/src/cose/sign.md
Normal file
58
docs/foundation/flow/cbor/src/cose/sign.md
Normal 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.
|
||||
60
docs/foundation/flow/cbor/src/cose/sign1.md
Normal file
60
docs/foundation/flow/cbor/src/cose/sign1.md
Normal 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.
|
||||
53
docs/foundation/flow/cbor/src/cwt/claims.md
Normal file
53
docs/foundation/flow/cbor/src/cwt/claims.md
Normal 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`.
|
||||
51
docs/foundation/flow/cbor/src/cwt/verify.md
Normal file
51
docs/foundation/flow/cbor/src/cwt/verify.md
Normal 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.
|
||||
91
docs/foundation/flow/cbor/src/decoder/decoder.md
Normal file
91
docs/foundation/flow/cbor/src/decoder/decoder.md
Normal 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.
|
||||
52
docs/foundation/flow/cbor/src/decoder/half-float.md
Normal file
52
docs/foundation/flow/cbor/src/decoder/half-float.md
Normal 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.
|
||||
57
docs/foundation/flow/cbor/src/decoder/well-formed.md
Normal file
57
docs/foundation/flow/cbor/src/decoder/well-formed.md
Normal 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.
|
||||
65
docs/foundation/flow/cbor/src/deterministic/deterministic.md
Normal file
65
docs/foundation/flow/cbor/src/deterministic/deterministic.md
Normal 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.
|
||||
68
docs/foundation/flow/cbor/src/diagnostic/diagnostic.md
Normal file
68
docs/foundation/flow/cbor/src/diagnostic/diagnostic.md
Normal 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.
|
||||
89
docs/foundation/flow/cbor/src/encoder/encoder.md
Normal file
89
docs/foundation/flow/cbor/src/encoder/encoder.md
Normal 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.
|
||||
58
docs/foundation/flow/cbor/src/encoder/half-float.md
Normal file
58
docs/foundation/flow/cbor/src/encoder/half-float.md
Normal 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.
|
||||
77
docs/foundation/flow/cbor/src/errors/error-codes.md
Normal file
77
docs/foundation/flow/cbor/src/errors/error-codes.md
Normal 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.
|
||||
66
docs/foundation/flow/cbor/src/sequences/sequences.md
Normal file
66
docs/foundation/flow/cbor/src/sequences/sequences.md
Normal 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.
|
||||
98
docs/foundation/flow/cbor/src/streaming/chunk-decoder.md
Normal file
98
docs/foundation/flow/cbor/src/streaming/chunk-decoder.md
Normal 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.
|
||||
79
docs/foundation/flow/cbor/src/streaming/pull-decoder.md
Normal file
79
docs/foundation/flow/cbor/src/streaming/pull-decoder.md
Normal 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.
|
||||
64
docs/foundation/flow/cbor/src/tags/builtin.md
Normal file
64
docs/foundation/flow/cbor/src/tags/builtin.md
Normal 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.
|
||||
65
docs/foundation/flow/cbor/src/tags/registry.md
Normal file
65
docs/foundation/flow/cbor/src/tags/registry.md
Normal 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.
|
||||
59
docs/foundation/flow/cbor/src/tags/tagged.md
Normal file
59
docs/foundation/flow/cbor/src/tags/tagged.md
Normal 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.
|
||||
Reference in New Issue
Block a user