@blamejs/core 0.7.18 → 0.7.19

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 (167) hide show
  1. package/CHANGELOG.md +425 -423
  2. package/README.md +150 -150
  3. package/bin/blamejs.js +0 -0
  4. package/index.js +310 -308
  5. package/lib/api-key.js +660 -660
  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-external.js +365 -0
  16. package/lib/auth/jwt.js +337 -311
  17. package/lib/auth/lockout.js +436 -436
  18. package/lib/auth/oauth.js +721 -721
  19. package/lib/auth/passkey.js +181 -181
  20. package/lib/auth/password.js +628 -594
  21. package/lib/backup/bundle.js +217 -217
  22. package/lib/backup/crypto.js +176 -176
  23. package/lib/backup/index.js +515 -515
  24. package/lib/backup/manifest.js +282 -282
  25. package/lib/break-glass.js +1338 -1338
  26. package/lib/bundler.js +441 -441
  27. package/lib/cache-redis.js +256 -256
  28. package/lib/cache.js +1206 -1206
  29. package/lib/canonical-json.js +115 -115
  30. package/lib/chain-writer.js +234 -234
  31. package/lib/cli-helpers.js +206 -206
  32. package/lib/cli.js +2334 -2334
  33. package/lib/cluster-provider-db.js +317 -317
  34. package/lib/cluster-storage.js +226 -226
  35. package/lib/cluster.js +703 -703
  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 -275
  43. package/lib/db-declare-row-policy.js +267 -267
  44. package/lib/db-declare-view.js +420 -420
  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/forms.js +422 -422
  56. package/lib/framework-error.js +293 -293
  57. package/lib/framework-schema.js +717 -717
  58. package/lib/handlers.js +350 -350
  59. package/lib/http-client-cookie-jar.js +508 -508
  60. package/lib/http-client.js +1195 -1195
  61. package/lib/i18n.js +878 -878
  62. package/lib/jobs.js +185 -185
  63. package/lib/log-stream-cloudwatch.js +369 -369
  64. package/lib/log-stream-local.js +146 -146
  65. package/lib/log-stream-otlp-grpc.js +410 -410
  66. package/lib/log-stream-otlp.js +286 -286
  67. package/lib/log-stream-syslog.js +302 -302
  68. package/lib/log-stream-webhook.js +199 -199
  69. package/lib/log-stream.js +330 -330
  70. package/lib/log.js +500 -500
  71. package/lib/mail-bounce.js +528 -528
  72. package/lib/mail-dkim.js +369 -369
  73. package/lib/mail.js +981 -981
  74. package/lib/metrics.js +683 -683
  75. package/lib/middleware/api-encrypt.js +936 -936
  76. package/lib/middleware/attach-user.js +157 -157
  77. package/lib/middleware/bearer-auth.js +152 -0
  78. package/lib/middleware/body-parser.js +1170 -1170
  79. package/lib/middleware/bot-guard.js +178 -178
  80. package/lib/middleware/compression.js +452 -452
  81. package/lib/middleware/cors.js +314 -314
  82. package/lib/middleware/csp-nonce.js +348 -348
  83. package/lib/middleware/csrf-protect.js +316 -316
  84. package/lib/middleware/db-role-for.js +264 -264
  85. package/lib/middleware/health.js +392 -392
  86. package/lib/middleware/index.js +82 -79
  87. package/lib/middleware/rate-limit.js +358 -358
  88. package/lib/middleware/request-id.js +61 -61
  89. package/lib/middleware/request-log.js +168 -168
  90. package/lib/middleware/require-auth.js +104 -104
  91. package/lib/middleware/security-headers.js +116 -116
  92. package/lib/middleware/sse.js +166 -166
  93. package/lib/migrations.js +383 -383
  94. package/lib/mtls-ca.js +518 -518
  95. package/lib/mtls-engine-default.js +481 -481
  96. package/lib/network-dns.js +632 -632
  97. package/lib/network-heartbeat.js +290 -290
  98. package/lib/network-nts.js +574 -574
  99. package/lib/network-proxy.js +265 -265
  100. package/lib/network-tls.js +328 -328
  101. package/lib/network.js +233 -233
  102. package/lib/notify.js +612 -612
  103. package/lib/ntp-check.js +229 -229
  104. package/lib/numeric-bounds.js +111 -111
  105. package/lib/object-store/azure-blob-bucket-ops.js +349 -349
  106. package/lib/object-store/azure-blob.js +488 -488
  107. package/lib/object-store/gcs-bucket-ops.js +351 -351
  108. package/lib/object-store/gcs.js +519 -519
  109. package/lib/object-store/http-put.js +153 -153
  110. package/lib/object-store/index.js +197 -197
  111. package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
  112. package/lib/object-store/sigv4.js +903 -903
  113. package/lib/observability.js +151 -151
  114. package/lib/otel-export.js +269 -269
  115. package/lib/pagination.js +464 -464
  116. package/lib/parsers/index.js +80 -80
  117. package/lib/parsers/safe-env.js +642 -642
  118. package/lib/parsers/safe-ini.js +292 -292
  119. package/lib/parsers/safe-toml.js +784 -784
  120. package/lib/parsers/safe-xml.js +390 -390
  121. package/lib/parsers/safe-yaml.js +1015 -1015
  122. package/lib/permissions.js +708 -708
  123. package/lib/pqc-agent.js +87 -87
  124. package/lib/pqc-gate.js +279 -279
  125. package/lib/protobuf-encoder.js +190 -190
  126. package/lib/protocol-dispatcher.js +161 -161
  127. package/lib/pubsub-redis.js +167 -167
  128. package/lib/pubsub.js +429 -429
  129. package/lib/queue-local.js +476 -476
  130. package/lib/queue-redis.js +745 -745
  131. package/lib/queue-sqs.js +319 -319
  132. package/lib/queue.js +695 -695
  133. package/lib/redis-client.js +519 -519
  134. package/lib/request-helpers.js +340 -340
  135. package/lib/restore-bundle.js +237 -237
  136. package/lib/restore-rollback.js +259 -259
  137. package/lib/restore.js +409 -409
  138. package/lib/retry.js +376 -376
  139. package/lib/router.js +748 -748
  140. package/lib/safe-async.js +735 -735
  141. package/lib/safe-buffer.js +237 -237
  142. package/lib/safe-json.js +541 -541
  143. package/lib/safe-schema.js +1266 -1266
  144. package/lib/safe-url.js +159 -159
  145. package/lib/scheduler.js +706 -706
  146. package/lib/security-assert.js +373 -373
  147. package/lib/seeders.js +618 -618
  148. package/lib/session.js +535 -478
  149. package/lib/slug.js +269 -269
  150. package/lib/ssrf-guard.js +401 -401
  151. package/lib/storage.js +471 -471
  152. package/lib/subject.js +281 -281
  153. package/lib/template.js +791 -791
  154. package/lib/testing.js +798 -798
  155. package/lib/time.js +310 -310
  156. package/lib/totp.js +302 -302
  157. package/lib/tracing.js +494 -494
  158. package/lib/uuid.js +132 -132
  159. package/lib/validate-opts.js +340 -340
  160. package/lib/vault/index.js +308 -308
  161. package/lib/vault/rotate.js +784 -784
  162. package/lib/vault/wrap.js +296 -296
  163. package/lib/vendor/noble-ciphers.cjs +9 -9
  164. package/lib/webhook.js +595 -595
  165. package/lib/websocket.js +1048 -1048
  166. package/package.json +77 -77
  167. 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
+ };