Safe Buffer

Buffer-safety primitives that centralize the input-normalize, capped-collection, and secure-zero patterns previously scattered across parsers, atomic-file, object-store, and log-stream modules.

The safety guarantees the family enforces:

1. Type-discriminated input — every helper accepts the exact set of byte-shaped inputs it documents (Buffer / Uint8Array / string) and throws on anything else, instead of silently coercing undefined to "undefined" or letting an Object slip through Buffer.from.

2. Caller-supplied byte cap enforced BEFORE allocation. Numeric maxBytes opts are validated as positive finite integers — Infinity, NaN, and negative values throw at config time rather than disabling the cap. The bounded-chunk collector checks the running total on every push so a hostile 10-GB upstream rejects on the chunk that overflows, not after accumulating the full payload in memory.

3. UTF-8 BOM (U+FEFF) stripped by default in normalizeText so Windows-authored config files don't break downstream parsers that don't expect a leading BOM.

4. Best-effort secret hygiene via secureZero — buf.fill(0) clears the visible Buffer so a heap-dump won't show the secret in that allocation. JavaScript can't guarantee zeroing across V8 copies, but the in-buffer reference is gone.

5. Format-aware error classes. Each call site (xml-safe, json-safe, atomic-file, …) passes its own errorClass so the byte-handling lives here but the thrown error matches the caller's contract (e.code === "xml/too-large" etc.). A default SafeBufferError is used when no class is supplied.

The byte-shape predicates (HEX_RE, BASE64URL_RE, TRACE_ID_HEX_RE, SPAN_ID_HEX_RE, RFC7230_TCHAR_RE, CRLF_RE, TRAILING_HSPACE_RE) plus their helper functions (isHex, hasCrlf, stripCrlf, stripTrailingHspace) live alongside the buffer helpers because every caller that bounds bytes also tends to validate the textual shape of those bytes (header tokens, hex digests, JOSE compact serialisations, DKIM canonicalization).

b.safeBuffer.normalizeText(input, opts?) #

0.4.9
{
  maxBytes: number,        // optional positive finite int; UTF-8 byte cap
  stripBom: boolean,       // default true; remove leading U+FEFF
  errorClass: Function,    // caller-supplied Error subclass for thrown errors
  typeCode: string,        // default "buffer/wrong-input-type"
  sizeCode: string,        // default "buffer/too-large"
  typeMessage: string,     // override the wrong-input-type message
  sizeMessage: string,     // override the too-large message
}

Normalize a byte-shaped input (string / Buffer / Uint8Array) to a UTF-8 string with the byte cap enforced BEFORE the result is handed back. Anything outside the documented input set throws — null, undefined, plain objects, numbers all reject instead of being coerced via Buffer.from. The leading UTF-8 BOM (U+FEFF) is stripped by default so Windows-authored config files don't break downstream parsers.

Numeric maxBytes is validated as a positive finite integer at call-time — Infinity / NaN / negative throw rather than silently disabling the cap.

var b = require("blamejs");
var s = b.safeBuffer.normalizeText(Buffer.from("hello"));
// → "hello"

// BOM stripped by default.
var bom = Buffer.from([0xEF, 0xBB, 0xBF, 0x68, 0x69]);
b.safeBuffer.normalizeText(bom);
// → "hi"

// Non-byte input throws instead of coercing to "undefined".
try { b.safeBuffer.normalizeText(undefined); }
catch (e) { e.code; }
// → "buffer/wrong-input-type"

// maxBytes enforced; Infinity rejected at config time.
try { b.safeBuffer.normalizeText("xxx", { maxBytes: Infinity }); }
catch (e) { e.code; }
// → "buffer/bad-arg"

b.safeBuffer.toBuffer(data, opts?) #

0.4.9
{
  maxBytes: number,        // optional positive finite int; byte cap
  encoding: string,        // string→Buffer encoding. default "utf8" (e.g. "hex", "base64")
  allowString: boolean,    // accept a string input (coerced via `encoding`). default true;
                           //   false = byte inputs only (Buffer/Uint8Array) — a string throws
                           //   (COSE_Key / mdoc CBOR / DNSSEC bytes are byte-only by spec)
  errorClass: Function,    // caller-supplied Error subclass; thrown as new Class(message, code)
  errorFactory: Function,  // (code, message) -> Error; for caller error classes whose
                           //   constructor is (code, message) — sidesteps the errorClass order
  typeCode: string,        // default "buffer/wrong-input-type"
  sizeCode: string,        // default "buffer/too-large"
  typeMessage: string,     // override the wrong-input-type message
  sizeMessage: string,     // override the too-large message
}

Coerce a byte-shaped input (Buffer / Uint8Array / string) to a Buffer with the byte cap enforced before return. Unlike raw Buffer.from, an Object / number / undefined does NOT slip through — every non-byte input throws with a documented code. Buffer.isBuffer(data) returns the input unchanged (zero copy); Uint8Array is wrapped, string is encoded as UTF-8.

var b = require("blamejs");
var buf = b.safeBuffer.toBuffer("hello");
buf.length;
// → 5

// Buffer passes through unchanged (zero copy).
var input = Buffer.from([1, 2, 3]);
b.safeBuffer.toBuffer(input) === input;
// → true

// Object input throws instead of coercing.
try { b.safeBuffer.toBuffer({ not: "bytes" }); }
catch (e) { e.code; }
// → "buffer/wrong-input-type"

// maxBytes cap.
try { b.safeBuffer.toBuffer("abcdef", { maxBytes: 3 }); }
catch (e) { e.code; }
// → "buffer/too-large"

b.safeBuffer.makeByteCoercer(opts) #

stable0.15.13
{
  errorClass:    Function,  // required — (code, message) error constructor
  typeCode:      string,    // required — error code on a type mismatch
  messagePrefix: string,    // text before `what`. default: ""
  messageSuffix: string,    // text after `what`. default: ""
  allowString:   boolean,   // forwarded to toBuffer. default: true
  encoding:      string,    // forwarded to toBuffer. default: "utf8"
}

Bind toBuffer to one module's error contract, returning a coerce(value, what) that validates value is a byte input (with the module's allowString / encoding policy) and, on a type mismatch, throws the module's own error class with a per-field message messagePrefix + what + messageSuffix. The mirror of b.audit.namespaced / b.observability.namespaced for the byte-input boundary: each module bound toBuffer to its error class + code + message template in a hand-rolled function _bytes(x, what) { return toBuffer(x, { errorFactory: (c, m) => new XError(c, m), … }); } wrapper — this owns that binding once.

what names the field being coerced ("issuerAuth", "x coordinate") and is interpolated into the message so one coercer serves every call site in a module. The byte-mode is whatever toBuffer accepts: allowString: false for strict byte-only inputs (COSE / mdoc / DNSSEC wire data), or encoding ("hex" / "base64") for modules that accept an encoded string alongside raw bytes.

var b = require("blamejs");

function DnssecError(code, msg) { this.code = code; this.message = msg; }
var toBytes = b.safeBuffer.makeByteCoercer({
  errorClass:    DnssecError,
  typeCode:      "dnssec/bad-bytes",
  messagePrefix: "dnssec: ",
  messageSuffix: " must be a Buffer",
  allowString:   false,
});
toBytes(Buffer.from([1, 2]), "RRSIG");   // → 
// toBytes("nope", "RRSIG") throws DnssecError("dnssec/bad-bytes",
//   "dnssec: RRSIG must be a Buffer")

b.safeBuffer.byteLengthOf(value, encoding?) #

0.15.13

The byte length of a string OR a byte container, measured correctly for either. A String's .length counts UTF-16 code units, NOT bytes — comparing it to a cap named in bytes under-enforces the cap on multibyte input (a 2-4 byte character counts as 1, so the real ceiling is up to ~4x the configured limit). This primitive returns Buffer.byteLength(value, encoding) for a string and value.length for a Buffer / Uint8Array (whose .length already IS the byte count), so a byte cap is enforced the same way regardless of whether the value arrived decoded or raw. Route every byte-cap comparison through it instead of value.length > someBytesCap.

Throws TypeError on any other type (a defensive net — callers type-check their input before measuring).

var b = require("blamejs");
b.safeBuffer.byteLengthOf("a");          // → 1
b.safeBuffer.byteLengthOf("中");     // → 3  (one CJK char, 3 UTF-8 bytes)
"中".length;                          // → 1  (UTF-16 code units — the trap)
b.safeBuffer.byteLengthOf(Buffer.from([1, 2, 3])); // → 3

b.safeBuffer.byteLengthOfIfMeasurable(value) #

0.16.36

Like byteLengthOf, but returns null for a value that is not a measurable byte-carrier (a plain Array, an array-like object with a numeric .length, a number, ...) instead of throwing.

For capping the size of an UNTRUSTED metadata bag whose byte field may be any shape: a content guard measures its cap only when the value is measurable and treats an unmeasurable value as uncapped-here — its magic/shape inspection reads only the leading bytes, so it is O(1)-bounded regardless of a claimed .length — rather than throwing out of its documented never-throw-on-hostile-metadata inspection contract. Route a hostile-metadata byte cap through this instead of gating byteLengthOf on a hand-rolled typeof x.length === "number" check (which admits array-likes and crashes byteLengthOf).

var b = require("blamejs");
b.safeBuffer.byteLengthOfIfMeasurable("abc");          // → 3
b.safeBuffer.byteLengthOfIfMeasurable([1, 2, 3]);      // → null (a plain Array)
b.safeBuffer.byteLengthOfIfMeasurable({ length: 1e9 }); // → null (array-like)

b.safeBuffer.boundedChunkCollector(opts) #

0.4.9
{
  maxBytes: number,        // REQUIRED positive finite int; total byte cap
  errorClass: Function,    // caller-supplied Error subclass
  sizeCode: string,        // default "buffer/too-large"
  sizeMessage: string,     // override the too-large message
}

Streaming-body collector that enforces maxBytes at every push() — never after. A hostile upstream sending a 10-GB response rejects on the chunk that overflows the cap, instead of accumulating the full 10 GB in memory before the framework discovers the problem.

maxBytes is REQUIRED (positive finite integer). Infinity is rejected at construction because it defeats the entire purpose of the bounded collector. Each push() accepts Buffer / Uint8Array / string; non-byte chunks throw.

Returns { push, result, bytesCollected }. Call result() when the stream ends to get the concatenated Buffer.

var b = require("blamejs");
var c = b.safeBuffer.boundedChunkCollector({ maxBytes: 1024 });
c.push(Buffer.from("hello "));
c.push(Buffer.from("world"));
c.bytesCollected();
// → 11
c.result().toString("utf8");
// → "hello world"

// Cap enforced at push, not at result().
var c2 = b.safeBuffer.boundedChunkCollector({ maxBytes: 4 });
c2.push(Buffer.from("abc"));
try { c2.push(Buffer.from("defgh")); }
catch (e) { e.code; }
// → "buffer/too-large"

// Infinity rejected at construction.
try { b.safeBuffer.boundedChunkCollector({ maxBytes: Infinity }); }
catch (e) { e.code; }
// → "buffer/bad-arg"

b.safeBuffer.collectStream(stream, opts) #

0.14.18
{
  maxBytes:    number,     // REQUIRED positive finite int; total byte cap
  errorClass:  Function,   // caller Error subclass for the too-large reject
  sizeCode:    string,     // default "buffer/too-large"
  sizeMessage: string,     // override the too-large message
}

Read a Node Readable (an http.IncomingMessage request body, a file stream, an upstream response) fully into one Buffer with the byte cap enforced at every chunk — the streaming sibling of boundedChunkCollector. boundedChunkCollector is a push-based collector object; collectStream is the pump around it, so callers compose the stream case instead of reaching for a (stream, opts) overload that does not exist.

Resolves with the concatenated Buffer when the stream ends. Rejects (and destroys the stream) the moment a chunk would overflow maxBytes, so a hostile sender cannot force unbounded buffering. A bad maxBytes (missing / non-finite / Infinity) rejects rather than throwing synchronously.

var body = await b.safeBuffer.collectStream(req, { maxBytes: 65536 });
var json = b.safeJson.parse(body.toString("utf8"));
// → the parsed request body, never more than 64 KiB buffered

b.safeBuffer.secureZero(buf) #

0.4.9

Best-effort secret hygiene. buf.fill(0) clears the visible Buffer / Uint8Array so a heap-dump won't show the secret in that allocation. JavaScript can't guarantee zeroing across V8 internal copies (string interning, JIT-spilled registers), but the in-buffer reference is gone and that's the only handle the framework can reliably wipe.

Silently no-ops on non-byte inputs and on locked / shared buffers that throw on .fill — the caller's contract is "I'm done with this", not "guarantee zeroing succeeded." Pair with Buffer allocations whose lifetime is short and well-scoped.

var b = require("blamejs");
var key = Buffer.from("super-secret-key");
// ... use key ...
b.safeBuffer.secureZero(key);
key[0];
// → 0

// No-op on non-byte input.
b.safeBuffer.secureZero("a string");
// → undefined

b.safeBuffer.stripTrailingHspace(s) #

0.7.0

Strip trailing horizontal whitespace (spaces and tabs only) from a string — the "rstrip" semantic used by DKIM canonicalization (RFC 6376 §3.4.4 relaxed body), .env parsers, and YAML scalar readers. Does NOT touch CR / LF — pair with stripCrlf when you need full whitespace stripping. Non-string input passes through unchanged so the helper is safe in mixed pipelines.

var b = require("blamejs");
b.safeBuffer.stripTrailingHspace("hello   ");
// → "hello"

// Tabs stripped too; internal whitespace preserved.
b.safeBuffer.stripTrailingHspace("a b\t\t");
// → "a b"

// CR / LF intentionally preserved.
b.safeBuffer.stripTrailingHspace("hello \n");
// → "hello \n"

// Non-string passthrough.
b.safeBuffer.stripTrailingHspace(42);
// → 42

b.safeBuffer.indexAfterOpenTag(html, tagName) #

0.15.11

Find the offset in html just past the first opening tag (case-insensitive), or -1 when the tag is absent or unterminated. The insertion point a response rewriter uses to splice content right after / without a regex.

This replaces the html.match(/]*>/i) shape, which is O(n^2) in V8: a body carrying many starts with no closing > (e.g. rendered user content) makes the engine retry the greedy [^>]* from every offset — a -repeated 200K-char body benchmarks in seconds. This is a single forward indexOf walk: linear in the input, and stricter than the regex — it requires a real tag boundary after the name (whitespace, >, or /), so is not mistaken for . Non-string input returns -1.

var b = require("blamejs");
b.safeBuffer.indexAfterOpenTag("hi", "body");
// → 19  (just past the '>' of )

b.safeBuffer.indexAfterOpenTag("

no body here

", "body"); // → -1

b.safeBuffer.isHex(s, expectedLength?) #

0.7.0

Predicate for non-empty all-hex strings (case-insensitive). Pass expectedLength to bound the protocol-fixed digests — SHA3-512 is 128 hex chars, SHA-256 is 64, etc. Without expectedLength the predicate is length-agnostic and the caller is responsible for bounding length per protocol (X.509 serial, DKIM hash, audit-chain digest).

Non-string input returns false so the helper is safe in defensive request-shape readers.

var b = require("blamejs");
b.safeBuffer.isHex("deadbeef");
// → true

// Length-bounded check (SHA-256 = 64 hex chars).
b.safeBuffer.isHex("deadbeef", 64);
// → false

// Mixed case accepted.
b.safeBuffer.isHex("DeadBeef");
// → true

// Non-string returns false.
b.safeBuffer.isHex(null);
// → false

b.safeBuffer.hasCrlf(s) #

0.7.0

Detect CR or LF in a string — the canonical injection vector for HTTP-header / SMTP-envelope smuggling. Header values containing CR or LF must be rejected before serialization or stripped via stripCrlf. Non-string input returns false so callers can chain the predicate without pre-typechecking.

var b = require("blamejs");
b.safeBuffer.hasCrlf("X-Custom-Header: ok");
// → false

// Injection attempt.
b.safeBuffer.hasCrlf("ok\r\nX-Injected: bad");
// → true

// Bare LF also detected.
b.safeBuffer.hasCrlf("ok\nbad");
// → true

// Non-string returns false.
b.safeBuffer.hasCrlf(undefined);
// → false

b.safeBuffer.stripCrlf(s, replacement?) #

0.7.0

Remove every CR and LF from a string, replacing each with the replacement argument (default ""). Use this when the framework must serialize an operator-supplied string into a CRLF-delimited protocol (HTTP header value, SMTP envelope field) and prefers silent stripping over rejecting the request — most security- critical sites should use hasCrlf + reject instead.

Non-string input passes through unchanged.

var b = require("blamejs");
b.safeBuffer.stripCrlf("ok\r\nbad");
// → "okbad"

// Custom replacement (e.g. space).
b.safeBuffer.stripCrlf("a\nb\nc", " ");
// → "a b c"

// Non-string passthrough.
b.safeBuffer.stripCrlf(42);
// → 42

b.safeBuffer.foldHeaderText(value, replacement?) #

stable0.15.68

Neutralize free-text bound for a CRLF-delimited protocol line: replace every CR and LF with replacement (default a single space) so the text folds onto one line, AND remove every NUL byte. Use this for a value that may LEGITIMATELY wrap — a multi-line SMTP 5xx reply folded into one diagnostic line — where assertHeaderSafe (reject) would be too strict. Unlike bare stripCrlf, this also strips NUL, which is never valid in an RFC 5322 header value and which downstream SMTP / mail parsers treat specially. Non-string input passes through unchanged.

b.safeBuffer.foldHeaderText("550 mailbox full\r\nX-Injected: evil");
// → "550 mailbox full X-Injected: evil"

b.safeBuffer.assertHeaderSafe(value, label, ErrorClass, code) #

stable0.15.68

Throw when a string bound for a CRLF-delimited protocol line — an SMTP / RFC 5322 header value, an HTTP header — contains CR, LF, or a NUL byte, the canonical header-injection / smuggling vector. Route every Name: value\r\n builder's STRUCTURED fields (addresses, domains, identifiers, MTA names) through this; they can never legitimately carry those bytes. For free-text that may legitimately wrap (a multi-line SMTP reply folded into one diagnostic line), fold it with stripCrlf instead of rejecting. Throws new ErrorClass(code, ...) so each caller reports in its own error domain (the validateOpts convention). A non-string value passes through untouched — callers type-check separately.

b.safeBuffer.assertHeaderSafe("rcpt@example.com", "to", MailError, "mail/bad-header");
// → "rcpt@example.com"

b.safeBuffer.assertHeaderSafe("rcpt\r\nBcc: evil@x", "to", MailError, "mail/bad-header");
// → throws MailError("mail/bad-header")

b.safeBuffer.quoteString(s) #

stable0.16.9

Serialize a value as an RFC quoted-string: coerce to string, escape every backslash and DQUOTE with a leading backslash, and wrap the result in DQUOTEs. One serializer for the quoted-string grammars the framework emits — RFC 8941 §3.3.3 Structured Fields sf-string (Cache-Status, Signature-Input, Server-Timing desc), RFC 8288 Link header parameters, RFC 8601 §2.2 Authentication-Results reason, RFC 3501 §4.3 IMAP quoted strings, and RFC 5804 §1.2 ManageSieve strings — so an unescaped quote can never terminate the string early and smuggle extra parameters into the protocol line.

Escaping only — it does not validate a grammar's character range. A grammar that forbids bytes a quoted-string cannot carry (sf-string is printable-ASCII only; IMAP quoted strings cannot carry CR / LF) enforces its range check before calling this.

var b = require("blamejs");
b.safeBuffer.quoteString("cache miss");
// → "\"cache miss\""

// A quote or backslash in the value cannot break out of the string.
b.safeBuffer.quoteString('say "hi"');
// → "\"say \\\"hi\\\"\""

Last updated 2026-08-08T16:39:49.652Z by seeder.