@blamejs/core 0.4.1
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 +230 -0
- package/LICENSE +201 -0
- package/LTS-CALENDAR.md +29 -0
- package/MIGRATING.md +7 -0
- package/NOTICE +59 -0
- package/README.md +100 -0
- package/bin/blamejs.js +13 -0
- package/index.js +253 -0
- package/lib/api-key.js +705 -0
- package/lib/api-snapshot.js +335 -0
- package/lib/app-shutdown.js +381 -0
- package/lib/app.js +364 -0
- package/lib/atomic-file.js +525 -0
- package/lib/audit-chain.js +168 -0
- package/lib/audit-sign.js +319 -0
- package/lib/audit-tools.js +682 -0
- package/lib/audit.js +753 -0
- package/lib/auth/jwt.js +280 -0
- package/lib/auth/oauth.js +691 -0
- package/lib/auth/passkey.js +185 -0
- package/lib/auth/password.js +139 -0
- package/lib/auth/totp.js +17 -0
- package/lib/auth-header.js +81 -0
- package/lib/backup/bundle.js +219 -0
- package/lib/backup/crypto.js +174 -0
- package/lib/backup/index.js +490 -0
- package/lib/backup/manifest.js +275 -0
- package/lib/bundler.js +295 -0
- package/lib/cache.js +819 -0
- package/lib/chain-writer.js +234 -0
- package/lib/cli-helpers.js +201 -0
- package/lib/cli.js +1377 -0
- package/lib/cluster-provider-db.js +245 -0
- package/lib/cluster-storage.js +166 -0
- package/lib/cluster.js +691 -0
- package/lib/consent.js +222 -0
- package/lib/constants.js +186 -0
- package/lib/cookies.js +293 -0
- package/lib/credential-hash.js +303 -0
- package/lib/crypto-field.js +159 -0
- package/lib/crypto.js +250 -0
- package/lib/db-query.js +297 -0
- package/lib/db-schema.js +250 -0
- package/lib/db.js +1054 -0
- package/lib/deprecate.js +226 -0
- package/lib/dev.js +324 -0
- package/lib/error-page.js +424 -0
- package/lib/events.js +135 -0
- package/lib/external-db.js +422 -0
- package/lib/forms.js +378 -0
- package/lib/framework-error.js +189 -0
- package/lib/framework-schema.js +604 -0
- package/lib/handlers.js +350 -0
- package/lib/html-balance.js +227 -0
- package/lib/http-client.js +615 -0
- package/lib/i18n.js +780 -0
- package/lib/jobs.js +181 -0
- package/lib/lazy-require.js +48 -0
- package/lib/log-stream-local.js +137 -0
- package/lib/log-stream-webhook.js +170 -0
- package/lib/log-stream.js +211 -0
- package/lib/log.js +355 -0
- package/lib/mail-bounce.js +507 -0
- package/lib/mail.js +701 -0
- package/lib/metrics.js +647 -0
- package/lib/middleware/api-encrypt.js +553 -0
- package/lib/middleware/attach-user.js +156 -0
- package/lib/middleware/body-parser.js +883 -0
- package/lib/middleware/bot-guard.js +148 -0
- package/lib/middleware/compression.js +436 -0
- package/lib/middleware/cors.js +236 -0
- package/lib/middleware/csp-nonce.js +332 -0
- package/lib/middleware/csrf-protect.js +275 -0
- package/lib/middleware/error-handler.js +46 -0
- package/lib/middleware/health.js +358 -0
- package/lib/middleware/index.js +52 -0
- package/lib/middleware/rate-limit.js +319 -0
- package/lib/middleware/request-id.js +53 -0
- package/lib/middleware/require-auth.js +95 -0
- package/lib/middleware/security-headers.js +91 -0
- package/lib/migrations.js +353 -0
- package/lib/mtls-ca.js +333 -0
- package/lib/mtls-engine-default.js +285 -0
- package/lib/nonce-store.js +177 -0
- package/lib/notify.js +643 -0
- package/lib/ntp-check.js +178 -0
- package/lib/object-store/azure-blob.js +467 -0
- package/lib/object-store/gcs.js +469 -0
- package/lib/object-store/http-put.js +153 -0
- package/lib/object-store/index.js +140 -0
- package/lib/object-store/local.js +163 -0
- package/lib/object-store/retry.js +15 -0
- package/lib/object-store/sigv4.js +535 -0
- package/lib/observability.js +114 -0
- package/lib/pagination.js +371 -0
- package/lib/parsers/index.js +64 -0
- package/lib/parsers/safe-csv.js +224 -0
- package/lib/parsers/safe-env.js +614 -0
- package/lib/parsers/safe-toml.js +745 -0
- package/lib/parsers/safe-xml.js +379 -0
- package/lib/parsers/safe-yaml.js +977 -0
- package/lib/permissions.js +430 -0
- package/lib/pqc-agent.js +85 -0
- package/lib/pqc-gate.js +266 -0
- package/lib/protocol-dispatcher.js +144 -0
- package/lib/queue-local.js +327 -0
- package/lib/queue.js +430 -0
- package/lib/redact.js +192 -0
- package/lib/render.js +193 -0
- package/lib/request-helpers.js +178 -0
- package/lib/restore-bundle.js +239 -0
- package/lib/restore-rollback.js +254 -0
- package/lib/restore.js +301 -0
- package/lib/retry.js +329 -0
- package/lib/router.js +437 -0
- package/lib/safe-async.js +520 -0
- package/lib/safe-buffer.js +162 -0
- package/lib/safe-json.js +532 -0
- package/lib/safe-schema.js +1176 -0
- package/lib/safe-sql.js +157 -0
- package/lib/safe-url.js +109 -0
- package/lib/scheduler.js +680 -0
- package/lib/seeders.js +622 -0
- package/lib/session.js +304 -0
- package/lib/slug.js +243 -0
- package/lib/static.js +268 -0
- package/lib/storage.js +470 -0
- package/lib/subject.js +281 -0
- package/lib/template.js +781 -0
- package/lib/testing.js +621 -0
- package/lib/totp.js +285 -0
- package/lib/tracing.js +484 -0
- package/lib/validate-opts.js +56 -0
- package/lib/vault/index.js +299 -0
- package/lib/vault/passphrase-ops.js +311 -0
- package/lib/vault/passphrase-source.js +198 -0
- package/lib/vault/rotate.js +761 -0
- package/lib/vault/wrap.js +289 -0
- package/lib/vendor/MANIFEST.json +84 -0
- package/lib/vendor/argon2/argon2.cjs +466 -0
- package/lib/vendor/argon2/argon2.d.cts +62 -0
- package/lib/vendor/argon2/package.json +1 -0
- package/lib/vendor/argon2/prebuilds/darwin-arm64/argon2.armv8.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/darwin-x64/argon2.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/freebsd-arm64/argon2.armv8.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/freebsd-x64/argon2.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.musl.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.musl.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-x64/argon2.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-x64/argon2.musl.node +0 -0
- package/lib/vendor/argon2/prebuilds/win32-x64/argon2.glibc.node +0 -0
- package/lib/vendor/noble-ciphers.cjs +9 -0
- package/lib/vendor/pki.cjs +181 -0
- package/lib/vendor/simplewebauthn-server.cjs +328 -0
- package/lib/webhook.js +632 -0
- package/lib/websocket-channels.js +413 -0
- package/lib/websocket.js +833 -0
- package/package.json +39 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Log streaming dispatcher — operational logs to one or many sinks with
|
|
4
|
+
* redaction and bidirectional command channel.
|
|
5
|
+
*
|
|
6
|
+
* Same dispatcher pattern: backends configured per-name with a protocol
|
|
7
|
+
* + protocol-specific options. Built-in protocols are 'local'
|
|
8
|
+
* (append-only file with rotation) and 'webhook' (generic HTTP POST —
|
|
9
|
+
* covers Splunk HEC, Datadog, Sumo Logic, Loki, custom). Specialized
|
|
10
|
+
* adapters (syslog over TLS, OpenTelemetry OTLP, AWS CloudWatch,
|
|
11
|
+
* Vector/Fluentd) are listed as deferred and surface a clear error
|
|
12
|
+
* when selected.
|
|
13
|
+
*
|
|
14
|
+
* Every emit goes through lib/redact.js BEFORE any sink sees it. PHI/PCI
|
|
15
|
+
* never reaches operational logs even on a misconfigured field name —
|
|
16
|
+
* pattern detectors catch credit-card-shaped values, JWTs, PEM blocks,
|
|
17
|
+
* AWS access keys, vault-sealed strings, SSN-shaped values, etc.
|
|
18
|
+
*
|
|
19
|
+
* Bidirectional command channel:
|
|
20
|
+
* logStream.onIncoming(handler) registers a handler for inbound events.
|
|
21
|
+
* Operators wire their HTTP route (or other transport — webhook receiver,
|
|
22
|
+
* SSE, message-queue subscriber) to call logStream.deliverIncoming(payload)
|
|
23
|
+
* which invokes registered handlers. The framework doesn't prescribe the
|
|
24
|
+
* transport — it provides the dispatch.
|
|
25
|
+
*
|
|
26
|
+
* Public API:
|
|
27
|
+
* logStream.init({ sinks: { name: { protocol, ... } }, classification? })
|
|
28
|
+
* logStream.emit(level, message, meta?) (sync — non-blocking)
|
|
29
|
+
* logStream.info(msg, meta?) / .warn / .error / .debug
|
|
30
|
+
* logStream.onIncoming(handler) (handler returns Promise)
|
|
31
|
+
* logStream.deliverIncoming(payload, opts?)
|
|
32
|
+
* logStream.shutdown()
|
|
33
|
+
* logStream.listSinks() → [{ name, protocol, stats }]
|
|
34
|
+
*/
|
|
35
|
+
var localProto = require("./log-stream-local");
|
|
36
|
+
var webhookProto = require("./log-stream-webhook");
|
|
37
|
+
var redactor = require("./redact");
|
|
38
|
+
var lazyRequire = require("./lazy-require");
|
|
39
|
+
var protocolDispatcher = require("./protocol-dispatcher");
|
|
40
|
+
var { LogStreamError } = require("./framework-error");
|
|
41
|
+
|
|
42
|
+
var dispatcher = protocolDispatcher.create({
|
|
43
|
+
name: "log-stream",
|
|
44
|
+
errorClass: LogStreamError,
|
|
45
|
+
protocols: { "local": localProto, "webhook": webhookProto },
|
|
46
|
+
deferred: {
|
|
47
|
+
"syslog": { description: "RFC 5424 syslog over TLS" },
|
|
48
|
+
"otlp": { description: "OpenTelemetry Logs OTLP/HTTP" },
|
|
49
|
+
"cloudwatch": { description: "AWS CloudWatch Logs (PutLogEvents)" },
|
|
50
|
+
},
|
|
51
|
+
fallbackProtocol: "local",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
var LEVELS = ["debug", "info", "warn", "error"];
|
|
55
|
+
var LEVEL_PRIORITY = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
56
|
+
|
|
57
|
+
var _err = LogStreamError.factory;
|
|
58
|
+
|
|
59
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
60
|
+
|
|
61
|
+
var initialized = false;
|
|
62
|
+
var sinks = {};
|
|
63
|
+
var minLevel = "info";
|
|
64
|
+
var incomingHandlers = [];
|
|
65
|
+
|
|
66
|
+
function init(opts) {
|
|
67
|
+
if (initialized) return;
|
|
68
|
+
if (!opts || !opts.sinks) throw new Error("logStream.init({ sinks }) is required");
|
|
69
|
+
|
|
70
|
+
sinks = {};
|
|
71
|
+
for (var name in opts.sinks) {
|
|
72
|
+
var cfg = opts.sinks[name];
|
|
73
|
+
var proto = dispatcher.resolve(cfg.protocol);
|
|
74
|
+
sinks[name] = {
|
|
75
|
+
name: name,
|
|
76
|
+
protocol: cfg.protocol,
|
|
77
|
+
raw: proto.create(cfg),
|
|
78
|
+
levelFilter: cfg.minLevel || null,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
minLevel = (opts.minLevel || "info").toLowerCase();
|
|
83
|
+
initialized = true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function _shouldEmit(level, sinkLevelFilter) {
|
|
87
|
+
var threshold = sinkLevelFilter ? LEVEL_PRIORITY[sinkLevelFilter] : LEVEL_PRIORITY[minLevel];
|
|
88
|
+
return LEVEL_PRIORITY[level] >= threshold;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function emit(level, message, meta) {
|
|
92
|
+
if (!initialized) return;
|
|
93
|
+
if (LEVELS.indexOf(level) === -1) {
|
|
94
|
+
throw _err("INVALID_LEVEL", "log level must be one of " + LEVELS.join(", "), true);
|
|
95
|
+
}
|
|
96
|
+
// Build the record. Redact metadata BEFORE distribution to any sink.
|
|
97
|
+
var record = {
|
|
98
|
+
ts: Date.now(),
|
|
99
|
+
level: level,
|
|
100
|
+
message: message == null ? null : String(message),
|
|
101
|
+
};
|
|
102
|
+
if (meta) {
|
|
103
|
+
record.meta = redactor.redact(meta);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Fire-and-forget to all sinks. Sink errors don't bubble — they're
|
|
107
|
+
// captured by audit (system.log.sink-failure) so an external sink
|
|
108
|
+
// outage doesn't take down the app's request handlers.
|
|
109
|
+
Object.keys(sinks).forEach(function (name) {
|
|
110
|
+
var sink = sinks[name];
|
|
111
|
+
if (!_shouldEmit(level, sink.levelFilter)) return;
|
|
112
|
+
Promise.resolve()
|
|
113
|
+
.then(function () { return sink.raw.emit(record); })
|
|
114
|
+
.catch(function (e) {
|
|
115
|
+
audit().safeEmit({
|
|
116
|
+
action: "system.log.sink-failure",
|
|
117
|
+
outcome: "failure",
|
|
118
|
+
reason: (e && e.message) || String(e),
|
|
119
|
+
metadata: { sink: name, level: level },
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function debug(message, meta) { emit("debug", message, meta); }
|
|
126
|
+
function info(message, meta) { emit("info", message, meta); }
|
|
127
|
+
function warn(message, meta) { emit("warn", message, meta); }
|
|
128
|
+
function error(message, meta) { emit("error", message, meta); }
|
|
129
|
+
|
|
130
|
+
// ---- Bidirectional incoming command channel ----
|
|
131
|
+
|
|
132
|
+
function onIncoming(handler) {
|
|
133
|
+
if (typeof handler !== "function") {
|
|
134
|
+
throw _err("INVALID_HANDLER", "onIncoming requires a function handler", true);
|
|
135
|
+
}
|
|
136
|
+
incomingHandlers.push(handler);
|
|
137
|
+
return function () {
|
|
138
|
+
var idx = incomingHandlers.indexOf(handler);
|
|
139
|
+
if (idx >= 0) incomingHandlers.splice(idx, 1);
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function deliverIncoming(payload, opts) {
|
|
144
|
+
opts = opts || {};
|
|
145
|
+
var redacted = redactor.redact(payload);
|
|
146
|
+
// Audit-log the inbound command BEFORE invoking handlers — even handler
|
|
147
|
+
// exceptions don't lose the receipt.
|
|
148
|
+
audit().safeEmit({
|
|
149
|
+
actor: opts.actor || {},
|
|
150
|
+
action: "system.log.incoming",
|
|
151
|
+
metadata: { payload: redacted, source: opts.source || null },
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
var results = [];
|
|
155
|
+
for (var i = 0; i < incomingHandlers.length; i++) {
|
|
156
|
+
try {
|
|
157
|
+
results.push({ ok: true, value: await incomingHandlers[i](payload, opts) });
|
|
158
|
+
} catch (e) {
|
|
159
|
+
results.push({ ok: false, error: (e && e.message) || String(e) });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return results;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function shutdown() {
|
|
166
|
+
if (!initialized) return;
|
|
167
|
+
for (var name in sinks) {
|
|
168
|
+
try {
|
|
169
|
+
if (typeof sinks[name].raw.close === "function") await sinks[name].raw.close();
|
|
170
|
+
} catch (_e) { /* best effort on shutdown */ }
|
|
171
|
+
}
|
|
172
|
+
sinks = {};
|
|
173
|
+
incomingHandlers = [];
|
|
174
|
+
initialized = false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function listSinks() {
|
|
178
|
+
if (!initialized) return [];
|
|
179
|
+
return Object.keys(sinks).map(function (name) {
|
|
180
|
+
var s = sinks[name];
|
|
181
|
+
var stats = (typeof s.raw.stats === "function") ? s.raw.stats() : null;
|
|
182
|
+
return { name: name, protocol: s.protocol, stats: stats };
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function _resetForTest() {
|
|
187
|
+
Object.keys(sinks).forEach(function (n) {
|
|
188
|
+
try { if (sinks[n].raw.close) sinks[n].raw.close(); } catch (_e) {}
|
|
189
|
+
});
|
|
190
|
+
sinks = {};
|
|
191
|
+
incomingHandlers = [];
|
|
192
|
+
initialized = false;
|
|
193
|
+
audit.reset();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = {
|
|
197
|
+
init: init,
|
|
198
|
+
emit: emit,
|
|
199
|
+
debug: debug,
|
|
200
|
+
info: info,
|
|
201
|
+
warn: warn,
|
|
202
|
+
error: error,
|
|
203
|
+
onIncoming: onIncoming,
|
|
204
|
+
deliverIncoming: deliverIncoming,
|
|
205
|
+
shutdown: shutdown,
|
|
206
|
+
listSinks: listSinks,
|
|
207
|
+
LEVELS: LEVELS,
|
|
208
|
+
PROTOCOLS: dispatcher.protocols,
|
|
209
|
+
DEFERRED_PROTOCOLS: dispatcher.deferred,
|
|
210
|
+
_resetForTest: _resetForTest,
|
|
211
|
+
};
|
package/lib/log.js
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* log — structured JSON application logger with request-id correlation.
|
|
4
|
+
*
|
|
5
|
+
* Distinct concern from lib/logger.js: logger.js is the framework's
|
|
6
|
+
* own boot/operational chatter to console with `[blamejs:<name>] `
|
|
7
|
+
* prefix (humans watching `npm start`). lib/log.js is the app-level
|
|
8
|
+
* structured logger meant to be ingested by a log aggregator.
|
|
9
|
+
*
|
|
10
|
+
* Each line is one JSON object on a single line, terminated with `\n`.
|
|
11
|
+
* Levels: debug (0) < info (1) < warn (2) < error (3) < fatal (4).
|
|
12
|
+
* info-and-below routes to stdout; warn-and-up routes to stderr.
|
|
13
|
+
*
|
|
14
|
+
* var log = b.log.create({
|
|
15
|
+
* level: "info", // env LOG_LEVEL > opts.level > "info"
|
|
16
|
+
* base: { service: "myapp", version: "1.2.3" },
|
|
17
|
+
* redact: true, // run extras through lib/redact
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* log.info("user logged in", { userId: "u-1" });
|
|
21
|
+
* log.error("payment failed", { orderId, err: e.message });
|
|
22
|
+
*
|
|
23
|
+
* // Child with bound context
|
|
24
|
+
* var authLog = log.bind({ component: "auth" });
|
|
25
|
+
* authLog.info("password verified", { userId: "u-1" });
|
|
26
|
+
*
|
|
27
|
+
* // Request correlation via AsyncLocalStorage (Node async context)
|
|
28
|
+
* await log.runWithRequestId("req-abc", async function () {
|
|
29
|
+
* log.info("inside request"); // → ..., "requestId": "req-abc"
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* // Router middleware that allocates a requestId and binds it for
|
|
33
|
+
* // the entire request async chain
|
|
34
|
+
* r.use(log.middleware());
|
|
35
|
+
*
|
|
36
|
+
* Field merge order (last wins):
|
|
37
|
+
* 1. base context from create()
|
|
38
|
+
* 2. bound context from bind() (each ancestor up the chain)
|
|
39
|
+
* 3. requestId from ALS (if set)
|
|
40
|
+
* 4. extra arg from .info(msg, extra)
|
|
41
|
+
* 5. core fields: timestamp, level, message
|
|
42
|
+
*
|
|
43
|
+
* Core fields cannot be overwritten by extras — log.info("hi", { level: "X" })
|
|
44
|
+
* keeps level: "info" in the emitted line, with an _overwriteAttempt
|
|
45
|
+
* flag if the operator tried to clobber.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
var { AsyncLocalStorage } = require("node:async_hooks");
|
|
49
|
+
var redact = require("./redact");
|
|
50
|
+
var validateOpts = require("./validate-opts");
|
|
51
|
+
var { FrameworkError } = require("./framework-error");
|
|
52
|
+
|
|
53
|
+
var LEVELS = { debug: 0, info: 1, warn: 2, error: 3, fatal: 4 };
|
|
54
|
+
var LEVEL_NAMES = Object.keys(LEVELS);
|
|
55
|
+
|
|
56
|
+
class LogError extends FrameworkError {
|
|
57
|
+
constructor(code, message) {
|
|
58
|
+
super(message, code);
|
|
59
|
+
this.name = "LogError";
|
|
60
|
+
this.permanent = true;
|
|
61
|
+
this.isLogError = true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Single ALS shared across all log instances so request-id propagates
|
|
66
|
+
// regardless of which instance emitted the line. Keyed map so
|
|
67
|
+
// operators can attach more than just requestId (e.g. tenantId).
|
|
68
|
+
var _als = new AsyncLocalStorage();
|
|
69
|
+
|
|
70
|
+
function _getStore() { return _als.getStore() || null; }
|
|
71
|
+
|
|
72
|
+
function _normalizeDestination(d, fallback) {
|
|
73
|
+
if (d === "stdout") return process.stdout;
|
|
74
|
+
if (d === "stderr") return process.stderr;
|
|
75
|
+
if (d && typeof d.write === "function") return d;
|
|
76
|
+
if (typeof d === "function") return { write: d };
|
|
77
|
+
if (d === undefined || d === null) return fallback;
|
|
78
|
+
throw new LogError("log/bad-destination",
|
|
79
|
+
"destination must be 'stdout', 'stderr', a stream with .write, or a function");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function _normalizeLevel(level) {
|
|
83
|
+
if (typeof level === "number") {
|
|
84
|
+
if (level < 0 || level > 4 || !Number.isFinite(level)) {
|
|
85
|
+
throw new LogError("log/bad-level", "numeric level must be 0-4");
|
|
86
|
+
}
|
|
87
|
+
return level;
|
|
88
|
+
}
|
|
89
|
+
if (typeof level === "string") {
|
|
90
|
+
if (LEVELS[level] === undefined) {
|
|
91
|
+
throw new LogError("log/bad-level",
|
|
92
|
+
"level must be one of " + LEVEL_NAMES.join(", "));
|
|
93
|
+
}
|
|
94
|
+
return LEVELS[level];
|
|
95
|
+
}
|
|
96
|
+
throw new LogError("log/bad-level", "level must be a string or number");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
var _CORE_FIELDS = ["timestamp", "level", "message", "requestId"];
|
|
100
|
+
|
|
101
|
+
function _mergeExtras(into, extras, redactExtras) {
|
|
102
|
+
if (!extras || typeof extras !== "object") return false;
|
|
103
|
+
var src = redactExtras ? redact.redact(extras) : extras;
|
|
104
|
+
var keys = Object.keys(src);
|
|
105
|
+
var clobberAttempt = false;
|
|
106
|
+
for (var i = 0; i < keys.length; i++) {
|
|
107
|
+
var k = keys[i];
|
|
108
|
+
if (_CORE_FIELDS.indexOf(k) !== -1) {
|
|
109
|
+
// Operator tried to overwrite a core field — preserve the core
|
|
110
|
+
// value but flag it so misconfig surfaces in the line.
|
|
111
|
+
clobberAttempt = true;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
into[k] = src[k];
|
|
115
|
+
}
|
|
116
|
+
return clobberAttempt;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function create(opts) {
|
|
120
|
+
opts = opts || {};
|
|
121
|
+
validateOpts(opts, [
|
|
122
|
+
"level", "destination", "errorDestination",
|
|
123
|
+
"format", "redact", "base", "clock",
|
|
124
|
+
], "b.log");
|
|
125
|
+
|
|
126
|
+
// Resolve initial level: env > opts > default
|
|
127
|
+
var envLevel = process.env.LOG_LEVEL;
|
|
128
|
+
var level;
|
|
129
|
+
if (envLevel && LEVELS[envLevel] !== undefined) {
|
|
130
|
+
level = LEVELS[envLevel];
|
|
131
|
+
} else if (opts.level !== undefined) {
|
|
132
|
+
level = _normalizeLevel(opts.level);
|
|
133
|
+
} else {
|
|
134
|
+
level = LEVELS.info;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
var stdoutDest = _normalizeDestination(opts.destination, process.stdout);
|
|
138
|
+
var stderrDest = _normalizeDestination(opts.errorDestination, process.stderr);
|
|
139
|
+
|
|
140
|
+
var format = opts.format || "json"; // reserved for future formats
|
|
141
|
+
if (format !== "json") {
|
|
142
|
+
throw new LogError("log/bad-format",
|
|
143
|
+
"only 'json' format is supported (got '" + format + "')");
|
|
144
|
+
}
|
|
145
|
+
var redactOn = opts.redact !== false;
|
|
146
|
+
var base = opts.base ? Object.assign({}, opts.base) : {};
|
|
147
|
+
|
|
148
|
+
// Clock injection lets tests pin timestamps deterministically.
|
|
149
|
+
var clock = typeof opts.clock === "function" ? opts.clock : function () { return new Date(); };
|
|
150
|
+
|
|
151
|
+
function _emit(levelName, message, extras, boundChain) {
|
|
152
|
+
if (LEVELS[levelName] < level) return;
|
|
153
|
+
|
|
154
|
+
var entry = {};
|
|
155
|
+
entry.timestamp = clock().toISOString();
|
|
156
|
+
entry.level = levelName;
|
|
157
|
+
entry.message = typeof message === "string" ? message : String(message);
|
|
158
|
+
|
|
159
|
+
// Merge base, then each ancestor's bound context (root → leaf)
|
|
160
|
+
Object.assign(entry, base);
|
|
161
|
+
if (boundChain) {
|
|
162
|
+
for (var i = 0; i < boundChain.length; i++) Object.assign(entry, boundChain[i]);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Request id from ALS — overrides only if not already set by base/bound
|
|
166
|
+
var store = _getStore();
|
|
167
|
+
if (store && store.requestId && entry.requestId === undefined) {
|
|
168
|
+
entry.requestId = store.requestId;
|
|
169
|
+
}
|
|
170
|
+
// Merge any other ALS-bound fields (operator may have set tenantId etc.)
|
|
171
|
+
if (store && store._extra) {
|
|
172
|
+
var ekeys = Object.keys(store._extra);
|
|
173
|
+
for (var j = 0; j < ekeys.length; j++) {
|
|
174
|
+
var ek = ekeys[j];
|
|
175
|
+
if (entry[ek] === undefined) entry[ek] = store._extra[ek];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Re-stamp core fields — entries from base/bound context cannot
|
|
180
|
+
// overwrite timestamp/level/message
|
|
181
|
+
entry.timestamp = clock().toISOString();
|
|
182
|
+
entry.level = levelName;
|
|
183
|
+
entry.message = typeof message === "string" ? message : String(message);
|
|
184
|
+
|
|
185
|
+
var clobbered = _mergeExtras(entry, extras, redactOn);
|
|
186
|
+
if (clobbered) entry._overwriteAttempt = true;
|
|
187
|
+
|
|
188
|
+
var line;
|
|
189
|
+
try { line = JSON.stringify(entry) + "\n"; }
|
|
190
|
+
catch (_e) {
|
|
191
|
+
// Circular ref or non-serializable extra — emit a fallback line.
|
|
192
|
+
line = JSON.stringify({
|
|
193
|
+
timestamp: entry.timestamp,
|
|
194
|
+
level: levelName,
|
|
195
|
+
message: entry.message,
|
|
196
|
+
_logError: "extras not serializable",
|
|
197
|
+
}) + "\n";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
var dest = (LEVELS[levelName] >= LEVELS.error) ? stderrDest : stdoutDest;
|
|
201
|
+
try { dest.write(line); }
|
|
202
|
+
catch (_e) { /* destination write best-effort — never throw out of a log call */ }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function _makeInstance(boundChain) {
|
|
206
|
+
function child(extra) {
|
|
207
|
+
if (!extra || typeof extra !== "object") {
|
|
208
|
+
throw new LogError("log/bad-bind", "bind(extra) requires an object");
|
|
209
|
+
}
|
|
210
|
+
// Preserve frozen ancestor chain; append a copy so callers can
|
|
211
|
+
// mutate their original without affecting the bound logger.
|
|
212
|
+
var nextChain = boundChain.concat([Object.assign({}, extra)]);
|
|
213
|
+
return _makeInstance(nextChain);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function level_in(name) { return LEVELS[name] !== undefined && LEVELS[name] >= level; }
|
|
217
|
+
function setLevel(l) { level = _normalizeLevel(l); }
|
|
218
|
+
function getLevel() { return LEVEL_NAMES[level]; }
|
|
219
|
+
|
|
220
|
+
function debug(msg, extra) { _emit("debug", msg, extra, boundChain); }
|
|
221
|
+
function info(msg, extra) { _emit("info", msg, extra, boundChain); }
|
|
222
|
+
function warn(msg, extra) { _emit("warn", msg, extra, boundChain); }
|
|
223
|
+
function error(msg, extra) { _emit("error", msg, extra, boundChain); }
|
|
224
|
+
function fatal(msg, extra) { _emit("fatal", msg, extra, boundChain); }
|
|
225
|
+
|
|
226
|
+
function runWithRequestId(id, fn) {
|
|
227
|
+
var store = { requestId: id || null, _extra: {} };
|
|
228
|
+
return _als.run(store, fn);
|
|
229
|
+
}
|
|
230
|
+
function runWithContext(ctx, fn) {
|
|
231
|
+
var existing = _getStore();
|
|
232
|
+
var rid = (ctx && ctx.requestId) || (existing && existing.requestId) || null;
|
|
233
|
+
var extra = Object.assign({},
|
|
234
|
+
existing && existing._extra ? existing._extra : {},
|
|
235
|
+
ctx || {});
|
|
236
|
+
delete extra.requestId;
|
|
237
|
+
return _als.run({ requestId: rid, _extra: extra }, fn);
|
|
238
|
+
}
|
|
239
|
+
function getRequestId() {
|
|
240
|
+
var s = _getStore();
|
|
241
|
+
return s ? s.requestId : null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function middleware(mwOpts) {
|
|
245
|
+
mwOpts = mwOpts || {};
|
|
246
|
+
var headerName = (mwOpts.headerName || "x-request-id").toLowerCase();
|
|
247
|
+
var setOnRes = mwOpts.setHeader !== false;
|
|
248
|
+
var generate = typeof mwOpts.generate === "function"
|
|
249
|
+
? mwOpts.generate
|
|
250
|
+
: function () {
|
|
251
|
+
// 16 random hex chars — short, sufficient correlation entropy
|
|
252
|
+
return require("crypto").randomBytes(8).toString("hex");
|
|
253
|
+
};
|
|
254
|
+
return function logRequestIdMiddleware(req, res, next) {
|
|
255
|
+
var inbound = req.headers && req.headers[headerName];
|
|
256
|
+
var id = (typeof inbound === "string" && inbound.length > 0 && inbound.length <= 200)
|
|
257
|
+
? inbound
|
|
258
|
+
: generate();
|
|
259
|
+
// Strip CRLF defensively before reflecting back into a header
|
|
260
|
+
id = String(id).replace(/[\r\n]/g, "");
|
|
261
|
+
req.id = id;
|
|
262
|
+
if (setOnRes && typeof res.setHeader === "function") {
|
|
263
|
+
try { res.setHeader("X-Request-Id", id); } catch (_e) { /* header may be locked */ }
|
|
264
|
+
}
|
|
265
|
+
runWithRequestId(id, function () { next(); });
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
debug: debug,
|
|
271
|
+
info: info,
|
|
272
|
+
warn: warn,
|
|
273
|
+
error: error,
|
|
274
|
+
fatal: fatal,
|
|
275
|
+
bind: child,
|
|
276
|
+
setLevel: setLevel,
|
|
277
|
+
getLevel: getLevel,
|
|
278
|
+
isLevelEnabled: level_in,
|
|
279
|
+
runWithRequestId: runWithRequestId,
|
|
280
|
+
runWithContext: runWithContext,
|
|
281
|
+
getRequestId: getRequestId,
|
|
282
|
+
middleware: middleware,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return _makeInstance([]);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ---- Boot logger ----
|
|
290
|
+
//
|
|
291
|
+
// Framework-internal modules emit human-readable startup chatter
|
|
292
|
+
// during boot ("[blamejs:db] ready", "[blamejs:vault] WARNING: …"),
|
|
293
|
+
// distinct from the structured app-level logger above. The boot
|
|
294
|
+
// channel is TTY-aware:
|
|
295
|
+
//
|
|
296
|
+
// - stdout is a TTY → "[blamejs:<name>] <message>" line
|
|
297
|
+
// - stdout is piped → JSON line { timestamp, level, message,
|
|
298
|
+
// component: <name>, boot: true }
|
|
299
|
+
//
|
|
300
|
+
// This keeps `npm start` readable for humans while letting log
|
|
301
|
+
// aggregators ingest boot chatter as structured records.
|
|
302
|
+
//
|
|
303
|
+
// Returned object is a callable (info path) plus .info / .warn /
|
|
304
|
+
// .error / .prefix members so calls like `log("ready")` and
|
|
305
|
+
// `log.warn("…")` both work.
|
|
306
|
+
function boot(name) {
|
|
307
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
308
|
+
throw new LogError("log/bad-name", "log.boot(name) requires a non-empty name");
|
|
309
|
+
}
|
|
310
|
+
var prefix = "[blamejs:" + name + "] ";
|
|
311
|
+
|
|
312
|
+
function _emit(levelName, msg) {
|
|
313
|
+
// Route through console.{log,error} (not process.{stdout,stderr}.write)
|
|
314
|
+
// so test runners that capture console output behave as operators
|
|
315
|
+
// expect — same rationale as the original lib/logger.js.
|
|
316
|
+
var sink = (LEVELS[levelName] >= LEVELS.warn) ? console.error : console.log;
|
|
317
|
+
var stream = (LEVELS[levelName] >= LEVELS.warn) ? process.stderr : process.stdout;
|
|
318
|
+
var isTty = !!(stream && stream.isTTY);
|
|
319
|
+
if (isTty) {
|
|
320
|
+
sink(prefix + String(msg));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
var entry = {
|
|
324
|
+
timestamp: new Date().toISOString(),
|
|
325
|
+
level: levelName,
|
|
326
|
+
message: String(msg),
|
|
327
|
+
component: name,
|
|
328
|
+
boot: true,
|
|
329
|
+
};
|
|
330
|
+
sink(JSON.stringify(entry));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function info(msg) { _emit("info", msg); }
|
|
334
|
+
function warn(msg) { _emit("warn", msg); }
|
|
335
|
+
function error(msg) { _emit("error", msg); }
|
|
336
|
+
|
|
337
|
+
// The returned function is the info path so `log(msg)` matches the
|
|
338
|
+
// existing call shape across the codebase.
|
|
339
|
+
info.info = info;
|
|
340
|
+
info.warn = warn;
|
|
341
|
+
info.error = error;
|
|
342
|
+
info.prefix = prefix;
|
|
343
|
+
return info;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
module.exports = {
|
|
347
|
+
create: create,
|
|
348
|
+
boot: boot,
|
|
349
|
+
LEVELS: LEVELS,
|
|
350
|
+
LogError: LogError,
|
|
351
|
+
// Module-level helpers for code paths that don't have a logger
|
|
352
|
+
// instance handy but still need to read ALS state.
|
|
353
|
+
getRequestId: function () { var s = _getStore(); return s ? s.requestId : null; },
|
|
354
|
+
runWithRequestId: function (id, fn) { return _als.run({ requestId: id || null, _extra: {} }, fn); },
|
|
355
|
+
};
|