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

@vanaheimr/metrological-cbor

v0.11.0

Published

Reference implementation of Metrological CBOR (mCBOR), CBOR tag 44252: quantity values with unit of measure, SI prefix and GUM measurement uncertainty.

Downloads

1,179

Readme

Metrological CBOR for TypeScript

CI Nightly npm

A reference implementation of Metrological CBOR (Tag 44252) for TypeScript: a compact, signable binary representation of a measured physical quantity with unit of measure, SI prefix and GUM measurement uncertainty.

44252([4([-1, 50]), 4, -3])                     ->  5.0 mA

D9ACDC 84 C482211959D8 05 00 A201C482210C0202   -> (230.00 ±0.12) V, k=2	

A bare 230 is meaningless without "volt". A reading of 1.10 kWh says more than 1.1 kWh, because the trailing zero states the resolution of the measurement. And a value without a statement of uncertainty is, strictly speaking, not a measurement result at all. Tag 44252 carries all three — unit, exact decimal scale and GUM uncertainty — while a generic CBOR decoder that has never heard of the tag still sees a well-formed array of standard numbers.

Current Status

Installation

npm install @vanaheimr/metrological-cbor

Node 20 or newer. No runtime dependencies. ESM and CommonJS, with type declarations.

Reading and writing a metrological value

import { decodeMetrologicalValue, encodeMetrologicalValue,
         metrologicalValue, decimal, unitById, Units, SIPrefix,
         bytesToHex, hexToBytes } from '@vanaheimr/metrological-cbor';

// 5.0 mA — nine bytes on the wire
const reading = decodeMetrologicalValue(hexToBytes('D9ACDC83C4822018320422'));

reading.formatValue();     // '5.0'  — the trailing zero is the resolution
reading.prefix;            // -3
reading.unit;              // the ampere

bytesToHex(encodeMetrologicalValue(reading));   // back to the identical bytes

Writing one is the same in reverse:

const energy = metrologicalValue({
    value:  decimal(110, -2),       // 1.10, exactly
    unit:   unitById(Units.WattHour),
    prefix: SIPrefix.Kilo,
});

bytesToHex(encodeMetrologicalValue(energy));    // 'D9ACDC83C48221186E0203'

Decoding is strict by default: bytes that are not the encoding a conforming encoder would have produced are rejected, which is what data that was signed requires. { strict: false } accepts those spellings and normalises them. { units: 'preserve' } on the way out reproduces a symbolic unit as it arrived, so a signature over a document this library did not write survives.

A whole document through JSON

import { mcborToJson, jsonToMcbor } from '@vanaheimr/metrological-cbor';

mcborToJson(meterReading);
// {
//   meter:       '1ISA0000000042',
//   transaction: 'a4f1c9e2',
//   context:     'Transaction.Begin',
//   time:        '2026-08-15T08:14:00Z',
//   energy:      '(1234.567 ±12.3) kWh, k=2, p=0.95, dist=normal'
// }

The measurement is one string — value, decimal scale, unit, prefix, magnitude, coverage factor, coverage probability and distribution, all of it — and everything else is ordinary JSON. jsonToMcbor reads it back, byte-identical for documents of readings, text, integers within the safe range, booleans, nulls, arrays and text-keyed maps.

What JSON cannot hold exactly is refused rather than rounded: an integer beyond 2^53 is an error, not the nearest double. Byte strings, floats and dates convert one way, and say so.

Which strings are readings is the caller's decision, and there is no default guess. readings defaults to 'none': a string stays a string. Turning a prose field into a measurement is the failure nothing downstream can notice — the result is a perfectly well-formed reading of something nobody measured — so it is not something a caller gets without asking.

jsonToMcbor(json, { readings: 'auto' });                                  // try every string
jsonToMcbor(json, { readings: (text, path) => path.at(-1) === 'energy' }); // or decide per path

'auto' tries every string against the reading grammar, which is what recovers a document nobody described and what the round trip above needs. Its hazard is the documented one: a free-text field holding "1 h" becomes one hour. An application with a schema should use the predicate.

A reading as text

import { formatMetrologicalValue, parseMetrologicalValue } from '@vanaheimr/metrological-cbor';

formatMetrologicalValue(reading);            // '(230.00 ±0.12) V, k=2'
formatMetrologicalValue(reading, { ascii: true });  // '(230.00 +/-0.12) V, k=2'

parseMetrologicalValue('9.81 m·s⁻²');        // and 'm*s^-2'

This is a second encoding rather than a pretty-printing: what is written reads back to the same bytes, which is what will let a whole document travel through JSON with every measurement intact. The grammar is docs/text-format.md.

Two of its rules exist because a generated reading found them missing. A prefix is folded into a symbol only where the result reads back as the same unit — a centi-day would fold into cd, which is the candela — and a superscript is only written where symbol and exponent do not together spell some other symbol: the metre cubed is m^3, because is the registered cubic metre.

What else works today

import { UnitRegistry, Units, METROLOGICAL_VALUE_TAG } from '@vanaheimr/metrological-cbor';

const registry = UnitRegistry.standard;

registry.byId(Units.Volt).symbol;     // 'V'
registry.bySymbol('Wh').id;           // 2
registry.bySymbol('Ohm').id;          // 14, via the alias
registry.bySymbol('Ω').id;       // 14, the OHM SIGN normalises onto U+03A9
registry.byId(Units.DegreeCelsius);   // affine: true

METROLOGICAL_VALUE_TAG;               // 44252

The CBOR core reads and writes the format the tag lives in:

import { decodeHex, encodeToHex, diagnostic, walk } from '@vanaheimr/metrological-cbor';

const reading = decodeHex('D9ACDC 83 C482201832 04 22');

diagnostic(reading);        // '44252([4([-1, 50]), 4, -3])'  — 5.0 mA
encodeToHex(reading);       // back to the identical bytes

Integers are integers whatever their magnitude — a bignum mantissa is a bigint, not a lost decimal place — and the encoder is deterministic, so the same value always produces the same bytes and therefore the same signature. Decoding is strict by default: anything that is not the encoding a deterministic encoder would have produced is rejected, which is what data that was signed requires.

And the model expresses a reading, exactly:

import { metrologicalValue, decimal, integer, unitById, uncertainty,
         Units, SIPrefix, standardUncertainty } from '@vanaheimr/metrological-cbor';

// (230.00 ±0.12) V, k = 2 — a calibration certificate, as written
const voltage = metrologicalValue({
    value:       decimal(23000, -2),      // 230.00, and the trailing zeros are data
    unit:        unitById(Units.Volt),
    uncertainty: uncertainty({ magnitude: decimal(12, -2), coverageFactor: integer(2) }),
});

voltage.formatValue();                    // '230.00'
standardUncertainty(voltage.uncertainty!, { scale: 3, rounding: 'half-even' });  // 0.060

The magnitude stays as the certificate reported it, together with the coverage factor it belongs to — it is never normalised to u behind your back. Deriving u = U / k makes you state the scale and the rounding, because choosing a precision for a measurement result is not the library's decision.

Lookups reject rather than guess, because a value silently attributed to the wrong unit is worse than a decoding failure:

registry.byId(0);        // UnitError ERR_UNIT_ID_RESERVED
registry.byId(70000);    // UnitError ERR_UNIT_ID_OUT_OF_RANGE
registry.byId(45);       // UnitError ERR_UNIT_UNKNOWN
registry.tryById(45);    // undefined, where the caller prefers that

Registries are immutable, so an application registering a private-use unit cannot change how unrelated code decodes the wire:

const extended = registry.withPrivateUnits({
    id:     40000,          // 32768..65535 is the private-use range
    symbol: 'flurbo',
    name:   'flurbo',
});

Signing it

The library does no cryptography and never will. Signing belongs to COSE, and a data format that also carried a crypto stack would be unusable as the leaf of somebody else's schema. What the library does is produce the bytes a signature is over, exactly — and examples/06 checks that against the specification's own worked record:

station   ES256   verifies
          re-sign reproduces the recorded signature byte for byte

meter[0]  ESB256  verifies   (1234.567 ±12.3) kWh, k=2, p=0.95, dist=normal
meter[1]  ESB256  verifies   (1259.869 ±12.6) kWh, k=2, p=0.95, dist=normal

operator  ES384   verifies

The second line is the stronger claim. That record is signed deterministically (RFC 6979), so a signature is a function of what it signs: re-signing the Sig_structure this library builds reproduces the recorded signature byte for byte, which a construction differing by one byte could not do.

Examples

Six runnable programs, in examples/:

npx tsx examples/01-a-reading.ts

They are tested by running them, because documentation that is not executed rots.

Two commitments

Two design commitments run through all of it:

  • No binary floating point. An IEEE 754 double can represent neither 0.1 exactly nor a decimal scale at all. Mantissas are bigint, formatting and parsing are exact string arithmetic, and a linter rule keeps it that way.
  • The written representation is data. 4([-1, 50]) and 5 denote the same quantity but different measurement resolutions, and both survive a decode/encode round trip unchanged.

The unit registry

50 units, transcribed from Section 4 of the specification. The single-byte identifications 1..23 are allocated by frequency rather than by taxonomy — the watt-hour is 2 and the candela is 25 — because those 23 places are the scarcest thing the registry has to give away, and the cost is paid once per value transmitted, forever.

src/registry/units.json is the single source of truth; src/registry/units.generated.ts is produced from it and never edited by hand. tests/registry/specification.test.ts parses the specification document itself and compares it with the registry in both directions — table rows, alias list, affine marker, SenML mappings and the unit-factor examples — so the two cannot silently drift apart.

Working on it

git clone https://github.com/Vanaheimr/MetrologicalCBOR.TS.git
cd MetrologicalCBOR.TS && npm ci && npm run verify

verify is the one definition of what is checked — registry, types, lint, build, tests, API documentation — and it is what CI runs, so a green run on your machine means the same thing as a green run there.

The fuzz suites take an environment variable, because a pull request and a nightly run can afford different things:

MCBOR_FUZZ_RUNS=200000 npm run test:fuzz

Two hundred thousand cases per property is the nightly figure. What survived it, and where every normative requirement of the specification went, is in docs/conformance.md.

The specification lives in its own repository and is not committed here — npm run fetch:spec retrieves it, and the suites that compare against it skip rather than fail where it is absent.

Further reading, all of it in the repository: the conformance matrix, the text grammar, what a release takes, the rules that are not negotiable, and what counts as a vulnerability in a library that parses signed, legally relevant measurement data.

Publishing

npm version 0.11.0 --no-git-tag-version
git commit -a
git tag -s v0.11.0 -m "v0.11.0"
git push origin master v0.11.0
git push git1 master v0.11.0
git push git2 master v0.11.0

npm run verify
npm pack --dry-run
npm pack
npm login
npm whoami
npm publish

Related

License

Apache License 2.0 and NOTICE.

Copyright 2026 GraphDefined GmbH