@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.
Files changed (160) hide show
  1. package/CHANGELOG.md +230 -0
  2. package/LICENSE +201 -0
  3. package/LTS-CALENDAR.md +29 -0
  4. package/MIGRATING.md +7 -0
  5. package/NOTICE +59 -0
  6. package/README.md +100 -0
  7. package/bin/blamejs.js +13 -0
  8. package/index.js +253 -0
  9. package/lib/api-key.js +705 -0
  10. package/lib/api-snapshot.js +335 -0
  11. package/lib/app-shutdown.js +381 -0
  12. package/lib/app.js +364 -0
  13. package/lib/atomic-file.js +525 -0
  14. package/lib/audit-chain.js +168 -0
  15. package/lib/audit-sign.js +319 -0
  16. package/lib/audit-tools.js +682 -0
  17. package/lib/audit.js +753 -0
  18. package/lib/auth/jwt.js +280 -0
  19. package/lib/auth/oauth.js +691 -0
  20. package/lib/auth/passkey.js +185 -0
  21. package/lib/auth/password.js +139 -0
  22. package/lib/auth/totp.js +17 -0
  23. package/lib/auth-header.js +81 -0
  24. package/lib/backup/bundle.js +219 -0
  25. package/lib/backup/crypto.js +174 -0
  26. package/lib/backup/index.js +490 -0
  27. package/lib/backup/manifest.js +275 -0
  28. package/lib/bundler.js +295 -0
  29. package/lib/cache.js +819 -0
  30. package/lib/chain-writer.js +234 -0
  31. package/lib/cli-helpers.js +201 -0
  32. package/lib/cli.js +1377 -0
  33. package/lib/cluster-provider-db.js +245 -0
  34. package/lib/cluster-storage.js +166 -0
  35. package/lib/cluster.js +691 -0
  36. package/lib/consent.js +222 -0
  37. package/lib/constants.js +186 -0
  38. package/lib/cookies.js +293 -0
  39. package/lib/credential-hash.js +303 -0
  40. package/lib/crypto-field.js +159 -0
  41. package/lib/crypto.js +250 -0
  42. package/lib/db-query.js +297 -0
  43. package/lib/db-schema.js +250 -0
  44. package/lib/db.js +1054 -0
  45. package/lib/deprecate.js +226 -0
  46. package/lib/dev.js +324 -0
  47. package/lib/error-page.js +424 -0
  48. package/lib/events.js +135 -0
  49. package/lib/external-db.js +422 -0
  50. package/lib/forms.js +378 -0
  51. package/lib/framework-error.js +189 -0
  52. package/lib/framework-schema.js +604 -0
  53. package/lib/handlers.js +350 -0
  54. package/lib/html-balance.js +227 -0
  55. package/lib/http-client.js +615 -0
  56. package/lib/i18n.js +780 -0
  57. package/lib/jobs.js +181 -0
  58. package/lib/lazy-require.js +48 -0
  59. package/lib/log-stream-local.js +137 -0
  60. package/lib/log-stream-webhook.js +170 -0
  61. package/lib/log-stream.js +211 -0
  62. package/lib/log.js +355 -0
  63. package/lib/mail-bounce.js +507 -0
  64. package/lib/mail.js +701 -0
  65. package/lib/metrics.js +647 -0
  66. package/lib/middleware/api-encrypt.js +553 -0
  67. package/lib/middleware/attach-user.js +156 -0
  68. package/lib/middleware/body-parser.js +883 -0
  69. package/lib/middleware/bot-guard.js +148 -0
  70. package/lib/middleware/compression.js +436 -0
  71. package/lib/middleware/cors.js +236 -0
  72. package/lib/middleware/csp-nonce.js +332 -0
  73. package/lib/middleware/csrf-protect.js +275 -0
  74. package/lib/middleware/error-handler.js +46 -0
  75. package/lib/middleware/health.js +358 -0
  76. package/lib/middleware/index.js +52 -0
  77. package/lib/middleware/rate-limit.js +319 -0
  78. package/lib/middleware/request-id.js +53 -0
  79. package/lib/middleware/require-auth.js +95 -0
  80. package/lib/middleware/security-headers.js +91 -0
  81. package/lib/migrations.js +353 -0
  82. package/lib/mtls-ca.js +333 -0
  83. package/lib/mtls-engine-default.js +285 -0
  84. package/lib/nonce-store.js +177 -0
  85. package/lib/notify.js +643 -0
  86. package/lib/ntp-check.js +178 -0
  87. package/lib/object-store/azure-blob.js +467 -0
  88. package/lib/object-store/gcs.js +469 -0
  89. package/lib/object-store/http-put.js +153 -0
  90. package/lib/object-store/index.js +140 -0
  91. package/lib/object-store/local.js +163 -0
  92. package/lib/object-store/retry.js +15 -0
  93. package/lib/object-store/sigv4.js +535 -0
  94. package/lib/observability.js +114 -0
  95. package/lib/pagination.js +371 -0
  96. package/lib/parsers/index.js +64 -0
  97. package/lib/parsers/safe-csv.js +224 -0
  98. package/lib/parsers/safe-env.js +614 -0
  99. package/lib/parsers/safe-toml.js +745 -0
  100. package/lib/parsers/safe-xml.js +379 -0
  101. package/lib/parsers/safe-yaml.js +977 -0
  102. package/lib/permissions.js +430 -0
  103. package/lib/pqc-agent.js +85 -0
  104. package/lib/pqc-gate.js +266 -0
  105. package/lib/protocol-dispatcher.js +144 -0
  106. package/lib/queue-local.js +327 -0
  107. package/lib/queue.js +430 -0
  108. package/lib/redact.js +192 -0
  109. package/lib/render.js +193 -0
  110. package/lib/request-helpers.js +178 -0
  111. package/lib/restore-bundle.js +239 -0
  112. package/lib/restore-rollback.js +254 -0
  113. package/lib/restore.js +301 -0
  114. package/lib/retry.js +329 -0
  115. package/lib/router.js +437 -0
  116. package/lib/safe-async.js +520 -0
  117. package/lib/safe-buffer.js +162 -0
  118. package/lib/safe-json.js +532 -0
  119. package/lib/safe-schema.js +1176 -0
  120. package/lib/safe-sql.js +157 -0
  121. package/lib/safe-url.js +109 -0
  122. package/lib/scheduler.js +680 -0
  123. package/lib/seeders.js +622 -0
  124. package/lib/session.js +304 -0
  125. package/lib/slug.js +243 -0
  126. package/lib/static.js +268 -0
  127. package/lib/storage.js +470 -0
  128. package/lib/subject.js +281 -0
  129. package/lib/template.js +781 -0
  130. package/lib/testing.js +621 -0
  131. package/lib/totp.js +285 -0
  132. package/lib/tracing.js +484 -0
  133. package/lib/validate-opts.js +56 -0
  134. package/lib/vault/index.js +299 -0
  135. package/lib/vault/passphrase-ops.js +311 -0
  136. package/lib/vault/passphrase-source.js +198 -0
  137. package/lib/vault/rotate.js +761 -0
  138. package/lib/vault/wrap.js +289 -0
  139. package/lib/vendor/MANIFEST.json +84 -0
  140. package/lib/vendor/argon2/argon2.cjs +466 -0
  141. package/lib/vendor/argon2/argon2.d.cts +62 -0
  142. package/lib/vendor/argon2/package.json +1 -0
  143. package/lib/vendor/argon2/prebuilds/darwin-arm64/argon2.armv8.glibc.node +0 -0
  144. package/lib/vendor/argon2/prebuilds/darwin-x64/argon2.glibc.node +0 -0
  145. package/lib/vendor/argon2/prebuilds/freebsd-arm64/argon2.armv8.glibc.node +0 -0
  146. package/lib/vendor/argon2/prebuilds/freebsd-x64/argon2.glibc.node +0 -0
  147. package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.glibc.node +0 -0
  148. package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.musl.node +0 -0
  149. package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.glibc.node +0 -0
  150. package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.musl.node +0 -0
  151. package/lib/vendor/argon2/prebuilds/linux-x64/argon2.glibc.node +0 -0
  152. package/lib/vendor/argon2/prebuilds/linux-x64/argon2.musl.node +0 -0
  153. package/lib/vendor/argon2/prebuilds/win32-x64/argon2.glibc.node +0 -0
  154. package/lib/vendor/noble-ciphers.cjs +9 -0
  155. package/lib/vendor/pki.cjs +181 -0
  156. package/lib/vendor/simplewebauthn-server.cjs +328 -0
  157. package/lib/webhook.js +632 -0
  158. package/lib/websocket-channels.js +413 -0
  159. package/lib/websocket.js +833 -0
  160. 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
+ };