@claudewerk/envelope-codec

中文文档
0.2.1发布于 2026/10/27MITTypeScriptES Module零依赖近 30 天下载 1862

Pure encode/decode codec for { env, body? } messages. Envelope is channel-encoded (JSON or CBOR); body is opaque octets passed through byte-for-byte.

原始使用文档
文档目录13 节
  1. What it supports
  2. Install
  3. Usage
  4. Codec interface
  5. Wire format
  6. JSON channel (mandatory baseline)
  7. CBOR channel (RFC 8949, optional)
  8. Binary-frame discriminator (1 byte)
  9. Hardened decode (untrusted input)
  10. Pass-through mode
  11. Negotiation
  12. Logging
  13. License

envelope-codec

A standalone, dependency-free codec for messages of shape { env, body? }.

The whole point: a message is an envelope (structured routing/control metadata) plus an optional body (opaque octets). The codec encodes ONLY the envelope in the channel encoding; the body is passed through byte-for-byte and never decoded or inspected. That is what lets a body be any encoding, or sealed/encrypted, at zero cost to the codec.

It knows nothing about WebSockets, queues, or transports. It is a pure encode/decode pair. The Codec shape is duck-type compatible with what a reliable-connection transport expects -- but nothing here imports such a library.

  • Runtime deps: zero. cbor-x and pino are OPTIONAL peer dependencies, lazily required. The package works fully with neither installed (JSON-only).
  • Bun (bun test, bun runtime).

What it supports

Everything below is implemented and covered by bun test (51 tests):

  • Two channel encodings for the envelope -- JSON (mandatory baseline, UTF-8 JSON text) and CBOR (RFC 8949, optional via cbor-x).
  • Opaque body, byte-for-byte. JSON carries a small body inline as base64 or, in out-of-band mode, as a length/contentType descriptor only. CBOR carries a binary body as a native untagged byte string -- no base64 tax. Decode returns the exact input octets in every mode; the codec never parses the body.
  • 1-byte binary-frame discriminator (0x01 envelope / 0x02 raw chunk, with a documented registry for control/application frames), resolved BEFORE any decode so the receiver never trial-parses.
  • Pass-through mode -- wrap an arbitrary opaque frame as the body under a default envelope, so the codec can back a transport of raw bytes.
  • Hardened decode of untrusted input -- size cap before decode, max nesting depth, max collection size, duplicate-key rejection, and (CBOR) rejection of indefinite lengths and tags (no tag->class instantiation). Every failure is a typed CodecError, never a leaked throw or a hang.
  • Encoding negotiation -- preference-ordered and version-aware; peers with different local orderings still agree without a round trip.
  • Injectable logging -- silent by default; console or optional pino.

Install

bun add @claudewerk/envelope-codec
bun add cbor-x   # optional: enables the CBOR channel
bun add pino     # optional: structured logging

Usage

import { jsonCodec, cborCodec, negotiate, codecFor } from '@claudewerk/envelope-codec';

const enc = negotiate(['cbor', 'json'], theirAdvertised); // 'cbor' | 'json'
const codec = codecFor(enc);

const wire = codec.encode({
  env: { to: 'node-7', kind: 'sync', seq: 9 },
  body: new Uint8Array([0xde, 0xad, 0xbe, 0xef]), // opaque octets
  contentType: 'application/octet-stream',
});

const msg = codec.decode(wire);
// msg.env  -> { to: 'node-7', kind: 'sync', seq: 9 }   (opaque object, not interpreted)
// msg.body -> Uint8Array [0xde,0xad,0xbe,0xef]          (exact input bytes, untouched)
// msg.contentType -> 'application/octet-stream'

Codec interface

interface Codec {
  encoding: 'json' | 'cbor';
  encode(msg: { env: object; body?: Uint8Array; contentType?: string }): Uint8Array | string;
  decode(data: Uint8Array | string): { env: object; body?: Uint8Array; contentType?: string; bodyBytes?: number };
}

jsonCodec(opts?): Codec;
cborCodec(opts?): Codec;   // throws CodecError('CBOR_UNAVAILABLE') if cbor-x is absent
negotiate(ours: string[], theirs: string[]): 'json' | 'cbor';
codecFor(encoding, opts?): Codec;

Options (both codecs): { maxBytes, limits: { maxDepth, maxItems }, log, passthrough }. jsonCodec also takes { oob } (out-of-band body, see below).

In pass-through mode (passthrough: true) encode() also accepts a raw Uint8Array. See Pass-through mode.

Wire format

The env is opaque -- the codec never reads its fields. Both channels serialize the same logical wire object with keys env, optional contentType, and an optional body carried differently per channel.

JSON channel (mandatory baseline)

The envelope is UTF-8 JSON text:

{
  "env": { "...": "..." },
  "contentType": "application/octet-stream",
  "body": "<base64 of the opaque body>"
}

A binary body rides inline as base64 under body. base64 round-trips the exact octets, so decode restores them unchanged; the bytes are never parsed.

Out-of-band mode (jsonCodec({ oob: true })): no bytes are inlined; only a descriptor is emitted and the body travels on a side channel.

{ "env": { "...": "..." }, "contentType": "video/mp4", "bodyBytes": 1048576 }

Decode then returns { env, contentType, bodyBytes } and no body.

CBOR channel (RFC 8949, optional)

The envelope is a CBOR map; a binary body is a native CBOR byte string (major type 2) -- no base64 tax:

map(2) {
  "env"  => map(...)      ; the opaque envelope
  "body" => bytes(N)      ; major type 2, untagged -- the opaque body
}
; "contentType" => text   ; present when set

The body byte string is untagged (header 0x40 | len), so it is a plain byte string, not a tagged/typed-array value.

Binary-frame discriminator (1 byte)

In binary (CBOR) mode a caller may interleave envelope messages with raw opaque chunks (e.g. bulk transfer). A single leading byte distinguishes them and is resolved BEFORE any decode, so the receiver never trial-parses:

Discriminator registry (owned by this package; a composing protocol must stay within it). This codec emits and consumes only 0x01/0x02; the rest are reserved so higher layers can add frames without ever reassigning these two.

Byte(s)ClassMeaning
0x00reservednever emit
0x01envelopean encoded { env, body? }
0x02chunkraw/opaque bytes, never decoded
0x03..0x0fcontrolreserved for protocol control frames
0x10..0xffapplicationapplication-defined

frameType(bytes) routes only the two bytes this codec owns (returns 'envelope'/'chunk', throws BAD_FRAME otherwise). frameClass(byte) classifies any byte against the registry (reserved/envelope/chunk/ control/application) for a higher layer's own routing.

import { framed, frameType, encodeChunk, decodeChunk } from '@claudewerk/envelope-codec';

const codec = framed(cborCodec());     // encode() now prepends 0x01
const wire = codec.encode({ env, body });

// receiver: route on the discriminator, THEN decode -- no trial parse
switch (frameType(wire)) {
  case 'envelope': return codec.decode(wire);
  case 'chunk':    return handleRaw(decodeChunk(wire));
}

Design note: the spec said "expose it as an option". frameType() + framed() is a deliberate improvement -- the discriminator MUST be resolved by the receiver before decode, and a decode-time option cannot do that without already committing to a parse. Routing is therefore a separate, pre-decode step.

Hardened decode (untrusted input)

Every decode path enforces limits and turns any failure into a typed CodecError (never a leaked throw or a hang):

GuardJSONCBORCodecError.code
Size cap before decode✓✓TOO_LARGE
Max nesting depth✓✓TOO_DEEP
Max items per collection✓✓TOO_MANY_ITEMS
Duplicate keys rejected✓✓DUP_KEY
Indefinite-length rejected--✓INDEFINITE_LENGTH
Tags rejected (no class inst.)--✓UNSUPPORTED
Malformed input✓✓MALFORMED

CBOR input is structurally scanned by a hand-written pass (cbor-scan.js) BEFORE cbor-x ever touches it, so no attacker-controlled shape is instantiated and no tag maps to a class. JSON is scanned before JSON.parse to catch depth and duplicate keys, which JSON.parse alone cannot.

Defaults: maxBytes = 16 MiB, maxDepth = 64, maxItems = 100000. Override via the limits / maxBytes options.

Pass-through mode

To back a transport whose frames are ARBITRARY bytes (not already { env, body }), enable passthrough. encode() then also accepts a raw Uint8Array and wraps it as the body under a default (empty) envelope, so not every frame has to be an envelope to flow through the codec.

const codec = cborCodec({ passthrough: true });        // or an env object: { passthrough: { src: 'transport' } }

codec.encode(new Uint8Array([1, 2, 3]));               // -> encodes { env: {}, body: <those bytes> }
codec.encode({ env: { k: 1 }, body });                 // normal messages still work

const { env, body } = codec.decode(wire);              // body is the exact opaque frame back

Without passthrough, feeding raw bytes to encode() is a CodecError (MALFORMED) -- a message is required.

Negotiation

Preference-ordered and version-aware, so two peers with different local orderings still agree without a round trip.

negotiate(ours, theirs) // -> 'json' | 'cbor'
  • Canonical PREFERENCE = ['cbor', 'json'] (most-preferred first) is owned by this package and exported.
  • Advertised encodings are tokens name[/version] (e.g. 'cbor/1'); matching is by name.
  • Result = the first name in PREFERENCE that BOTH sides advertise, availability-gated (cbor only if the cbor-x peer dep is present here).
  • JSON is the always-available fallback.
negotiate(['json', 'cbor'], ['cbor', 'json']) // -> 'cbor'  (order-independent)
negotiate(['cbor/1'],       ['cbor/2'])       // -> 'cbor'  (version-agnostic match)
negotiate(['cbor'],         ['json'])         // -> 'json'

Logging

Injectable Logger (debug/info/warn/error); silent by default. consoleLogger and pinoLogger() (lazy, optional pino) are provided. Pass any object with the four methods as { log }.

License

MIT

版本与发布记录

@claudewerk/envelope-codec 共发布 3 个版本,首次发布于 2026/09/28;过去 12 个月发布了 3 个版本。

版本发布日期距今
0.2.1latest2026/10/2713 天前
0.2.02026/09/281 个月前
0.1.02026/09/281 个月前

关于 @claudewerk/envelope-codec 的常见问题

@claudewerk/envelope-codec 是什么?

Pure encode/decode codec for { env, body? } messages. Envelope is channel-encoded (JSON or CBOR); body is opaque octets passed through byte-for-byte.

如何安装 @claudewerk/envelope-codec?

在项目目录运行 npm install @claudewerk/envelope-codec,或使用 pnpm add / yarn add / bun add。

@claudewerk/envelope-codec 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@claudewerk/envelope-codec 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@claudewerk/envelope-codec 可以商用吗?

@claudewerk/envelope-codec 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@claudewerk/envelope-codec 的下载量有多少?

根据 npm 官方下载统计,@claudewerk/envelope-codec 在 2026/10/10 – 2026/11/08 这 30 天内被下载 1,862 次,日均约 62 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@claudewerk/envelope-codec 还在维护吗?

最新版本 0.2.1 发布于 2026/10/27(13 天前);至今共发布 3 个版本,其中过去 12 个月发布了 3 个;首次发布于 2026/09/28。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。