@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.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.7.x
10
10
 
11
+ - **0.7.103** (2026-05-06) — W3C distributed tracing suite. End-to-end OTel-shaped tracing without a vendored OTel SDK: tracestate + Baggage parsers, span builder, OTLP/JSON exporter, HTTP-server span middleware, log correlation. **`b.observability.traceContext.parseTracestate / buildTracestate`** — W3C Trace Context §3.3 vendor data: enforces vendor-key shape (lcase-alnum + `_-*/`, optional `<tenant>@<system>`), value charset (printable ASCII excluding `,` and `=`), 32-entry cap, 512-char total cap, dup-key-keep-first per §3.3.1.5. **`b.observability.baggage.parse / build`** — W3C Baggage spec parser + builder for operator-supplied context (tenantId, region, experimentId, etc.) propagated across service boundaries. RFC 7230 tchar key grammar, percent-encoded UTF-8 values, optional per-entry properties (`key=value;property=value`), 64-entry / 8192-char caps. **`b.observability.tracer.create({ service, resource, onEnd })`** — OTel-shaped span builder. `tracer.start(name, opts)` returns a span with `setAttribute` / `setAttributes` / `addEvent` / `recordException` / `setStatus` / `end` / `isRecording` / `toJSON`. OTLP/JSON-compatible output (Trace v1) with `traceId` / `spanId` / `parentSpanId` / `name` / `kind` / `startTimeUnixNano` / `endTimeUnixNano` / `attributes` / `events` / `status` / `resource` / `scope` / `droppedAttributesCount` / `droppedEventsCount`. Attribute caps (128 keys, 1024-char values), event cap (128) per OTLP defaults. `tracer.startChildOf(parent, name)` derives child spans sharing the trace context. **`b.observability.tracer.spanToTraceparent(span)`** — emits the canonical W3C `traceparent` for outbound propagation. **`b.observability.otlpExporter.create({ endpoint, ... })`** — buffered OTLP/HTTP JSON span exporter. Batches spans (default 200), flushes on size + interval (default 5s), retries 5xx + 408/429 with exponential backoff, drops oldest on queue overflow (default 4096). Custom `fetchImpl` opt for testing or non-default HTTP transports; `allowedProtocols` opt for cleartext dev collectors. **`b.middleware.tracePropagate`** extended to also read inbound `tracestate` and stamp `req.trace.tracestate` as the parsed entries array (or `[]` when missing); when `setResponseHeader: true`, echoes both `traceparent` and `tracestate` on the response. **`b.middleware.spanHttpServer({ tracer, ... })`** — auto-creates a root server span per HTTP request, populates OTel `SEMCONV.HTTP_*` / `URL_*` / `SERVER_*` / `CLIENT_*` attributes, attaches the span to `req.span`, ends on response close, fires `onEnd(span.toJSON())` for export. `ignorePaths` (string + RegExp) keeps healthz / static-asset routes out of span volume; `captureRequestHeaders` / `captureResponseHeaders` lift named headers into the span as `http.request.header.*` / `http.response.header.*` attributes. **`b.middleware.traceLogCorrelation({ logger })`** — wraps a `b.log` instance for the request lifetime so every `info()` / `warn()` / `error()` / etc. emission inside the handler auto-includes `trace_id` + `span_id` from the active context (via `req.trace` + `req.span`). Pass-through when no trace context present. Internal sweep: `safeBuffer.TRACE_ID_HEX_RE` / `SPAN_ID_HEX_RE` / `RFC7230_TCHAR_RE` extracted as shared regex constants; `guard-mime` / `middleware/headers` / `observability` consolidated against the new shared constants.
12
+
13
+ - **0.7.102** (2026-05-06) — `b.middleware.tracePropagate({ generateIfMissing, ... })` — middleware that consumes the inbound `traceparent` header per W3C Trace Context and stamps `req.trace = { traceId, parentId, sampled, hadUpstream }` for downstream handlers + propagation into outbound HTTP calls. `generateIfMissing: true` (default) synthesises a fresh trace when the inbound header is missing/malformed and stamps `hadUpstream: false`. `auditOnMissing: true` emits `system.trace.synthesised` audit events on every locally-originated trace. `setResponseHeader: true` echoes the resolved traceparent on the response (useful when the framework is the back-end of an L7 router that wants to log it). Composes with `b.observability.traceContext` (v0.7.101) for outbound propagation: downstream handlers read `req.trace.traceId` and pass it to `traceContext.build({ traceId: req.trace.traceId, parentId: traceContext.newParentId(), sampled: req.trace.sampled })` for the `traceparent` header on upstream calls.
14
+
11
15
  - **0.7.101** (2026-05-06) — `b.observability.traceContext` — W3C Trace Context (https://www.w3.org/TR/trace-context-1/) parser + builder. **`traceContext.parse(headerValue)`** consumes a `traceparent` HTTP header value (`00-<32hex traceId>-<16hex parentId>-<2hex flags>`) and returns `{ version, traceId, parentId, flags, sampled }` or null on malformed input. Enforces the §3.2.2.3 / §3.2.2.4 all-zero-rejection rule (zero trace-id and zero parent-id are explicitly forbidden by spec). **`traceContext.build({ traceId, parentId, sampled })`** produces a v1 `traceparent` header value; throws on bad input shape. **`traceContext.newTraceId()` / `newParentId()`** generate fresh randomized 128-bit / 64-bit hex strings (with the all-zero retry path the spec requires). Operators wiring distributed tracing across services use these to propagate trace IDs across an outbound HTTP call without a vendored OTel SDK — pair with the SEMCONV constants from v0.7.95 to build OTel-aligned spans on top.
12
16
 
13
17
  - **0.7.100** (2026-05-06) — `b.network.tls.expiryMonitor({ intervalMs, windowMs, onExpiring })` — periodic CA-trust-store expiry monitor. Runs `expiringSoon(windowMs)` on a schedule; emits `network.tls.ca.expiry_check` audit event on every check (with `expiring` count + `total` CA count), `network.tls.ca.expiring` audit event when any CA falls inside the window, and the matching `network.tls.ca.expiring` observability counter. Optional `onExpiring(rows)` operator hook fires on every check that surfaces expiring CAs so operators can wire pager / Slack alerts. Audit metadata captures the expiring CA labels + the earliest `validTo` timestamp so dashboards can compute "days until first expiry" without re-querying. Returns a handle with `.stop()` for graceful shutdown. Closes the v0.7.26 OCSP/CT batch's continuous-trust-monitoring follow-up.
package/lib/guard-mime.js CHANGED
@@ -36,6 +36,7 @@ var lazyRequire = require("./lazy-require");
36
36
  var gateContract = require("./gate-contract");
37
37
  var C = require("./constants");
38
38
  var numericBounds = require("./numeric-bounds");
39
+ var safeBuffer = require("./safe-buffer");
39
40
  var { GuardMimeError } = require("./framework-error");
40
41
 
41
42
  var observability = lazyRequire(function () { return require("./observability"); });
@@ -48,8 +49,8 @@ var _err = GuardMimeError.factory;
48
49
  // 127 octets per token.
49
50
  var TOKEN_RE = /^[A-Za-z0-9][A-Za-z0-9!#$&\-^_.+]{0,126}$/;
50
51
 
51
- // Parameter token (RFC 7231 §3.1.1.1): tchar set.
52
- var PARAM_TOKEN_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
52
+ // Parameter token (RFC 7231 §3.1.1.1): tchar set per RFC 7230.
53
+ var PARAM_TOKEN_RE = safeBuffer.RFC7230_TCHAR_RE;
53
54
 
54
55
  // Quoted-string body (between double quotes) per RFC 7230 §3.2.6.
55
56
  var QUOTED_STRING_BODY_RE = /^[\t\x20-\x7e]*$/; // allow:raw-byte-literal — printable ASCII range
@@ -34,12 +34,13 @@
34
34
  */
35
35
 
36
36
  var lazyRequire = require("../lazy-require");
37
+ var safeBuffer = require("../safe-buffer");
37
38
 
38
39
  var observability = lazyRequire(function () { return require("../observability"); });
39
40
  void observability;
40
41
 
41
- // RFC 9110 §5.1 token grammar — tchar set.
42
- var TOKEN_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
42
+ // RFC 9110 §5.1 token grammar — tchar set per RFC 7230.
43
+ var TOKEN_RE = safeBuffer.RFC7230_TCHAR_RE;
43
44
 
44
45
  var DEPRECATED_TRUST_HEADERS = Object.freeze([
45
46
  "x-forwarded-for",
@@ -45,7 +45,10 @@ var requireContentType = require("./require-content-type");
45
45
  var requireMethods = require("./require-methods");
46
46
  var securityHeaders = require("./security-headers");
47
47
  var securityTxt = require("./security-txt");
48
+ var spanHttpServer = require("./span-http-server");
48
49
  var sse = require("./sse");
50
+ var traceLogCorrelation = require("./trace-log-correlation");
51
+ var tracePropagate = require("./trace-propagate");
49
52
  var tusUpload = require("./tus-upload");
50
53
  var webAppManifest = require("./web-app-manifest");
51
54
 
@@ -80,6 +83,9 @@ module.exports = {
80
83
  dpop: dpop.create,
81
84
  hostAllowlist: hostAllowlist.create,
82
85
  networkAllowlist: networkAllowlist.create,
86
+ spanHttpServer: spanHttpServer.create,
87
+ traceLogCorrelation: traceLogCorrelation.create,
88
+ tracePropagate: tracePropagate.create,
83
89
  tusUpload: tusUpload.create,
84
90
  webAppManifest: webAppManifest.create,
85
91
 
@@ -112,6 +118,9 @@ module.exports = {
112
118
  dpop: dpop,
113
119
  hostAllowlist: hostAllowlist,
114
120
  networkAllowlist: networkAllowlist,
121
+ spanHttpServer: spanHttpServer,
122
+ traceLogCorrelation: traceLogCorrelation,
123
+ tracePropagate: tracePropagate,
115
124
  tusUpload: tusUpload,
116
125
  webAppManifest: webAppManifest,
117
126
  },
@@ -0,0 +1,243 @@
1
+ "use strict";
2
+ /**
3
+ * spanHttpServer middleware — auto-creates a root span per HTTP
4
+ * request, populates OTel SEMCONV.HTTP_* attributes, attaches
5
+ * the span to req.span, and ends the span on response close.
6
+ *
7
+ * var tracer = b.observability.tracer.create({ service: "checkout" });
8
+ * var exporter = b.observability.otlpExporter.create({
9
+ * endpoint: "https://collector.example.com/v1/traces",
10
+ * });
11
+ *
12
+ * router.use(b.middleware.tracePropagate()); // populates req.trace
13
+ * router.use(b.middleware.spanHttpServer({
14
+ * tracer: tracer,
15
+ * onEnd: exporter.queue,
16
+ * ignorePaths: ["/healthz", /^\/static/],
17
+ * captureRequestHeaders: ["user-agent", "x-tenant-id"],
18
+ * captureResponseHeaders: ["content-type", "content-length"],
19
+ * }));
20
+ *
21
+ * app.get("/checkout", function (req, res) {
22
+ * // req.span is the active root server span
23
+ * req.span.setAttribute("checkout.cart_size", cart.items.length);
24
+ * var childSpan = tracer.startChildOf(req.span, "db.query");
25
+ * // ... do query work
26
+ * childSpan.end();
27
+ * res.json({ ok: true });
28
+ * });
29
+ *
30
+ * Span attributes auto-populated per OTel HTTP-server semconv:
31
+ * - http.request.method, http.route (when available)
32
+ * - url.scheme, url.path, url.query
33
+ * - server.address, client.address
34
+ * - user_agent.original
35
+ * - http.response.status_code (set when response writeHead fires)
36
+ *
37
+ * Span kind: "server".
38
+ *
39
+ * Skip paths: opts.ignorePaths accepts an array of strings (exact match)
40
+ * or RegExp instances. Use this to keep healthz / static-asset routes
41
+ * out of the span volume.
42
+ */
43
+
44
+ var lazyRequire = require("../lazy-require");
45
+ var requestHelpers = require("../request-helpers");
46
+ var validateOpts = require("../validate-opts");
47
+ var { defineClass } = require("../framework-error");
48
+
49
+ var SpanHttpError = defineClass("SpanHttpError", { alwaysPermanent: true });
50
+
51
+ var observability = lazyRequire(function () { return require("../observability"); });
52
+
53
+ function _shouldIgnore(path, ignorePaths) {
54
+ if (!ignorePaths || !Array.isArray(ignorePaths)) return false;
55
+ for (var i = 0; i < ignorePaths.length; i++) {
56
+ var rule = ignorePaths[i];
57
+ if (typeof rule === "string" && rule === path) return true;
58
+ if (rule instanceof RegExp && rule.test(path)) return true;
59
+ }
60
+ return false;
61
+ }
62
+
63
+ function _splitUrl(url) {
64
+ if (typeof url !== "string" || url.length === 0) return { path: "/", query: null };
65
+ var qIdx = url.indexOf("?");
66
+ if (qIdx === -1) return { path: url, query: null };
67
+ return { path: url.slice(0, qIdx), query: url.slice(qIdx + 1) };
68
+ }
69
+
70
+ function _scheme(req) {
71
+ var x = req.headers && (req.headers["x-forwarded-proto"] || "");
72
+ if (typeof x === "string" && x.length > 0) {
73
+ var first = x.split(",")[0].trim().toLowerCase();
74
+ if (first === "http" || first === "https") return first;
75
+ }
76
+ return (req.socket && req.socket.encrypted) ? "https" : "http";
77
+ }
78
+
79
+ function _serverAddress(req) {
80
+ var hostHeader = req.headers && (req.headers["x-forwarded-host"] || req.headers.host);
81
+ if (typeof hostHeader === "string" && hostHeader.length > 0) {
82
+ return hostHeader.split(",")[0].trim();
83
+ }
84
+ return null;
85
+ }
86
+
87
+ function _captureHeaderAttrs(req, captureList, prefix) {
88
+ if (!Array.isArray(captureList) || captureList.length === 0) return {};
89
+ var out = Object.create(null);
90
+ for (var i = 0; i < captureList.length; i++) {
91
+ var name = String(captureList[i] || "").toLowerCase();
92
+ if (!name) continue;
93
+ var v = req.headers && req.headers[name];
94
+ if (v === undefined) continue;
95
+ if (Array.isArray(v)) v = v.join(", ");
96
+ out[prefix + "." + name] = String(v);
97
+ }
98
+ return out;
99
+ }
100
+
101
+ function _captureResponseHeaderAttrs(res, captureList, prefix) {
102
+ if (!Array.isArray(captureList) || captureList.length === 0) return {};
103
+ var out = Object.create(null);
104
+ for (var i = 0; i < captureList.length; i++) {
105
+ var name = String(captureList[i] || "").toLowerCase();
106
+ if (!name) continue;
107
+ var v;
108
+ try { v = res.getHeader(name); } catch (_e) { continue; }
109
+ if (v === undefined || v === null) continue;
110
+ if (Array.isArray(v)) v = v.join(", ");
111
+ out[prefix + "." + name] = String(v);
112
+ }
113
+ return out;
114
+ }
115
+
116
+ function create(opts) {
117
+ validateOpts.requireObject(opts, "middleware.spanHttpServer", SpanHttpError);
118
+ validateOpts(opts, [
119
+ "tracer", "onEnd", "ignorePaths",
120
+ "captureRequestHeaders", "captureResponseHeaders",
121
+ "spanNameFn", "audit",
122
+ ], "middleware.spanHttpServer");
123
+
124
+ if (!opts.tracer || typeof opts.tracer.start !== "function") {
125
+ throw new SpanHttpError("span-http/bad-tracer",
126
+ "middleware.spanHttpServer: tracer must be a b.observability.tracer.create() instance");
127
+ }
128
+ validateOpts.optionalFunction(opts.onEnd,
129
+ "middleware.spanHttpServer: onEnd", SpanHttpError, "span-http/bad-opts");
130
+ validateOpts.optionalFunction(opts.spanNameFn,
131
+ "middleware.spanHttpServer: spanNameFn", SpanHttpError, "span-http/bad-opts");
132
+
133
+ var tracer = opts.tracer;
134
+ var onEnd = opts.onEnd || null;
135
+ var ignorePaths = opts.ignorePaths || null;
136
+ var captureReqHeaders = opts.captureRequestHeaders || null;
137
+ var captureResHeaders = opts.captureResponseHeaders || null;
138
+ var spanNameFn = opts.spanNameFn || null;
139
+ var auditOn = opts.audit !== false;
140
+
141
+ return function spanHttpServerMiddleware(req, res, next) {
142
+ var SEMCONV = observability().SEMCONV;
143
+ var url = _splitUrl(req.url || "/");
144
+ if (_shouldIgnore(url.path, ignorePaths)) return next();
145
+
146
+ var spanName;
147
+ if (typeof spanNameFn === "function") {
148
+ try { spanName = String(spanNameFn(req)); }
149
+ catch (_e) { spanName = "http.server.request"; }
150
+ } else {
151
+ spanName = (req.method ? req.method.toUpperCase() + " " : "") + (url.path || "/");
152
+ }
153
+
154
+ var traceId = req.trace && req.trace.traceId;
155
+ var parentId = req.trace && req.trace.parentId;
156
+ var sampled = !req.trace || req.trace.sampled !== false;
157
+
158
+ var span = tracer.start(spanName, {
159
+ traceId: traceId,
160
+ parentId: parentId,
161
+ sampled: sampled,
162
+ kind: "server",
163
+ attributes: Object.assign({},
164
+ {
165
+ [SEMCONV.HTTP_REQUEST_METHOD]: (req.method || "").toUpperCase(),
166
+ [SEMCONV.URL_SCHEME]: _scheme(req),
167
+ [SEMCONV.URL_PATH]: url.path,
168
+ },
169
+ url.query !== null ? { [SEMCONV.URL_QUERY]: url.query } : {},
170
+ (function () {
171
+ var serverAddr = _serverAddress(req);
172
+ return serverAddr ? { [SEMCONV.SERVER_ADDRESS]: serverAddr } : {};
173
+ })(),
174
+ (function () {
175
+ var clientAddr = requestHelpers.clientIp(req);
176
+ return clientAddr ? { [SEMCONV.CLIENT_ADDRESS]: clientAddr } : {};
177
+ })(),
178
+ (function () {
179
+ var ua = req.headers && req.headers["user-agent"];
180
+ return ua ? { [SEMCONV.USER_AGENT_ORIGINAL]: String(ua) } : {};
181
+ })(),
182
+ _captureHeaderAttrs(req, captureReqHeaders, "http.request.header")),
183
+ });
184
+
185
+ req.span = span;
186
+
187
+ var ended = false;
188
+ function _finish(err) {
189
+ if (ended) return;
190
+ ended = true;
191
+ try {
192
+ var status = res.statusCode;
193
+ if (typeof status === "number") {
194
+ span.setAttribute(SEMCONV.HTTP_RESPONSE_STATUS_CODE, status);
195
+ if (status >= 500) {
196
+ span.setStatus("error", "HTTP " + status);
197
+ } else if (status >= 400) {
198
+ // Per OTel semconv: client errors don't auto-set error status
199
+ // (they're "expected" failures). Operators that want to flag
200
+ // 4xx as errors call span.setStatus("error", ...) themselves.
201
+ span.setStatus("ok");
202
+ } else {
203
+ span.setStatus("ok");
204
+ }
205
+ }
206
+ if (err) span.recordException(err);
207
+ var resHeaders = _captureResponseHeaderAttrs(res, captureResHeaders, "http.response.header");
208
+ var resHeaderKeys = Object.keys(resHeaders);
209
+ for (var i = 0; i < resHeaderKeys.length; i++) {
210
+ span.setAttribute(resHeaderKeys[i], resHeaders[resHeaderKeys[i]]);
211
+ }
212
+ if (req.route && req.route.path) {
213
+ span.setAttribute(SEMCONV.HTTP_ROUTE, String(req.route.path));
214
+ }
215
+ } catch (_e) { /* drop-silent — observability sink */ }
216
+ try { span.end(); }
217
+ catch (_e) { /* drop-silent */ }
218
+ if (typeof onEnd === "function") {
219
+ try { onEnd(span.toJSON()); }
220
+ catch (_e) { /* operator hook — drop-silent */ }
221
+ }
222
+ if (auditOn) {
223
+ try {
224
+ observability().safeEvent("middleware.spanHttpServer.complete", 1, {
225
+ kind: "server",
226
+ sampled: span.sampled ? "1" : "0",
227
+ });
228
+ } catch (_e) { /* drop-silent */ }
229
+ }
230
+ }
231
+
232
+ res.on("finish", function () { _finish(null); });
233
+ res.on("close", function () { _finish(null); });
234
+ res.on("error", function (e) { _finish(e); });
235
+
236
+ return next();
237
+ };
238
+ }
239
+
240
+ module.exports = {
241
+ create: create,
242
+ SpanHttpError: SpanHttpError,
243
+ };
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ /**
3
+ * trace-log-correlation middleware — wraps the operator's b.log
4
+ * instance for the request lifetime so every log() / info() / warn()
5
+ * / error() / debug() call inside the handler auto-includes the
6
+ * canonical trace_id + span_id (and tenant context from W3C Baggage
7
+ * when present).
8
+ *
9
+ * var log = b.log.boot("api");
10
+ * router.use(b.middleware.tracePropagate());
11
+ * router.use(b.middleware.traceLogCorrelation({
12
+ * logger: log,
13
+ * reqField: "log", // attaches as req.log
14
+ * }));
15
+ *
16
+ * app.get("/widgets", function (req, res) {
17
+ * // req.log is the wrapped logger; every emission carries
18
+ * // trace_id + span_id + (optional) baggage attributes
19
+ * req.log.info("loading widgets");
20
+ * // → { ..., trace_id: "abc...", span_id: "def...",
21
+ * // baggage: { tenant: "acme" } }
22
+ * });
23
+ *
24
+ * The wrapper is a thin adapter: it does not change log levels,
25
+ * sinks, or the b.log API surface. Logs pass through to the
26
+ * wrapped logger with the trace fields injected via the meta-object
27
+ * second argument.
28
+ *
29
+ * When no req.trace is present (unusual — operators typically mount
30
+ * tracePropagate first), the wrapper is a no-op pass-through; logs
31
+ * still flow but without correlation fields.
32
+ */
33
+
34
+ var lazyRequire = require("../lazy-require");
35
+ var validateOpts = require("../validate-opts");
36
+ var { defineClass } = require("../framework-error");
37
+
38
+ var TraceLogError = defineClass("TraceLogError", { alwaysPermanent: true });
39
+
40
+ var observability = lazyRequire(function () { return require("../observability"); });
41
+
42
+ var LOG_LEVELS = ["debug", "info", "warn", "error", "fatal"];
43
+
44
+ function _baggageToObject(entries) {
45
+ if (!Array.isArray(entries) || entries.length === 0) return null;
46
+ var out = Object.create(null);
47
+ for (var i = 0; i < entries.length; i++) {
48
+ out[entries[i].key] = entries[i].value;
49
+ }
50
+ return out;
51
+ }
52
+
53
+ function _wrapLogger(baseLogger, req, opts) {
54
+ if (!baseLogger || typeof baseLogger !== "object") return baseLogger;
55
+ var wrapped = Object.create(null);
56
+ // Preserve any non-level properties the operator put on the
57
+ // logger (e.g. boot context, child-logger metadata).
58
+ var keys = Object.keys(baseLogger);
59
+ for (var i = 0; i < keys.length; i++) {
60
+ if (LOG_LEVELS.indexOf(keys[i]) === -1) wrapped[keys[i]] = baseLogger[keys[i]];
61
+ }
62
+
63
+ function _enrichMeta(meta) {
64
+ var enriched = Object.assign({}, meta || {});
65
+ if (req && req.trace) {
66
+ enriched.trace_id = req.trace.traceId;
67
+ // span_id prefers the active span (set by spanHttpServer) over
68
+ // the trace context's parentId
69
+ if (req.span && typeof req.span.spanId === "string") {
70
+ enriched.span_id = req.span.spanId;
71
+ } else if (typeof req.trace.parentId === "string") {
72
+ enriched.span_id = req.trace.parentId;
73
+ }
74
+ if (opts.includeBaggage !== false) {
75
+ var bg = _baggageToObject(req.trace.tracestate);
76
+ // tracestate is vendor-trace data; baggage is operator data.
77
+ // Operators usually want baggage in logs, not tracestate.
78
+ // We don't have a separate req.baggage today; keep this as
79
+ // the path for when tracePropagate exposes it. For now,
80
+ // emit the resolved tracestate shape under "trace_state".
81
+ if (bg) enriched.trace_state = bg;
82
+ }
83
+ }
84
+ return enriched;
85
+ }
86
+
87
+ // Bind each level on the underlying logger so it emits with the
88
+ // enriched meta. We don't replace the underlying logger's bound
89
+ // emitter shape — it still receives meta as the second argument.
90
+ for (var li = 0; li < LOG_LEVELS.length; li++) {
91
+ (function (lvl) {
92
+ if (typeof baseLogger[lvl] !== "function") return;
93
+ wrapped[lvl] = function (msg, meta) {
94
+ try { return baseLogger[lvl](msg, _enrichMeta(meta)); }
95
+ catch (_e) { /* drop-silent — log sink */ }
96
+ };
97
+ })(LOG_LEVELS[li]);
98
+ }
99
+ // Pass through anything else the logger might expose (boot, child, etc.)
100
+ if (typeof baseLogger.boot === "function") wrapped.boot = baseLogger.boot.bind(baseLogger);
101
+ if (typeof baseLogger.child === "function") wrapped.child = baseLogger.child.bind(baseLogger);
102
+ return wrapped;
103
+ }
104
+
105
+ function create(opts) {
106
+ validateOpts.requireObject(opts, "middleware.traceLogCorrelation", TraceLogError);
107
+ validateOpts(opts, [
108
+ "logger", "reqField", "includeBaggage", "audit",
109
+ ], "middleware.traceLogCorrelation");
110
+
111
+ if (!opts.logger || typeof opts.logger !== "object") {
112
+ throw new TraceLogError("trace-log/bad-logger",
113
+ "middleware.traceLogCorrelation: logger must be a b.log instance");
114
+ }
115
+ var reqField = opts.reqField || "log";
116
+ if (typeof reqField !== "string" || reqField.length === 0) {
117
+ throw new TraceLogError("trace-log/bad-reqfield",
118
+ "middleware.traceLogCorrelation: reqField must be a non-empty string");
119
+ }
120
+
121
+ return function traceLogCorrelationMiddleware(req, res, next) {
122
+ req[reqField] = _wrapLogger(opts.logger, req, opts);
123
+ void observability; // touch lazyRequire so the dep is captured
124
+ return next();
125
+ };
126
+ }
127
+
128
+ module.exports = {
129
+ create: create,
130
+ TraceLogError: TraceLogError,
131
+ // exported for tests
132
+ _wrapLogger: _wrapLogger,
133
+ LOG_LEVELS: LOG_LEVELS,
134
+ };
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ /**
3
+ * trace-propagate middleware — consumes the inbound `traceparent`
4
+ * header per W3C Trace Context (https://www.w3.org/TR/trace-context-1/)
5
+ * and stamps `req.trace = { traceId, parentId, sampled, hadUpstream }`
6
+ * for downstream handlers + propagation into outbound HTTP calls.
7
+ *
8
+ * router.use(b.middleware.tracePropagate({
9
+ * generateIfMissing: true, // default — synthesise when absent
10
+ * auditOnMissing: true, // emit `system.trace.synthesised` event
11
+ * setResponseHeader: true, // echo the resolved traceparent on res
12
+ * }));
13
+ *
14
+ * app.get("/widgets", function (req, res) {
15
+ * // req.trace.traceId is the canonical id for this request
16
+ * b.observability.event("widgets.request", 1, {
17
+ * [b.observability.SEMCONV.HTTP_REQUEST_METHOD]: req.method,
18
+ * });
19
+ * // Propagate to an upstream call:
20
+ * fetch(upstreamUrl, {
21
+ * headers: {
22
+ * traceparent: b.observability.traceContext.build({
23
+ * traceId: req.trace.traceId,
24
+ * parentId: b.observability.traceContext.newParentId(),
25
+ * sampled: req.trace.sampled,
26
+ * }),
27
+ * },
28
+ * });
29
+ * });
30
+ *
31
+ * On bad / missing inbound traceparent:
32
+ * - generateIfMissing: true (default) → synthesise a fresh trace,
33
+ * stamp `hadUpstream: false` so downstream code knows this trace
34
+ * was originated locally
35
+ * - generateIfMissing: false → leave `req.trace = null`; downstream
36
+ * code that depends on a trace MUST handle the null case
37
+ */
38
+
39
+ var lazyRequire = require("../lazy-require");
40
+ var validateOpts = require("../validate-opts");
41
+ var { defineClass } = require("../framework-error");
42
+
43
+ var TracePropagateError = defineClass("TracePropagateError", { alwaysPermanent: true });
44
+
45
+ var observability = lazyRequire(function () { return require("../observability"); });
46
+ var audit = lazyRequire(function () { return require("../audit"); });
47
+
48
+ function create(opts) {
49
+ opts = opts || {};
50
+ validateOpts(opts, [
51
+ "generateIfMissing", "auditOnMissing",
52
+ "setResponseHeader", "audit",
53
+ ], "middleware.tracePropagate");
54
+
55
+ var generateIfMissing = opts.generateIfMissing !== false; // default true
56
+ var auditOnMissing = opts.auditOnMissing === true; // default false (every request that doesn't carry a trace is noisy)
57
+ var setResponseHeader = opts.setResponseHeader === true; // default false
58
+ var auditOn = opts.audit !== false;
59
+
60
+ return function tracePropagateMiddleware(req, res, next) {
61
+ var tc = observability().traceContext;
62
+ var inbound = req.headers && req.headers.traceparent;
63
+ var parsed = (typeof inbound === "string") ? tc.parse(inbound) : null;
64
+ var inboundTracestate = req.headers && req.headers.tracestate;
65
+ var tracestateEntries = (typeof inboundTracestate === "string")
66
+ ? tc.parseTracestate(inboundTracestate)
67
+ : null;
68
+ if (parsed) {
69
+ req.trace = {
70
+ traceId: parsed.traceId,
71
+ parentId: parsed.parentId,
72
+ sampled: parsed.sampled,
73
+ hadUpstream: true,
74
+ tracestate: tracestateEntries || [],
75
+ };
76
+ } else if (generateIfMissing) {
77
+ req.trace = {
78
+ traceId: tc.newTraceId(),
79
+ parentId: tc.newParentId(),
80
+ sampled: true,
81
+ hadUpstream: false,
82
+ tracestate: [],
83
+ };
84
+ if (auditOnMissing && auditOn) {
85
+ try {
86
+ audit().safeEmit({
87
+ action: "system.trace.synthesised",
88
+ outcome: "ok",
89
+ metadata: { route: req.url || "/", traceId: req.trace.traceId },
90
+ });
91
+ } catch (_e) { /* drop-silent — observability sink */ }
92
+ }
93
+ } else {
94
+ req.trace = null;
95
+ }
96
+
97
+ if (setResponseHeader && req.trace && !res.headersSent) {
98
+ try {
99
+ res.setHeader("traceparent", tc.build({
100
+ traceId: req.trace.traceId,
101
+ parentId: req.trace.parentId,
102
+ sampled: req.trace.sampled,
103
+ }));
104
+ if (req.trace.tracestate && req.trace.tracestate.length > 0) {
105
+ res.setHeader("tracestate", tc.buildTracestate(req.trace.tracestate));
106
+ }
107
+ } catch (_e) { /* drop-silent — header set best-effort */ }
108
+ }
109
+ return next();
110
+ };
111
+ }
112
+
113
+ module.exports = {
114
+ create: create,
115
+ TracePropagateError: TracePropagateError,
116
+ };