@blamejs/core 0.7.102 → 0.7.104

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/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,9 @@ 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");
49
51
  var tracePropagate = require("./trace-propagate");
50
52
  var tusUpload = require("./tus-upload");
51
53
  var webAppManifest = require("./web-app-manifest");
@@ -81,7 +83,9 @@ module.exports = {
81
83
  dpop: dpop.create,
82
84
  hostAllowlist: hostAllowlist.create,
83
85
  networkAllowlist: networkAllowlist.create,
84
- tracePropagate: tracePropagate.create,
86
+ spanHttpServer: spanHttpServer.create,
87
+ traceLogCorrelation: traceLogCorrelation.create,
88
+ tracePropagate: tracePropagate.create,
85
89
  tusUpload: tusUpload.create,
86
90
  webAppManifest: webAppManifest.create,
87
91
 
@@ -114,7 +118,9 @@ module.exports = {
114
118
  dpop: dpop,
115
119
  hostAllowlist: hostAllowlist,
116
120
  networkAllowlist: networkAllowlist,
117
- tracePropagate: tracePropagate,
121
+ spanHttpServer: spanHttpServer,
122
+ traceLogCorrelation: traceLogCorrelation,
123
+ tracePropagate: tracePropagate,
118
124
  tusUpload: tusUpload,
119
125
  webAppManifest: webAppManifest,
120
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
+ };
@@ -61,12 +61,17 @@ function create(opts) {
61
61
  var tc = observability().traceContext;
62
62
  var inbound = req.headers && req.headers.traceparent;
63
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;
64
68
  if (parsed) {
65
69
  req.trace = {
66
70
  traceId: parsed.traceId,
67
71
  parentId: parsed.parentId,
68
72
  sampled: parsed.sampled,
69
73
  hadUpstream: true,
74
+ tracestate: tracestateEntries || [],
70
75
  };
71
76
  } else if (generateIfMissing) {
72
77
  req.trace = {
@@ -74,6 +79,7 @@ function create(opts) {
74
79
  parentId: tc.newParentId(),
75
80
  sampled: true,
76
81
  hadUpstream: false,
82
+ tracestate: [],
77
83
  };
78
84
  if (auditOnMissing && auditOn) {
79
85
  try {
@@ -95,6 +101,9 @@ function create(opts) {
95
101
  parentId: req.trace.parentId,
96
102
  sampled: req.trace.sampled,
97
103
  }));
104
+ if (req.trace.tracestate && req.trace.tracestate.length > 0) {
105
+ res.setHeader("tracestate", tc.buildTracestate(req.trace.tracestate));
106
+ }
98
107
  } catch (_e) { /* drop-silent — header set best-effort */ }
99
108
  }
100
109
  return next();