@blamejs/core 0.7.4 → 0.7.18
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 +423 -395
- package/README.md +150 -149
- package/bin/blamejs.js +0 -0
- package/index.js +308 -284
- package/lib/api-key.js +660 -663
- package/lib/api-snapshot.js +338 -338
- package/lib/app-shutdown.js +385 -385
- package/lib/app.js +365 -365
- package/lib/archive.js +250 -250
- package/lib/atomic-file.js +544 -544
- package/lib/audit-chain.js +177 -177
- package/lib/audit-sign.js +344 -344
- package/lib/audit-tools.js +677 -677
- package/lib/audit.js +766 -766
- package/lib/auth/jwt.js +311 -311
- package/lib/auth/lockout.js +436 -436
- package/lib/auth/oauth.js +721 -721
- package/lib/auth/passkey.js +181 -181
- package/lib/auth/password.js +594 -594
- package/lib/backup/bundle.js +217 -217
- package/lib/backup/crypto.js +176 -176
- package/lib/backup/index.js +515 -515
- package/lib/backup/manifest.js +282 -282
- package/lib/break-glass.js +1338 -1338
- package/lib/bundler.js +441 -441
- package/lib/cache-redis.js +256 -256
- package/lib/cache.js +1206 -1206
- package/lib/canonical-json.js +115 -115
- package/lib/chain-writer.js +234 -234
- package/lib/cli-helpers.js +206 -206
- package/lib/cli.js +2334 -2334
- package/lib/cluster-provider-db.js +317 -317
- package/lib/cluster-storage.js +226 -226
- package/lib/cluster.js +703 -703
- package/lib/codepoint-class.js +196 -0
- package/lib/config-drift.js +301 -301
- package/lib/consent.js +222 -222
- package/lib/constants.js +191 -191
- package/lib/cookies.js +315 -315
- package/lib/credential-hash.js +322 -322
- package/lib/crypto.js +266 -266
- package/lib/csv.js +275 -286
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -421
- package/lib/db-query.js +406 -406
- package/lib/db-schema.js +319 -319
- package/lib/db.js +1288 -1288
- package/lib/deprecate.js +222 -222
- package/lib/dev.js +335 -335
- package/lib/dual-control.js +473 -473
- package/lib/error-page.js +420 -420
- package/lib/external-db-migrate.js +441 -441
- package/lib/external-db.js +1061 -1061
- package/lib/file-type.js +273 -273
- package/lib/file-upload.js +213 -10
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -215
- package/lib/framework-schema.js +717 -717
- package/lib/gate-contract.js +971 -0
- package/lib/guard-all.js +405 -0
- package/lib/guard-archive.js +739 -0
- package/lib/guard-csv.js +816 -0
- package/lib/guard-email.js +744 -0
- package/lib/guard-filename.js +724 -0
- package/lib/guard-html.js +976 -0
- package/lib/guard-json.js +729 -0
- package/lib/guard-markdown.js +586 -0
- package/lib/guard-svg.js +976 -0
- package/lib/guard-xml.js +405 -0
- package/lib/guard-yaml.js +529 -0
- package/lib/handlers.js +350 -350
- package/lib/http-client-cookie-jar.js +508 -508
- package/lib/http-client.js +1195 -1195
- package/lib/i18n.js +878 -878
- package/lib/jobs.js +185 -185
- package/lib/log-stream-cloudwatch.js +369 -369
- package/lib/log-stream-local.js +146 -146
- package/lib/log-stream-otlp-grpc.js +410 -410
- package/lib/log-stream-otlp.js +286 -286
- package/lib/log-stream-syslog.js +302 -302
- package/lib/log-stream-webhook.js +199 -199
- package/lib/log-stream.js +330 -330
- package/lib/log.js +500 -500
- package/lib/mail-bounce.js +528 -528
- package/lib/mail-dkim.js +369 -362
- package/lib/mail.js +981 -962
- package/lib/metrics.js +683 -683
- package/lib/middleware/api-encrypt.js +936 -936
- package/lib/middleware/attach-user.js +157 -157
- package/lib/middleware/body-parser.js +1170 -1091
- package/lib/middleware/bot-guard.js +178 -178
- package/lib/middleware/compression.js +452 -452
- package/lib/middleware/cors.js +314 -314
- package/lib/middleware/csp-nonce.js +348 -348
- package/lib/middleware/csrf-protect.js +316 -316
- package/lib/middleware/db-role-for.js +264 -264
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +79 -79
- package/lib/middleware/rate-limit.js +358 -358
- package/lib/middleware/request-id.js +61 -61
- package/lib/middleware/request-log.js +168 -168
- package/lib/middleware/require-auth.js +104 -104
- package/lib/middleware/security-headers.js +116 -116
- package/lib/middleware/sse.js +166 -166
- package/lib/migrations.js +383 -383
- package/lib/mtls-ca.js +518 -518
- package/lib/mtls-engine-default.js +481 -481
- package/lib/network-dns.js +632 -632
- package/lib/network-heartbeat.js +290 -290
- package/lib/network-nts.js +574 -574
- package/lib/network-proxy.js +265 -265
- package/lib/network-tls.js +328 -328
- package/lib/network.js +233 -233
- package/lib/notify.js +612 -612
- package/lib/ntp-check.js +229 -229
- package/lib/numeric-bounds.js +111 -91
- package/lib/object-store/azure-blob-bucket-ops.js +349 -349
- package/lib/object-store/azure-blob.js +488 -488
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -519
- package/lib/object-store/http-put.js +153 -153
- package/lib/object-store/index.js +197 -197
- package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
- package/lib/object-store/sigv4.js +903 -903
- package/lib/observability.js +151 -151
- package/lib/otel-export.js +269 -269
- package/lib/pagination.js +464 -464
- package/lib/parsers/index.js +80 -80
- package/lib/parsers/safe-env.js +642 -642
- package/lib/parsers/safe-ini.js +292 -292
- package/lib/parsers/safe-toml.js +784 -784
- package/lib/parsers/safe-xml.js +390 -390
- package/lib/parsers/safe-yaml.js +1015 -1015
- package/lib/permissions.js +708 -708
- package/lib/pqc-agent.js +87 -87
- package/lib/pqc-gate.js +279 -279
- package/lib/protobuf-encoder.js +190 -190
- package/lib/protocol-dispatcher.js +161 -161
- package/lib/pubsub-redis.js +167 -167
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -745
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -519
- package/lib/request-helpers.js +340 -340
- package/lib/restore-bundle.js +237 -237
- package/lib/restore-rollback.js +259 -259
- package/lib/restore.js +409 -409
- package/lib/retry.js +376 -376
- package/lib/router.js +748 -748
- package/lib/safe-async.js +735 -735
- package/lib/safe-buffer.js +237 -237
- package/lib/safe-json.js +541 -541
- package/lib/safe-schema.js +1266 -1266
- package/lib/safe-url.js +159 -159
- package/lib/scheduler.js +706 -706
- package/lib/security-assert.js +373 -373
- package/lib/seeders.js +618 -618
- package/lib/session.js +478 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/static.js +184 -4
- package/lib/storage.js +471 -471
- package/lib/subject.js +281 -281
- package/lib/template.js +791 -791
- package/lib/testing.js +798 -798
- package/lib/time.js +310 -310
- package/lib/totp.js +302 -302
- package/lib/tracing.js +494 -494
- package/lib/uuid.js +132 -132
- package/lib/validate-opts.js +340 -319
- package/lib/vault/index.js +308 -308
- package/lib/vault/rotate.js +784 -784
- package/lib/vault/wrap.js +296 -296
- package/lib/vendor/noble-ciphers.cjs +9 -9
- package/lib/webhook.js +595 -595
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Request-ID middleware. Propagates an existing X-Request-Id header (or
|
|
4
|
-
* trace ID from upstream) when present and well-formed; otherwise
|
|
5
|
-
* generates a fresh 32-hex value. Sets req.requestId AND emits the same
|
|
6
|
-
* value as a response header so downstream services + auditors can
|
|
7
|
-
* correlate.
|
|
8
|
-
*
|
|
9
|
-
* Threading the request ID into audit.record() metadata is what makes
|
|
10
|
-
* the cross-event correlation traceable; apps should pass this through
|
|
11
|
-
* to every audit.record() they call within the request lifecycle.
|
|
12
|
-
*
|
|
13
|
-
* Options:
|
|
14
|
-
* {
|
|
15
|
-
* headerName: 'X-Request-Id'
|
|
16
|
-
* trustUpstream: true // propagate upstream id if it matches
|
|
17
|
-
* // the format check; false → always
|
|
18
|
-
* // generate fresh
|
|
19
|
-
* formatRegex: /^[A-Za-z0-9._-]{8,128}$/
|
|
20
|
-
* }
|
|
21
|
-
*/
|
|
22
|
-
var C = require("../constants");
|
|
23
|
-
var { generateToken } = require("../crypto");
|
|
24
|
-
var validateOpts = require("../validate-opts");
|
|
25
|
-
|
|
26
|
-
var DEFAULT_FORMAT = /^[A-Za-z0-9._-]{8,128}$/;
|
|
27
|
-
// Hard cap on inbound header length. The DEFAULT_FORMAT regex caps at
|
|
28
|
-
// 128 chars, but operator-supplied formatRegex values may be looser;
|
|
29
|
-
// length-bound the candidate before .test() so a multi-megabyte header
|
|
30
|
-
// can't drive ReDoS even against a careless operator pattern.
|
|
31
|
-
var MAX_INBOUND_LEN = C.BYTES.bytes(256);
|
|
32
|
-
|
|
33
|
-
function create(opts) {
|
|
34
|
-
opts = opts || {};
|
|
35
|
-
validateOpts(opts, [
|
|
36
|
-
"headerName", "trustUpstream", "formatRegex",
|
|
37
|
-
], "middleware.requestId");
|
|
38
|
-
var headerName = (opts.headerName || "X-Request-Id");
|
|
39
|
-
var headerNameLower = headerName.toLowerCase();
|
|
40
|
-
var trustUpstream = opts.trustUpstream !== false;
|
|
41
|
-
var format = opts.formatRegex || DEFAULT_FORMAT;
|
|
42
|
-
|
|
43
|
-
return function requestId(req, res, next) {
|
|
44
|
-
var inbound = req.headers && req.headers[headerNameLower];
|
|
45
|
-
var id;
|
|
46
|
-
if (trustUpstream && typeof inbound === "string" &&
|
|
47
|
-
inbound.length > 0 && inbound.length <= MAX_INBOUND_LEN &&
|
|
48
|
-
format.test(inbound)) {
|
|
49
|
-
id = inbound;
|
|
50
|
-
} else {
|
|
51
|
-
id = generateToken(C.BYTES.bytes(16)); // 32 hex chars
|
|
52
|
-
}
|
|
53
|
-
req.requestId = id;
|
|
54
|
-
if (typeof res.setHeader === "function") {
|
|
55
|
-
res.setHeader(headerName, id);
|
|
56
|
-
}
|
|
57
|
-
next();
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
module.exports = { create: create };
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Request-ID middleware. Propagates an existing X-Request-Id header (or
|
|
4
|
+
* trace ID from upstream) when present and well-formed; otherwise
|
|
5
|
+
* generates a fresh 32-hex value. Sets req.requestId AND emits the same
|
|
6
|
+
* value as a response header so downstream services + auditors can
|
|
7
|
+
* correlate.
|
|
8
|
+
*
|
|
9
|
+
* Threading the request ID into audit.record() metadata is what makes
|
|
10
|
+
* the cross-event correlation traceable; apps should pass this through
|
|
11
|
+
* to every audit.record() they call within the request lifecycle.
|
|
12
|
+
*
|
|
13
|
+
* Options:
|
|
14
|
+
* {
|
|
15
|
+
* headerName: 'X-Request-Id'
|
|
16
|
+
* trustUpstream: true // propagate upstream id if it matches
|
|
17
|
+
* // the format check; false → always
|
|
18
|
+
* // generate fresh
|
|
19
|
+
* formatRegex: /^[A-Za-z0-9._-]{8,128}$/
|
|
20
|
+
* }
|
|
21
|
+
*/
|
|
22
|
+
var C = require("../constants");
|
|
23
|
+
var { generateToken } = require("../crypto");
|
|
24
|
+
var validateOpts = require("../validate-opts");
|
|
25
|
+
|
|
26
|
+
var DEFAULT_FORMAT = /^[A-Za-z0-9._-]{8,128}$/;
|
|
27
|
+
// Hard cap on inbound header length. The DEFAULT_FORMAT regex caps at
|
|
28
|
+
// 128 chars, but operator-supplied formatRegex values may be looser;
|
|
29
|
+
// length-bound the candidate before .test() so a multi-megabyte header
|
|
30
|
+
// can't drive ReDoS even against a careless operator pattern.
|
|
31
|
+
var MAX_INBOUND_LEN = C.BYTES.bytes(256);
|
|
32
|
+
|
|
33
|
+
function create(opts) {
|
|
34
|
+
opts = opts || {};
|
|
35
|
+
validateOpts(opts, [
|
|
36
|
+
"headerName", "trustUpstream", "formatRegex",
|
|
37
|
+
], "middleware.requestId");
|
|
38
|
+
var headerName = (opts.headerName || "X-Request-Id");
|
|
39
|
+
var headerNameLower = headerName.toLowerCase();
|
|
40
|
+
var trustUpstream = opts.trustUpstream !== false;
|
|
41
|
+
var format = opts.formatRegex || DEFAULT_FORMAT;
|
|
42
|
+
|
|
43
|
+
return function requestId(req, res, next) {
|
|
44
|
+
var inbound = req.headers && req.headers[headerNameLower];
|
|
45
|
+
var id;
|
|
46
|
+
if (trustUpstream && typeof inbound === "string" &&
|
|
47
|
+
inbound.length > 0 && inbound.length <= MAX_INBOUND_LEN &&
|
|
48
|
+
format.test(inbound)) {
|
|
49
|
+
id = inbound;
|
|
50
|
+
} else {
|
|
51
|
+
id = generateToken(C.BYTES.bytes(16)); // 32 hex chars
|
|
52
|
+
}
|
|
53
|
+
req.requestId = id;
|
|
54
|
+
if (typeof res.setHeader === "function") {
|
|
55
|
+
res.setHeader(headerName, id);
|
|
56
|
+
}
|
|
57
|
+
next();
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { create: create };
|
|
@@ -1,168 +1,168 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* request-log — HTTP access-log middleware. Captures method, path,
|
|
4
|
-
* status, duration, response bytes, requestId, actor IP, user-agent
|
|
5
|
-
* and emits one structured log entry per request via b.log.
|
|
6
|
-
*
|
|
7
|
-
* router.use(b.middleware.requestLog({
|
|
8
|
-
* logger: b.log.boot("http"), // any b.log instance
|
|
9
|
-
* skipPaths: ["/healthz", /^\/static/],
|
|
10
|
-
* trustProxy: true, // honors X-Forwarded-For
|
|
11
|
-
* levelFn: function (status) { // default: 5xx=error, 4xx=warn, else info
|
|
12
|
-
* return status >= 500 ? "error" : status >= 400 ? "warn" : "info";
|
|
13
|
-
* },
|
|
14
|
-
* fields: ["method", "path", "status", "durationMs",
|
|
15
|
-
* "actorIp", "userAgent", "requestId", "bytes"],
|
|
16
|
-
* }));
|
|
17
|
-
*
|
|
18
|
-
* The middleware adopts `b.requestHelpers.captureResponseStatus` to
|
|
19
|
-
* read the final status reliably (handlers that call `res.writeHead`
|
|
20
|
-
* vs `res.statusCode = ...` vs `res.status(...).send(...)` all work).
|
|
21
|
-
*
|
|
22
|
-
* trustProxy gates X-Forwarded-For consumption — same boundary as the
|
|
23
|
-
* rest of the framework. Default false; operators behind a sanitizing
|
|
24
|
-
* reverse proxy opt in.
|
|
25
|
-
*
|
|
26
|
-
* Emits at log-level keyed off response status by default. Operators
|
|
27
|
-
* who want one-level-fits-all pass a static string for `level` (e.g.
|
|
28
|
-
* "debug" to keep access logs out of production stdout) or a function
|
|
29
|
-
* for fully custom logic (e.g. "warn" only on slow-path requests).
|
|
30
|
-
*/
|
|
31
|
-
var C = require("../constants");
|
|
32
|
-
var requestHelpers = require("../request-helpers");
|
|
33
|
-
var validateOpts = require("../validate-opts");
|
|
34
|
-
|
|
35
|
-
var DEFAULT_FIELDS = [
|
|
36
|
-
"method", "path", "status", "durationMs", "bytes",
|
|
37
|
-
"actorIp", "userAgent", "requestId",
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
function _defaultLevel(status) {
|
|
41
|
-
if (status >= 500) return "error";
|
|
42
|
-
if (status >= 400) return "warn";
|
|
43
|
-
return "info";
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function create(opts) {
|
|
47
|
-
opts = opts || {};
|
|
48
|
-
validateOpts(opts, [
|
|
49
|
-
"logger", "skipPaths", "trustProxy", "level", "levelFn", "fields",
|
|
50
|
-
], "middleware.requestLog");
|
|
51
|
-
var logger = opts.logger;
|
|
52
|
-
if (!logger || typeof logger.info !== "function") {
|
|
53
|
-
throw new Error("middleware.requestLog: opts.logger must be a b.log instance " +
|
|
54
|
-
"(call b.log.boot(...) or b.log.create({...}))");
|
|
55
|
-
}
|
|
56
|
-
var skipPaths = Array.isArray(opts.skipPaths) ? opts.skipPaths.slice() : [];
|
|
57
|
-
for (var i = 0; i < skipPaths.length; i++) {
|
|
58
|
-
if (typeof skipPaths[i] !== "string" && !(skipPaths[i] instanceof RegExp)) {
|
|
59
|
-
throw new Error("middleware.requestLog: skipPaths[" + i + "] must be a string prefix or RegExp");
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
var trustProxy = opts.trustProxy === true || typeof opts.trustProxy === "number"
|
|
63
|
-
? opts.trustProxy : false;
|
|
64
|
-
var levelFn;
|
|
65
|
-
if (typeof opts.levelFn === "function") {
|
|
66
|
-
levelFn = opts.levelFn;
|
|
67
|
-
} else if (typeof opts.level === "string") {
|
|
68
|
-
var fixedLevel = opts.level;
|
|
69
|
-
levelFn = function () { return fixedLevel; };
|
|
70
|
-
} else {
|
|
71
|
-
levelFn = _defaultLevel;
|
|
72
|
-
}
|
|
73
|
-
var fields = Array.isArray(opts.fields) && opts.fields.length > 0
|
|
74
|
-
? opts.fields.slice()
|
|
75
|
-
: DEFAULT_FIELDS;
|
|
76
|
-
|
|
77
|
-
function _shouldSkip(req) {
|
|
78
|
-
var path = req.pathname || (req.url || "").split("?")[0];
|
|
79
|
-
for (var i = 0; i < skipPaths.length; i++) {
|
|
80
|
-
var entry = skipPaths[i];
|
|
81
|
-
if (typeof entry === "string") { if (path.indexOf(entry) === 0) return true; }
|
|
82
|
-
else if (entry.test(path)) return true;
|
|
83
|
-
}
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return function requestLog(req, res, next) {
|
|
88
|
-
if (_shouldSkip(req)) return next();
|
|
89
|
-
var startedAt = process.hrtime ? process.hrtime() : null;
|
|
90
|
-
var startedMs = Date.now();
|
|
91
|
-
var bytes = 0;
|
|
92
|
-
var statusFromWriteHead = null;
|
|
93
|
-
var emitted = false;
|
|
94
|
-
|
|
95
|
-
// Tally bytes off res.write / res.end and read the final status
|
|
96
|
-
// when end fires. Inlined here (rather than composing
|
|
97
|
-
// captureResponseStatus + a separate byte-counter) so the log
|
|
98
|
-
// entry sees the fully-populated bytes counter — wrap order
|
|
99
|
-
// matters: bytes must be incremented BEFORE the log emit.
|
|
100
|
-
var origWrite = res.write;
|
|
101
|
-
var origEnd = res.end;
|
|
102
|
-
var origWriteHead = res.writeHead;
|
|
103
|
-
res.writeHead = function (s) {
|
|
104
|
-
statusFromWriteHead = s;
|
|
105
|
-
return origWriteHead.apply(res, arguments);
|
|
106
|
-
};
|
|
107
|
-
res.write = function (chunk, enc, cb) {
|
|
108
|
-
if (chunk != null) {
|
|
109
|
-
var len = Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(String(chunk), enc || "utf8");
|
|
110
|
-
bytes += len;
|
|
111
|
-
}
|
|
112
|
-
return origWrite.call(res, chunk, enc, cb);
|
|
113
|
-
};
|
|
114
|
-
res.end = function (chunk, enc, cb) {
|
|
115
|
-
if (chunk != null) {
|
|
116
|
-
var len = Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(String(chunk), enc || "utf8");
|
|
117
|
-
bytes += len;
|
|
118
|
-
}
|
|
119
|
-
_emit();
|
|
120
|
-
return origEnd.call(res, chunk, enc, cb);
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
function _emit() {
|
|
124
|
-
if (emitted) return;
|
|
125
|
-
emitted = true;
|
|
126
|
-
var statusCode = statusFromWriteHead != null
|
|
127
|
-
? statusFromWriteHead
|
|
128
|
-
: (typeof res.statusCode === "number" ? res.statusCode : requestHelpers.HTTP_STATUS.OK);
|
|
129
|
-
var durationMs;
|
|
130
|
-
if (startedAt && process.hrtime) {
|
|
131
|
-
var d = process.hrtime(startedAt);
|
|
132
|
-
durationMs = C.TIME.seconds(d[0]) + (d[1] / 1e6);
|
|
133
|
-
} else {
|
|
134
|
-
durationMs = Date.now() - startedMs;
|
|
135
|
-
}
|
|
136
|
-
var entry = {};
|
|
137
|
-
var actor = requestHelpers.extractActorContext(req, {
|
|
138
|
-
ip: requestHelpers.clientIp(req, { trustProxy: trustProxy }),
|
|
139
|
-
});
|
|
140
|
-
var src = {
|
|
141
|
-
method: req.method,
|
|
142
|
-
path: req.pathname || (req.url || "").split("?")[0],
|
|
143
|
-
status: statusCode,
|
|
144
|
-
durationMs: Math.round(durationMs * 100) / 100,
|
|
145
|
-
bytes: bytes,
|
|
146
|
-
actorIp: actor.ip,
|
|
147
|
-
userAgent: actor.userAgent,
|
|
148
|
-
requestId: actor.requestId,
|
|
149
|
-
sessionId: actor.sessionId,
|
|
150
|
-
userId: actor.userId,
|
|
151
|
-
route: actor.route,
|
|
152
|
-
};
|
|
153
|
-
for (var fi = 0; fi < fields.length; fi++) {
|
|
154
|
-
var f = fields[fi];
|
|
155
|
-
if (Object.prototype.hasOwnProperty.call(src, f)) entry[f] = src[f];
|
|
156
|
-
}
|
|
157
|
-
var level = levelFn(statusCode, req, res);
|
|
158
|
-
var fn = typeof logger[level] === "function" ? logger[level] : logger.info;
|
|
159
|
-
try { fn.call(logger, "http", entry); } catch (_e) { /* never let log-emit failure crash the response */ }
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return next();
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
module.exports = {
|
|
167
|
-
create: create,
|
|
168
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* request-log — HTTP access-log middleware. Captures method, path,
|
|
4
|
+
* status, duration, response bytes, requestId, actor IP, user-agent
|
|
5
|
+
* and emits one structured log entry per request via b.log.
|
|
6
|
+
*
|
|
7
|
+
* router.use(b.middleware.requestLog({
|
|
8
|
+
* logger: b.log.boot("http"), // any b.log instance
|
|
9
|
+
* skipPaths: ["/healthz", /^\/static/],
|
|
10
|
+
* trustProxy: true, // honors X-Forwarded-For
|
|
11
|
+
* levelFn: function (status) { // default: 5xx=error, 4xx=warn, else info
|
|
12
|
+
* return status >= 500 ? "error" : status >= 400 ? "warn" : "info";
|
|
13
|
+
* },
|
|
14
|
+
* fields: ["method", "path", "status", "durationMs",
|
|
15
|
+
* "actorIp", "userAgent", "requestId", "bytes"],
|
|
16
|
+
* }));
|
|
17
|
+
*
|
|
18
|
+
* The middleware adopts `b.requestHelpers.captureResponseStatus` to
|
|
19
|
+
* read the final status reliably (handlers that call `res.writeHead`
|
|
20
|
+
* vs `res.statusCode = ...` vs `res.status(...).send(...)` all work).
|
|
21
|
+
*
|
|
22
|
+
* trustProxy gates X-Forwarded-For consumption — same boundary as the
|
|
23
|
+
* rest of the framework. Default false; operators behind a sanitizing
|
|
24
|
+
* reverse proxy opt in.
|
|
25
|
+
*
|
|
26
|
+
* Emits at log-level keyed off response status by default. Operators
|
|
27
|
+
* who want one-level-fits-all pass a static string for `level` (e.g.
|
|
28
|
+
* "debug" to keep access logs out of production stdout) or a function
|
|
29
|
+
* for fully custom logic (e.g. "warn" only on slow-path requests).
|
|
30
|
+
*/
|
|
31
|
+
var C = require("../constants");
|
|
32
|
+
var requestHelpers = require("../request-helpers");
|
|
33
|
+
var validateOpts = require("../validate-opts");
|
|
34
|
+
|
|
35
|
+
var DEFAULT_FIELDS = [
|
|
36
|
+
"method", "path", "status", "durationMs", "bytes",
|
|
37
|
+
"actorIp", "userAgent", "requestId",
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
function _defaultLevel(status) {
|
|
41
|
+
if (status >= 500) return "error";
|
|
42
|
+
if (status >= 400) return "warn";
|
|
43
|
+
return "info";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function create(opts) {
|
|
47
|
+
opts = opts || {};
|
|
48
|
+
validateOpts(opts, [
|
|
49
|
+
"logger", "skipPaths", "trustProxy", "level", "levelFn", "fields",
|
|
50
|
+
], "middleware.requestLog");
|
|
51
|
+
var logger = opts.logger;
|
|
52
|
+
if (!logger || typeof logger.info !== "function") {
|
|
53
|
+
throw new Error("middleware.requestLog: opts.logger must be a b.log instance " +
|
|
54
|
+
"(call b.log.boot(...) or b.log.create({...}))");
|
|
55
|
+
}
|
|
56
|
+
var skipPaths = Array.isArray(opts.skipPaths) ? opts.skipPaths.slice() : [];
|
|
57
|
+
for (var i = 0; i < skipPaths.length; i++) {
|
|
58
|
+
if (typeof skipPaths[i] !== "string" && !(skipPaths[i] instanceof RegExp)) {
|
|
59
|
+
throw new Error("middleware.requestLog: skipPaths[" + i + "] must be a string prefix or RegExp");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
var trustProxy = opts.trustProxy === true || typeof opts.trustProxy === "number"
|
|
63
|
+
? opts.trustProxy : false;
|
|
64
|
+
var levelFn;
|
|
65
|
+
if (typeof opts.levelFn === "function") {
|
|
66
|
+
levelFn = opts.levelFn;
|
|
67
|
+
} else if (typeof opts.level === "string") {
|
|
68
|
+
var fixedLevel = opts.level;
|
|
69
|
+
levelFn = function () { return fixedLevel; };
|
|
70
|
+
} else {
|
|
71
|
+
levelFn = _defaultLevel;
|
|
72
|
+
}
|
|
73
|
+
var fields = Array.isArray(opts.fields) && opts.fields.length > 0
|
|
74
|
+
? opts.fields.slice()
|
|
75
|
+
: DEFAULT_FIELDS;
|
|
76
|
+
|
|
77
|
+
function _shouldSkip(req) {
|
|
78
|
+
var path = req.pathname || (req.url || "").split("?")[0];
|
|
79
|
+
for (var i = 0; i < skipPaths.length; i++) {
|
|
80
|
+
var entry = skipPaths[i];
|
|
81
|
+
if (typeof entry === "string") { if (path.indexOf(entry) === 0) return true; }
|
|
82
|
+
else if (entry.test(path)) return true;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return function requestLog(req, res, next) {
|
|
88
|
+
if (_shouldSkip(req)) return next();
|
|
89
|
+
var startedAt = process.hrtime ? process.hrtime() : null;
|
|
90
|
+
var startedMs = Date.now();
|
|
91
|
+
var bytes = 0;
|
|
92
|
+
var statusFromWriteHead = null;
|
|
93
|
+
var emitted = false;
|
|
94
|
+
|
|
95
|
+
// Tally bytes off res.write / res.end and read the final status
|
|
96
|
+
// when end fires. Inlined here (rather than composing
|
|
97
|
+
// captureResponseStatus + a separate byte-counter) so the log
|
|
98
|
+
// entry sees the fully-populated bytes counter — wrap order
|
|
99
|
+
// matters: bytes must be incremented BEFORE the log emit.
|
|
100
|
+
var origWrite = res.write;
|
|
101
|
+
var origEnd = res.end;
|
|
102
|
+
var origWriteHead = res.writeHead;
|
|
103
|
+
res.writeHead = function (s) {
|
|
104
|
+
statusFromWriteHead = s;
|
|
105
|
+
return origWriteHead.apply(res, arguments);
|
|
106
|
+
};
|
|
107
|
+
res.write = function (chunk, enc, cb) {
|
|
108
|
+
if (chunk != null) {
|
|
109
|
+
var len = Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(String(chunk), enc || "utf8");
|
|
110
|
+
bytes += len;
|
|
111
|
+
}
|
|
112
|
+
return origWrite.call(res, chunk, enc, cb);
|
|
113
|
+
};
|
|
114
|
+
res.end = function (chunk, enc, cb) {
|
|
115
|
+
if (chunk != null) {
|
|
116
|
+
var len = Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(String(chunk), enc || "utf8");
|
|
117
|
+
bytes += len;
|
|
118
|
+
}
|
|
119
|
+
_emit();
|
|
120
|
+
return origEnd.call(res, chunk, enc, cb);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
function _emit() {
|
|
124
|
+
if (emitted) return;
|
|
125
|
+
emitted = true;
|
|
126
|
+
var statusCode = statusFromWriteHead != null
|
|
127
|
+
? statusFromWriteHead
|
|
128
|
+
: (typeof res.statusCode === "number" ? res.statusCode : requestHelpers.HTTP_STATUS.OK);
|
|
129
|
+
var durationMs;
|
|
130
|
+
if (startedAt && process.hrtime) {
|
|
131
|
+
var d = process.hrtime(startedAt);
|
|
132
|
+
durationMs = C.TIME.seconds(d[0]) + (d[1] / 1e6);
|
|
133
|
+
} else {
|
|
134
|
+
durationMs = Date.now() - startedMs;
|
|
135
|
+
}
|
|
136
|
+
var entry = {};
|
|
137
|
+
var actor = requestHelpers.extractActorContext(req, {
|
|
138
|
+
ip: requestHelpers.clientIp(req, { trustProxy: trustProxy }),
|
|
139
|
+
});
|
|
140
|
+
var src = {
|
|
141
|
+
method: req.method,
|
|
142
|
+
path: req.pathname || (req.url || "").split("?")[0],
|
|
143
|
+
status: statusCode,
|
|
144
|
+
durationMs: Math.round(durationMs * 100) / 100,
|
|
145
|
+
bytes: bytes,
|
|
146
|
+
actorIp: actor.ip,
|
|
147
|
+
userAgent: actor.userAgent,
|
|
148
|
+
requestId: actor.requestId,
|
|
149
|
+
sessionId: actor.sessionId,
|
|
150
|
+
userId: actor.userId,
|
|
151
|
+
route: actor.route,
|
|
152
|
+
};
|
|
153
|
+
for (var fi = 0; fi < fields.length; fi++) {
|
|
154
|
+
var f = fields[fi];
|
|
155
|
+
if (Object.prototype.hasOwnProperty.call(src, f)) entry[f] = src[f];
|
|
156
|
+
}
|
|
157
|
+
var level = levelFn(statusCode, req, res);
|
|
158
|
+
var fn = typeof logger[level] === "function" ? logger[level] : logger.info;
|
|
159
|
+
try { fn.call(logger, "http", entry); } catch (_e) { /* never let log-emit failure crash the response */ }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return next();
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = {
|
|
167
|
+
create: create,
|
|
168
|
+
};
|