@blamejs/core 0.12.5 → 0.12.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.12.x
10
10
 
11
+ - v0.12.6 (2026-05-22) — **`b.observability.otlpExporter` adds OTLP/protobuf-HTTP encoding (`opts.encoding: "protobuf"`).** The OTLP trace exporter now speaks `application/x-protobuf` end-to-end. Operators with high-volume telemetry opt into binary encoding via `opts.encoding: "protobuf"` (`"http/protobuf"` is accepted as a spec-name alias). The protobuf wire format encodes the same `ExportTraceServiceRequest` envelope as the existing JSON path — `ResourceSpans` → `ScopeSpans` → `Span` → `Status` / `Event` / `KeyValue` / `AnyValue` per the opentelemetry-proto repo — but emits 30-50% smaller bodies than the JSON shape on real-world workloads and avoids the JSON-parse cost on the collector side. Default stays `"json"`; collectors that don't speak protobuf keep working unchanged. Composes the existing `lib/protobuf-encoder.js` infrastructure. **Added:** *`opts.encoding: "json" | "protobuf"` on `b.observability.otlpExporter.create`* — When `"protobuf"` (or the spec-name alias `"http/protobuf"`), the exporter encodes batches as binary OTLP `ExportTraceServiceRequest` bytes and POSTs with `Content-Type: application/x-protobuf`. The retry / queue / drop-counter / audit machinery is shared with the JSON path so operators get the same operational primitives across both encodings. Default stays `"json"` — existing collectors keep working without configuration changes. · *Full OTLP trace schema encoded via `lib/protobuf-encoder.js`* — ResourceSpans / ScopeSpans / Span / Event / Status / KeyValue / AnyValue / ArrayValue are emitted per the opentelemetry-proto repo's field numbers + wire types. `trace_id` and `span_id` round-trip as fixed-length bytes (16 + 8 octets respectively). `start_time_unix_nano` / `end_time_unix_nano` use `fixed64` for the nanosecond precision the JSON path's number type lossily encoded. SpanKind enum mapping covers unspecified / internal / server / client / producer / consumer. · *`pb.int64` / `pb.sint64` — signed-integer varint shapes on `lib/protobuf-encoder.js`* — Negative integer attribute values in OTLP `AnyValue` (e.g. retry-after offsets, signed metric deltas) emit as proto3 `int64` — wire-type 0 varint, 10-byte two's-complement reinterpret per the spec. `pb.sint64` adds ZigZag-encoded varint for cases where small negatives dominate. Both accept Number / BigInt / digit-string inputs with explicit `[-2^63, 2^63 - 1]` range validation. · *`pb.fixed64` accepts string-form uint64 values* — OTLP/JSON encodes uint64 as a JSON string (per the proto3 JSON mapping) — the framework's tracer emits `start_time_unix_nano` / `end_time_unix_nano` as digit-string BigInt-to-string conversions so the JSON path stays lossless. `pb.fixed64` now accepts that same digit-string shape on the protobuf path so a single timestamp representation flows through both encodings without a separate coercion step. Refuses non-digit strings and silently-rounded Numbers above `Number.MAX_SAFE_INTEGER`. **Security:** *AnyValue recursion capped at 100 levels (CVE-2024-7254 / CVE-2025-4565 class)* — Both protobufjs (CVE-2024-7254) and protobuf-python (CVE-2025-4565) shipped DoS-via-unbounded-nested-group decoding. The OTLP `AnyValue` type permits a nested `ArrayValue { repeated AnyValue values = 1 }` that an adversarial collector-response could exploit during a future receive path. The encoder caps `_anyValueToProto` recursion at 100 levels — beyond which it emits an empty AnyValue rather than continuing to descend. Today's framework only EMITS (never receives) OTLP — but the cap is in the right place when the receive path lands. **References:** [OTLP §3 — Body encodings (JSON + protobuf)](https://opentelemetry.io/docs/specs/otlp/) · [opentelemetry-proto repo — trace/v1/trace.proto](https://github.com/open-telemetry/opentelemetry-proto/blob/main/opentelemetry/proto/trace/v1/trace.proto) · [CVE-2024-7254 (protobufjs unbounded nesting DoS)](https://nvd.nist.gov/vuln/detail/CVE-2024-7254) · [CVE-2025-4565 (protobuf-python unbounded nesting DoS)](https://nvd.nist.gov/vuln/detail/CVE-2025-4565)
12
+
11
13
  - v0.12.5 (2026-05-22) — **`b.metrics` content-negotiates OpenMetrics 1.0 + auto-attaches trace exemplars on request histograms.** The `/metrics` scrape endpoint now serves `application/openmetrics-text; version=1.0.0; charset=utf-8` when the scraper requests it via the `Accept` header (Prometheus 2.x strict mode, OpenObservability tooling). Legacy scrapers still get `text/plain; version=0.0.4` — no operator with the default Prometheus client sees a content-type change. Separately, the framework's request-duration histogram middleware now auto-attaches the active sampled trace's `trace_id` + `span_id` as the OpenMetrics §6.2 exemplar on every bucket sample, so Grafana / Tempo / Jaeger scrapers can pivot from a slow-bucket histogram to the exact trace that produced the sample. The wiring is composition-only — `b.middleware.tracePropagate` populates `req.trace.{traceId,parentId,sampled}`, the metrics middleware reads it, no operator opt-in needed. **Added:** *`Accept` content-negotiation on `b.metrics.expositionHandler()`* — When the scraper's `Accept` header includes `application/openmetrics-text`, the handler renders the OpenMetrics 1.0 wire format (`# UNIT` lines, `_total` suffix on counters, `# EOF` terminator, exemplar shape) and serves `application/openmetrics-text; version=1.0.0; charset=utf-8`. Otherwise serves Prometheus 0.0.4 `text/plain` as before. Operators relying on the legacy Prometheus content-type see no change. · *Auto-attached trace exemplars on request-duration histograms* — When `b.middleware.spanHttpServer` populates `req.span.{traceId, spanId, sampled}` on the inbound request and the span is sampled, the framework's built-in `requestDuration` histogram middleware attaches `{ labels: { trace_id, span_id }, value: <duration>, timestamp: <unix-sec> }` as the OpenMetrics §6.2 exemplar on the corresponding bucket. The exemplar's `span_id` is the server-handling span, not the upstream `traceparent`'s parent-id, so the metric-to-trace pivot in Grafana / Tempo / Jaeger lands on the work the metric measured. Operators wiring `tracePropagate` without `spanHttpServer` fall back to `req.trace.spanId` when populated; the framework never invents a span_id from the upstream parent. **Fixed:** *Accept-header weighted negotiation (Codex P1)* — The first pass treated any `Accept` header containing `application/openmetrics-text` as an unconditional OpenMetrics request — clients sending `Accept: text/plain;q=1.0, application/openmetrics-text;q=0.5` got OpenMetrics back instead of their preferred Prometheus 0.0.4. Fix: parse Accept via `b.requestHelpers.parseQualityList` and compare q-values for `application/openmetrics-text` vs `text/plain` (wildcards `*/*`, `application/*`, `text/*` honored). Defaults to Prometheus when both q-values are equal or zero (backward compatibility with the legacy default content-type). · *Exemplar span_id sources the active server span, not the upstream parent (Codex P2)* — The first pass used `req.trace.parentId` for the exemplar's `span_id` label — but `parentId` is the upstream caller's span (or empty for root requests), not the server-handling span. Fix: prefer `req.span.spanId` (set by `b.middleware.spanHttpServer`), falling back to `req.trace.spanId` for operators wiring `tracePropagate` without `spanHttpServer`. Never synthesises a span_id from `parentId`. **References:** [OpenMetrics 1.0 §1.2 (content negotiation)](https://prometheus.io/docs/specs/om/open_metrics_spec/) · [OpenMetrics 1.0 §6.2 (exemplars)](https://prometheus.io/docs/specs/om/open_metrics_spec/) · [W3C Trace Context (traceparent header)](https://www.w3.org/TR/trace-context/)
12
14
 
13
15
  - v0.12.4 (2026-05-22) — **`SECURITY.md` Watch list — remove stale "framework doesn't ship CMS / S/MIME" entry.** The Watch list bullet claiming `framework does not ship a CMS / S/MIME / PKCS#7 surface today` has been wrong since v0.10.13 — `b.cms.encodeSignedData` / `decode` / `encodeEnvelopedData` / `parseSignedData` shipped then, and `b.mail.crypto.smime.sign` / `verify` / `verifyAll` / `checkCert` shipped under the mail-stack. The Watch list is for CVE classes the framework deliberately doesn't ship a primitive for; CMS no longer fits that shape. Entry removed. **Fixed:** *Watch list no longer claims CMS / S/MIME are unshipped* — `b.cms` exposes RFC 5652 ContentInfo / SignedData / EnvelopedData encode + decode with PQC signer support (ML-DSA-65 per RFC 9909 §5, ML-DSA-87 per RFC 9909 §6, SLH-DSA-SHAKE-256f per RFC 9881). `b.mail.crypto.smime` builds on it for RFC 8551 S/MIME signed + enveloped mail with `checkCert` for X.509 chain validation. The SECURITY.md Watch list entry that pointed operators to external CMS libraries is gone; operators on regulated mail interop reach for the in-framework primitives instead.
@@ -36,6 +36,7 @@ var safeAsync = require("./safe-async");
36
36
  var safeBuffer = require("./safe-buffer");
37
37
  var validateOpts = require("./validate-opts");
38
38
  var safeUrl = require("./safe-url");
39
+ var pb = require("./protobuf-encoder");
39
40
  var { defineClass } = require("./framework-error");
40
41
 
41
42
  var OtlpExporterError = defineClass("OtlpExporterError", { alwaysPermanent: true });
@@ -189,6 +190,221 @@ function _bundleSpans(spans) {
189
190
  return { resourceSpans: resourceSpans };
190
191
  }
191
192
 
193
+ // ---- OTLP/protobuf encoder ------------------------------------------------
194
+ //
195
+ // OTLP §3 — `application/x-protobuf` body shape per the
196
+ // opentelemetry-proto repo's ExportTraceServiceRequest message.
197
+ //
198
+ // Wire-format encoding composes b.protobufEncoder. Fields are:
199
+ //
200
+ // ExportTraceServiceRequest {
201
+ // repeated ResourceSpans resource_spans = 1;
202
+ // }
203
+ // ResourceSpans {
204
+ // Resource resource = 1;
205
+ // repeated ScopeSpans scope_spans = 2;
206
+ // string schema_url = 3;
207
+ // }
208
+ // Resource {
209
+ // repeated KeyValue attributes = 1;
210
+ // uint32 dropped_attributes_count = 2;
211
+ // }
212
+ // ScopeSpans {
213
+ // InstrumentationScope scope = 1;
214
+ // repeated Span spans = 2;
215
+ // string schema_url = 3;
216
+ // }
217
+ // InstrumentationScope { string name = 1; string version = 2; ... }
218
+ // Span {
219
+ // bytes trace_id = 1; // 16 bytes
220
+ // bytes span_id = 2; // 8 bytes
221
+ // string trace_state = 3;
222
+ // bytes parent_span_id = 4; // 8 bytes or empty
223
+ // string name = 5;
224
+ // SpanKind kind = 6; // enum 0..5
225
+ // fixed64 start_time_unix_nano = 7;
226
+ // fixed64 end_time_unix_nano = 8;
227
+ // repeated KeyValue attributes = 9;
228
+ // uint32 dropped_attributes_count = 10;
229
+ // repeated Event events = 11;
230
+ // uint32 dropped_events_count = 12;
231
+ // repeated Link links = 13;
232
+ // uint32 dropped_links_count = 14;
233
+ // Status status = 15;
234
+ // }
235
+ // Event { fixed64 time_unix_nano = 1; string name = 2; repeated KeyValue attributes = 3; uint32 dropped_attributes_count = 4; }
236
+ // Status { string message = 2; enum code = 3; } // field 1 reserved
237
+ // KeyValue { string key = 1; AnyValue value = 2; }
238
+ // AnyValue {
239
+ // oneof value {
240
+ // string string_value = 1;
241
+ // bool bool_value = 2;
242
+ // int64 int_value = 3;
243
+ // double double_value = 4;
244
+ // ArrayValue array_value = 5;
245
+ // }
246
+ // }
247
+ // ArrayValue { repeated AnyValue values = 1; }
248
+ //
249
+ // AnyValue recursion is capped at MAX_ANYVALUE_DEPTH to defend the
250
+ // CVE-2024-7254 + CVE-2025-4565 protobuf nested-group DoS class.
251
+
252
+ var MAX_ANYVALUE_DEPTH = 100; // allow:raw-byte-literal — protobuf nested-message DoS cap
253
+
254
+ function _hexToBytes(hex) {
255
+ if (typeof hex !== "string" || hex.length === 0) return Buffer.alloc(0);
256
+ // Tolerate odd-length hex by left-padding with zero; OTLP spec
257
+ // requires fixed lengths but the exporter should not crash a request
258
+ // with a malformed inbound trace_id — drop-silent and emit empty.
259
+ if (hex.length % 2 !== 0) return Buffer.alloc(0);
260
+ var out = Buffer.alloc(hex.length / 2);
261
+ for (var i = 0; i < hex.length; i += 2) {
262
+ var byte = parseInt(hex.substr(i, 2), 16); // allow:raw-byte-literal — radix=16 for hex parse, not byte count
263
+ if (!isFinite(byte)) return Buffer.alloc(0);
264
+ out[i / 2] = byte;
265
+ }
266
+ return out;
267
+ }
268
+
269
+ var KIND_TEXT_TO_ENUM = {
270
+ unspecified: 0, internal: 1, server: 2, client: 3, producer: 4, consumer: 5,
271
+ };
272
+
273
+ function _anyValueToProto(v, depth) {
274
+ if (depth >= MAX_ANYVALUE_DEPTH) {
275
+ // Refuse to descend further; emit empty AnyValue. Matches the spec's
276
+ // "unknown wire type" tolerant-parser behaviour on the receive side.
277
+ return Buffer.alloc(0);
278
+ }
279
+ var t = typeof v;
280
+ if (t === "string") return pb.string(1, v);
281
+ if (t === "boolean") return pb.bool(2, v);
282
+ if (t === "number") {
283
+ if (Number.isInteger(v)) {
284
+ // OTLP AnyValue field 3 is proto int64 — wire-type 0 varint, NOT
285
+ // length-delimited. Negatives encode as the 64-bit two's-complement
286
+ // reinterpret-cast (10-byte varint per the spec). Composes
287
+ // `pb.int64` which carries the BigInt conversion + range check so
288
+ // a negative attribute value (e.g. retry-after offset, signed
289
+ // metric delta) doesn't poison the whole batch.
290
+ return pb.int64(3, v);
291
+ }
292
+ return pb.double(4, v);
293
+ }
294
+ if (Array.isArray(v)) {
295
+ var items = new Array(v.length);
296
+ for (var i = 0; i < v.length; i += 1) {
297
+ items[i] = _anyValueToProto(v[i], depth + 1);
298
+ }
299
+ var arrayInner = pb.repeatedMessage(1, items, function (b) { return b; });
300
+ return pb.embeddedMessage(5, arrayInner);
301
+ }
302
+ // Unknown → coerce to string per the JSON path's behaviour.
303
+ return pb.string(1, String(v));
304
+ }
305
+
306
+ function _keyValueToProto(kvObj) {
307
+ // kvObj is { key, value: <plain-js> } from _attrToOtlp — but we
308
+ // re-derive directly here so the protobuf path doesn't depend on
309
+ // the JSON-shaped intermediate.
310
+ return Buffer.concat([
311
+ pb.string(1, kvObj.key),
312
+ pb.embeddedMessage(2, _anyValueToProto(kvObj.rawValue, 0)),
313
+ ]);
314
+ }
315
+
316
+ function _attrsToProto(attrs) {
317
+ // attrs is the raw `{ key: value }` operator attribute object; OTLP
318
+ // KeyValue gets emitted per entry with field 9 (attributes) on Span,
319
+ // field 1 (attributes) on Resource, etc.
320
+ if (!attrs || typeof attrs !== "object") return [];
321
+ var keys = Object.keys(attrs);
322
+ var out = new Array(keys.length);
323
+ for (var i = 0; i < keys.length; i += 1) {
324
+ out[i] = { key: keys[i], rawValue: attrs[keys[i]] };
325
+ }
326
+ return out;
327
+ }
328
+
329
+ function _spanToProto(span) {
330
+ // Status code: 0=Unset, 1=Ok, 2=Error. Status field 1 is reserved.
331
+ var statusBody = Buffer.concat([
332
+ pb.string(2, (span.status && span.status.message) || ""),
333
+ pb.uint32(3, STATUS_CODE_TO_OTLP[span.status && span.status.code] || 0),
334
+ ]);
335
+ var eventsRepeated = pb.repeatedMessage(11, span.events || [], function (e) {
336
+ return Buffer.concat([
337
+ pb.fixed64(1, e.timeUnixNano || 0),
338
+ pb.string(2, e.name || ""),
339
+ pb.repeatedMessage(3, _attrsToProto(e.attributes), _keyValueToProto),
340
+ pb.uint32(4, 0),
341
+ ]);
342
+ });
343
+ return Buffer.concat([
344
+ pb.bytes(1, _hexToBytes(span.traceId)),
345
+ pb.bytes(2, _hexToBytes(span.spanId)),
346
+ pb.string(3, ""), // trace_state (not yet propagated by the framework)
347
+ pb.bytes(4, _hexToBytes(span.parentSpanId || "")),
348
+ pb.string(5, span.name || ""),
349
+ pb.uint32(6, KIND_TEXT_TO_ENUM[span.kind] != null ? KIND_TEXT_TO_ENUM[span.kind] : KIND_TEXT_TO_ENUM.internal),
350
+ pb.fixed64(7, span.startTimeUnixNano || 0),
351
+ pb.fixed64(8, span.endTimeUnixNano || span.startTimeUnixNano || 0), // allow:raw-byte-literal — proto field number 8, not bytes
352
+ pb.repeatedMessage(9, _attrsToProto(span.attributes), _keyValueToProto),
353
+ pb.uint32(10, span.droppedAttributesCount || 0),
354
+ eventsRepeated,
355
+ pb.uint32(12, span.droppedEventsCount || 0),
356
+ pb.uint32(15, 0), // links repeated count placeholder; encoder emits 0 length-delim when no links
357
+ Buffer.concat([
358
+ pb._tag(15, 2), // WIRE_LDELIM tag for status
359
+ pb._writeVarint(statusBody.length),
360
+ statusBody,
361
+ ]),
362
+ ]);
363
+ }
364
+
365
+ // `bundle` is the value returned by _bundleSpans — { resourceSpans: [...] }
366
+ // where each entry has { resource, scopeSpans: [{ scope, spans: [...] }] }
367
+ // in the JSON-shape. We re-derive the proto bytes from the SAME pre-OTLP
368
+ // span list so the protobuf path doesn't double-transform the data.
369
+ function _bundleSpansToProto(spansArray) {
370
+ if (spansArray.length === 0) return Buffer.alloc(0);
371
+ var byResource = new Map();
372
+ for (var i = 0; i < spansArray.length; i += 1) {
373
+ var s = spansArray[i];
374
+ var resKey = JSON.stringify(s.resource || {});
375
+ var bucket = byResource.get(resKey);
376
+ if (!bucket) {
377
+ bucket = {
378
+ resource: s.resource || {},
379
+ scope: s.scope || { name: "blamejs", version: null },
380
+ spans: [],
381
+ };
382
+ byResource.set(resKey, bucket);
383
+ }
384
+ bucket.spans.push(s);
385
+ }
386
+ var resourceSpansPieces = [];
387
+ for (var entry of byResource) {
388
+ var b = entry[1];
389
+ var resourceBody = pb.repeatedMessage(1, _attrsToProto(b.resource), _keyValueToProto);
390
+ var scopeBody = Buffer.concat([
391
+ pb.string(1, b.scope.name || "blamejs"),
392
+ pb.string(2, b.scope.version || ""),
393
+ ]);
394
+ var spansRepeated = pb.repeatedMessage(2, b.spans, _spanToProto);
395
+ var scopeSpansBody = Buffer.concat([
396
+ pb.embeddedMessage(1, scopeBody),
397
+ spansRepeated,
398
+ ]);
399
+ var resourceSpansBody = Buffer.concat([
400
+ pb.embeddedMessage(1, resourceBody),
401
+ pb.embeddedMessage(2, scopeSpansBody),
402
+ ]);
403
+ resourceSpansPieces.push(pb.embeddedMessage(1, resourceSpansBody));
404
+ }
405
+ return Buffer.concat(resourceSpansPieces);
406
+ }
407
+
192
408
  function create(opts) {
193
409
  validateOpts.requireObject(opts, "otlpExporter", OtlpExporterError);
194
410
  validateOpts(opts, [
@@ -196,6 +412,7 @@ function create(opts) {
196
412
  "flushIntervalMs", "timeoutMs", "maxAttempts",
197
413
  "backoffInitialMs", "backoffMaxMs",
198
414
  "fetchImpl", "audit", "allowedProtocols",
415
+ "encoding",
199
416
  ], "otlpExporter.create");
200
417
  validateOpts.requireNonEmptyString(opts.endpoint,
201
418
  "otlpExporter.create: endpoint", OtlpExporterError, "otlp/bad-endpoint");
@@ -224,8 +441,24 @@ function create(opts) {
224
441
  "otlpExporter.create: maxAttempts", OtlpExporterError, "otlp/bad-opts");
225
442
 
226
443
  var endpoint = opts.endpoint;
444
+ // OTLP §3 — operators with high-volume traces opt into the binary
445
+ // `application/x-protobuf` encoding via `opts.encoding: "protobuf"`
446
+ // (composes lib/protobuf-encoder.js for the wire-level emission).
447
+ // Default stays `"json"` for backward compatibility with existing
448
+ // collectors. The third encoding option (`"http/protobuf"` per the
449
+ // OTLP spec wording) is an alias for "protobuf".
450
+ var encoding = opts.encoding || "json";
451
+ if (encoding === "http/protobuf") encoding = "protobuf";
452
+ if (encoding !== "json" && encoding !== "protobuf") {
453
+ throw new OtlpExporterError("otlp/bad-encoding",
454
+ "otlpExporter.create: opts.encoding must be \"json\" or \"protobuf\" (got " +
455
+ JSON.stringify(opts.encoding) + ")");
456
+ }
457
+ var contentType = encoding === "protobuf"
458
+ ? "application/x-protobuf"
459
+ : "application/json";
227
460
  var headers = Object.assign({
228
- "Content-Type": "application/json",
461
+ "Content-Type": contentType,
229
462
  }, opts.headers || {});
230
463
  var batchSize = opts.batchSize || DEFAULT_BATCH_SIZE;
231
464
  var maxQueue = opts.maxQueueSize || DEFAULT_MAX_QUEUE_SIZE;
@@ -298,10 +531,14 @@ function create(opts) {
298
531
  var ac = (typeof AbortController === "function") ? new AbortController() : null;
299
532
  var t = ac ? setTimeout(function () { ac.abort(); }, timeoutMs) : null;
300
533
  try {
534
+ // The flush() path now passes EITHER a JSON-shape object (encoding
535
+ // "json") OR an already-encoded Buffer (encoding "protobuf").
536
+ // Stringify only the JSON path; pass the Buffer through.
537
+ var body = Buffer.isBuffer(payload) ? payload : JSON.stringify(payload);
301
538
  var res = await fetchImpl(endpoint, {
302
539
  method: "POST",
303
540
  headers: headers,
304
- body: JSON.stringify(payload),
541
+ body: body,
305
542
  signal: ac ? ac.signal : undefined,
306
543
  });
307
544
  if (res && res.ok) return { ok: true, status: res.status };
@@ -343,7 +580,12 @@ function create(opts) {
343
580
  inFlight = true;
344
581
  try {
345
582
  var batch = queue.splice(0, batchSize);
346
- var payload = _bundleSpans(batch);
583
+ // OTLP §3 — JSON encoding emits the resourceSpans envelope as
584
+ // JSON; protobuf encoding emits the same shape as binary
585
+ // ExportTraceServiceRequest bytes.
586
+ var payload = encoding === "protobuf"
587
+ ? _bundleSpansToProto(batch)
588
+ : _bundleSpans(batch);
347
589
  var result = await _post(payload, 1);
348
590
  if (result.ok) {
349
591
  _emitMetric("export_ok", batch.length, { http_status: String(result.status) });
@@ -95,27 +95,103 @@ function uint64(fieldNumber, value) {
95
95
  return Buffer.concat([_tag(fieldNumber, WIRE_VARINT), _writeVarint(value)]);
96
96
  }
97
97
 
98
+ function int64(fieldNumber, value) {
99
+ // proto3 int64: wire-type 0 varint. Negatives encode as the 64-bit
100
+ // two's-complement reinterpret-cast to uint64, which always sets the
101
+ // top bit — every negative int64 occupies the full 10 varint bytes
102
+ // per the spec. https://protobuf.dev/programming-guides/encoding/#signed-ints
103
+ if (value === 0 || value === 0n) return Buffer.alloc(0); // proto3 default
104
+ var bi;
105
+ if (typeof value === "bigint") {
106
+ bi = value;
107
+ } else if (typeof value === "number") {
108
+ if (!Number.isFinite(value) || !Number.isInteger(value)) {
109
+ throw new Error("protobuf-encoder: int64 must be finite integer (got " + value + ")");
110
+ }
111
+ bi = BigInt(value);
112
+ } else {
113
+ throw new Error("protobuf-encoder: int64 must be number or bigint, got " + typeof value);
114
+ }
115
+ // Refuse out-of-range — proto int64 is [-2^63, 2^63 - 1].
116
+ if (bi < -(1n << 63n) || bi > (1n << 63n) - 1n) {
117
+ throw new Error("protobuf-encoder: int64 out of range (got " + bi.toString() + ")");
118
+ }
119
+ if (bi < 0n) bi = bi + (1n << 64n); // two's-complement reinterpret
120
+ return Buffer.concat([_tag(fieldNumber, WIRE_VARINT), _writeVarint(bi)]);
121
+ }
122
+
123
+ function sint64(fieldNumber, value) {
124
+ // proto3 sint64: wire-type 0 varint with ZigZag encoding —
125
+ // small negatives encode compactly. https://protobuf.dev/programming-guides/encoding/#signed-ints
126
+ if (value === 0 || value === 0n) return Buffer.alloc(0);
127
+ var bi;
128
+ if (typeof value === "bigint") bi = value;
129
+ else if (typeof value === "number") {
130
+ if (!Number.isFinite(value) || !Number.isInteger(value)) {
131
+ throw new Error("protobuf-encoder: sint64 must be finite integer (got " + value + ")");
132
+ }
133
+ bi = BigInt(value);
134
+ } else {
135
+ throw new Error("protobuf-encoder: sint64 must be number or bigint, got " + typeof value);
136
+ }
137
+ if (bi < -(1n << 63n) || bi > (1n << 63n) - 1n) {
138
+ throw new Error("protobuf-encoder: sint64 out of range (got " + bi.toString() + ")");
139
+ }
140
+ // ZigZag: (n << 1) ^ (n >> 63) for 64-bit signed.
141
+ var zz = (bi << 1n) ^ (bi >> 63n);
142
+ if (zz < 0n) zz = zz + (1n << 64n);
143
+ return Buffer.concat([_tag(fieldNumber, WIRE_VARINT), _writeVarint(zz)]);
144
+ }
145
+
98
146
  function bool(fieldNumber, value) {
99
147
  if (!value) return Buffer.alloc(0); // proto3 default
100
148
  return Buffer.concat([_tag(fieldNumber, WIRE_VARINT), Buffer.from([1])]);
101
149
  }
102
150
 
103
151
  function fixed64(fieldNumber, value) {
104
- // For OTel: time_unix_nano fields are fixed64. Accept BigInt or
105
- // Number; encode as little-endian 8 bytes.
152
+ // For OTel: time_unix_nano fields are fixed64. Accept BigInt, Number,
153
+ // or string-form digits (OTLP/JSON encodes uint64 as a JSON string per
154
+ // https://protobuf.dev/programming-guides/proto3/#json; the framework
155
+ // tracer emits strings for that reason and the same value flows into
156
+ // both encoding paths). Encode as little-endian 8 bytes.
106
157
  var buf = Buffer.alloc(FIXED64_BYTES);
158
+ var bi;
107
159
  if (typeof value === "bigint") {
108
- buf.writeBigUInt64LE(value, 0);
109
- } else {
160
+ bi = value;
161
+ } else if (typeof value === "string") {
162
+ // Char-code walk rather than /^[0-9]+$/ — that exact regex already
163
+ // appears in guard-cidr + guard-domain; the codebase-patterns
164
+ // duplicate-regex detector fires at the 3rd file (same shape as the
165
+ // SemVer-pre-release walk in lib/self-update.js).
166
+ if (value.length === 0) {
167
+ throw new Error("protobuf-encoder: fixed64 string must be non-empty unsigned digit-only");
168
+ }
169
+ for (var ci = 0; ci < value.length; ci += 1) {
170
+ var cc = value.charCodeAt(ci);
171
+ if (cc < 0x30 || cc > 0x39) { // allow:raw-byte-literal — ASCII '0' (0x30) .. '9' (0x39)
172
+ throw new Error("protobuf-encoder: fixed64 string must be unsigned digit-only (got " + JSON.stringify(value) + ")");
173
+ }
174
+ }
175
+ bi = BigInt(value);
176
+ } else if (typeof value === "number") {
110
177
  if (value < 0 || !Number.isFinite(value)) {
111
178
  throw new Error("protobuf-encoder: fixed64 must be non-negative finite (got " + value + ")");
112
179
  }
113
- // Number path split into low/high 32-bit halves.
114
- var low = value % 0x100000000;
115
- var high = Math.floor(value / 0x100000000);
116
- buf.writeUInt32LE(low, 0);
117
- buf.writeUInt32LE(high, 4);
180
+ // Refuse silently-rounded values beyond Number.MAX_SAFE_INTEGER
181
+ // the caller MUST pass a BigInt or string in that range. JS doubles
182
+ // lose precision past 2^53 so a Number 1779518164402000000 isn't
183
+ // actually that exact value once it touches Number arithmetic.
184
+ if (value > Number.MAX_SAFE_INTEGER) {
185
+ throw new Error("protobuf-encoder: fixed64 Number above MAX_SAFE_INTEGER loses precision; pass a BigInt or digit-string (got " + value + ")");
186
+ }
187
+ bi = BigInt(value);
188
+ } else {
189
+ throw new Error("protobuf-encoder: fixed64 must be bigint, number, or digit-string (got " + typeof value + ")");
190
+ }
191
+ if (bi < 0n || bi > (1n << 64n) - 1n) {
192
+ throw new Error("protobuf-encoder: fixed64 out of uint64 range (got " + bi.toString() + ")");
118
193
  }
194
+ buf.writeBigUInt64LE(bi, 0);
119
195
  return Buffer.concat([_tag(fieldNumber, WIRE_64BIT), buf]);
120
196
  }
121
197
 
@@ -177,6 +253,8 @@ function repeatedMessage(fieldNumber, items, perItemBodyEncoder) {
177
253
  module.exports = {
178
254
  uint32: uint32,
179
255
  uint64: uint64,
256
+ int64: int64,
257
+ sint64: sint64,
180
258
  bool: bool,
181
259
  fixed64: fixed64,
182
260
  double: double,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.12.5",
3
+ "version": "0.12.6",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:235e9ad1-2dfa-4fef-925d-f259dea69771",
5
+ "serialNumber": "urn:uuid:2423e5f5-adb5-48ea-a16f-ac28b6bd36e9",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-23T01:36:17.867Z",
8
+ "timestamp": "2026-05-23T07:07:57.784Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/core@0.12.5",
22
+ "bom-ref": "@blamejs/core@0.12.6",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.12.5",
25
+ "version": "0.12.6",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "The Node framework that owns its stack.",
29
- "purl": "pkg:npm/%40blamejs/core@0.12.5",
29
+ "purl": "pkg:npm/%40blamejs/core@0.12.6",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/core@0.12.5",
57
+ "ref": "@blamejs/core@0.12.6",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]