@blamejs/core 0.7.18 → 0.7.20

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 (169) hide show
  1. package/CHANGELOG.md +427 -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 +350 -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 +399 -316
  84. package/lib/middleware/db-role-for.js +264 -264
  85. package/lib/middleware/fetch-metadata.js +129 -0
  86. package/lib/middleware/health.js +392 -392
  87. package/lib/middleware/index.js +85 -79
  88. package/lib/middleware/rate-limit.js +358 -358
  89. package/lib/middleware/request-id.js +61 -61
  90. package/lib/middleware/request-log.js +168 -168
  91. package/lib/middleware/require-auth.js +104 -104
  92. package/lib/middleware/security-headers.js +121 -116
  93. package/lib/middleware/sse.js +166 -166
  94. package/lib/migrations.js +383 -383
  95. package/lib/mtls-ca.js +518 -518
  96. package/lib/mtls-engine-default.js +481 -481
  97. package/lib/network-dns.js +632 -632
  98. package/lib/network-heartbeat.js +290 -290
  99. package/lib/network-nts.js +574 -574
  100. package/lib/network-proxy.js +265 -265
  101. package/lib/network-tls.js +328 -328
  102. package/lib/network.js +233 -233
  103. package/lib/notify.js +612 -612
  104. package/lib/ntp-check.js +229 -229
  105. package/lib/numeric-bounds.js +111 -111
  106. package/lib/object-store/azure-blob-bucket-ops.js +349 -349
  107. package/lib/object-store/azure-blob.js +488 -488
  108. package/lib/object-store/gcs-bucket-ops.js +351 -351
  109. package/lib/object-store/gcs.js +519 -519
  110. package/lib/object-store/http-put.js +153 -153
  111. package/lib/object-store/index.js +197 -197
  112. package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
  113. package/lib/object-store/sigv4.js +903 -903
  114. package/lib/observability.js +151 -151
  115. package/lib/otel-export.js +269 -269
  116. package/lib/pagination.js +464 -464
  117. package/lib/parsers/index.js +80 -80
  118. package/lib/parsers/safe-env.js +642 -642
  119. package/lib/parsers/safe-ini.js +292 -292
  120. package/lib/parsers/safe-toml.js +784 -784
  121. package/lib/parsers/safe-xml.js +390 -390
  122. package/lib/parsers/safe-yaml.js +1015 -1015
  123. package/lib/permissions.js +708 -708
  124. package/lib/pqc-agent.js +87 -87
  125. package/lib/pqc-gate.js +279 -279
  126. package/lib/protobuf-encoder.js +190 -190
  127. package/lib/protocol-dispatcher.js +161 -161
  128. package/lib/pubsub-redis.js +167 -167
  129. package/lib/pubsub.js +429 -429
  130. package/lib/queue-local.js +476 -476
  131. package/lib/queue-redis.js +745 -745
  132. package/lib/queue-sqs.js +319 -319
  133. package/lib/queue.js +695 -695
  134. package/lib/redis-client.js +519 -519
  135. package/lib/request-helpers.js +340 -340
  136. package/lib/restore-bundle.js +237 -237
  137. package/lib/restore-rollback.js +259 -259
  138. package/lib/restore.js +409 -409
  139. package/lib/retry.js +376 -376
  140. package/lib/router.js +748 -748
  141. package/lib/safe-async.js +735 -735
  142. package/lib/safe-buffer.js +237 -237
  143. package/lib/safe-json.js +541 -541
  144. package/lib/safe-schema.js +1266 -1266
  145. package/lib/safe-url.js +159 -159
  146. package/lib/scheduler.js +706 -706
  147. package/lib/security-assert.js +373 -373
  148. package/lib/seeders.js +618 -618
  149. package/lib/session.js +535 -478
  150. package/lib/slug.js +269 -269
  151. package/lib/ssrf-guard.js +401 -401
  152. package/lib/static.js +7 -5
  153. package/lib/storage.js +471 -471
  154. package/lib/subject.js +281 -281
  155. package/lib/template.js +791 -791
  156. package/lib/testing.js +798 -798
  157. package/lib/time.js +310 -310
  158. package/lib/totp.js +302 -302
  159. package/lib/tracing.js +494 -494
  160. package/lib/uuid.js +132 -132
  161. package/lib/validate-opts.js +340 -340
  162. package/lib/vault/index.js +308 -308
  163. package/lib/vault/rotate.js +784 -784
  164. package/lib/vault/wrap.js +296 -296
  165. package/lib/vendor/noble-ciphers.cjs +9 -9
  166. package/lib/webhook.js +595 -595
  167. package/lib/websocket.js +1048 -1048
  168. package/package.json +77 -77
  169. 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
+ };