@blamejs/core 0.7.1 → 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.
Files changed (180) hide show
  1. package/CHANGELOG.md +423 -389
  2. package/README.md +150 -149
  3. package/bin/blamejs.js +0 -0
  4. package/index.js +308 -282
  5. package/lib/api-key.js +660 -672
  6. package/lib/api-snapshot.js +338 -338
  7. package/lib/app-shutdown.js +385 -385
  8. package/lib/app.js +365 -365
  9. package/lib/archive.js +250 -250
  10. package/lib/atomic-file.js +544 -544
  11. package/lib/audit-chain.js +177 -177
  12. package/lib/audit-sign.js +344 -344
  13. package/lib/audit-tools.js +677 -677
  14. package/lib/audit.js +766 -766
  15. package/lib/auth/jwt.js +311 -311
  16. package/lib/auth/lockout.js +436 -436
  17. package/lib/auth/oauth.js +721 -721
  18. package/lib/auth/passkey.js +181 -181
  19. package/lib/auth/password.js +594 -594
  20. package/lib/backup/bundle.js +217 -217
  21. package/lib/backup/crypto.js +176 -176
  22. package/lib/backup/index.js +515 -515
  23. package/lib/backup/manifest.js +282 -282
  24. package/lib/break-glass.js +1338 -1338
  25. package/lib/bundler.js +441 -441
  26. package/lib/cache-redis.js +256 -266
  27. package/lib/cache.js +1206 -1211
  28. package/lib/canonical-json.js +115 -115
  29. package/lib/chain-writer.js +234 -234
  30. package/lib/cli-helpers.js +206 -206
  31. package/lib/cli.js +2334 -2334
  32. package/lib/cluster-provider-db.js +317 -317
  33. package/lib/cluster-storage.js +226 -226
  34. package/lib/cluster.js +703 -703
  35. package/lib/codepoint-class.js +196 -0
  36. package/lib/config-drift.js +301 -301
  37. package/lib/consent.js +222 -222
  38. package/lib/constants.js +191 -191
  39. package/lib/cookies.js +315 -315
  40. package/lib/credential-hash.js +322 -322
  41. package/lib/crypto.js +266 -266
  42. package/lib/csv.js +275 -286
  43. package/lib/db-declare-row-policy.js +267 -267
  44. package/lib/db-declare-view.js +420 -421
  45. package/lib/db-query.js +406 -406
  46. package/lib/db-schema.js +319 -319
  47. package/lib/db.js +1288 -1288
  48. package/lib/deprecate.js +222 -222
  49. package/lib/dev.js +335 -335
  50. package/lib/dual-control.js +473 -473
  51. package/lib/error-page.js +420 -420
  52. package/lib/external-db-migrate.js +441 -441
  53. package/lib/external-db.js +1061 -1061
  54. package/lib/file-type.js +273 -273
  55. package/lib/file-upload.js +1136 -0
  56. package/lib/forms.js +422 -422
  57. package/lib/framework-error.js +293 -202
  58. package/lib/framework-schema.js +717 -717
  59. package/lib/gate-contract.js +971 -0
  60. package/lib/guard-all.js +405 -0
  61. package/lib/guard-archive.js +739 -0
  62. package/lib/guard-csv.js +816 -0
  63. package/lib/guard-email.js +744 -0
  64. package/lib/guard-filename.js +724 -0
  65. package/lib/guard-html.js +976 -0
  66. package/lib/guard-json.js +729 -0
  67. package/lib/guard-markdown.js +586 -0
  68. package/lib/guard-svg.js +976 -0
  69. package/lib/guard-xml.js +405 -0
  70. package/lib/guard-yaml.js +529 -0
  71. package/lib/handlers.js +350 -350
  72. package/lib/http-client-cookie-jar.js +508 -508
  73. package/lib/http-client.js +1195 -1195
  74. package/lib/i18n.js +878 -878
  75. package/lib/jobs.js +185 -185
  76. package/lib/log-stream-cloudwatch.js +369 -369
  77. package/lib/log-stream-local.js +146 -146
  78. package/lib/log-stream-otlp-grpc.js +410 -410
  79. package/lib/log-stream-otlp.js +286 -286
  80. package/lib/log-stream-syslog.js +302 -302
  81. package/lib/log-stream-webhook.js +199 -199
  82. package/lib/log-stream.js +330 -330
  83. package/lib/log.js +500 -500
  84. package/lib/mail-bounce.js +528 -528
  85. package/lib/mail-dkim.js +369 -362
  86. package/lib/mail.js +981 -962
  87. package/lib/metrics.js +683 -683
  88. package/lib/middleware/api-encrypt.js +936 -573
  89. package/lib/middleware/attach-user.js +157 -157
  90. package/lib/middleware/body-parser.js +1170 -1091
  91. package/lib/middleware/bot-guard.js +178 -178
  92. package/lib/middleware/compression.js +452 -452
  93. package/lib/middleware/cors.js +314 -314
  94. package/lib/middleware/csp-nonce.js +348 -348
  95. package/lib/middleware/csrf-protect.js +316 -316
  96. package/lib/middleware/db-role-for.js +264 -269
  97. package/lib/middleware/health.js +392 -392
  98. package/lib/middleware/index.js +79 -79
  99. package/lib/middleware/rate-limit.js +358 -358
  100. package/lib/middleware/request-id.js +61 -61
  101. package/lib/middleware/request-log.js +168 -168
  102. package/lib/middleware/require-auth.js +104 -104
  103. package/lib/middleware/security-headers.js +116 -116
  104. package/lib/middleware/sse.js +166 -166
  105. package/lib/migrations.js +383 -383
  106. package/lib/mtls-ca.js +518 -518
  107. package/lib/mtls-engine-default.js +481 -481
  108. package/lib/network-dns.js +632 -632
  109. package/lib/network-heartbeat.js +290 -290
  110. package/lib/network-nts.js +574 -574
  111. package/lib/network-proxy.js +265 -265
  112. package/lib/network-tls.js +328 -328
  113. package/lib/network.js +233 -233
  114. package/lib/notify.js +612 -614
  115. package/lib/ntp-check.js +229 -229
  116. package/lib/numeric-bounds.js +111 -91
  117. package/lib/object-store/azure-blob-bucket-ops.js +349 -349
  118. package/lib/object-store/azure-blob.js +488 -451
  119. package/lib/object-store/gcs-bucket-ops.js +351 -351
  120. package/lib/object-store/gcs.js +519 -479
  121. package/lib/object-store/http-put.js +153 -153
  122. package/lib/object-store/index.js +197 -197
  123. package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
  124. package/lib/object-store/sigv4.js +903 -855
  125. package/lib/observability.js +151 -151
  126. package/lib/otel-export.js +269 -269
  127. package/lib/pagination.js +464 -464
  128. package/lib/parsers/index.js +80 -80
  129. package/lib/parsers/safe-env.js +642 -642
  130. package/lib/parsers/safe-ini.js +292 -292
  131. package/lib/parsers/safe-toml.js +784 -784
  132. package/lib/parsers/safe-xml.js +390 -390
  133. package/lib/parsers/safe-yaml.js +1015 -1015
  134. package/lib/permissions.js +708 -708
  135. package/lib/pqc-agent.js +87 -87
  136. package/lib/pqc-gate.js +279 -279
  137. package/lib/protobuf-encoder.js +190 -190
  138. package/lib/protocol-dispatcher.js +161 -161
  139. package/lib/pubsub-redis.js +167 -177
  140. package/lib/pubsub.js +429 -429
  141. package/lib/queue-local.js +476 -476
  142. package/lib/queue-redis.js +745 -752
  143. package/lib/queue-sqs.js +319 -319
  144. package/lib/queue.js +695 -695
  145. package/lib/redis-client.js +519 -489
  146. package/lib/request-helpers.js +340 -336
  147. package/lib/restore-bundle.js +237 -237
  148. package/lib/restore-rollback.js +259 -259
  149. package/lib/restore.js +409 -409
  150. package/lib/retry.js +376 -376
  151. package/lib/router.js +748 -748
  152. package/lib/safe-async.js +735 -735
  153. package/lib/safe-buffer.js +237 -237
  154. package/lib/safe-json.js +541 -541
  155. package/lib/safe-schema.js +1266 -1266
  156. package/lib/safe-url.js +159 -159
  157. package/lib/scheduler.js +706 -706
  158. package/lib/security-assert.js +373 -373
  159. package/lib/seeders.js +618 -630
  160. package/lib/session.js +478 -478
  161. package/lib/slug.js +269 -269
  162. package/lib/ssrf-guard.js +401 -401
  163. package/lib/static.js +879 -114
  164. package/lib/storage.js +471 -471
  165. package/lib/subject.js +281 -281
  166. package/lib/template.js +791 -791
  167. package/lib/testing.js +798 -798
  168. package/lib/time.js +310 -310
  169. package/lib/totp.js +302 -302
  170. package/lib/tracing.js +494 -494
  171. package/lib/uuid.js +132 -132
  172. package/lib/validate-opts.js +340 -270
  173. package/lib/vault/index.js +308 -308
  174. package/lib/vault/rotate.js +784 -784
  175. package/lib/vault/wrap.js +296 -296
  176. package/lib/vendor/noble-ciphers.cjs +9 -9
  177. package/lib/webhook.js +595 -598
  178. package/lib/websocket.js +1048 -1048
  179. package/package.json +77 -77
  180. package/sbom.cyclonedx.json +7 -7
package/lib/log.js CHANGED
@@ -1,500 +1,500 @@
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
- * Default routing: debug / info / warn → stdout; error / fatal → stderr.
13
- * Multi-sink config (`sinks: [...]`) takes full control of routing.
14
- *
15
- * var log = b.log.create({
16
- * level: "info", // env LOG_LEVEL > opts.level > "info"
17
- * base: { service: "myapp", version: "1.2.3" },
18
- * redact: true, // run extras through lib/redact
19
- * });
20
- *
21
- * // Multi-sink: each sink gets every line at-or-above its own level.
22
- * // Default (no `sinks` opt) splits info-and-below to stdout and
23
- * // warn-and-up to stderr — same as before.
24
- * var log = b.log.create({
25
- * level: "debug",
26
- * sinks: [
27
- * { stream: process.stdout, level: "info" },
28
- * { stream: fs.createWriteStream("./logs/debug.log"), level: "debug" },
29
- * { stream: fs.createWriteStream("./logs/errors.log"), level: "error" },
30
- * ],
31
- * });
32
- * // sinks: [...] is mutually exclusive with destination/errorDestination.
33
- *
34
- * log.info("user logged in", { userId: "u-1" });
35
- * log.error("payment failed", { orderId, err: e.message });
36
- *
37
- * // Child with bound context
38
- * var authLog = log.bind({ component: "auth" });
39
- * authLog.info("password verified", { userId: "u-1" });
40
- *
41
- * // Request correlation via AsyncLocalStorage (Node async context)
42
- * await log.runWithRequestId("req-abc", async function () {
43
- * log.info("inside request"); // → ..., "requestId": "req-abc"
44
- * });
45
- *
46
- * // Router middleware that allocates a requestId and binds it for
47
- * // the entire request async chain
48
- * r.use(log.middleware());
49
- *
50
- * Field merge order (last wins):
51
- * 1. base context from create()
52
- * 2. bound context from bind() (each ancestor up the chain)
53
- * 3. requestId from ALS (if set)
54
- * 4. extra arg from .info(msg, extra)
55
- * 5. core fields: timestamp, level, message
56
- *
57
- * Core fields cannot be overwritten by extras — log.info("hi", { level: "X" })
58
- * keeps level: "info" in the emitted line, with an _overwriteAttempt
59
- * flag if the operator tried to clobber.
60
- */
61
-
62
- var { AsyncLocalStorage } = require("node:async_hooks");
63
- var C = require("./constants");
64
- var lazyRequire = require("./lazy-require");
65
- var redact = require("./redact");
66
- var safeBuffer = require("./safe-buffer");
67
- var validateOpts = require("./validate-opts");
68
- var { FrameworkError } = require("./framework-error");
69
-
70
- // Lazy resolves to break load-order cycles:
71
- // - parsers/safe-env requires log (boot logger for env-read errors).
72
- // log can't require safe-env at top because safe-env's top-level
73
- // `var log = boot("env")` would see log's exports half-built.
74
- // - crypto requires constants + framework-error + vendor; it does NOT
75
- // require log, so a top-of-file require would technically work, but
76
- // keeping crypto lazy keeps log loadable from anywhere without
77
- // pulling the whole crypto bundle into the framework's earliest
78
- // boot path (request-id middleware needs only generateToken).
79
- var safeEnv = lazyRequire(function () { return require("./parsers/safe-env"); });
80
- var crypto = lazyRequire(function () { return require("./crypto"); });
81
-
82
- // Request-id correlation token — 8 bytes hex-encoded (16 chars). Short
83
- // enough to read in a log line, long enough to keep collisions far below
84
- // audible-noise even at 1M req/s sustained. Routed through C.BYTES so
85
- // the file's byte arithmetic has a single source of truth.
86
- var REQUEST_ID_BYTES = C.BYTES.bytes(8);
87
-
88
- var LEVELS = { debug: 0, info: 1, warn: 2, error: 3, fatal: 4 };
89
- var LEVEL_NAMES = Object.keys(LEVELS);
90
-
91
- class LogError extends FrameworkError {
92
- constructor(code, message) {
93
- super(message, code);
94
- this.name = "LogError";
95
- this.permanent = true;
96
- this.isLogError = true;
97
- }
98
- }
99
-
100
- // Single ALS shared across all log instances so request-id propagates
101
- // regardless of which instance emitted the line. Keyed map so
102
- // operators can attach more than just requestId (e.g. tenantId).
103
- var _als = new AsyncLocalStorage();
104
-
105
- function _getStore() { return _als.getStore() || null; }
106
-
107
- function _normalizeDestination(d, fallback) {
108
- if (d === "stdout") return process.stdout;
109
- if (d === "stderr") return process.stderr;
110
- if (d && typeof d.write === "function") return d;
111
- if (typeof d === "function") return { write: d };
112
- if (d === undefined || d === null) return fallback;
113
- throw new LogError("log/bad-destination",
114
- "destination must be 'stdout', 'stderr', a stream with .write, or a function");
115
- }
116
-
117
- function _normalizeLevel(level) {
118
- if (typeof level === "number") {
119
- if (level < 0 || level > 4 || !Number.isFinite(level)) {
120
- throw new LogError("log/bad-level", "numeric level must be 0-4");
121
- }
122
- return level;
123
- }
124
- if (typeof level === "string") {
125
- if (LEVELS[level] === undefined) {
126
- throw new LogError("log/bad-level",
127
- "level must be one of " + LEVEL_NAMES.join(", "));
128
- }
129
- return LEVELS[level];
130
- }
131
- throw new LogError("log/bad-level", "level must be a string or number");
132
- }
133
-
134
- var _CORE_FIELDS = ["timestamp", "level", "message", "requestId"];
135
-
136
- function _mergeExtras(into, extras, redactExtras) {
137
- if (!extras || typeof extras !== "object") return false;
138
- var src = redactExtras ? redact.redact(extras) : extras;
139
- var keys = Object.keys(src);
140
- var clobberAttempt = false;
141
- for (var i = 0; i < keys.length; i++) {
142
- var k = keys[i];
143
- if (_CORE_FIELDS.indexOf(k) !== -1) {
144
- // Operator tried to overwrite a core field — preserve the core
145
- // value but flag it so misconfig surfaces in the line.
146
- clobberAttempt = true;
147
- continue;
148
- }
149
- into[k] = src[k];
150
- }
151
- return clobberAttempt;
152
- }
153
-
154
- function _resolveSinks(opts) {
155
- // Three input shapes — pick exactly one:
156
- // (a) opts.sinks: [{ stream, level }, ...]
157
- // (b) opts.destination + opts.errorDestination (legacy two-sink split)
158
- // (c) neither — defaults to stdout for info-and-below, stderr for warn-and-up
159
- if (Array.isArray(opts.sinks)) {
160
- if (opts.destination !== undefined || opts.errorDestination !== undefined) {
161
- throw new LogError("log/conflicting-sinks",
162
- "log.create: pass either { sinks: [...] } OR { destination, errorDestination }, not both");
163
- }
164
- if (opts.sinks.length === 0) {
165
- throw new LogError("log/no-sinks",
166
- "log.create: sinks: [] would silently drop every line — pass at least one sink");
167
- }
168
- return opts.sinks.map(function (s, i) {
169
- if (!s || typeof s !== "object") {
170
- throw new LogError("log/bad-sink", "sinks[" + i + "]: expected object with { stream, level? }");
171
- }
172
- var allowed = ["stream", "level"];
173
- var keys = Object.keys(s);
174
- for (var j = 0; j < keys.length; j++) {
175
- if (allowed.indexOf(keys[j]) === -1) {
176
- throw new LogError("log/bad-sink",
177
- "sinks[" + i + "]: unknown key '" + keys[j] + "' (allowed: " + allowed.join(", ") + ")");
178
- }
179
- }
180
- var stream = _normalizeDestination(s.stream, null);
181
- if (!stream) {
182
- throw new LogError("log/bad-sink", "sinks[" + i + "]: stream is required");
183
- }
184
- // Per-sink level: missing → no filter beyond the global; present → must be valid.
185
- var minLevel = (s.level === undefined) ? null : _normalizeLevel(s.level);
186
- return { stream: stream, minLevel: minLevel };
187
- });
188
- }
189
- // Legacy / default — synthesize the two-sink split.
190
- var stdoutDest = _normalizeDestination(opts.destination, process.stdout);
191
- var stderrDest = _normalizeDestination(opts.errorDestination, process.stderr);
192
- return [
193
- // Order matters for emit fan-out: stdout sink catches debug-info-warn;
194
- // stderr catches error-and-up. Existing behavior — same boundary.
195
- { stream: stdoutDest, minLevel: null, _maxLevelExclusive: LEVELS.error },
196
- { stream: stderrDest, minLevel: LEVELS.error },
197
- ];
198
- }
199
-
200
- function create(opts) {
201
- opts = opts || {};
202
- validateOpts(opts, [
203
- "level", "destination", "errorDestination", "sinks",
204
- "format", "redact", "base", "clock",
205
- ], "b.log");
206
-
207
- // Resolve initial level: env > opts > default
208
- // safeEnv enforces the size cap + missing/empty handling; LEVELS lookup
209
- // gates an unrecognised value (silently falls through to opts/default
210
- // rather than throwing — bad LOG_LEVEL on the env should not crash boot).
211
- var envLevel = safeEnv().readVar("LOG_LEVEL");
212
- var level;
213
- if (envLevel && LEVELS[envLevel] !== undefined) {
214
- level = LEVELS[envLevel];
215
- } else if (opts.level !== undefined) {
216
- level = _normalizeLevel(opts.level);
217
- } else {
218
- level = LEVELS.info;
219
- }
220
-
221
- var sinks = _resolveSinks(opts);
222
-
223
- var format = opts.format || "json"; // reserved for future formats
224
- if (format !== "json") {
225
- throw new LogError("log/bad-format",
226
- "only 'json' format is supported (got '" + format + "')");
227
- }
228
- var redactOn = opts.redact !== false;
229
- var base = opts.base ? Object.assign({}, opts.base) : {};
230
-
231
- // Clock injection lets tests pin timestamps deterministically.
232
- var clock = typeof opts.clock === "function" ? opts.clock : function () { return new Date(); };
233
-
234
- function _emit(levelName, message, extras, boundChain) {
235
- if (LEVELS[levelName] < level) return;
236
-
237
- var entry = {};
238
- entry.timestamp = clock().toISOString();
239
- entry.level = levelName;
240
- entry.message = typeof message === "string" ? message : String(message);
241
-
242
- // Merge base, then each ancestor's bound context (root → leaf)
243
- Object.assign(entry, base);
244
- if (boundChain) {
245
- for (var i = 0; i < boundChain.length; i++) Object.assign(entry, boundChain[i]);
246
- }
247
-
248
- // Request id from ALS — overrides only if not already set by base/bound
249
- var store = _getStore();
250
- if (store && store.requestId && entry.requestId === undefined) {
251
- entry.requestId = store.requestId;
252
- }
253
- // Merge any other ALS-bound fields (operator may have set tenantId etc.)
254
- if (store && store._extra) {
255
- var ekeys = Object.keys(store._extra);
256
- for (var j = 0; j < ekeys.length; j++) {
257
- var ek = ekeys[j];
258
- if (entry[ek] === undefined) entry[ek] = store._extra[ek];
259
- }
260
- }
261
-
262
- // Re-stamp core fields — entries from base/bound context cannot
263
- // overwrite timestamp/level/message
264
- entry.timestamp = clock().toISOString();
265
- entry.level = levelName;
266
- entry.message = typeof message === "string" ? message : String(message);
267
-
268
- var clobbered = _mergeExtras(entry, extras, redactOn);
269
- if (clobbered) entry._overwriteAttempt = true;
270
-
271
- var line;
272
- try { line = JSON.stringify(entry) + "\n"; }
273
- catch (_e) {
274
- // Circular ref or non-serializable extra — emit a fallback line.
275
- line = JSON.stringify({
276
- timestamp: entry.timestamp,
277
- level: levelName,
278
- message: entry.message,
279
- _logError: "extras not serializable",
280
- }) + "\n";
281
- }
282
-
283
- var lvlNum = LEVELS[levelName];
284
- for (var s = 0; s < sinks.length; s++) {
285
- var sink = sinks[s];
286
- if (sink.minLevel !== null && lvlNum < sink.minLevel) continue;
287
- // Legacy default-sinks split uses an exclusive upper bound so the
288
- // stdout sink catches only info-and-below (warn+ goes to stderr).
289
- if (sink._maxLevelExclusive !== undefined && lvlNum >= sink._maxLevelExclusive) continue;
290
- try { sink.stream.write(line); }
291
- catch (_e) { /* sink write best-effort — never throw out of a log call */ }
292
- }
293
- }
294
-
295
- function _makeInstance(boundChain) {
296
- function child(extra) {
297
- if (!extra || typeof extra !== "object") {
298
- throw new LogError("log/bad-bind", "bind(extra) requires an object");
299
- }
300
- // Preserve frozen ancestor chain; append a copy so callers can
301
- // mutate their original without affecting the bound logger.
302
- var nextChain = boundChain.concat([Object.assign({}, extra)]);
303
- return _makeInstance(nextChain);
304
- }
305
-
306
- function level_in(name) { return LEVELS[name] !== undefined && LEVELS[name] >= level; }
307
- function setLevel(l) { level = _normalizeLevel(l); }
308
- function getLevel() { return LEVEL_NAMES[level]; }
309
-
310
- function debug(msg, extra) { _emit("debug", msg, extra, boundChain); }
311
- function info(msg, extra) { _emit("info", msg, extra, boundChain); }
312
- function warn(msg, extra) { _emit("warn", msg, extra, boundChain); }
313
- function error(msg, extra) { _emit("error", msg, extra, boundChain); }
314
- function fatal(msg, extra) { _emit("fatal", msg, extra, boundChain); }
315
-
316
- function runWithRequestId(id, fn) {
317
- var store = { requestId: id || null, _extra: {} };
318
- return _als.run(store, fn);
319
- }
320
- function runWithContext(ctx, fn) {
321
- var existing = _getStore();
322
- var rid = (ctx && ctx.requestId) || (existing && existing.requestId) || null;
323
- var extra = Object.assign({},
324
- existing && existing._extra ? existing._extra : {},
325
- ctx || {});
326
- delete extra.requestId;
327
- return _als.run({ requestId: rid, _extra: extra }, fn);
328
- }
329
- function getRequestId() {
330
- var s = _getStore();
331
- return s ? s.requestId : null;
332
- }
333
-
334
- function middleware(mwOpts) {
335
- mwOpts = mwOpts || {};
336
- var headerName = (mwOpts.headerName || "x-request-id").toLowerCase();
337
- var setOnRes = mwOpts.setHeader !== false;
338
- var generate = typeof mwOpts.generate === "function"
339
- ? mwOpts.generate
340
- : function () {
341
- // 16 random hex chars — short, sufficient correlation entropy.
342
- // Routes through the framework token primitive so the entropy
343
- // source matches the rest of the codebase.
344
- return crypto().generateToken(REQUEST_ID_BYTES);
345
- };
346
- return function logRequestIdMiddleware(req, res, next) {
347
- var inbound = req.headers && req.headers[headerName];
348
- var id = (typeof inbound === "string" && inbound.length > 0 && inbound.length <= 200)
349
- ? inbound
350
- : generate();
351
- // Strip CRLF defensively before reflecting back into a header
352
- id = safeBuffer.stripCrlf(String(id));
353
- req.id = id;
354
- if (setOnRes && typeof res.setHeader === "function") {
355
- try { res.setHeader("X-Request-Id", id); } catch (_e) { /* header may be locked */ }
356
- }
357
- runWithRequestId(id, function () { next(); });
358
- };
359
- }
360
-
361
- return {
362
- debug: debug,
363
- info: info,
364
- warn: warn,
365
- error: error,
366
- fatal: fatal,
367
- bind: child,
368
- setLevel: setLevel,
369
- getLevel: getLevel,
370
- isLevelEnabled: level_in,
371
- runWithRequestId: runWithRequestId,
372
- runWithContext: runWithContext,
373
- getRequestId: getRequestId,
374
- middleware: middleware,
375
- };
376
- }
377
-
378
- return _makeInstance([]);
379
- }
380
-
381
- // ---- Boot logger ----
382
- //
383
- // Framework-internal modules emit human-readable startup chatter
384
- // during boot ("[blamejs:db] ready", "[blamejs:vault] WARNING: …"),
385
- // distinct from the structured app-level logger above. The boot
386
- // channel is TTY-aware:
387
- //
388
- // - stdout is a TTY → "[blamejs:<name>] <message>" line
389
- // - stdout is piped → JSON line { timestamp, level, message,
390
- // component: <name>, boot: true }
391
- //
392
- // This keeps `npm start` readable for humans while letting log
393
- // aggregators ingest boot chatter as structured records.
394
- //
395
- // Returned object is a callable (info path) plus .info / .warn /
396
- // .error / .prefix members so calls like `log("ready")` and
397
- // `log.warn("…")` both work.
398
- function boot(name) {
399
- if (typeof name !== "string" || name.length === 0) {
400
- throw new LogError("log/bad-name", "log.boot(name) requires a non-empty name");
401
- }
402
- var prefix = "[blamejs:" + name + "] ";
403
-
404
- function _emit(levelName, msg) {
405
- // Route through console.{log,error} (not process.{stdout,stderr}.write)
406
- // so test runners that capture console output behave as operators
407
- // expect — same rationale as the original lib/logger.js.
408
- var sink = (LEVELS[levelName] >= LEVELS.warn) ? console.error : console.log;
409
- var stream = (LEVELS[levelName] >= LEVELS.warn) ? process.stderr : process.stdout;
410
- var isTty = !!(stream && stream.isTTY);
411
- if (isTty) {
412
- sink(prefix + String(msg));
413
- return;
414
- }
415
- var entry = {
416
- timestamp: new Date().toISOString(),
417
- level: levelName,
418
- message: String(msg),
419
- component: name,
420
- boot: true,
421
- };
422
- sink(JSON.stringify(entry));
423
- }
424
-
425
- function debug(msg, fields) {
426
- // Boot-time debug entries route through console.log unless LOG_LEVEL
427
- // suppresses them. fields (when present) get JSON-appended so
428
- // operators see the structured context.
429
- if (LEVELS.debug < _bootMinLevel()) return;
430
- var rendered = msg;
431
- if (fields !== undefined) {
432
- try { rendered = msg + " " + JSON.stringify(fields); }
433
- catch (_e) { rendered = msg; }
434
- }
435
- _emit("debug", rendered);
436
- }
437
- function info(msg) { _emit("info", msg); }
438
- function warn(msg) { _emit("warn", msg); }
439
- function error(msg) { _emit("error", msg); }
440
-
441
- // The returned function is the info path so `log(msg)` matches the
442
- // existing call shape across the codebase.
443
- info.debug = debug;
444
- info.info = info;
445
- info.warn = warn;
446
- info.error = error;
447
- info.prefix = prefix;
448
- return info;
449
- }
450
-
451
- // makeViaOrFallback — closure factory for operator-log routing. Used by
452
- // bundler / dev / error-page / pqc-gate (and similar primitives) that
453
- // accept opts.log but must keep emitting through a per-module fallback
454
- // when the operator didn't pass one. Replaces the per-file
455
- // `function _logVia(log, level, message, fields) { if (log && typeof
456
- // log[level] === "function") { try { log[level](message, fields); }
457
- // catch ... } return; } ... fallback;` boilerplate.
458
- //
459
- // var _logVia = log.makeViaOrFallback(opts.log, log.boot("bundler"));
460
- // _logVia("error", "build-failed", { reason: "..." });
461
- //
462
- // The operator log call is best-effort: a misbehaving log[level] swallows
463
- // internally rather than crash the caller. Fallback is invoked only when
464
- // the operator log is absent or doesn't expose the requested level.
465
- function makeViaOrFallback(operatorLog, fallbackLog) {
466
- return function (level, message, fields) {
467
- if (operatorLog && typeof operatorLog[level] === "function") {
468
- try { operatorLog[level](message, fields); }
469
- catch (_e) { /* operator log best-effort */ }
470
- return;
471
- }
472
- var line = message + (fields ? " " + JSON.stringify(fields) : "");
473
- var fb = (level === "error" || level === "fatal") ? fallbackLog.error
474
- : (level === "warn") ? fallbackLog.warn
475
- : fallbackLog.info;
476
- if (typeof fb === "function") fb(line);
477
- };
478
- }
479
-
480
- // Boot-time minimum level (debug suppressed unless explicitly enabled).
481
- // Uses raw process.env per the documented load-cycle exception: log.js
482
- // runs before safeEnv on the boot path; safeEnv requires log, so log
483
- // can't go through safeEnv to read its own level.
484
- function _bootMinLevel() {
485
- // allow:raw-process-env — see header comment above
486
- var raw = process.env.BLAMEJS_BOOT_LOG_LEVEL || process.env.LOG_LEVEL || "info";
487
- return LEVELS[raw] != null ? LEVELS[raw] : LEVELS.info;
488
- }
489
-
490
- module.exports = {
491
- create: create,
492
- boot: boot,
493
- makeViaOrFallback: makeViaOrFallback,
494
- LEVELS: LEVELS,
495
- LogError: LogError,
496
- // Module-level helpers for code paths that don't have a logger
497
- // instance handy but still need to read ALS state.
498
- getRequestId: function () { var s = _getStore(); return s ? s.requestId : null; },
499
- runWithRequestId: function (id, fn) { return _als.run({ requestId: id || null, _extra: {} }, fn); },
500
- };
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
+ * Default routing: debug / info / warn → stdout; error / fatal → stderr.
13
+ * Multi-sink config (`sinks: [...]`) takes full control of routing.
14
+ *
15
+ * var log = b.log.create({
16
+ * level: "info", // env LOG_LEVEL > opts.level > "info"
17
+ * base: { service: "myapp", version: "1.2.3" },
18
+ * redact: true, // run extras through lib/redact
19
+ * });
20
+ *
21
+ * // Multi-sink: each sink gets every line at-or-above its own level.
22
+ * // Default (no `sinks` opt) splits info-and-below to stdout and
23
+ * // warn-and-up to stderr — same as before.
24
+ * var log = b.log.create({
25
+ * level: "debug",
26
+ * sinks: [
27
+ * { stream: process.stdout, level: "info" },
28
+ * { stream: fs.createWriteStream("./logs/debug.log"), level: "debug" },
29
+ * { stream: fs.createWriteStream("./logs/errors.log"), level: "error" },
30
+ * ],
31
+ * });
32
+ * // sinks: [...] is mutually exclusive with destination/errorDestination.
33
+ *
34
+ * log.info("user logged in", { userId: "u-1" });
35
+ * log.error("payment failed", { orderId, err: e.message });
36
+ *
37
+ * // Child with bound context
38
+ * var authLog = log.bind({ component: "auth" });
39
+ * authLog.info("password verified", { userId: "u-1" });
40
+ *
41
+ * // Request correlation via AsyncLocalStorage (Node async context)
42
+ * await log.runWithRequestId("req-abc", async function () {
43
+ * log.info("inside request"); // → ..., "requestId": "req-abc"
44
+ * });
45
+ *
46
+ * // Router middleware that allocates a requestId and binds it for
47
+ * // the entire request async chain
48
+ * r.use(log.middleware());
49
+ *
50
+ * Field merge order (last wins):
51
+ * 1. base context from create()
52
+ * 2. bound context from bind() (each ancestor up the chain)
53
+ * 3. requestId from ALS (if set)
54
+ * 4. extra arg from .info(msg, extra)
55
+ * 5. core fields: timestamp, level, message
56
+ *
57
+ * Core fields cannot be overwritten by extras — log.info("hi", { level: "X" })
58
+ * keeps level: "info" in the emitted line, with an _overwriteAttempt
59
+ * flag if the operator tried to clobber.
60
+ */
61
+
62
+ var { AsyncLocalStorage } = require("node:async_hooks");
63
+ var C = require("./constants");
64
+ var lazyRequire = require("./lazy-require");
65
+ var redact = require("./redact");
66
+ var safeBuffer = require("./safe-buffer");
67
+ var validateOpts = require("./validate-opts");
68
+ var { FrameworkError } = require("./framework-error");
69
+
70
+ // Lazy resolves to break load-order cycles:
71
+ // - parsers/safe-env requires log (boot logger for env-read errors).
72
+ // log can't require safe-env at top because safe-env's top-level
73
+ // `var log = boot("env")` would see log's exports half-built.
74
+ // - crypto requires constants + framework-error + vendor; it does NOT
75
+ // require log, so a top-of-file require would technically work, but
76
+ // keeping crypto lazy keeps log loadable from anywhere without
77
+ // pulling the whole crypto bundle into the framework's earliest
78
+ // boot path (request-id middleware needs only generateToken).
79
+ var safeEnv = lazyRequire(function () { return require("./parsers/safe-env"); });
80
+ var crypto = lazyRequire(function () { return require("./crypto"); });
81
+
82
+ // Request-id correlation token — 8 bytes hex-encoded (16 chars). Short
83
+ // enough to read in a log line, long enough to keep collisions far below
84
+ // audible-noise even at 1M req/s sustained. Routed through C.BYTES so
85
+ // the file's byte arithmetic has a single source of truth.
86
+ var REQUEST_ID_BYTES = C.BYTES.bytes(8);
87
+
88
+ var LEVELS = { debug: 0, info: 1, warn: 2, error: 3, fatal: 4 };
89
+ var LEVEL_NAMES = Object.keys(LEVELS);
90
+
91
+ class LogError extends FrameworkError {
92
+ constructor(code, message) {
93
+ super(message, code);
94
+ this.name = "LogError";
95
+ this.permanent = true;
96
+ this.isLogError = true;
97
+ }
98
+ }
99
+
100
+ // Single ALS shared across all log instances so request-id propagates
101
+ // regardless of which instance emitted the line. Keyed map so
102
+ // operators can attach more than just requestId (e.g. tenantId).
103
+ var _als = new AsyncLocalStorage();
104
+
105
+ function _getStore() { return _als.getStore() || null; }
106
+
107
+ function _normalizeDestination(d, fallback) {
108
+ if (d === "stdout") return process.stdout;
109
+ if (d === "stderr") return process.stderr;
110
+ if (d && typeof d.write === "function") return d;
111
+ if (typeof d === "function") return { write: d };
112
+ if (d === undefined || d === null) return fallback;
113
+ throw new LogError("log/bad-destination",
114
+ "destination must be 'stdout', 'stderr', a stream with .write, or a function");
115
+ }
116
+
117
+ function _normalizeLevel(level) {
118
+ if (typeof level === "number") {
119
+ if (level < 0 || level > 4 || !Number.isFinite(level)) {
120
+ throw new LogError("log/bad-level", "numeric level must be 0-4");
121
+ }
122
+ return level;
123
+ }
124
+ if (typeof level === "string") {
125
+ if (LEVELS[level] === undefined) {
126
+ throw new LogError("log/bad-level",
127
+ "level must be one of " + LEVEL_NAMES.join(", "));
128
+ }
129
+ return LEVELS[level];
130
+ }
131
+ throw new LogError("log/bad-level", "level must be a string or number");
132
+ }
133
+
134
+ var _CORE_FIELDS = ["timestamp", "level", "message", "requestId"];
135
+
136
+ function _mergeExtras(into, extras, redactExtras) {
137
+ if (!extras || typeof extras !== "object") return false;
138
+ var src = redactExtras ? redact.redact(extras) : extras;
139
+ var keys = Object.keys(src);
140
+ var clobberAttempt = false;
141
+ for (var i = 0; i < keys.length; i++) {
142
+ var k = keys[i];
143
+ if (_CORE_FIELDS.indexOf(k) !== -1) {
144
+ // Operator tried to overwrite a core field — preserve the core
145
+ // value but flag it so misconfig surfaces in the line.
146
+ clobberAttempt = true;
147
+ continue;
148
+ }
149
+ into[k] = src[k];
150
+ }
151
+ return clobberAttempt;
152
+ }
153
+
154
+ function _resolveSinks(opts) {
155
+ // Three input shapes — pick exactly one:
156
+ // (a) opts.sinks: [{ stream, level }, ...]
157
+ // (b) opts.destination + opts.errorDestination (legacy two-sink split)
158
+ // (c) neither — defaults to stdout for info-and-below, stderr for warn-and-up
159
+ if (Array.isArray(opts.sinks)) {
160
+ if (opts.destination !== undefined || opts.errorDestination !== undefined) {
161
+ throw new LogError("log/conflicting-sinks",
162
+ "log.create: pass either { sinks: [...] } OR { destination, errorDestination }, not both");
163
+ }
164
+ if (opts.sinks.length === 0) {
165
+ throw new LogError("log/no-sinks",
166
+ "log.create: sinks: [] would silently drop every line — pass at least one sink");
167
+ }
168
+ return opts.sinks.map(function (s, i) {
169
+ if (!s || typeof s !== "object") {
170
+ throw new LogError("log/bad-sink", "sinks[" + i + "]: expected object with { stream, level? }");
171
+ }
172
+ var allowed = ["stream", "level"];
173
+ var keys = Object.keys(s);
174
+ for (var j = 0; j < keys.length; j++) {
175
+ if (allowed.indexOf(keys[j]) === -1) {
176
+ throw new LogError("log/bad-sink",
177
+ "sinks[" + i + "]: unknown key '" + keys[j] + "' (allowed: " + allowed.join(", ") + ")");
178
+ }
179
+ }
180
+ var stream = _normalizeDestination(s.stream, null);
181
+ if (!stream) {
182
+ throw new LogError("log/bad-sink", "sinks[" + i + "]: stream is required");
183
+ }
184
+ // Per-sink level: missing → no filter beyond the global; present → must be valid.
185
+ var minLevel = (s.level === undefined) ? null : _normalizeLevel(s.level);
186
+ return { stream: stream, minLevel: minLevel };
187
+ });
188
+ }
189
+ // Legacy / default — synthesize the two-sink split.
190
+ var stdoutDest = _normalizeDestination(opts.destination, process.stdout);
191
+ var stderrDest = _normalizeDestination(opts.errorDestination, process.stderr);
192
+ return [
193
+ // Order matters for emit fan-out: stdout sink catches debug-info-warn;
194
+ // stderr catches error-and-up. Existing behavior — same boundary.
195
+ { stream: stdoutDest, minLevel: null, _maxLevelExclusive: LEVELS.error },
196
+ { stream: stderrDest, minLevel: LEVELS.error },
197
+ ];
198
+ }
199
+
200
+ function create(opts) {
201
+ opts = opts || {};
202
+ validateOpts(opts, [
203
+ "level", "destination", "errorDestination", "sinks",
204
+ "format", "redact", "base", "clock",
205
+ ], "b.log");
206
+
207
+ // Resolve initial level: env > opts > default
208
+ // safeEnv enforces the size cap + missing/empty handling; LEVELS lookup
209
+ // gates an unrecognised value (silently falls through to opts/default
210
+ // rather than throwing — bad LOG_LEVEL on the env should not crash boot).
211
+ var envLevel = safeEnv().readVar("LOG_LEVEL");
212
+ var level;
213
+ if (envLevel && LEVELS[envLevel] !== undefined) {
214
+ level = LEVELS[envLevel];
215
+ } else if (opts.level !== undefined) {
216
+ level = _normalizeLevel(opts.level);
217
+ } else {
218
+ level = LEVELS.info;
219
+ }
220
+
221
+ var sinks = _resolveSinks(opts);
222
+
223
+ var format = opts.format || "json"; // reserved for future formats
224
+ if (format !== "json") {
225
+ throw new LogError("log/bad-format",
226
+ "only 'json' format is supported (got '" + format + "')");
227
+ }
228
+ var redactOn = opts.redact !== false;
229
+ var base = opts.base ? Object.assign({}, opts.base) : {};
230
+
231
+ // Clock injection lets tests pin timestamps deterministically.
232
+ var clock = typeof opts.clock === "function" ? opts.clock : function () { return new Date(); };
233
+
234
+ function _emit(levelName, message, extras, boundChain) {
235
+ if (LEVELS[levelName] < level) return;
236
+
237
+ var entry = {};
238
+ entry.timestamp = clock().toISOString();
239
+ entry.level = levelName;
240
+ entry.message = typeof message === "string" ? message : String(message);
241
+
242
+ // Merge base, then each ancestor's bound context (root → leaf)
243
+ Object.assign(entry, base);
244
+ if (boundChain) {
245
+ for (var i = 0; i < boundChain.length; i++) Object.assign(entry, boundChain[i]);
246
+ }
247
+
248
+ // Request id from ALS — overrides only if not already set by base/bound
249
+ var store = _getStore();
250
+ if (store && store.requestId && entry.requestId === undefined) {
251
+ entry.requestId = store.requestId;
252
+ }
253
+ // Merge any other ALS-bound fields (operator may have set tenantId etc.)
254
+ if (store && store._extra) {
255
+ var ekeys = Object.keys(store._extra);
256
+ for (var j = 0; j < ekeys.length; j++) {
257
+ var ek = ekeys[j];
258
+ if (entry[ek] === undefined) entry[ek] = store._extra[ek];
259
+ }
260
+ }
261
+
262
+ // Re-stamp core fields — entries from base/bound context cannot
263
+ // overwrite timestamp/level/message
264
+ entry.timestamp = clock().toISOString();
265
+ entry.level = levelName;
266
+ entry.message = typeof message === "string" ? message : String(message);
267
+
268
+ var clobbered = _mergeExtras(entry, extras, redactOn);
269
+ if (clobbered) entry._overwriteAttempt = true;
270
+
271
+ var line;
272
+ try { line = JSON.stringify(entry) + "\n"; }
273
+ catch (_e) {
274
+ // Circular ref or non-serializable extra — emit a fallback line.
275
+ line = JSON.stringify({
276
+ timestamp: entry.timestamp,
277
+ level: levelName,
278
+ message: entry.message,
279
+ _logError: "extras not serializable",
280
+ }) + "\n";
281
+ }
282
+
283
+ var lvlNum = LEVELS[levelName];
284
+ for (var s = 0; s < sinks.length; s++) {
285
+ var sink = sinks[s];
286
+ if (sink.minLevel !== null && lvlNum < sink.minLevel) continue;
287
+ // Legacy default-sinks split uses an exclusive upper bound so the
288
+ // stdout sink catches only info-and-below (warn+ goes to stderr).
289
+ if (sink._maxLevelExclusive !== undefined && lvlNum >= sink._maxLevelExclusive) continue;
290
+ try { sink.stream.write(line); }
291
+ catch (_e) { /* sink write best-effort — never throw out of a log call */ }
292
+ }
293
+ }
294
+
295
+ function _makeInstance(boundChain) {
296
+ function child(extra) {
297
+ if (!extra || typeof extra !== "object") {
298
+ throw new LogError("log/bad-bind", "bind(extra) requires an object");
299
+ }
300
+ // Preserve frozen ancestor chain; append a copy so callers can
301
+ // mutate their original without affecting the bound logger.
302
+ var nextChain = boundChain.concat([Object.assign({}, extra)]);
303
+ return _makeInstance(nextChain);
304
+ }
305
+
306
+ function level_in(name) { return LEVELS[name] !== undefined && LEVELS[name] >= level; }
307
+ function setLevel(l) { level = _normalizeLevel(l); }
308
+ function getLevel() { return LEVEL_NAMES[level]; }
309
+
310
+ function debug(msg, extra) { _emit("debug", msg, extra, boundChain); }
311
+ function info(msg, extra) { _emit("info", msg, extra, boundChain); }
312
+ function warn(msg, extra) { _emit("warn", msg, extra, boundChain); }
313
+ function error(msg, extra) { _emit("error", msg, extra, boundChain); }
314
+ function fatal(msg, extra) { _emit("fatal", msg, extra, boundChain); }
315
+
316
+ function runWithRequestId(id, fn) {
317
+ var store = { requestId: id || null, _extra: {} };
318
+ return _als.run(store, fn);
319
+ }
320
+ function runWithContext(ctx, fn) {
321
+ var existing = _getStore();
322
+ var rid = (ctx && ctx.requestId) || (existing && existing.requestId) || null;
323
+ var extra = Object.assign({},
324
+ existing && existing._extra ? existing._extra : {},
325
+ ctx || {});
326
+ delete extra.requestId;
327
+ return _als.run({ requestId: rid, _extra: extra }, fn);
328
+ }
329
+ function getRequestId() {
330
+ var s = _getStore();
331
+ return s ? s.requestId : null;
332
+ }
333
+
334
+ function middleware(mwOpts) {
335
+ mwOpts = mwOpts || {};
336
+ var headerName = (mwOpts.headerName || "x-request-id").toLowerCase();
337
+ var setOnRes = mwOpts.setHeader !== false;
338
+ var generate = typeof mwOpts.generate === "function"
339
+ ? mwOpts.generate
340
+ : function () {
341
+ // 16 random hex chars — short, sufficient correlation entropy.
342
+ // Routes through the framework token primitive so the entropy
343
+ // source matches the rest of the codebase.
344
+ return crypto().generateToken(REQUEST_ID_BYTES);
345
+ };
346
+ return function logRequestIdMiddleware(req, res, next) {
347
+ var inbound = req.headers && req.headers[headerName];
348
+ var id = (typeof inbound === "string" && inbound.length > 0 && inbound.length <= 200)
349
+ ? inbound
350
+ : generate();
351
+ // Strip CRLF defensively before reflecting back into a header
352
+ id = safeBuffer.stripCrlf(String(id));
353
+ req.id = id;
354
+ if (setOnRes && typeof res.setHeader === "function") {
355
+ try { res.setHeader("X-Request-Id", id); } catch (_e) { /* header may be locked */ }
356
+ }
357
+ runWithRequestId(id, function () { next(); });
358
+ };
359
+ }
360
+
361
+ return {
362
+ debug: debug,
363
+ info: info,
364
+ warn: warn,
365
+ error: error,
366
+ fatal: fatal,
367
+ bind: child,
368
+ setLevel: setLevel,
369
+ getLevel: getLevel,
370
+ isLevelEnabled: level_in,
371
+ runWithRequestId: runWithRequestId,
372
+ runWithContext: runWithContext,
373
+ getRequestId: getRequestId,
374
+ middleware: middleware,
375
+ };
376
+ }
377
+
378
+ return _makeInstance([]);
379
+ }
380
+
381
+ // ---- Boot logger ----
382
+ //
383
+ // Framework-internal modules emit human-readable startup chatter
384
+ // during boot ("[blamejs:db] ready", "[blamejs:vault] WARNING: …"),
385
+ // distinct from the structured app-level logger above. The boot
386
+ // channel is TTY-aware:
387
+ //
388
+ // - stdout is a TTY → "[blamejs:<name>] <message>" line
389
+ // - stdout is piped → JSON line { timestamp, level, message,
390
+ // component: <name>, boot: true }
391
+ //
392
+ // This keeps `npm start` readable for humans while letting log
393
+ // aggregators ingest boot chatter as structured records.
394
+ //
395
+ // Returned object is a callable (info path) plus .info / .warn /
396
+ // .error / .prefix members so calls like `log("ready")` and
397
+ // `log.warn("…")` both work.
398
+ function boot(name) {
399
+ if (typeof name !== "string" || name.length === 0) {
400
+ throw new LogError("log/bad-name", "log.boot(name) requires a non-empty name");
401
+ }
402
+ var prefix = "[blamejs:" + name + "] ";
403
+
404
+ function _emit(levelName, msg) {
405
+ // Route through console.{log,error} (not process.{stdout,stderr}.write)
406
+ // so test runners that capture console output behave as operators
407
+ // expect — same rationale as the original lib/logger.js.
408
+ var sink = (LEVELS[levelName] >= LEVELS.warn) ? console.error : console.log;
409
+ var stream = (LEVELS[levelName] >= LEVELS.warn) ? process.stderr : process.stdout;
410
+ var isTty = !!(stream && stream.isTTY);
411
+ if (isTty) {
412
+ sink(prefix + String(msg));
413
+ return;
414
+ }
415
+ var entry = {
416
+ timestamp: new Date().toISOString(),
417
+ level: levelName,
418
+ message: String(msg),
419
+ component: name,
420
+ boot: true,
421
+ };
422
+ sink(JSON.stringify(entry));
423
+ }
424
+
425
+ function debug(msg, fields) {
426
+ // Boot-time debug entries route through console.log unless LOG_LEVEL
427
+ // suppresses them. fields (when present) get JSON-appended so
428
+ // operators see the structured context.
429
+ if (LEVELS.debug < _bootMinLevel()) return;
430
+ var rendered = msg;
431
+ if (fields !== undefined) {
432
+ try { rendered = msg + " " + JSON.stringify(fields); }
433
+ catch (_e) { rendered = msg; }
434
+ }
435
+ _emit("debug", rendered);
436
+ }
437
+ function info(msg) { _emit("info", msg); }
438
+ function warn(msg) { _emit("warn", msg); }
439
+ function error(msg) { _emit("error", msg); }
440
+
441
+ // The returned function is the info path so `log(msg)` matches the
442
+ // existing call shape across the codebase.
443
+ info.debug = debug;
444
+ info.info = info;
445
+ info.warn = warn;
446
+ info.error = error;
447
+ info.prefix = prefix;
448
+ return info;
449
+ }
450
+
451
+ // makeViaOrFallback — closure factory for operator-log routing. Used by
452
+ // bundler / dev / error-page / pqc-gate (and similar primitives) that
453
+ // accept opts.log but must keep emitting through a per-module fallback
454
+ // when the operator didn't pass one. Replaces the per-file
455
+ // `function _logVia(log, level, message, fields) { if (log && typeof
456
+ // log[level] === "function") { try { log[level](message, fields); }
457
+ // catch ... } return; } ... fallback;` boilerplate.
458
+ //
459
+ // var _logVia = log.makeViaOrFallback(opts.log, log.boot("bundler"));
460
+ // _logVia("error", "build-failed", { reason: "..." });
461
+ //
462
+ // The operator log call is best-effort: a misbehaving log[level] swallows
463
+ // internally rather than crash the caller. Fallback is invoked only when
464
+ // the operator log is absent or doesn't expose the requested level.
465
+ function makeViaOrFallback(operatorLog, fallbackLog) {
466
+ return function (level, message, fields) {
467
+ if (operatorLog && typeof operatorLog[level] === "function") {
468
+ try { operatorLog[level](message, fields); }
469
+ catch (_e) { /* operator log best-effort */ }
470
+ return;
471
+ }
472
+ var line = message + (fields ? " " + JSON.stringify(fields) : "");
473
+ var fb = (level === "error" || level === "fatal") ? fallbackLog.error
474
+ : (level === "warn") ? fallbackLog.warn
475
+ : fallbackLog.info;
476
+ if (typeof fb === "function") fb(line);
477
+ };
478
+ }
479
+
480
+ // Boot-time minimum level (debug suppressed unless explicitly enabled).
481
+ // Uses raw process.env per the documented load-cycle exception: log.js
482
+ // runs before safeEnv on the boot path; safeEnv requires log, so log
483
+ // can't go through safeEnv to read its own level.
484
+ function _bootMinLevel() {
485
+ // allow:raw-process-env — see header comment above
486
+ var raw = process.env.BLAMEJS_BOOT_LOG_LEVEL || process.env.LOG_LEVEL || "info";
487
+ return LEVELS[raw] != null ? LEVELS[raw] : LEVELS.info;
488
+ }
489
+
490
+ module.exports = {
491
+ create: create,
492
+ boot: boot,
493
+ makeViaOrFallback: makeViaOrFallback,
494
+ LEVELS: LEVELS,
495
+ LogError: LogError,
496
+ // Module-level helpers for code paths that don't have a logger
497
+ // instance handy but still need to read ALS state.
498
+ getRequestId: function () { var s = _getStore(); return s ? s.requestId : null; },
499
+ runWithRequestId: function (id, fn) { return _als.run({ requestId: id || null, _extra: {} }, fn); },
500
+ };