@blamejs/core 0.8.43 → 0.8.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +93 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- package/sbom.cyclonedx.json +7 -7
package/lib/log.js
CHANGED
|
@@ -1,62 +1,57 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* @module b.log
|
|
4
|
+
* @featured true
|
|
5
|
+
* @nav Observability
|
|
6
|
+
* @title Log
|
|
4
7
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
8
|
+
* @intro
|
|
9
|
+
* Structured JSON application logger meant to be ingested by a log
|
|
10
|
+
* aggregator. Each emitted line is a single JSON object terminated
|
|
11
|
+
* with `\n`; the log level is encoded as the string field `level`,
|
|
12
|
+
* not as console color. Distinct from `b.log.boot` — that path is
|
|
13
|
+
* framework-internal startup chatter to the TTY (humans watching
|
|
14
|
+
* `npm start`); `create()` is what apps wire into their request
|
|
15
|
+
* lifecycle.
|
|
9
16
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
17
|
+
* Levels: debug (0) < info (1) < warn (2) < error (3) < fatal (4).
|
|
18
|
+
* Default routing: debug / info / warn → stdout; error / fatal →
|
|
19
|
+
* stderr. Multi-sink config (`sinks: [...]`) takes full control of
|
|
20
|
+
* routing — each sink gets every line at-or-above its own per-sink
|
|
21
|
+
* level, useful when the operator wants debug to a file but warn+
|
|
22
|
+
* to stderr.
|
|
14
23
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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.
|
|
24
|
+
* Redact-aware: `extras` passed to `.info(msg, extras)` flow through
|
|
25
|
+
* `b.redact` by default, so password / token / cardNumber-shaped
|
|
26
|
+
* keys never reach the log line. Operators opt out with
|
|
27
|
+
* `redact: false` only when the logger sits behind a downstream
|
|
28
|
+
* redactor.
|
|
33
29
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
30
|
+
* Request correlation rides on Node's AsyncLocalStorage. The
|
|
31
|
+
* middleware allocates a `requestId` (or honors an inbound
|
|
32
|
+
* `X-Request-Id` header) and binds it for the entire async chain;
|
|
33
|
+
* every `log.info` inside the request automatically picks up the
|
|
34
|
+
* id without the caller threading it explicitly. OpenTelemetry
|
|
35
|
+
* trace correlation rides the same channel — `runWithContext`
|
|
36
|
+
* merges arbitrary fields (tenantId, traceId) into the bound store.
|
|
36
37
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
38
|
+
* Child loggers via `log.bind({ component: "auth" })` carry the
|
|
39
|
+
* bound fields into every emitted line; chains compose, so an
|
|
40
|
+
* auth-handler logger can bind its own `userId` on top.
|
|
40
41
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* Field merge order (last wins): base context → bound chain → ALS
|
|
43
|
+
* store → caller's extras → core fields (timestamp / level /
|
|
44
|
+
* message). Extras that try to clobber a core field are dropped
|
|
45
|
+
* and the line carries `_overwriteAttempt: true` so misconfig is
|
|
46
|
+
* visible.
|
|
45
47
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
48
|
+
* Trojan-Source defense (CVE-2021-42574) is baked in: Unicode
|
|
49
|
+
* bidi / format controls in messages are escaped to `\uXXXX`
|
|
50
|
+
* literals before they reach the wire so a hostile message can't
|
|
51
|
+
* re-order the visible line in a TTY / syslog reader.
|
|
49
52
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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.
|
|
53
|
+
* @card
|
|
54
|
+
* Structured JSON application logger meant to be ingested by a log aggregator.
|
|
60
55
|
*/
|
|
61
56
|
|
|
62
57
|
var { AsyncLocalStorage } = require("node:async_hooks");
|
|
@@ -197,6 +192,46 @@ function _resolveSinks(opts) {
|
|
|
197
192
|
];
|
|
198
193
|
}
|
|
199
194
|
|
|
195
|
+
/**
|
|
196
|
+
* @primitive b.log.create
|
|
197
|
+
* @signature b.log.create(opts)
|
|
198
|
+
* @since 0.1.70
|
|
199
|
+
* @status stable
|
|
200
|
+
* @related b.log.boot, b.log.makeViaOrFallback, b.redact.redact
|
|
201
|
+
*
|
|
202
|
+
* Build a structured JSON logger instance. Returns an object with
|
|
203
|
+
* `.debug` / `.info` / `.warn` / `.error` / `.fatal` emitters, plus
|
|
204
|
+
* `.bind(extra)` for child loggers, `.middleware()` for router-side
|
|
205
|
+
* request-id binding, `.runWithRequestId(id, fn)` /
|
|
206
|
+
* `.runWithContext(ctx, fn)` for ad-hoc AsyncLocalStorage scopes,
|
|
207
|
+
* and `.setLevel` / `.getLevel` / `.isLevelEnabled` for runtime
|
|
208
|
+
* level control. Level resolution is `LOG_LEVEL` env > `opts.level`
|
|
209
|
+
* > `"info"`.
|
|
210
|
+
*
|
|
211
|
+
* @opts
|
|
212
|
+
* level: "info", // string or 0-4
|
|
213
|
+
* base: { service: "myapp" }, // merged into every line
|
|
214
|
+
* redact: true, // run extras through b.redact
|
|
215
|
+
* sinks: [
|
|
216
|
+
* { stream: process.stdout, level: "info" },
|
|
217
|
+
* { stream: fs.createWriteStream("./errors.log"), level: "error" },
|
|
218
|
+
* ],
|
|
219
|
+
* destination: process.stdout, // legacy single-sink
|
|
220
|
+
* errorDestination: process.stderr, // legacy two-sink split
|
|
221
|
+
* format: "json",
|
|
222
|
+
* clock: function () { return new Date(); }, // test seam
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* var log = b.log.create({
|
|
226
|
+
* level: "info",
|
|
227
|
+
* base: { service: "myapp", version: "1.2.3" },
|
|
228
|
+
* });
|
|
229
|
+
* log.info("user logged in", { userId: "u-1" });
|
|
230
|
+
* var authLog = log.bind({ component: "auth" });
|
|
231
|
+
* authLog.warn("rate-limited", { ip: "203.0.113.7" });
|
|
232
|
+
* // → {"timestamp":"...","level":"info","message":"user logged in",
|
|
233
|
+
* // "service":"myapp","version":"1.2.3","userId":"u-1"}
|
|
234
|
+
*/
|
|
200
235
|
function create(opts) {
|
|
201
236
|
opts = opts || {};
|
|
202
237
|
validateOpts(opts, [
|
|
@@ -386,23 +421,30 @@ function create(opts) {
|
|
|
386
421
|
return _makeInstance([]);
|
|
387
422
|
}
|
|
388
423
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
424
|
+
/**
|
|
425
|
+
* @primitive b.log.boot
|
|
426
|
+
* @signature b.log.boot(name)
|
|
427
|
+
* @since 0.7.0
|
|
428
|
+
* @status stable
|
|
429
|
+
* @related b.log.create, b.log.makeViaOrFallback
|
|
430
|
+
*
|
|
431
|
+
* Framework-internal boot logger for human-readable startup chatter
|
|
432
|
+
* (`[blamejs:db] ready`, `[blamejs:vault] WARNING: ...`). TTY-aware:
|
|
433
|
+
* when stdout is a terminal it emits a prefixed line; when stdout is
|
|
434
|
+
* piped it emits a one-line JSON object so log aggregators can ingest
|
|
435
|
+
* boot chatter as structured records. The returned value is a
|
|
436
|
+
* callable (info path) plus `.debug` / `.info` / `.warn` / `.error` /
|
|
437
|
+
* `.prefix` members so `log("ready")` and `log.warn("...")` both
|
|
438
|
+
* work.
|
|
439
|
+
*
|
|
440
|
+
* @example
|
|
441
|
+
* var log = b.log.boot("db");
|
|
442
|
+
* log("ready");
|
|
443
|
+
* log.warn("connection slow");
|
|
444
|
+
* // → "[blamejs:db] ready" (TTY)
|
|
445
|
+
* // → {"timestamp":"...","level":"info","message":"ready",
|
|
446
|
+
* // "component":"db","boot":true} (piped)
|
|
447
|
+
*/
|
|
406
448
|
function boot(name) {
|
|
407
449
|
if (typeof name !== "string" || name.length === 0) {
|
|
408
450
|
throw new LogError("log/bad-name", "log.boot(name) requires a non-empty name");
|
|
@@ -456,20 +498,27 @@ function boot(name) {
|
|
|
456
498
|
return info;
|
|
457
499
|
}
|
|
458
500
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
501
|
+
/**
|
|
502
|
+
* @primitive b.log.makeViaOrFallback
|
|
503
|
+
* @signature b.log.makeViaOrFallback(operatorLog, fallbackLog)
|
|
504
|
+
* @since 0.7.30
|
|
505
|
+
* @status stable
|
|
506
|
+
* @related b.log.create, b.log.boot
|
|
507
|
+
*
|
|
508
|
+
* Closure factory for operator-log routing. Used by primitives
|
|
509
|
+
* (bundler, dev server, error-page renderer, pqc-gate, ...) that
|
|
510
|
+
* accept `opts.log` but must keep emitting through a per-module
|
|
511
|
+
* fallback when the operator didn't pass one. The operator log call
|
|
512
|
+
* is best-effort — a misbehaving `log[level]` is swallowed rather
|
|
513
|
+
* than crashing the caller. Fallback fires only when the operator
|
|
514
|
+
* log is absent or doesn't expose the requested level.
|
|
515
|
+
*
|
|
516
|
+
* @example
|
|
517
|
+
* var fallback = b.log.boot("bundler");
|
|
518
|
+
* var via = b.log.makeViaOrFallback(null, fallback);
|
|
519
|
+
* via("error", "build-failed", { reason: "missing entrypoint" });
|
|
520
|
+
* // → "[blamejs:bundler] build-failed {\"reason\":\"missing entrypoint\"}"
|
|
521
|
+
*/
|
|
473
522
|
function makeViaOrFallback(operatorLog, fallbackLog) {
|
|
474
523
|
return function (level, message, fields) {
|
|
475
524
|
if (operatorLog && typeof operatorLog[level] === "function") {
|
|
@@ -514,14 +563,63 @@ function _bootMinLevel() {
|
|
|
514
563
|
return LEVELS[raw] != null ? LEVELS[raw] : LEVELS.info;
|
|
515
564
|
}
|
|
516
565
|
|
|
566
|
+
/**
|
|
567
|
+
* @primitive b.log.getRequestId
|
|
568
|
+
* @signature b.log.getRequestId()
|
|
569
|
+
* @since 0.1.70
|
|
570
|
+
* @status stable
|
|
571
|
+
* @related b.log.runWithRequestId, b.log.create
|
|
572
|
+
*
|
|
573
|
+
* Read the current AsyncLocalStorage-bound request id, or `null`
|
|
574
|
+
* when called outside a `runWithRequestId` / middleware-wrapped
|
|
575
|
+
* scope. The module-level helper exists for code paths that don't
|
|
576
|
+
* have a logger instance handy but still need to read the
|
|
577
|
+
* request-correlation token (e.g. an external SDK callback that
|
|
578
|
+
* must include the id in a remote span).
|
|
579
|
+
*
|
|
580
|
+
* @example
|
|
581
|
+
* await b.log.runWithRequestId("req-abc", async function () {
|
|
582
|
+
* var id = b.log.getRequestId();
|
|
583
|
+
* // → "req-abc"
|
|
584
|
+
* });
|
|
585
|
+
*/
|
|
586
|
+
function getRequestId() {
|
|
587
|
+
var s = _getStore();
|
|
588
|
+
return s ? s.requestId : null;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* @primitive b.log.runWithRequestId
|
|
593
|
+
* @signature b.log.runWithRequestId(id, fn)
|
|
594
|
+
* @since 0.1.70
|
|
595
|
+
* @status stable
|
|
596
|
+
* @related b.log.getRequestId, b.log.create
|
|
597
|
+
*
|
|
598
|
+
* Run `fn` inside an AsyncLocalStorage scope where
|
|
599
|
+
* `b.log.getRequestId()` returns `id`. Every `b.log.create`-built
|
|
600
|
+
* logger inside the scope automatically picks up the id on each
|
|
601
|
+
* emitted line. Returns whatever `fn` returns (including a Promise);
|
|
602
|
+
* the binding propagates through `await` boundaries via Node's
|
|
603
|
+
* async-context plumbing.
|
|
604
|
+
*
|
|
605
|
+
* @example
|
|
606
|
+
* var result = await b.log.runWithRequestId("req-abc", async function () {
|
|
607
|
+
* return b.log.getRequestId();
|
|
608
|
+
* });
|
|
609
|
+
* // → "req-abc"
|
|
610
|
+
*/
|
|
611
|
+
function runWithRequestId(id, fn) {
|
|
612
|
+
return _als.run({ requestId: id || null, _extra: {} }, fn);
|
|
613
|
+
}
|
|
614
|
+
|
|
517
615
|
module.exports = {
|
|
518
|
-
create:
|
|
519
|
-
boot:
|
|
616
|
+
create: create,
|
|
617
|
+
boot: boot,
|
|
520
618
|
makeViaOrFallback: makeViaOrFallback,
|
|
521
|
-
LEVELS:
|
|
522
|
-
LogError:
|
|
619
|
+
LEVELS: LEVELS,
|
|
620
|
+
LogError: LogError,
|
|
523
621
|
// Module-level helpers for code paths that don't have a logger
|
|
524
622
|
// instance handy but still need to read ALS state.
|
|
525
|
-
getRequestId:
|
|
526
|
-
runWithRequestId:
|
|
623
|
+
getRequestId: getRequestId,
|
|
624
|
+
runWithRequestId: runWithRequestId,
|
|
527
625
|
};
|
package/lib/mail-bounce.js
CHANGED
|
@@ -1,66 +1,39 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* @module b.mailBounce
|
|
4
|
+
* @nav Communication
|
|
5
|
+
* @title Mail Bounce
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* -
|
|
7
|
+
* @intro
|
|
8
|
+
* Inbound mail bounce-handler — parse the vendor's webhook DSN /
|
|
9
|
+
* complaint / delivery payload, normalize it into one event shape,
|
|
10
|
+
* classify hard vs soft bounces, and feed an operator-supplied
|
|
11
|
+
* suppression-list hook.
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* Outbound mail comes back as a hard bounce (permanent — invalid
|
|
14
|
+
* address), a soft bounce (transient — mailbox full, greylisted), a
|
|
15
|
+
* spam / abuse complaint, a delivery confirmation, or a list-
|
|
16
|
+
* unsubscribe. Vendors (Postmark, AWS SES via SNS, Resend) ship the
|
|
17
|
+
* same information dressed up in three different JSON shapes. This
|
|
18
|
+
* module owns the translation so operators write a single
|
|
19
|
+
* reconciliation path regardless of vendor.
|
|
17
20
|
*
|
|
18
|
-
*
|
|
21
|
+
* `b.mailBounce.parse` is the pure synchronous parser; it returns the
|
|
22
|
+
* normalized event `{ vendor, type, subType, recipient, messageId,
|
|
23
|
+
* reason, timestamp, raw }`. `b.mailBounce.handler` wires that
|
|
24
|
+
* parser into an Express-style middleware that buffers the body,
|
|
25
|
+
* runs an operator `verify` hook (HMAC, Basic Auth, SNS-Signature),
|
|
26
|
+
* emits a `system.mail.bounce` audit row, and calls the operator's
|
|
27
|
+
* `onBounce(event)` so the suppression list can be updated before
|
|
28
|
+
* the 200 response goes back.
|
|
19
29
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* type: "bounce" | "complaint" | "delivery",
|
|
25
|
-
* subType: "hard" | "soft" | "abuse" | ... | null,
|
|
26
|
-
* recipient: "user@example.com",
|
|
27
|
-
* messageId: "<framework-emitted Message-ID>" | null,
|
|
28
|
-
* reason: "smtp 550 ..." | null,
|
|
29
|
-
* timestamp: "2026-04-28T..." (ISO 8601),
|
|
30
|
-
* raw: <input payload, untouched>,
|
|
31
|
-
* }
|
|
30
|
+
* Generic RFC 3464 DSN is intentionally NOT a built-in vendor —
|
|
31
|
+
* parsing arbitrary MTA reports needs a full email parser the
|
|
32
|
+
* framework does not vendor for this surface. Operators with raw
|
|
33
|
+
* DSN inflow supply `{ parser }` to plug a custom normalizer.
|
|
32
34
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* the request body, calls verify(req, body) if supplied, runs the
|
|
36
|
-
* parser, emits system.mail.bounce, calls onBounce(event), and
|
|
37
|
-
* responds 200. Validation errors land as 400 with a JSON error.
|
|
38
|
-
* Verification failures land as 401.
|
|
39
|
-
*
|
|
40
|
-
* Operator example (Postmark):
|
|
41
|
-
*
|
|
42
|
-
* var bounce = b.mailBounce.handler({
|
|
43
|
-
* vendor: "postmark",
|
|
44
|
-
* verify: function (req) {
|
|
45
|
-
* // Postmark recommends Basic Auth on the webhook URL.
|
|
46
|
-
* return req.headers.authorization === expectedBasic;
|
|
47
|
-
* },
|
|
48
|
-
* onBounce: function (event) {
|
|
49
|
-
* // Mark the recipient as bounced in your data store.
|
|
50
|
-
* return repo.users.markBounced(event.recipient, event.subType);
|
|
51
|
-
* },
|
|
52
|
-
* });
|
|
53
|
-
* r.post("/webhooks/postmark", bounce);
|
|
54
|
-
*
|
|
55
|
-
* Audit: every parsed bounce/complaint/delivery emits one
|
|
56
|
-
* `system.mail.bounce` row carrying the normalized fields. Audit is on
|
|
57
|
-
* by default and disabled per-instance via { audit: false }.
|
|
58
|
-
*
|
|
59
|
-
* Generic DSN (RFC 3464 multipart/report) is intentionally NOT included
|
|
60
|
-
* as a built-in vendor: parsing arbitrary email-back-from-MTA reports
|
|
61
|
-
* needs a full email parser, which the framework does not vendor for
|
|
62
|
-
* this surface. Operators with raw DSN inflow plug a custom parser via
|
|
63
|
-
* the { vendor: 'custom', parser } shape (see _customParser below).
|
|
35
|
+
* @card
|
|
36
|
+
* Inbound mail bounce-handler — parse the vendor's webhook DSN / complaint / delivery payload, normalize it into one event shape, classify hard vs soft bounces, and feed an operator-supplied suppression-list hook.
|
|
64
37
|
*/
|
|
65
38
|
|
|
66
39
|
var lazyRequire = require("./lazy-require");
|
|
@@ -368,6 +341,43 @@ var VENDORS = {
|
|
|
368
341
|
resend: _parseResend,
|
|
369
342
|
};
|
|
370
343
|
|
|
344
|
+
/**
|
|
345
|
+
* @primitive b.mailBounce.parse
|
|
346
|
+
* @signature b.mailBounce.parse(payload, opts)
|
|
347
|
+
* @since 0.5.0
|
|
348
|
+
* @status stable
|
|
349
|
+
* @related b.mailBounce.handler
|
|
350
|
+
*
|
|
351
|
+
* Pure synchronous parser. Routes `payload` through the chosen vendor
|
|
352
|
+
* parser (built-ins: `postmark`, `ses`, `resend`) and returns the
|
|
353
|
+
* normalized event. Operators with bespoke vendors supply
|
|
354
|
+
* `opts.parser` — a function `(payload) -> normalizedEvent` that the
|
|
355
|
+
* framework runs and then validates so a misbehaving custom parser
|
|
356
|
+
* cannot emit malformed audit rows.
|
|
357
|
+
*
|
|
358
|
+
* Throws `MailBounceError` (HTTP 400) on missing / unknown vendor,
|
|
359
|
+
* empty payload, or payload missing the required vendor-specific
|
|
360
|
+
* fields. Never mutates `payload` — the original is preserved on
|
|
361
|
+
* `event.raw` for downstream re-parsing.
|
|
362
|
+
*
|
|
363
|
+
* @opts
|
|
364
|
+
* vendor: "postmark" | "ses" | "resend", // required when `parser` is absent
|
|
365
|
+
* parser: function (payload): normalizedEvent, // alternative to `vendor`
|
|
366
|
+
*
|
|
367
|
+
* @example
|
|
368
|
+
* var event = b.mailBounce.parse({
|
|
369
|
+
* RecordType: "Bounce",
|
|
370
|
+
* Type: "HardBounce",
|
|
371
|
+
* Email: "user@example.com",
|
|
372
|
+
* MessageID: "abc-123",
|
|
373
|
+
* Description: "550 No such mailbox",
|
|
374
|
+
* BouncedAt: "2026-04-28T10:00:00Z",
|
|
375
|
+
* }, { vendor: "postmark" });
|
|
376
|
+
* event.type; // → "bounce"
|
|
377
|
+
* event.subType; // → "hard"
|
|
378
|
+
* event.recipient; // → "user@example.com"
|
|
379
|
+
* event.timestamp; // → "2026-04-28T10:00:00Z"
|
|
380
|
+
*/
|
|
371
381
|
function parse(payload, opts) {
|
|
372
382
|
opts = opts || {};
|
|
373
383
|
if (typeof opts.parser === "function") {
|
|
@@ -387,12 +397,47 @@ function parse(payload, opts) {
|
|
|
387
397
|
return fn(payload);
|
|
388
398
|
}
|
|
389
399
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
400
|
+
/**
|
|
401
|
+
* @primitive b.mailBounce.handler
|
|
402
|
+
* @signature b.mailBounce.handler(opts)
|
|
403
|
+
* @since 0.5.0
|
|
404
|
+
* @status stable
|
|
405
|
+
* @related b.mailBounce.parse, b.audit.safeEmit
|
|
406
|
+
*
|
|
407
|
+
* Returns an Express-style `(req, res)` middleware that buffers the
|
|
408
|
+
* inbound webhook body (capped at `maxBytes`), runs `verify(req, body,
|
|
409
|
+
* raw)` if supplied, parses via the configured vendor (or custom
|
|
410
|
+
* `parser`), emits one `system.mail.bounce` audit row, calls
|
|
411
|
+
* `onBounce(event)`, and responds 200. Failures map to 400 (bad
|
|
412
|
+
* payload), 401 (verify rejected), 413 (body too large), 500
|
|
413
|
+
* (`onBounce` threw).
|
|
414
|
+
*
|
|
415
|
+
* `audit` defaults to ON; pass `audit: false` to suppress the
|
|
416
|
+
* `system.mail.bounce` row when the operator already records the
|
|
417
|
+
* normalized event in their own data store.
|
|
418
|
+
*
|
|
419
|
+
* @opts
|
|
420
|
+
* vendor: "postmark" | "ses" | "resend",
|
|
421
|
+
* parser: function (payload): normalizedEvent, // alternative to `vendor`
|
|
422
|
+
* verify: function (req, body, raw): boolean, // optional authenticity gate
|
|
423
|
+
* onBounce: function (event): Promise|void, // operator suppression hook
|
|
424
|
+
* audit: boolean, // default: true
|
|
425
|
+
* maxBytes: number, // body cap; default 256 KiB
|
|
426
|
+
*
|
|
427
|
+
* @example
|
|
428
|
+
* var bounce = b.mailBounce.handler({
|
|
429
|
+
* vendor: "postmark",
|
|
430
|
+
* verify: function (req) {
|
|
431
|
+
* return req.headers.authorization === "Basic c2VjcmV0";
|
|
432
|
+
* },
|
|
433
|
+
* onBounce: function (event) {
|
|
434
|
+
* suppressionList[event.recipient] = event.subType;
|
|
435
|
+
* },
|
|
436
|
+
* maxBytes: b.constants.BYTES.kib(64),
|
|
437
|
+
* });
|
|
438
|
+
* typeof bounce; // → "function"
|
|
439
|
+
* bounce.length; // → 2
|
|
440
|
+
*/
|
|
396
441
|
function handler(opts) {
|
|
397
442
|
opts = opts || {};
|
|
398
443
|
var vendor = opts.vendor;
|