npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cbortech/cbor

v0.27.1

Published

Convert between CBOR, CDN (CBOR-EDN), and JavaScript values with CDDL validation

Readme

@cbortech/cbor

npm version zero dependencies types license platform

TypeScript library for converting between CBOR, CDN (CBOR-EDN), and JavaScript values, plus parsing, formatting, and validation for CDDL schemas.

A live playground is available at https://cbor.tech/cbor/.

Relationship between CBOR, CDN, and JavaScript values

This package exposes the CBOR facade plus separate CDN, CDDL, and AST entrypoints for tooling and extensions. Lower-level parser and encoder internals are not part of the documented public API.

Install

npm install @cbortech/cbor

For command-line conversion and inspection, a companion CLI package is available as @cbortech/cbor-cli.

npm install -g @cbortech/cbor-cli

For editor integration, try the companion VS Code extension, which is built with this package.

Import

import { CBOR } from '@cbortech/cbor';

Default import is also supported:

import CBOR from '@cbortech/cbor';

Quick Examples

JavaScript to CBOR bytes

import { CBOR } from '@cbortech/cbor';

const bytes = CBOR.encode({ hello: 'world', n: 42 });

console.log(bytes);
// Uint8Array(...)

CBOR bytes to JavaScript

import { CBOR } from '@cbortech/cbor';

const value = CBOR.decode(
  new Uint8Array([
    0xa2, 0x65, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x65, 0x77, 0x6f, 0x72, 0x6c,
    0x64, 0x61, 0x6e, 0x18, 0x2a,
  ])
);

console.log(value);
// { hello: 'world', n: 42 }

Validate with CDDL

Compile a CDDL schema and validate CBOR bytes, CDN text, or a CborItem AST against it:

import { CDDL } from '@cbortech/cbor/cddl';

const schema = CDDL.compile('point = { x: int, y: int }');
const result = schema.validate('{"x": 12, "y": -3}');

console.log(result.valid);
// true

CBOR Sequence to JavaScript values

decodeSeq reads concatenated CBOR items as a CBOR Sequence and yields each item as a JavaScript value.

import { CBOR } from '@cbortech/cbor';

const a = CBOR.encode({ id: 1 });
const b = CBOR.encode({ id: 2 });
const seq = new Uint8Array([...a, ...b]);

const values = [...CBOR.decodeSeq(seq)];
// [{ id: 1 }, { id: 2 }]

CBOR bytes to CDN

decompile converts CBOR binary data to a CDN text string. It handles CBOR Sequences automatically: multiple concatenated items produce newline-separated CDN output.

import { CBOR } from '@cbortech/cbor';

// Single item
const text = CBOR.decompile(new Uint8Array([0x83, 0x01, 0x02, 0x03]));
console.log(text);
// [1,2,3]

// CBOR Sequence — each item on its own line
const seq = new Uint8Array([...CBOR.encode(1), ...CBOR.encode('two')]);
console.log(CBOR.decompile(seq));
// 1
// "two"

CDN to CBOR bytes

compile converts a CDN text string to CBOR binary data. Multi-item CDN Sequences automatically produce a CBOR Sequence (RFC 8742): concatenated items.

import { CBOR } from '@cbortech/cbor';

// Single item
const bytes = CBOR.compile('[1, 2, 3]');
console.log(bytes);
// Uint8Array([0x83, 0x01, 0x02, 0x03])

// CDN Sequence — output is a CBOR Sequence
const seq = CBOR.compile('{"id":1}\n{"id":2}');
console.log([...CBOR.decodeSeq(seq)]);
// [{ id: 1 }, { id: 2 }]

CBOR bytes to hex dump

toHex converts CBOR binary data to an annotated hex dump string. CBOR Sequences are handled automatically: each item produces its own dump, separated by newlines.

import { CBOR } from '@cbortech/cbor';

const bytes = CBOR.encode([1, 2, 3]);
console.log(CBOR.toHex(bytes));
// 83        -- Array of length 3
//    01     -- 1
//    02     -- 2
//    03     -- 3

Hex dump to CBOR bytes

fromHex parses an annotated hex dump back to CBOR binary data. Multi-item dumps produce a CBOR Sequence (RFC 8742): concatenated items.

import { CBOR } from '@cbortech/cbor';

const bytes = CBOR.fromHex(`
83        -- Array of length 3
   01     -- 1
   02     -- 2
   03     -- 3
`);
console.log([...CBOR.decodeSeq(bytes)]);
// [[1, 2, 3]]

JavaScript to CDN

import { CBOR } from '@cbortech/cbor';

const text = CBOR.stringify({ a: 1, b: true, c: null });

console.log(text);
// {"a":1,"b":true,"c":null}

Pretty CDN

import { CBOR } from '@cbortech/cbor';

const text = CBOR.stringify({ items: [1, 2, 3], ok: true }, { indent: 2 });

console.log(text);
// {
//   "items": [
//     1,
//     2,
//     3
//   ],
//   "ok": true
// }

CDN to JavaScript

import { CBOR } from '@cbortech/cbor';

const value = CBOR.parse("[1, h'deadbeef', true, null]");

console.log(value);
// [1, Uint8Array(...), true, null]

CDN Sequence to JavaScript values

parseSeq parses multiple CDN items separated by whitespace, commas, or comments, and also accepts JSONL / NDJSON input.

import { CBOR } from '@cbortech/cbor';

const values = [...CBOR.parseSeq('1  "two"  [3]')];
// [1, 'two', [3]]

const jsonl = '{"id":1}\n{"id":2}\n{"id":3}';
const rows = [...CBOR.parseSeq(jsonl)];
// [{ id: 1 }, { id: 2 }, { id: 3 }]

Normalize CDN

import { CBOR } from '@cbortech/cbor';

const text = CBOR.format('{ "b" : [ 1,2 ], "a" : true }', { indent: 2 });

console.log(text);
// {
//   "b": [
//     1,
//     2
//   ],
//   "a": true
// }

Keep leaf containers on one line

inlineLeafContainers keeps a container on a single line when none of its entries contains an array or map (even wrapped in a tag) and every entry serializes without a line break. Nested leaf containers still collapse individually, so matrix-like data stays readable. It is applied when indent is specified.

import { CBOR } from '@cbortech/cbor';

const text = CBOR.format('{"m": [[1,2],[3,4]], "s": (_ "a", "b")}', {
  indent: 2,
  inlineLeafContainers: true,
});

console.log(text);
// {
//   "m": [
//     [1, 2],
//     [3, 4]
//   ],
//   "s": (_ "a", "b")
// }

Split text strings while formatting

splitNewline splits long text strings at newline characters using CDN string concatenation. It is applied when indent is specified.

import { CBOR } from '@cbortech/cbor';

const text = CBOR.format('{"text": "line1\\nline2\\nline3"}', {
  indent: 2,
  splitNewline: true,
});

console.log(text);
// {
//   "text": "line1\n" +
//     "line2\n" +
//     "line3"
// }

For strings that contain CDN or JSON-like content, splitCdn formats the string content with structure-aware line breaks and indentation, the same way the surrounding CDN is formatted. Both options can be combined, and they replace the deprecated array-valued textStringFormat option.

import { CBOR } from '@cbortech/cbor';

const text = CBOR.format('{"cdn": "[1,2,3]"}', {
  indent: 2,
  splitCdn: true,
});

console.log(text);
// {
//   "cdn": "[" +
//       "1," +
//       "2," +
//       "3" +
//     "]"
// }

Preserve raw text strings

By default, CBOR.format() converts raw backtick string literals (`...`, ``...``, …) to double-quoted form. preserveRawString re-emits them using their original source text instead. Preserved raw strings are emitted verbatim: they are never re-escaped, re-indented, or split by splitCdn / splitNewline. (Raw byte string forms such as h`...` are covered by preserveByteString.)

import { CBOR } from '@cbortech/cbor';

CBOR.format('`\\d+`');
// '"\\\\d+"'

CBOR.format('`\\d+`', { preserveRawString: true });
// '`\\d+`'

Preserve number literal spelling

By default, CBOR.format() normalizes integer and floating-point literals: hex/octal/binary integers (0xff, 0o377, 0b101) become decimal, trailing zeros and redundant encoding-indicator suffixes (1.50, 1.5_1) are dropped. preserveNumberFormat re-emits these literals using their original CDN source spelling instead, taking precedence over intFormat / floatFormat. It only affects literals parsed from CDN text — values built with CBOR.from() or decoded from CBOR bytes always use normal formatting.

import { CBOR } from '@cbortech/cbor';

CBOR.format('{"a": 0xff, "b": 1.50}');
// '{"a":255,"b":1.5}'

CBOR.format('{"a": 0xff, "b": 1.50}', { preserveNumberFormat: true });
// '{"a":0xff,"b":1.50}'

Preserve + string concatenation

Note: + string concatenation was removed in draft-26. This section is for handling legacy syntax.

By default, CBOR.format() joins + string concatenation into a single literal. preserveConcatenation keeps the original part boundaries for both text strings and byte strings; add preserveByteString to also keep the original spelling of byte string parts. Like the split options, it only takes effect when indent enables pretty-printing — single-line output always joins the parts.

preserveConcatenation interacts with the split options: splitCdn takes precedence when the string content parses as CDN, while splitNewline combines with it by further splitting the preserved parts at newline characters.

import { CBOR } from '@cbortech/cbor';

CBOR.format('"a" + "b"');
// '"ab"'

CBOR.format('"a" + "b"', { indent: 2, preserveConcatenation: true });
// "a" +
//   "b"

CBOR.format("h'68' + b64'aQ'", {
  indent: 2,
  preserveConcatenation: true,
  preserveByteString: true,
});
// h'68' +
//   b64'aQ'

To render the preserved concatenation using draft-27's t1<<...>> / b1<<...>> notation instead of +, see modernConcat in String Concatenation and Indefinite-Length Strings.

Preserve app-string/-sequence notation

Some built-in extensions (dt/DT, ip/IP) support prefix'...' (app-string), prefix`...` (backtick app-string), prefix<<...>> (app-sequence), and a raw tag literal (N(...)) notation for the same value, and by default regenerate prefix'...' from the resolved value on every CBOR.format() call — so DT`1969-07-21T02:56:16Z`, DT<<'1969-07-21T02:56:16Z'>>, and even the raw tag form 1(1749772800) all normalize to DT'...' notation, and a non-canonical DT'...' spelling (e.g. a +00:00 offset instead of Z) gets rewritten too. preserveAppPrefix keeps the original spelling instead — whichever form was used. It has no effect when appPrefix: false is also set (raw tag notation is used either way regardless of the original spelling), or on values not parsed from one of these forms.

import { CBOR } from '@cbortech/cbor';

CBOR.format('1(1749772800)');
// "DT'2025-06-13T00:00:00Z'"

CBOR.format('1(1749772800)', { preserveAppPrefix: true });
// "1(1749772800)"

CBOR.format("DT<<'1969-07-21T02:56:16Z'>>", { preserveAppPrefix: true });
// "DT<<'1969-07-21T02:56:16Z'>>"

CBOR.format('DT`1969-07-21T02:56:16Z`', { preserveAppPrefix: true });
// "DT`1969-07-21T02:56:16Z`"

Preserve comments

By default, CBOR.fromCDN() discards comments and CBOR.format() emits none. preserveComments: true captures them while parsing and re-emits each comment verbatim, with whichever marker (#, //, /* */, / /) it was originally written with.

import { CBOR } from '@cbortech/cbor';

const text = '{ "a": 1 } # trailing comment';

CBOR.format(text, { indent: 2 });
// '{\n  "a": 1\n}'

CBOR.format(text, { indent: 2, preserveComments: true });
// '{\n  "a": 1\n} # trailing comment'

To normalize every comment's marker instead of keeping the mix as originally written, use comments'c-style' for // and /* */, or 'cdn-style' for # and / /. It has no effect when preserveComments is true (verbatim wins); explicitly set comments: 'strip' (or leave both options unset) to drop comments entirely.

CBOR.format(text, { indent: 2, comments: 'c-style' });
// '{\n  "a": 1\n} // trailing comment'

Only effective when indent enables pretty-printing: single-line output strips all comments regardless, since line comments can only be terminated by a newline.

Preserve blank lines

By default, CBOR.format() drops blank lines between array/map entries (and (_ ...) chunks) when re-serializing. preserveBlankLines re-emits a single blank line above an entry that had one anywhere before it in the source, so paragraph-like groupings of entries survive a reformat — at most one blank line per gap, regardless of how many were originally there. Detection is based on entry positions alone: it does not require preserveComments and is unaffected by whether comments are emitted. Only effective when indent enables pretty-printing; a container with a preserved blank line is always rendered one entry per line, even under inlineLeafContainers. Included in preserveAll.

import { CBOR } from '@cbortech/cbor';

const src = `[
  1,
  2,

  3
]`;

CBOR.format(src, { indent: 2 });
// [
//   1,
//   2,
//   3
// ]

CBOR.format(src, { indent: 2, preserveBlankLines: true });
// [
//   1,
//   2,
//
//   3
// ]

Format with minimal changes

preserveAll turns on every preserve* option at once, to reformat CDN text — e.g. when reformatting on save in an editor — touching only whitespace/indentation and leaving most literals' original spelling untouched (bignums are the one exception; see preserveNumberFormat above). An explicitly-set individual option (including false) still wins over preserveAll.

import { CBOR } from '@cbortech/cbor';

CBOR.format('{"a":0xff,"b":1.5_1,"c":b64\'aGk=\'}', {
  indent: 2,
  preserveAll: true,
});
// {
//   "a": 0xff,
//   "b": 1.5_1,
//   "c": b64'aGk='
// }

CBOR.format() passes the same options to both fromCDN() and toCDN() internally, so this one option is enough. Calling them separately needs preserveAll (or preserveComments) on the fromCDN() side too, since comments must be captured while parsing to be re-emittable later:

const item = CBOR.fromCDN(text, { preserveAll: true });
item.toCDN({ preserveAll: true, indent: 2 });

Validate CBOR / CDN / hex dump

validate checks input for well-formedness and validity without throwing. Recoverable violations (e.g. duplicate map keys) are collected into warnings instead of stopping decoding; truly malformed data is reported via error instead (for CDN syntax errors, a CdnSyntaxError with its position fields intact). Informational hints — e.g. an app-string prefix that matches a known optional extension which isn't registered — never affect valid and are collected separately into hints. type selects the input format: 'cbor' (default), 'cdn', or 'hex'.

import { CBOR } from '@cbortech/cbor';

// CBOR bytes — duplicate map key "a" is a recoverable violation
CBOR.validate(new Uint8Array([0xa2, 0x61, 0x61, 0x01, 0x61, 0x61, 0x02]), {
  type: 'cbor',
});
// { valid: false, count: 1, warnings: [{ message: 'duplicate map key at offset 4', offset: 4 }], hints: [] }

// CDN text — well-formed input
CBOR.validate('{"a": 1}', { type: 'cdn' });
// { valid: true, count: 1, warnings: [], hints: [] }

// Annotated hex dump text — truncated array (length 3, only 2 elements present)
CBOR.validate('83  -- Array of length 3\n   01     -- 1\n   02     -- 2', {
  type: 'hex',
});
// { valid: false, count: 0, warnings: [], hints: [], error: Error(...) }

Working With The AST

CBOR.fromCBOR(), CBOR.fromCDN(), and CBOR.fromJS() return a CBOR item. Concrete node classes such as CborTextString, CborByteString, CborArray, and CborTag are exported from @cbortech/cbor/ast for extensions. Every item supports these methods:

import { CBOR } from '@cbortech/cbor';
import { CborItem } from '@cbortech/cbor/ast';

const item = CBOR.fromCDN('{ "x": 1 }');
item satisfies CborItem;

const bytes = item.toCBOR();
const text = item.toCDN();
const value = item.toJS();

Parse to AST, then serialize

import { CBOR } from '@cbortech/cbor';

const item = CBOR.fromCDN('[_ 1, 2, 3]');

console.log(item.toCDN());
// [_ 1,2,3]

console.log(item.toCBOR());
// Uint8Array(...)

Decode to AST, then inspect as CDN

import { CBOR } from '@cbortech/cbor';

const item = CBOR.fromCBOR(new Uint8Array([0x83, 0x01, 0x02, 0x03]));

console.log(item.toCDN());
// [1,2,3]

console.log(item.toJS());
// [1, 2, 3]

JSON-like API

CBOR.parse() and CBOR.stringify() intentionally feel similar to JSON.parse() and JSON.stringify().

Unlike JSON, CBOR can represent undefined as a value. Use CBOR.OMIT from a reviver or replacer when you want to remove an object entry or map entry explicitly, instead of producing an undefined value.

Reviver function

import { CBOR } from '@cbortech/cbor';

const value = CBOR.parse(
  '{"createdAt": "2026-05-06T00:00:00Z"}',
  (key, value) => {
    if (key === 'createdAt') return new Date(value);
    return value;
  }
);

console.log(value);
// { createdAt: 2026-05-06T00:00:00.000Z }

Replacer function

import { CBOR } from '@cbortech/cbor';

const text = CBOR.stringify({ id: 1, password: 'secret' }, (key, value) =>
  key === 'password' ? CBOR.OMIT : value
);

console.log(text);
// {"id":1}

Replacer key list

import { CBOR } from '@cbortech/cbor';

const text = CBOR.stringify(
  { id: 1, name: 'Alice', password: 'secret' },
  ['id', 'name'],
  2
);

console.log(text);
// {
//   "id": 1,
//   "name": "Alice"
// }

Default Options

Create a CBOR instance when you want to reuse the same options.

import { CBOR } from '@cbortech/cbor';

const cbor = new CBOR({
  extensions: [CBOR.dt_as_Date],
  indent: 2,
});

const value = cbor.parse("DT'2026-05-06T00:00:00Z'");

console.log(value);
// Date(...)

console.log(cbor.stringify({ value }));
// {
//   "value": DT'2026-05-06T00:00:00Z'
// }

Dates

CDN dt'...' and DT'...' literals are parsed by default. Add CBOR.dt_as_Date when you want JavaScript Date objects.

import { CBOR } from '@cbortech/cbor';

const value = CBOR.parse("DT'2026-05-06T00:00:00Z'", {
  extensions: [CBOR.dt_as_Date],
});

console.log(value instanceof Date);
// true
import { CBOR } from '@cbortech/cbor';

const text = CBOR.stringify(new Date('2026-05-06T00:00:00Z'), {
  extensions: [CBOR.dt_as_Date],
});

console.log(text);
// DT'2026-05-06T00:00:00Z'

Per-item option overrides

itemOptions on toJS() is called for every node before it is converted, so one part of a document can be converted differently from the rest. Return a partial options object to override options for that node and its descendants, or undefined to leave them unchanged. ctx.path identifies the node as a sequence of array indices and map keys, empty at the root.

It can be called more than once for the same node — when a reviver is also present, arrays and object-mode maps convert each child at least twice: once to build a value visible to an earlier sibling's own reviver call, and once more for the value that's actually kept. Write it as a pure function of node/ctx, not relying on how many times it runs.

import { CBOR } from '@cbortech/cbor';

const item = CBOR.fromCDN(
  `{"date1": DT'2026-08-23T00:00:00Z', "date2": DT'2026-08-23T00:00:00Z'}`
);

const value = item.toJS({
  stripTags: true,
  itemOptions: (_node, ctx) =>
    ctx.path.length === 1 && ctx.path[0] === 'date1'
      ? { extensions: [CBOR.dt_as_Date] }
      : undefined,
});

console.log(value);
// { date1: Date(...), date2: 1787443200 }

extensions on ToJSOptions also works on its own, without itemOptions, to reinterpret an entire tree: a value parsed with the default dt extension can still be converted with dt_as_Date (or vice versa) by passing extensions directly to toJS().

ctx.options carries the options already in effect for the node — the root options merged with whatever an ancestor's itemOptions already returned — so a callback can build on the current value of an option instead of overriding it outright:

itemOptions: (_node, ctx) => ({
  extensions: [...(ctx.options.extensions ?? []), CBOR.dt_as_Date],
});

toCDN() takes the same itemOptions option, formatting one part of a document differently from the rest:

import { CBOR } from '@cbortech/cbor';

const item = CBOR.fromCDN('{"raw": 255, "count": 255}');

const text = item.toCDN({
  itemOptions: (_node, ctx) =>
    ctx.path.length === 1 && ctx.path[0] === 'raw'
      ? { intFormat: 'hex' }
      : undefined,
});

console.log(text);
// {"raw":0xff,"count":255}

There is no reviver for toCDN(), so it can't be called more than once for that reason — but it can still be called more than once for a node that's also a toCDN() layout decision (inlineLeafContainers's one-line collapse check, or a tag/app-sequence value's own multi-word check re-render an entry purely to answer that question before the real render). Write it as a pure function here too.

String Concatenation and Indefinite-Length Strings

The t1 / b1 / ilbs / ilts app-extensions from draft-ietf-cbor-edn-literals-27 (§3.5 / §3.6) are enabled by default.

t1<<...>> and b1<<...>> join (text or byte) string arguments from left to right into a single text string (t1) or byte string (b1). Arguments may also be ellipses (...) to elide parts of a string.

import { CBOR } from '@cbortech/cbor';

const text = CBOR.fromCDN('t1<<"Hello ", "world">>');
console.log(text.toCDN({ appPrefix: false }));
// "Hello world"

const bytes = CBOR.fromCDN("b1<<'Hello ', h'776f726c64'>>");
console.log(bytes.toCDN({ appPrefix: false }));
// 'Hello world'

ilbs<<...>> / ilts<<...>> build an indefinite-length byte / text string with one chunk per argument, honoring encoding indicators on each argument. They replace the deprecated (_ chunk, ...) streamstring syntax for new CDN documents; this library keeps accepting the legacy syntax on input.

import { CBOR } from '@cbortech/cbor';

const v = CBOR.fromCDN("ilbs<<'Hello ', 'world'>>");
console.log(v.toCDN({ appPrefix: false }));
// (_ 'Hello ','world')

Emitting t1/b1/ilbs/ilts notation

Parsing already accepts t1/b1/ilbs/ilts notation, but by default toCDN()/CBOR.format() never emit it on their own: a preserved concatenation (preserveConcatenation) still renders as +, and an indefinite-length string still renders as the legacy (_ ...) streamstring form. modernConcat and modernStreamSyntax opt into emitting the draft-27 notation instead — both default to false (the legacy syntax), and both fall back to it when appPrefix is false.

import { CBOR } from '@cbortech/cbor';

CBOR.format('"a" + "b"', {
  indent: 2,
  preserveConcatenation: true,
  modernConcat: true,
});
// t1<<"a", "b">>

CBOR.format('(_ "a", "b")', { modernStreamSyntax: true });
// ilts<<"a","b">>

modernConcat also applies within a ... elision chain (§5.2), rendering "a" + ... + "b" as t1<<"a", ..., "b">> — unlike plain concatenation, this happens regardless of preserveConcatenation, since an elision chain has no single-literal collapsed form to fall back to in the first place.

[!NOTE] Neither option converts t1/b1/ilbs/ilts source back to the legacy notation: a value parsed from t1<<...>> (or ilbs<<...>>, etc.) keeps that exact spelling on output regardless of modernConcat / modernStreamSyntax — as long as appPrefix is not false, encodingIndicators is 'auto' (both defaults), and the source is either single-line or being rendered with indent enabled. encodingIndicators: 'always'/'never' or appPrefix: false still normalize it like any other app-string value (e.g. t1<<"a", "b">> becomes "ab"_i under encodingIndicators: 'always'), and a multi-line source falls back to normalized output in single-line mode (that layout can't be reproduced without indent):

CBOR.format('t1<<\n  "a",\n  "b"\n>>');
// '"ab"' — falls back: multi-line source, no `indent`
CBOR.format('t1<<\n  "a",\n  "b"\n>>', { indent: 2 });
// 't1<<\n  "a",\n  "b"\n>>' — kept verbatim

Both options only affect how a value reconstructed from a + chain or a (_ ...) chunk list is newly rendered.

[!NOTE] The identifiers t1 and b1 are explicitly provisional in draft-27 and may be renamed by the CBOR working group.

float

Interprets a hex bit-pattern as an IEEE 754 floating-point value (draft-ietf-cbor-edn-literals-27 §3.8). Enabled by default.

import { CBOR } from '@cbortech/cbor';

const v = CBOR.fromCDN("float'7e00'");
console.log(v.toCDN({ appPrefix: false }));
// NaN

// Interpret bytes as float bits
const v2 = CBOR.fromCDN("float<<h'3f800000'>>");
console.log(v2.toCDN({ appPrefix: false }));
// 1.0_2

Optional Extensions

This package includes several bundled extensions that are not enabled by default. Import what you need and pass it through the extensions option.

b32 / h32

Byte-string literals using RFC 4648 Base32 encoding. These prefixes are described in §8 of RFC 8949 and also mentioned in draft-ietf-cbor-edn-literals.

  • b32 — §6 Base32 (A–Z 2–7 alphabet)
  • h32 — §7 Base32Hex (0–9 A–V alphabet)
import { CBOR, b32, h32 } from '@cbortech/cbor';

const v1 = CBOR.fromCDN("b32'AEBAGBA'", { extensions: [b32] });
console.log(v1.toCDN({ appPrefix: false }));
// h'01020304'

const v2 = CBOR.fromCDN("h32'00P00'", { extensions: [h32] });
console.log(v2.toCDN({ appPrefix: false }));
// h'003200'

same

same<<expr, expr, ...>> verifies that every item in the sequence encodes to identical CBOR bytes and returns the first item. This extension is described in draft-bormann-cbor-edn-app-ext.

import { CBOR, same } from '@cbortech/cbor';

const v = CBOR.fromCDN("same<<h'0102', h'0102'>>", { extensions: [same] });
console.log(v.toCDN({ appPrefix: false }));
// h'0102'

// A single-item sequence always passes
const v2 = CBOR.fromCDN('same<<42>>', { extensions: [same] });
console.log(v2.toCDN({ appPrefix: false }));
// 42

Additional app-extensions are published as separate packages. Install the ones you need and pass them through the extensions option.

hash

hash is an app-extension defined in §3.4 of draft-ietf-cbor-edn-literals. It represents cryptographic hash values in the form hash'algorithm:value'. Because it requires an external cryptographic library, it is provided separately as @cbortech/hash-extension.

npm install @cbortech/hash-extension
import { CBOR } from '@cbortech/cbor';
import { hash } from '@cbortech/hash-extension';

const cbor = new CBOR({ extensions: [hash] });

const digest = cbor.parse(
  "hash'sha-256:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='"
);
// Uint8Array(32) [227, 176, 196, 66, 152, 252, 28, 20, 154, 251, 244, 200,
//                 153, 111, 185, 36, 39, 174, 65, 228, 100, 155, 147, 76,
//                 164, 149, 153, 27, 120, 82, 184, 85]

uuid

uuid is a library-specific app-extension, provided separately as @cbortech/uuid-extension.

npm install @cbortech/uuid-extension
import { CBOR } from '@cbortech/cbor';
import { uuid } from '@cbortech/uuid-extension';

const cbor = new CBOR({ extensions: [uuid] });

const id = cbor.parse("uuid'550e8400-e29b-41d4-a716-446655440000'");
// Uint8Array(16) [85, 14, 132, 0, 226, 155, 65, 212, 167, 22, 68, 102, 85, 68, 0, 0]

set / map

SET and MAP are library-specific app-extensions for tagged Set and Map values. They are provided together as @cbortech/set-map-extensions. SET<<[...]>> produces CBOR tag 258 over an array, and MAP<<{...}>> produces CBOR tag 259 over a map.

npm install @cbortech/set-map-extensions
import { CBOR } from '@cbortech/cbor';
import { set, map } from '@cbortech/set-map-extensions';

const cbor = new CBOR({ extensions: [set, map] });

const roles = cbor.parse('SET<<["admin", "editor"]>>');
// Set { 'admin', 'editor' }

const scores = cbor.parse('MAP<<{"alice": 98, "bob": 72}>>');
// Map { 'alice' => 98, 'bob' => 72 }

Tags

Use CBOR.Tag for CBOR tagged values in JavaScript.

import { CBOR } from '@cbortech/cbor';

const tagged = CBOR.Tag.set('hello', 42n);
const text = CBOR.stringify(tagged);

console.log(text);
// 42("hello")
import { CBOR } from '@cbortech/cbor';

const value = CBOR.parse('42("hello")');

console.log(CBOR.Tag.get(value));
// 42n

console.log(CBOR.Tag.getValue(value));
// "hello"

Use stripTags: true when you only need the tagged content as a plain JavaScript value.

import { CBOR } from '@cbortech/cbor';

const value = CBOR.parse('42("hello")', { stripTags: true });

console.log(value);
// "hello"

Simple Values

Use CBOR.Simple for CBOR simple values other than false, true, null, and undefined.

import { CBOR } from '@cbortech/cbor';

const text = CBOR.stringify(new CBOR.Simple(16));

console.log(text);
// simple(16)
import { CBOR } from '@cbortech/cbor';

const value = CBOR.parse('simple(16)');

console.log(value instanceof CBOR.Simple);
// true

console.log(value.value);
// 16

Maps

By default, CBOR maps with text keys become plain JavaScript objects.

import { CBOR } from '@cbortech/cbor';

const value = CBOR.parse('{"a": 1, "b": 2}');

console.log(value);
// { a: 1, b: 2 }

Use mapAs: 'entries' when you need to preserve non-string keys or duplicate keys.

import { CBOR } from '@cbortech/cbor';

const entries = CBOR.parse('{1: "one", 1: "uno"}', {
  mapAs: 'entries',
});

console.log(entries instanceof CBOR.MapEntries);
// true

console.log(entries);
// [[1, "one"], [1, "uno"]]

CBOR.MapEntries can be passed back to CBOR.stringify() or CBOR.encode().

import { CBOR } from '@cbortech/cbor';

const entries = new CBOR.MapEntries([1, 'one'], [1, 'uno']);

console.log(CBOR.stringify(entries));
// {1:"one",1:"uno"}

Hex Dumps

CBOR.toHex() and CBOR.fromHex() are the shortcut entry points (see Quick Examples). For full AST access — byte ranges, re-encoding, selective inspection — use item.toHexDump() and CBOR.fromHexDump() directly:

import { CBOR } from '@cbortech/cbor';

const item = CBOR.fromCDN('[_ 1, [2, 3]]');
const dump = item.toHexDump();

console.log(dump);
// 9F        -- Start indefinite-length array
// ...
import { CBOR } from '@cbortech/cbor';

const item = CBOR.fromHexDump(`
83        -- Array of length 3
   01     -- 1
   02     -- 2
   03     -- 3
`);

console.log(item.toCDN());
// [1,2,3]

Tokenization

The @cbortech/cbor/cdn subpath exposes the same lexer the parser uses, for tooling such as syntax highlighters that must stay in exact agreement with parsing behavior:

import { tokenize, tokenizeLenient } from '@cbortech/cbor/cdn';

const { tokens, comments } = tokenize('[1, "ab"] # note');
// tokens: [{ type: 'LBRACKET', offset: 0, endOffset: 1, ... }, ...]

const lenient = tokenizeLenient('[1, "ab');
// Never throws: clean tokens, then one ERROR token covering the
// unscannable tail, plus the failure in lenient.error.

Syntax errors thrown by fromCDN/parse/tokenize are CdnSyntaxError instances (a SyntaxError subclass, also exported from the main entry) and carry offset, line, column, and — where known — endOffset.

CDDL

The @cbortech/cbor/cddl subpath contains a parser, compiler, and validator for CDDL, the schema language for describing CBOR data structures. A compiled schema validates CBOR bytes, CDN text, or a CborItem AST:

import { CDDL } from '@cbortech/cbor/cddl';

const schema = CDDL.compile('point = { x: int, y: int }');

console.log(schema.validate('{"x": 12, "y": -3}').valid);
// true

Validation returns a result object rather than throwing. Failures include the instance path and source offsets for both the input and schema. Validation uses the first rule by default; pass rule to select another non-generic type rule. Options also include features, maxDepth, and maxSteps.

All control operators from RFC 8610 are implemented, along with RFC 9165's .plus, .cat, and .feature. Enable .feature names with the features validation option. Unsupported operators such as .abnf are reported in result.warnings and matched without their constraint.

The main CBOR facade also accepts a compiled schema or CDDL source text through the cddl option:

import { CBOR } from '@cbortech/cbor';

const value = CBOR.parse('{"x": 12, "y": -3}', {
  cddl: 'point = { x: int, y: int }',
});
// { x: 12, y: -3 }

Throwing methods such as parse, decode, and encode throw CddlMismatchError on a mismatch; CBOR.validate() instead collects failures in result.cddlErrors. Pass validator options through cddlValidationOptions, or set cddl as an instance default with new CBOR({ cddl: … }).

CDDL.compile() throws CddlSyntaxError or CddlSemanticError; use { strict: false } to collect semantic issues in schema.warnings instead. Compiled schemas can be formatted with schema.format(). The subpath also exports tokenize, tokenizeLenient, and a typed rule AST through schema.ast and schema.rules.

Public API

The documented public exports are:

  • CBOR
  • CdnSyntaxError
  • CddlMismatchError (thrown by the cddl option; see CDDL)

The CBOR facade also exposes:

  • CBOR.Tag
  • CBOR.Simple
  • CBOR.MapEntries
  • CBOR.dt_as_Date
  • CBOR.OMIT

Lower-level CDN tokenization lives in @cbortech/cbor/cdn (tokenize, tokenizeLenient, Token, TokenType, EdnComment), and AST node classes in @cbortech/cbor/ast.

The CDDL compiler lives in @cbortech/cbor/cddl (CDDL, CddlSchema, CddlSyntaxError, CddlSemanticError, CddlMismatchError, tokenize, tokenizeLenient, and the CDDL AST types).

Specifications

Implementation notes:

  • CDN follows draft-27 while retaining draft-25's (_ ...) streamstring syntax and + string-concatenation syntax.
  • CDDL implements every RFC 8610 control operator, plus RFC 9165's .plus, .cat, and .feature.
  • The RFC 9682 updates are implemented: its string-literal grammar (including \u{...}), empty data models at the syntax layer (a model with no rules is still a semantic error when compiled), and non-literal #6.<type> / #7.<type> head numbers. Comment PCHAR validation, bare CR line endings, and comments ending at EOF are intentionally accepted more leniently than the collected ABNF.

License

Apache-2.0