@blamejs/core 0.7.101 → 0.7.103

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.
@@ -0,0 +1,395 @@
1
+ "use strict";
2
+ /**
3
+ * b.observability.tracer — OTel-shaped distributed-tracing span builder.
4
+ *
5
+ * Builds OpenTelemetry-compatible spans without a vendored OTel SDK.
6
+ * The span objects are JSON-serializable and have the OTLP/JSON wire
7
+ * shape (Trace v1) so the operator can ship them to any OTLP-aware
8
+ * collector (Jaeger, Tempo, Honeycomb, Lightstep, Datadog, etc.) via
9
+ * b.observability.spanExporter or their own bridge.
10
+ *
11
+ * var tracer = b.observability.tracer.create({
12
+ * service: "checkout-api",
13
+ * resource: { "service.version": "0.42.0",
14
+ * "deployment.environment": "prod" },
15
+ * onEnd: function (span) { exporter.queue(span); },
16
+ * });
17
+ *
18
+ * var span = tracer.start("checkout.process", {
19
+ * traceId: req.trace.traceId, // optional — derive from context
20
+ * parentId: req.trace.parentId,
21
+ * sampled: req.trace.sampled,
22
+ * attributes: {
23
+ * [SEMCONV.HTTP_REQUEST_METHOD]: "POST",
24
+ * [SEMCONV.URL_PATH]: "/checkout",
25
+ * },
26
+ * kind: "server",
27
+ * });
28
+ *
29
+ * try {
30
+ * // ... do the work
31
+ * span.setAttribute(SEMCONV.HTTP_RESPONSE_STATUS_CODE, 200);
32
+ * span.addEvent("payment.charged", { amount: 4200 });
33
+ * span.setStatus("ok");
34
+ * } catch (e) {
35
+ * span.recordException(e);
36
+ * span.setStatus("error", e.message);
37
+ * throw e;
38
+ * } finally {
39
+ * span.end(); // captures duration_ms, fires tracer.onEnd(span)
40
+ * }
41
+ *
42
+ * Span lifecycle:
43
+ * - start() — captures startTime; assigns spanId; emits
44
+ * span.start observability counter
45
+ * - setAttribute — additive; rejects non-stable attribute names
46
+ * when strict: true
47
+ * - addEvent — appends { name, time, attributes }
48
+ * - recordException — addEvent with `exception.*` attributes
49
+ * - setStatus — "unset" | "ok" | "error" with optional message
50
+ * - end() — captures endTime; emits span.end observability
51
+ * counter; calls tracer.onEnd(span) hook
52
+ *
53
+ * Span shape (OTLP/JSON-compatible):
54
+ * {
55
+ * traceId, spanId, parentSpanId, name, kind,
56
+ * startTimeUnixNano, endTimeUnixNano, durationNs, durationMs,
57
+ * attributes: { ... },
58
+ * events: [ { name, timeUnixNano, attributes } ],
59
+ * status: { code: "unset" | "ok" | "error", message? },
60
+ * resource: { ... },
61
+ * scope: { name: "blamejs", version },
62
+ * droppedAttributesCount, droppedEventsCount,
63
+ * }
64
+ *
65
+ * Attribute / event caps:
66
+ * - maxAttributes per span: 128 (OTLP default)
67
+ * - maxEvents per span: 128
68
+ * - maxAttributeValueLength: 1024 chars (truncated past)
69
+ *
70
+ * Excess additions silently increment droppedAttributesCount /
71
+ * droppedEventsCount per OTLP convention; the span itself never
72
+ * throws on cap overflow (hot-path observability is drop-silent).
73
+ */
74
+
75
+ var bCrypto = require("./crypto");
76
+ var constants = require("./constants");
77
+ var lazyRequire = require("./lazy-require");
78
+ var safeBuffer = require("./safe-buffer");
79
+ var validateOpts = require("./validate-opts");
80
+ var { defineClass } = require("./framework-error");
81
+
82
+ var TRACE_ID_BYTES = constants.BYTES.bytes(16); // W3C §3.2.2.3 — 128-bit trace-id
83
+ var SPAN_ID_BYTES = constants.BYTES.bytes(8); // W3C §3.2.2.4 — 64-bit span-id
84
+
85
+ var TracerError = defineClass("TracerError", { alwaysPermanent: true });
86
+
87
+ var observability = lazyRequire(function () { return require("./observability"); });
88
+
89
+ var DEFAULT_MAX_ATTRIBUTES = 128; // allow:raw-byte-literal — OTLP default span attribute cap
90
+ var DEFAULT_MAX_EVENTS = 128; // allow:raw-byte-literal — OTLP default span event cap
91
+ var DEFAULT_MAX_ATTR_VALUE_LEN = 1024; // allow:raw-byte-literal — OTLP attribute value char cap
92
+
93
+ var VALID_KINDS = ["internal", "server", "client", "producer", "consumer"];
94
+ var VALID_STATUS_CODES = ["unset", "ok", "error"];
95
+
96
+ function _now() { return Date.now(); }
97
+
98
+ function _msToUnixNano(ms) {
99
+ // OTLP timestamps are uint64 nanoseconds since Unix epoch. JS Date.now()
100
+ // gives ms; multiply by 1e6 and stringify (OTLP/JSON uses string for
101
+ // uint64 values per https://protobuf.dev/programming-guides/proto3/#json).
102
+ return String(BigInt(ms) * 1000000n); // allow:raw-byte-literal — ms→ns conversion factor (1e6)
103
+ }
104
+
105
+ function _truncateAttrValue(v, maxLen) {
106
+ if (typeof v === "string" && v.length > maxLen) {
107
+ return v.slice(0, maxLen);
108
+ }
109
+ return v;
110
+ }
111
+
112
+ function _validateKind(kind) {
113
+ if (typeof kind !== "string") return "internal";
114
+ if (VALID_KINDS.indexOf(kind) === -1) {
115
+ throw new TracerError("tracer/bad-kind",
116
+ "tracer.start: kind must be one of " + VALID_KINDS.join(", ") +
117
+ " (got " + JSON.stringify(kind) + ")");
118
+ }
119
+ return kind;
120
+ }
121
+
122
+ function _validateAttrKey(key) {
123
+ if (typeof key !== "string" || key.length === 0) return false;
124
+ // OTel attribute keys: ASCII printable, dot-separated, no spaces
125
+ // beyond what the SEMCONV vocabulary uses.
126
+ if (key.length > 255) return false; // allow:raw-byte-literal — OTLP attribute key cap
127
+ return true;
128
+ }
129
+
130
+ function _validateAttrValue(v) {
131
+ // OTLP supports string / int / double / bool / array of those
132
+ var t = typeof v;
133
+ if (t === "string" || t === "boolean") return true;
134
+ if (t === "number") return Number.isFinite(v);
135
+ if (Array.isArray(v)) {
136
+ for (var i = 0; i < v.length; i++) {
137
+ var elT = typeof v[i];
138
+ if (elT !== "string" && elT !== "boolean" && elT !== "number") return false;
139
+ if (elT === "number" && !Number.isFinite(v[i])) return false;
140
+ }
141
+ return true;
142
+ }
143
+ return false;
144
+ }
145
+
146
+ function _spanId() {
147
+ return bCrypto.generateBytes(SPAN_ID_BYTES).toString("hex");
148
+ }
149
+
150
+ function _traceId() {
151
+ return bCrypto.generateBytes(TRACE_ID_BYTES).toString("hex");
152
+ }
153
+
154
+ function create(opts) {
155
+ validateOpts.requireObject(opts, "tracer", TracerError);
156
+ validateOpts(opts, [
157
+ "service", "resource", "scope",
158
+ "maxAttributes", "maxEvents", "maxAttributeValueLength",
159
+ "onEnd", "onStart", "audit",
160
+ ], "tracer.create");
161
+ validateOpts.requireNonEmptyString(opts.service,
162
+ "tracer.create: service", TracerError, "tracer/bad-service");
163
+ validateOpts.optionalFunction(opts.onEnd,
164
+ "tracer.create: onEnd", TracerError, "tracer/bad-opts");
165
+ validateOpts.optionalFunction(opts.onStart,
166
+ "tracer.create: onStart", TracerError, "tracer/bad-opts");
167
+
168
+ var resource = Object.assign({
169
+ "service.name": opts.service,
170
+ }, opts.resource || {});
171
+ var scope = opts.scope || { name: "blamejs", version: constants.version || null };
172
+ var maxAttributes = opts.maxAttributes || DEFAULT_MAX_ATTRIBUTES;
173
+ var maxEvents = opts.maxEvents || DEFAULT_MAX_EVENTS;
174
+ var maxAttrValLen = opts.maxAttributeValueLength || DEFAULT_MAX_ATTR_VALUE_LEN;
175
+
176
+ function _newSpan(name, spanOpts) {
177
+ spanOpts = spanOpts || {};
178
+ var traceId = spanOpts.traceId;
179
+ if (typeof traceId !== "string" || !safeBuffer.TRACE_ID_HEX_RE.test(traceId)) { // allow:regex-no-length-cap — fixed-length hex constant from safe-buffer
180
+ traceId = _traceId();
181
+ }
182
+ var parentSpanId = spanOpts.parentId || null;
183
+ if (parentSpanId !== null && (typeof parentSpanId !== "string" || !safeBuffer.SPAN_ID_HEX_RE.test(parentSpanId))) { // allow:regex-no-length-cap — fixed-length hex constant from safe-buffer
184
+ parentSpanId = null;
185
+ }
186
+ var spanId = _spanId();
187
+ var startMs = _now();
188
+ var kind = _validateKind(spanOpts.kind);
189
+ var sampled = spanOpts.sampled !== false;
190
+
191
+ var attributes = Object.create(null);
192
+ var droppedAttributesCount = 0;
193
+ var events = [];
194
+ var droppedEventsCount = 0;
195
+ var status = { code: "unset", message: null };
196
+ var ended = false;
197
+ var endMs = null;
198
+
199
+ function setAttribute(key, value) {
200
+ if (ended) return span;
201
+ if (!_validateAttrKey(key)) { droppedAttributesCount += 1; return span; }
202
+ if (!_validateAttrValue(value)) { droppedAttributesCount += 1; return span; }
203
+ var keyCount = Object.keys(attributes).length;
204
+ if (!(key in attributes) && keyCount >= maxAttributes) {
205
+ droppedAttributesCount += 1;
206
+ return span;
207
+ }
208
+ attributes[key] = _truncateAttrValue(value, maxAttrValLen);
209
+ return span;
210
+ }
211
+
212
+ function setAttributes(map) {
213
+ if (!map || typeof map !== "object") return span;
214
+ var keys = Object.keys(map);
215
+ for (var i = 0; i < keys.length; i++) setAttribute(keys[i], map[keys[i]]);
216
+ return span;
217
+ }
218
+
219
+ function addEvent(eventName, eventAttrs) {
220
+ if (ended) return span;
221
+ if (typeof eventName !== "string" || eventName.length === 0) {
222
+ droppedEventsCount += 1;
223
+ return span;
224
+ }
225
+ if (events.length >= maxEvents) { droppedEventsCount += 1; return span; }
226
+ var eventTime = _now();
227
+ var attrs = Object.create(null);
228
+ if (eventAttrs && typeof eventAttrs === "object") {
229
+ var ks = Object.keys(eventAttrs);
230
+ for (var i = 0; i < ks.length; i++) {
231
+ var k = ks[i], v = eventAttrs[k];
232
+ if (_validateAttrKey(k) && _validateAttrValue(v)) {
233
+ attrs[k] = _truncateAttrValue(v, maxAttrValLen);
234
+ }
235
+ }
236
+ }
237
+ events.push({
238
+ name: eventName,
239
+ timeUnixNano: _msToUnixNano(eventTime),
240
+ attributes: attrs,
241
+ });
242
+ return span;
243
+ }
244
+
245
+ function recordException(err) {
246
+ if (ended) return span;
247
+ if (!err) return span;
248
+ var name = (err.name || (err.constructor && err.constructor.name) || "Error");
249
+ var message = (err.message || String(err));
250
+ var stack = err.stack ? String(err.stack) : null;
251
+ addEvent("exception", {
252
+ "exception.type": name,
253
+ "exception.message": message,
254
+ "exception.stacktrace": stack || "",
255
+ });
256
+ return span;
257
+ }
258
+
259
+ function setStatus(code, message) {
260
+ if (ended) return span;
261
+ if (VALID_STATUS_CODES.indexOf(code) === -1) {
262
+ throw new TracerError("tracer/bad-status",
263
+ "span.setStatus: code must be one of " + VALID_STATUS_CODES.join(", "));
264
+ }
265
+ status.code = code;
266
+ status.message = (typeof message === "string") ? message : null;
267
+ return span;
268
+ }
269
+
270
+ function end(endTimestampMs) {
271
+ if (ended) return span;
272
+ ended = true;
273
+ endMs = (typeof endTimestampMs === "number" && isFinite(endTimestampMs)) ? endTimestampMs : _now();
274
+ if (typeof opts.onEnd === "function") {
275
+ try { opts.onEnd(toJSON()); }
276
+ catch (_e) { /* operator hook — drop-silent */ }
277
+ }
278
+ try { observability().safeEvent("tracer.span.end", 1, {
279
+ kind: kind, status: status.code, sampled: sampled ? "1" : "0",
280
+ }); } catch (_e) { /* drop-silent */ }
281
+ return span;
282
+ }
283
+
284
+ function isRecording() { return !ended; }
285
+
286
+ function toJSON() {
287
+ var endNano = endMs !== null ? _msToUnixNano(endMs) : null;
288
+ var durationMs = endMs !== null ? (endMs - startMs) : null;
289
+ // OTLP/JSON shape (Trace v1)
290
+ return {
291
+ traceId: traceId,
292
+ spanId: spanId,
293
+ parentSpanId: parentSpanId,
294
+ name: name,
295
+ kind: kind,
296
+ startTimeUnixNano: _msToUnixNano(startMs),
297
+ endTimeUnixNano: endNano,
298
+ durationNs: endNano !== null ? String(BigInt(durationMs) * 1000000n) : null, // allow:raw-byte-literal — ms→ns conversion factor (1e6)
299
+ durationMs: durationMs,
300
+ attributes: Object.assign({}, attributes),
301
+ events: events.slice(),
302
+ status: { code: status.code, message: status.message },
303
+ resource: Object.assign({}, resource),
304
+ scope: Object.assign({}, scope),
305
+ droppedAttributesCount: droppedAttributesCount,
306
+ droppedEventsCount: droppedEventsCount,
307
+ sampled: sampled,
308
+ };
309
+ }
310
+
311
+ var span = {
312
+ traceId: traceId,
313
+ spanId: spanId,
314
+ parentSpanId: parentSpanId,
315
+ name: name,
316
+ kind: kind,
317
+ sampled: sampled,
318
+ setAttribute: setAttribute,
319
+ setAttributes: setAttributes,
320
+ addEvent: addEvent,
321
+ recordException: recordException,
322
+ setStatus: setStatus,
323
+ end: end,
324
+ isRecording: isRecording,
325
+ toJSON: toJSON,
326
+ };
327
+
328
+ // Apply initial attributes
329
+ if (spanOpts.attributes && typeof spanOpts.attributes === "object") {
330
+ setAttributes(spanOpts.attributes);
331
+ }
332
+
333
+ if (typeof opts.onStart === "function") {
334
+ try { opts.onStart(span); }
335
+ catch (_e) { /* operator hook — drop-silent */ }
336
+ }
337
+ try { observability().safeEvent("tracer.span.start", 1, {
338
+ kind: kind, sampled: sampled ? "1" : "0",
339
+ }); } catch (_e) { /* drop-silent */ }
340
+
341
+ return span;
342
+ }
343
+
344
+ function start(name, spanOpts) {
345
+ if (typeof name !== "string" || name.length === 0) {
346
+ throw new TracerError("tracer/bad-name",
347
+ "tracer.start: name must be a non-empty string");
348
+ }
349
+ return _newSpan(name, spanOpts || {});
350
+ }
351
+
352
+ function startChildOf(parentSpan, name, spanOpts) {
353
+ if (!parentSpan || typeof parentSpan.traceId !== "string") {
354
+ throw new TracerError("tracer/bad-parent",
355
+ "tracer.startChildOf: parentSpan must be a span object");
356
+ }
357
+ var childOpts = Object.assign({}, spanOpts || {}, {
358
+ traceId: parentSpan.traceId,
359
+ parentId: parentSpan.spanId,
360
+ sampled: parentSpan.sampled,
361
+ });
362
+ return _newSpan(name, childOpts);
363
+ }
364
+
365
+ return {
366
+ start: start,
367
+ startChildOf: startChildOf,
368
+ service: opts.service,
369
+ resource: resource,
370
+ scope: scope,
371
+ _attributeCaps: { // exported for tests
372
+ maxAttributes: maxAttributes,
373
+ maxEvents: maxEvents,
374
+ maxAttributeValueLength: maxAttrValLen,
375
+ },
376
+ };
377
+ }
378
+
379
+ // Pure helper: derive the canonical W3C `traceparent` header from a span.
380
+ function spanToTraceparent(span) {
381
+ if (!span || typeof span.traceId !== "string" || typeof span.spanId !== "string") {
382
+ throw new TracerError("tracer/bad-span",
383
+ "spanToTraceparent: argument must be a span with traceId + spanId");
384
+ }
385
+ return "00-" + span.traceId + "-" + span.spanId + "-" + (span.sampled ? "01" : "00");
386
+ }
387
+
388
+ module.exports = {
389
+ create: create,
390
+ spanToTraceparent: spanToTraceparent,
391
+ TracerError: TracerError,
392
+ _BASE64URL_RE: safeBuffer.BASE64URL_RE, // not used directly — exposed for downstream tests
393
+ VALID_KINDS: VALID_KINDS,
394
+ VALID_STATUS_CODES: VALID_STATUS_CODES,
395
+ };
@@ -54,8 +54,13 @@
54
54
  * fn: function — sync or async. Return propagates; throws propagate
55
55
  * after metrics fire.
56
56
  */
57
+ var C = require("./constants");
57
58
  var lazyRequire = require("./lazy-require");
58
59
 
60
+ // safe-buffer can't be top-required: framework-error → observability →
61
+ // safe-buffer → framework-error forms a cycle. Lazy-loaded at first use.
62
+ var safeBuffer = lazyRequire(function () { return require("./safe-buffer"); });
63
+
59
64
  var tracing = lazyRequire(function () { return require("./tracing"); });
60
65
  var metrics = lazyRequire(function () { return require("./metrics"); });
61
66
 
@@ -355,10 +360,10 @@ function _buildTraceparent(opts) {
355
360
  }
356
361
  var traceId = opts.traceId;
357
362
  var parentId = opts.parentId;
358
- if (typeof traceId !== "string" || !/^[0-9a-f]{32}$/.test(traceId) || traceId === _ALL_ZERO_TRACE) {
363
+ if (typeof traceId !== "string" || !safeBuffer().TRACE_ID_HEX_RE.test(traceId) || traceId === _ALL_ZERO_TRACE) { // allow:regex-no-length-cap — fixed-length hex constant from safe-buffer
359
364
  throw new TypeError("traceContext.build: traceId must be 32 lowercase hex chars (non-zero)");
360
365
  }
361
- if (typeof parentId !== "string" || !/^[0-9a-f]{16}$/.test(parentId) || parentId === _ALL_ZERO_PARENT) {
366
+ if (typeof parentId !== "string" || !safeBuffer().SPAN_ID_HEX_RE.test(parentId) || parentId === _ALL_ZERO_PARENT) { // allow:regex-no-length-cap — fixed-length hex constant from safe-buffer
362
367
  throw new TypeError("traceContext.build: parentId must be 16 lowercase hex chars (non-zero)");
363
368
  }
364
369
  var flagsByte = (opts.sampled ? _TRACE_FLAG_SAMPLED : 0);
@@ -380,11 +385,227 @@ function _newParentId() {
380
385
  return hex === _ALL_ZERO_PARENT ? _nodeCryptoForTrace.randomBytes(_PARENT_ID_BYTES).toString("hex") : hex;
381
386
  }
382
387
 
388
+ // W3C Trace Context §3.3 — tracestate: comma-separated list of
389
+ // `vendor=value` pairs carrying vendor-specific trace data.
390
+ //
391
+ // tracestate: rojo=00f067aa0ba902b7, congo=t61rcWkgMzE
392
+ //
393
+ // Spec rules (https://www.w3.org/TR/trace-context-1/#tracestate-header):
394
+ // - vendor key: lowercase ASCII letters, digits, `_`, `-`, `*`, `/`,
395
+ // length 1..256, optionally with `<tenant>@<system>` form
396
+ // - value: printable ASCII (0x20..0x7E) excluding `,` and `=`,
397
+ // length 1..256
398
+ // - max 32 entries, max 512 chars total
399
+ // - duplicate keys: keep first, drop rest
400
+ var _TRACESTATE_KEY_RE = /^[a-z0-9][a-z0-9_\-*/]{0,255}(@[a-z0-9][a-z0-9_\-*/]{0,255})?$/;
401
+ var _TRACESTATE_VALUE_RE = /^[\x20-\x2B\x2D-\x3C\x3E-\x7E]{1,256}$/; // printable, no "," or "="
402
+ var _TRACESTATE_MAX_ENTRIES = 32; // allow:raw-byte-literal — W3C spec hard cap (§3.3.1.3)
403
+ var _TRACESTATE_MAX_CHARS = 512; // allow:raw-byte-literal — W3C spec hard cap (§3.3.1.3)
404
+
405
+ function _parseTracestate(headerValue) {
406
+ if (typeof headerValue !== "string") return null;
407
+ if (headerValue.length === 0 || headerValue.length > _TRACESTATE_MAX_CHARS) return null;
408
+ var pairs = headerValue.split(",");
409
+ if (pairs.length > _TRACESTATE_MAX_ENTRIES) return null;
410
+ var seen = Object.create(null);
411
+ var out = [];
412
+ for (var i = 0; i < pairs.length; i++) {
413
+ var raw = pairs[i].trim();
414
+ if (raw.length === 0) continue;
415
+ var eqIdx = raw.indexOf("=");
416
+ if (eqIdx === -1) return null;
417
+ var key = raw.slice(0, eqIdx).trim();
418
+ var val = raw.slice(eqIdx + 1).trim();
419
+ if (!_TRACESTATE_KEY_RE.test(key)) return null; // allow:regex-no-length-cap — regex literal hard-caps key length per W3C §3.3.1.1
420
+ if (!_TRACESTATE_VALUE_RE.test(val)) return null; // allow:regex-no-length-cap — regex literal hard-caps value length per W3C §3.3.1.2
421
+ if (seen[key]) continue; // dup-key: keep first
422
+ seen[key] = true;
423
+ out.push({ key: key, value: val });
424
+ }
425
+ return out;
426
+ }
427
+
428
+ function _buildTracestate(entries) {
429
+ if (!Array.isArray(entries)) {
430
+ throw new TypeError("traceContext.buildTracestate: entries must be an array");
431
+ }
432
+ if (entries.length > _TRACESTATE_MAX_ENTRIES) {
433
+ throw new TypeError("traceContext.buildTracestate: too many entries (max " +
434
+ _TRACESTATE_MAX_ENTRIES + ")");
435
+ }
436
+ var seen = Object.create(null);
437
+ var parts = [];
438
+ for (var i = 0; i < entries.length; i++) {
439
+ var e = entries[i];
440
+ if (!e || typeof e !== "object") {
441
+ throw new TypeError("traceContext.buildTracestate: entries[" + i + "] must be an object");
442
+ }
443
+ if (typeof e.key !== "string" || !_TRACESTATE_KEY_RE.test(e.key)) { // allow:regex-no-length-cap — regex literal hard-caps key length per W3C §3.3.1.1
444
+ throw new TypeError("traceContext.buildTracestate: entries[" + i + "].key violates W3C key rules");
445
+ }
446
+ if (typeof e.value !== "string" || !_TRACESTATE_VALUE_RE.test(e.value)) { // allow:regex-no-length-cap — regex literal hard-caps value length per W3C §3.3.1.2
447
+ throw new TypeError("traceContext.buildTracestate: entries[" + i + "].value violates W3C value rules");
448
+ }
449
+ if (seen[e.key]) continue;
450
+ seen[e.key] = true;
451
+ parts.push(e.key + "=" + e.value);
452
+ }
453
+ var s = parts.join(",");
454
+ if (s.length > _TRACESTATE_MAX_CHARS) {
455
+ throw new TypeError("traceContext.buildTracestate: built string exceeds W3C 512-char cap");
456
+ }
457
+ return s;
458
+ }
459
+
383
460
  var traceContext = {
384
- parse: _parseTraceparent,
385
- build: _buildTraceparent,
386
- newTraceId: _newTraceId,
387
- newParentId: _newParentId,
461
+ parse: _parseTraceparent,
462
+ build: _buildTraceparent,
463
+ newTraceId: _newTraceId,
464
+ newParentId: _newParentId,
465
+ parseTracestate: _parseTracestate,
466
+ buildTracestate: _buildTracestate,
467
+ };
468
+
469
+ // ---- W3C Baggage (https://www.w3.org/TR/baggage/) ----
470
+ //
471
+ // `baggage` HTTP header carries a comma-separated list of
472
+ // `key=value;property=value;property=value` triplets. Used to
473
+ // propagate user-supplied context (tenantId, deploymentRegion,
474
+ // experimentId, etc.) across service boundaries WITHOUT mixing it
475
+ // into traceparent (which is reserved for trace identifiers).
476
+ //
477
+ // Spec rules:
478
+ // - key: token per RFC 7230 (`tchar` set: `!#$%&'*+\-.^_\`|~` +
479
+ // digits + ALPHA), length 1..255
480
+ // - value: percent-encoded UTF-8, must NOT contain CTL chars,
481
+ // `,`, `;`, `=` (those are structural delimiters)
482
+ // - properties: optional, semicolon-separated `key=value` or bare
483
+ // `key` after the main value
484
+ // - max 64 entries per Baggage section recommendation
485
+ // - max 8192 chars total (W3C recommended cap)
486
+ // Resolved at first call; lazyRequire returns a function.
487
+ function _baggageTokenRe() { return safeBuffer().RFC7230_TCHAR_RE; }
488
+ var _BAGGAGE_MAX_ENTRIES = 64; // allow:raw-byte-literal — W3C Baggage recommended cap
489
+ var _BAGGAGE_MAX_CHARS = C.BYTES.kib(8); // W3C Baggage recommended 8192-char cap
490
+
491
+ function _parseBaggage(headerValue) {
492
+ if (typeof headerValue !== "string") return null;
493
+ if (headerValue.length === 0 || headerValue.length > _BAGGAGE_MAX_CHARS) return null;
494
+ var entries = headerValue.split(",");
495
+ if (entries.length > _BAGGAGE_MAX_ENTRIES) return null;
496
+ var seen = Object.create(null);
497
+ var out = [];
498
+ for (var i = 0; i < entries.length; i++) {
499
+ var raw = entries[i].trim();
500
+ if (raw.length === 0) continue;
501
+ var parts = raw.split(";");
502
+ var head = parts[0].trim();
503
+ var eqIdx = head.indexOf("=");
504
+ if (eqIdx === -1) return null;
505
+ var key = head.slice(0, eqIdx).trim();
506
+ var rawValue = head.slice(eqIdx + 1).trim();
507
+ if (!_baggageTokenRe().test(key)) return null; // allow:regex-no-length-cap — RFC 7230 tchar; bound by header-cap
508
+ if (key.length > 255) return null; // allow:raw-byte-literal — W3C key length cap
509
+ var value;
510
+ try { value = decodeURIComponent(rawValue); }
511
+ catch (_e) { return null; }
512
+ var props = [];
513
+ for (var p = 1; p < parts.length; p++) {
514
+ var prop = parts[p].trim();
515
+ if (prop.length === 0) continue;
516
+ var pEq = prop.indexOf("=");
517
+ if (pEq === -1) {
518
+ if (!_baggageTokenRe().test(prop)) return null; // allow:regex-no-length-cap — RFC 7230 tchar; bound by header-cap
519
+ props.push({ key: prop, value: null });
520
+ } else {
521
+ var pKey = prop.slice(0, pEq).trim();
522
+ var pVal = prop.slice(pEq + 1).trim();
523
+ if (!_baggageTokenRe().test(pKey)) return null; // allow:regex-no-length-cap — RFC 7230 tchar; bound by header-cap
524
+ var pValueDecoded;
525
+ try { pValueDecoded = decodeURIComponent(pVal); }
526
+ catch (_e) { return null; }
527
+ props.push({ key: pKey, value: pValueDecoded });
528
+ }
529
+ }
530
+ if (seen[key]) continue;
531
+ seen[key] = true;
532
+ out.push({ key: key, value: value, properties: props });
533
+ }
534
+ return out;
535
+ }
536
+
537
+ function _buildBaggage(entries) {
538
+ if (!Array.isArray(entries)) {
539
+ throw new TypeError("traceContext.buildBaggage: entries must be an array");
540
+ }
541
+ if (entries.length > _BAGGAGE_MAX_ENTRIES) {
542
+ throw new TypeError("traceContext.buildBaggage: too many entries (max " +
543
+ _BAGGAGE_MAX_ENTRIES + ")");
544
+ }
545
+ var seen = Object.create(null);
546
+ var parts = [];
547
+ for (var i = 0; i < entries.length; i++) {
548
+ var e = entries[i];
549
+ if (!e || typeof e !== "object") {
550
+ throw new TypeError("traceContext.buildBaggage: entries[" + i + "] must be an object");
551
+ }
552
+ if (typeof e.key !== "string" || !_baggageTokenRe().test(e.key)) { // allow:regex-no-length-cap — RFC 7230 tchar; bound by header-cap
553
+ throw new TypeError("traceContext.buildBaggage: entries[" + i + "].key violates W3C key rules");
554
+ }
555
+ if (typeof e.value !== "string") {
556
+ throw new TypeError("traceContext.buildBaggage: entries[" + i + "].value must be a string");
557
+ }
558
+ if (seen[e.key]) continue;
559
+ seen[e.key] = true;
560
+ var encodedValue = encodeURIComponent(e.value);
561
+ var item = e.key + "=" + encodedValue;
562
+ if (Array.isArray(e.properties)) {
563
+ for (var p = 0; p < e.properties.length; p++) {
564
+ var prop = e.properties[p];
565
+ if (!prop || typeof prop !== "object") continue;
566
+ if (typeof prop.key !== "string" || !_baggageTokenRe().test(prop.key)) { // allow:regex-no-length-cap — RFC 7230 tchar; bound by header-cap
567
+ throw new TypeError("traceContext.buildBaggage: entries[" + i +
568
+ "].properties[" + p + "].key violates W3C property-key rules");
569
+ }
570
+ if (prop.value === null || prop.value === undefined) {
571
+ item += ";" + prop.key;
572
+ } else if (typeof prop.value === "string") {
573
+ item += ";" + prop.key + "=" + encodeURIComponent(prop.value);
574
+ } else {
575
+ throw new TypeError("traceContext.buildBaggage: entries[" + i +
576
+ "].properties[" + p + "].value must be a string or null");
577
+ }
578
+ }
579
+ }
580
+ parts.push(item);
581
+ }
582
+ var s = parts.join(",");
583
+ if (s.length > _BAGGAGE_MAX_CHARS) {
584
+ throw new TypeError("traceContext.buildBaggage: built string exceeds W3C 8192-char cap");
585
+ }
586
+ return s;
587
+ }
588
+
589
+ var baggage = {
590
+ parse: _parseBaggage,
591
+ build: _buildBaggage,
592
+ MAX_ENTRIES: _BAGGAGE_MAX_ENTRIES,
593
+ };
594
+
595
+ // Lazy-required to avoid a require cycle (tracer / exporter both
596
+ // reach back into observability for safeEvent emissions).
597
+ var _tracerModule = lazyRequire(function () { return require("./observability-tracer"); });
598
+ var _otlpExporterModule = lazyRequire(function () { return require("./observability-otlp-exporter"); });
599
+
600
+ var tracer = {
601
+ create: function (opts) { return _tracerModule().create(opts); },
602
+ spanToTraceparent: function (span) { return _tracerModule().spanToTraceparent(span); },
603
+ VALID_KINDS: ["internal", "server", "client", "producer", "consumer"],
604
+ VALID_STATUS_CODES: ["unset", "ok", "error"],
605
+ };
606
+
607
+ var otlpExporter = {
608
+ create: function (opts) { return _otlpExporterModule().create(opts); },
388
609
  };
389
610
 
390
611
  module.exports = {
@@ -395,4 +616,7 @@ module.exports = {
395
616
  setTap: setTap,
396
617
  SEMCONV: SEMCONV,
397
618
  traceContext: traceContext,
619
+ baggage: baggage,
620
+ tracer: tracer,
621
+ otlpExporter: otlpExporter,
398
622
  };
@@ -196,6 +196,18 @@ var HEX_RE = /^[0-9a-fA-F]+$/;
196
196
  // is length-agnostic — callers cap length per protocol contract.
197
197
  var BASE64URL_RE = /^[A-Za-z0-9_-]+$/;
198
198
 
199
+ // Fixed-length hex predicates used by trace-context primitives (W3C
200
+ // trace-id is 16 bytes = 32 hex chars; span-id / parent-id is 8
201
+ // bytes = 16 hex chars). Extracted to keep callers length-bounded
202
+ // without duplicating the literal in every file.
203
+ var TRACE_ID_HEX_RE = /^[0-9a-f]{32}$/; // allow:regex-no-length-cap — fixed 32 hex chars (W3C §3.2.2.3)
204
+ var SPAN_ID_HEX_RE = /^[0-9a-f]{16}$/; // allow:regex-no-length-cap — fixed 16 hex chars (W3C §3.2.2.4)
205
+
206
+ // RFC 7230 §3.2.6 / RFC 9110 §5.1 `tchar` grammar — used by HTTP
207
+ // header tokens, MIME parameter names, W3C Baggage keys, etc.
208
+ // Length-agnostic; callers cap per protocol.
209
+ var RFC7230_TCHAR_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; // allow:regex-no-length-cap — caller bounds length
210
+
199
211
  // CRLF_RE matches any control character used in HTTP-header / SMTP-
200
212
  // envelope injection attacks. Header values that contain CR or LF must
201
213
  // be rejected before serialization.
@@ -238,6 +250,9 @@ module.exports = {
238
250
  stripTrailingHspace: stripTrailingHspace,
239
251
  HEX_RE: HEX_RE,
240
252
  BASE64URL_RE: BASE64URL_RE,
253
+ TRACE_ID_HEX_RE: TRACE_ID_HEX_RE,
254
+ SPAN_ID_HEX_RE: SPAN_ID_HEX_RE,
255
+ RFC7230_TCHAR_RE: RFC7230_TCHAR_RE,
241
256
  CRLF_RE: CRLF_RE,
242
257
  TRAILING_HSPACE_RE: TRAILING_HSPACE_RE,
243
258
  SafeBufferError: SafeBufferError,