@blamejs/core 0.9.49 → 0.10.2
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 +952 -908
- package/index.js +25 -0
- package/lib/_test/crypto-fixtures.js +67 -0
- package/lib/agent-event-bus.js +52 -6
- package/lib/agent-idempotency.js +169 -16
- package/lib/agent-orchestrator.js +263 -9
- package/lib/agent-posture-chain.js +163 -5
- package/lib/agent-saga.js +146 -16
- package/lib/agent-snapshot.js +349 -19
- package/lib/agent-stream.js +34 -2
- package/lib/agent-tenant.js +179 -23
- package/lib/agent-trace.js +84 -21
- package/lib/auth/aal.js +8 -1
- package/lib/auth/ciba.js +6 -1
- package/lib/auth/dpop.js +7 -2
- package/lib/auth/fal.js +17 -8
- package/lib/auth/jwt-external.js +128 -4
- package/lib/auth/oauth.js +232 -10
- package/lib/auth/oid4vci.js +67 -7
- package/lib/auth/openid-federation.js +71 -25
- package/lib/auth/passkey.js +140 -6
- package/lib/auth/sd-jwt-vc.js +78 -5
- package/lib/circuit-breaker.js +10 -2
- package/lib/cli.js +13 -0
- package/lib/compliance.js +176 -8
- package/lib/crypto-field.js +114 -14
- package/lib/crypto.js +216 -20
- package/lib/db.js +1 -0
- package/lib/guard-graphql.js +37 -0
- package/lib/guard-jmap.js +321 -0
- package/lib/guard-managesieve-command.js +566 -0
- package/lib/guard-pop3-command.js +317 -0
- package/lib/guard-regex.js +138 -1
- package/lib/guard-smtp-command.js +58 -3
- package/lib/guard-xml.js +39 -1
- package/lib/mail-agent.js +20 -7
- package/lib/mail-arc-sign.js +12 -8
- package/lib/mail-auth.js +323 -34
- package/lib/mail-crypto-pgp.js +934 -0
- package/lib/mail-crypto-smime.js +340 -0
- package/lib/mail-crypto.js +108 -0
- package/lib/mail-dav.js +1224 -0
- package/lib/mail-deploy.js +492 -0
- package/lib/mail-dkim.js +431 -26
- package/lib/mail-journal.js +435 -0
- package/lib/mail-scan.js +502 -0
- package/lib/mail-server-imap.js +64 -26
- package/lib/mail-server-jmap.js +488 -0
- package/lib/mail-server-managesieve.js +853 -0
- package/lib/mail-server-mx.js +40 -30
- package/lib/mail-server-pop3.js +836 -0
- package/lib/mail-server-rate-limit.js +13 -0
- package/lib/mail-server-submission.js +70 -24
- package/lib/mail-server-tls.js +445 -0
- package/lib/mail-sieve.js +557 -0
- package/lib/mail-spam-score.js +284 -0
- package/lib/mail.js +99 -0
- package/lib/metrics.js +80 -3
- package/lib/middleware/dpop.js +58 -3
- package/lib/middleware/idempotency-key.js +255 -42
- package/lib/middleware/protected-resource-metadata.js +114 -2
- package/lib/network-dns-resolver.js +33 -0
- package/lib/network-tls.js +46 -0
- package/lib/otel-export.js +13 -4
- package/lib/outbox.js +62 -12
- package/lib/pqc-agent.js +13 -5
- package/lib/retry.js +23 -9
- package/lib/router.js +23 -1
- package/lib/safe-ical.js +634 -0
- package/lib/safe-icap.js +502 -0
- package/lib/safe-mime.js +15 -0
- package/lib/safe-sieve.js +684 -0
- package/lib/safe-smtp.js +57 -0
- package/lib/safe-url.js +37 -0
- package/lib/safe-vcard.js +473 -0
- package/lib/self-update-standalone-verifier.js +32 -3
- package/lib/self-update.js +153 -33
- package/lib/vendor/MANIFEST.json +161 -156
- package/lib/vendor-data.js +127 -9
- package/lib/vex.js +324 -59
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/network-tls.js
CHANGED
|
@@ -3048,6 +3048,51 @@ function _checkServerIdentityStrict(host, cert) {
|
|
|
3048
3048
|
return checkServerIdentity9525(host, cert);
|
|
3049
3049
|
}
|
|
3050
3050
|
|
|
3051
|
+
// CVE-2026-21637 — Node propagates a synchronous throw from an
|
|
3052
|
+
// operator-supplied SNICallback up through the TLS handshake listener;
|
|
3053
|
+
// the unhandled throw on an unexpected servername crashes the
|
|
3054
|
+
// listener. RFC 6066 §3 expects the server to abort the handshake on a
|
|
3055
|
+
// failed callback, NOT crash the process.
|
|
3056
|
+
//
|
|
3057
|
+
// `wrapSNICallback(operatorCb)` returns a wrapper that:
|
|
3058
|
+
//
|
|
3059
|
+
// - Calls the operator callback in a try/catch.
|
|
3060
|
+
// - Surface a synchronous throw via the async (err, null) callback so
|
|
3061
|
+
// the TLS handshake aborts cleanly. Cb is best-effort: an operator
|
|
3062
|
+
// callback that throws AFTER invoking the callback already (double
|
|
3063
|
+
// invoke) gets the throw caught here without double-invoking again.
|
|
3064
|
+
// - Emit an audit event so a burst of crashes-that-weren't surfaces
|
|
3065
|
+
// in operator review.
|
|
3066
|
+
// - Returns the operator's original callback unchanged if it's not a
|
|
3067
|
+
// function (lets the caller pass undefined through without
|
|
3068
|
+
// special-casing).
|
|
3069
|
+
//
|
|
3070
|
+
// router.js routes its operator-supplied tlsOptions.SNICallback through
|
|
3071
|
+
// this helper before handing the options off to https.createServer.
|
|
3072
|
+
// Any future framework primitive that takes operator SNICallback
|
|
3073
|
+
// values does the same.
|
|
3074
|
+
function wrapSNICallback(operatorCb) {
|
|
3075
|
+
if (typeof operatorCb !== "function") return operatorCb;
|
|
3076
|
+
return function _wrappedSNICallback(servername, cb) {
|
|
3077
|
+
try {
|
|
3078
|
+
operatorCb(servername, cb);
|
|
3079
|
+
} catch (err) {
|
|
3080
|
+
try {
|
|
3081
|
+
audit().safeEmit({
|
|
3082
|
+
action: "network.tls.sni_callback_threw",
|
|
3083
|
+
outcome: "failure",
|
|
3084
|
+
metadata: {
|
|
3085
|
+
servername: typeof servername === "string" ? servername : null,
|
|
3086
|
+
reason: (err && err.message) ? err.message : String(err),
|
|
3087
|
+
},
|
|
3088
|
+
});
|
|
3089
|
+
} catch (_auditErr) { /* drop-silent — audit best-effort */ }
|
|
3090
|
+
try { cb(err, null); }
|
|
3091
|
+
catch (_cbErr) { /* cb already invoked or unavailable */ }
|
|
3092
|
+
}
|
|
3093
|
+
};
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3051
3096
|
module.exports = {
|
|
3052
3097
|
addCa: addCa,
|
|
3053
3098
|
addCaBundle: addCaBundle,
|
|
@@ -3073,6 +3118,7 @@ module.exports = {
|
|
|
3073
3118
|
parseEchConfigList: parseEchConfigList,
|
|
3074
3119
|
connectWithEch: connectWithEch,
|
|
3075
3120
|
checkServerIdentity9525: checkServerIdentity9525,
|
|
3121
|
+
wrapSNICallback: wrapSNICallback,
|
|
3076
3122
|
TlsTrustError: TlsTrustError,
|
|
3077
3123
|
NetworkTlsError: NetworkTlsError,
|
|
3078
3124
|
_resetForTest: _resetForTest,
|
package/lib/otel-export.js
CHANGED
|
@@ -50,6 +50,13 @@ var OtelExportError = defineClass("OtelExportError", { alwaysPermanent: false })
|
|
|
50
50
|
|
|
51
51
|
var DEFAULT_INTERVAL_MS = C.TIME.seconds(15);
|
|
52
52
|
|
|
53
|
+
// OTLP collector response is `Empty` (zero protobuf bytes) on success
|
|
54
|
+
// or a short ExportPartialSuccess message on partial accept. A response
|
|
55
|
+
// past this cap is a hostile / misbehaving collector; refusing the
|
|
56
|
+
// body keeps the exporter from buffering megabytes per flush (CVE-2026-
|
|
57
|
+
// 40891 / CVE-2026-40182 OTLP class).
|
|
58
|
+
var MAX_RESPONSE_BYTES = C.BYTES.mib(1);
|
|
59
|
+
|
|
53
60
|
// OTLP aggregation temporality:
|
|
54
61
|
// 1 = DELTA — counters report deltas since last export
|
|
55
62
|
// 2 = CUMULATIVE — counters report running totals
|
|
@@ -221,10 +228,12 @@ function create(opts) {
|
|
|
221
228
|
var body = JSON.stringify(payload);
|
|
222
229
|
try {
|
|
223
230
|
var res = await effectiveHttpClient.request({
|
|
224
|
-
method:
|
|
225
|
-
url:
|
|
226
|
-
headers:
|
|
227
|
-
body:
|
|
231
|
+
method: "POST",
|
|
232
|
+
url: endpoint,
|
|
233
|
+
headers: Object.assign({ "Content-Type": "application/json" }, headers),
|
|
234
|
+
body: body,
|
|
235
|
+
maxResponseBytes: MAX_RESPONSE_BYTES,
|
|
236
|
+
errorClass: OtelExportError,
|
|
228
237
|
});
|
|
229
238
|
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
230
239
|
throw new OtelExportError("otel-export/upstream-rejected",
|
package/lib/outbox.js
CHANGED
|
@@ -342,27 +342,77 @@ function create(opts) {
|
|
|
342
342
|
var stopping = false;
|
|
343
343
|
var inFlight = null;
|
|
344
344
|
|
|
345
|
+
// SUBSTRATE-23 — `FOR UPDATE SKIP LOCKED` is Postgres / MySQL 8+ only.
|
|
346
|
+
// SQLite (single-writer at the DB level, but WAL mode lets multiple
|
|
347
|
+
// processes share the file with concurrent SELECTs) doesn't support
|
|
348
|
+
// SKIP LOCKED — feeding it Postgres syntax silently double-publishes
|
|
349
|
+
// every row when two processes poll in parallel. Detect the dialect
|
|
350
|
+
// at runtime; only emit FOR UPDATE SKIP LOCKED when the backend
|
|
351
|
+
// declares postgres / mysql.
|
|
352
|
+
//
|
|
353
|
+
// Operator-visible: dialect comes from `externalDb.dialect` (set at
|
|
354
|
+
// `b.externalDb.create({ dialect: "postgres" | "mysql" | "sqlite" }`).
|
|
355
|
+
// Other backends fall back to the conservative "mark-then-update"
|
|
356
|
+
// path that works on every SQL dialect at the cost of a tiny race
|
|
357
|
+
// window between the SELECT + UPDATE (mitigated by status='in-flight'
|
|
358
|
+
// marker — duplicate publishes still bounded by retry visibility).
|
|
359
|
+
function _supportsForUpdateSkipLocked() {
|
|
360
|
+
var d = externalDb.dialect;
|
|
361
|
+
return d === "postgres" || d === "mysql";
|
|
362
|
+
}
|
|
363
|
+
|
|
345
364
|
async function _claimBatch() {
|
|
365
|
+
var supportsSkipLocked = _supportsForUpdateSkipLocked();
|
|
346
366
|
return await externalDb.transaction(async function (xdb) {
|
|
347
367
|
var nowExpr = _utcNowExpr(externalDb);
|
|
348
|
-
var
|
|
368
|
+
var selectSql =
|
|
349
369
|
"SELECT id, topic, payload, key, headers, attempts" +
|
|
350
370
|
" FROM " + quotedTable +
|
|
351
371
|
" WHERE status = 'pending' AND next_attempt_at <= $1" +
|
|
352
372
|
" ORDER BY next_attempt_at" +
|
|
353
|
-
" LIMIT $2"
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
373
|
+
" LIMIT $2";
|
|
374
|
+
if (supportsSkipLocked) {
|
|
375
|
+
selectSql += " FOR UPDATE SKIP LOCKED";
|
|
376
|
+
}
|
|
377
|
+
var rows = await xdb.query(selectSql, [nowExpr, batchSize]);
|
|
357
378
|
if (!rows || !rows.rows || rows.rows.length === 0) return [];
|
|
358
379
|
var ids = rows.rows.map(function (r) { return r.id; });
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
380
|
+
// Atomic claim: when the dialect lacks SKIP LOCKED, the UPDATE
|
|
381
|
+
// WHERE status='pending' AND id IN (...) ensures only ONE publisher
|
|
382
|
+
// sees each row transition from 'pending' to 'in-flight' — the
|
|
383
|
+
// other publisher's UPDATE matches zero rows and its batch shrinks.
|
|
384
|
+
// We re-select after the UPDATE to know which IDs we actually
|
|
385
|
+
// claimed (sqlite UPDATE doesn't return affected rows the same
|
|
386
|
+
// way Postgres does).
|
|
387
|
+
var actuallyClaimed;
|
|
388
|
+
if (supportsSkipLocked) {
|
|
389
|
+
// Postgres/MySQL: row lock held; ANY($1) update is safe.
|
|
390
|
+
await xdb.query(
|
|
391
|
+
"UPDATE " + quotedTable + " SET status = 'in-flight' WHERE id = ANY($1)",
|
|
392
|
+
[ids]
|
|
393
|
+
);
|
|
394
|
+
actuallyClaimed = rows.rows;
|
|
395
|
+
} else {
|
|
396
|
+
// SQLite (or "other") path: emit a portable UPDATE that
|
|
397
|
+
// refuses overlap by gating on status='pending'. After the
|
|
398
|
+
// update we re-read the in-flight rows we own; rows that
|
|
399
|
+
// another publisher beat us to are skipped.
|
|
400
|
+
// Use placeholders so the SQL stays parameterized regardless
|
|
401
|
+
// of dialect array semantics.
|
|
402
|
+
var placeholders = ids.map(function (_, i) { return "$" + (i + 1); }).join(",");
|
|
403
|
+
await xdb.query(
|
|
404
|
+
"UPDATE " + quotedTable +
|
|
405
|
+
" SET status = 'in-flight' WHERE status = 'pending' AND id IN (" + placeholders + ")",
|
|
406
|
+
ids
|
|
407
|
+
);
|
|
408
|
+
var afterRows = await xdb.query(
|
|
409
|
+
"SELECT id, topic, payload, key, headers, attempts FROM " + quotedTable +
|
|
410
|
+
" WHERE status = 'in-flight' AND id IN (" + placeholders + ")",
|
|
411
|
+
ids
|
|
412
|
+
);
|
|
413
|
+
actuallyClaimed = (afterRows && afterRows.rows) || [];
|
|
414
|
+
}
|
|
415
|
+
return actuallyClaimed.map(function (r) {
|
|
366
416
|
return {
|
|
367
417
|
id: r.id,
|
|
368
418
|
topic: r.topic,
|
package/lib/pqc-agent.js
CHANGED
|
@@ -253,13 +253,21 @@ function _getDefaultAgent() {
|
|
|
253
253
|
* logger.info("pqc-agent reloaded", res);
|
|
254
254
|
*/
|
|
255
255
|
function reload() {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
256
|
+
// CRYPTO-9 — null the cached agent BEFORE calling destroy. The
|
|
257
|
+
// previous order let a concurrent _getDefaultAgent() see the
|
|
258
|
+
// destroyed-not-null agent and hand it to a caller; the caller
|
|
259
|
+
// then tries to issue a request through a torn-down keep-alive
|
|
260
|
+
// pool and surfaces a "socket destroyed" error. Null-first means
|
|
261
|
+
// every concurrent _getDefaultAgent() either sees the live agent
|
|
262
|
+
// (request lands on the about-to-be-torn-down pool — natural
|
|
263
|
+
// graceful drain) or the null sentinel (builds fresh).
|
|
264
|
+
var prior = _defaultAgent;
|
|
265
|
+
_defaultAgent = null;
|
|
266
|
+
if (prior) {
|
|
267
|
+
try { prior.destroy(); }
|
|
259
268
|
catch (_e) { /* destroy is best-effort */ }
|
|
260
|
-
_defaultAgent = null;
|
|
261
269
|
}
|
|
262
|
-
return { destroyed:
|
|
270
|
+
return { destroyed: prior !== null };
|
|
263
271
|
}
|
|
264
272
|
|
|
265
273
|
module.exports = {
|
package/lib/retry.js
CHANGED
|
@@ -39,7 +39,6 @@
|
|
|
39
39
|
|
|
40
40
|
var C = require("./constants");
|
|
41
41
|
var lazyRequire = require("./lazy-require");
|
|
42
|
-
var nodeCrypto = require("node:crypto");
|
|
43
42
|
var numericChecks = require("./numeric-checks");
|
|
44
43
|
// safe-async re-exports withRetry + CircuitBreaker from this module, so a
|
|
45
44
|
// direct top-level require would create a cycle. Lazy-require defers the
|
|
@@ -239,10 +238,19 @@ function isRetryable(err) {
|
|
|
239
238
|
*
|
|
240
239
|
* Compute the backoff in milliseconds for a given (1-based) `attempt`
|
|
241
240
|
* number. Exponential growth `baseDelayMs * 2^(attempt-1)` capped at
|
|
242
|
-
* `maxDelayMs`, then subtract a
|
|
243
|
-
* `jitterFactor` so retrying clients
|
|
244
|
-
*
|
|
245
|
-
*
|
|
241
|
+
* `maxDelayMs`, then subtract a Math.random-sourced jitter sample
|
|
242
|
+
* scaled by `jitterFactor` so retrying clients spread across the
|
|
243
|
+
* millisecond window instead of realigning on the same boundary
|
|
244
|
+
* (thundering-herd avoidance). Throws TypeError when `attempt` is
|
|
245
|
+
* not a positive integer. `opts` defaults to `b.retry.DEFAULT_RETRY`
|
|
246
|
+
* when absent.
|
|
247
|
+
*
|
|
248
|
+
* Jitter is intentionally NOT a CSPRNG sample — the per-request delay
|
|
249
|
+
* is observable to every peer client by construction (the request
|
|
250
|
+
* that comes in carries its own arrival timing), so there is no
|
|
251
|
+
* confidentiality property a stronger random source would protect.
|
|
252
|
+
* Math.random is the right tool for thundering-herd avoidance and
|
|
253
|
+
* costs ~50x less than a CSPRNG randomInt() under a retry storm.
|
|
246
254
|
*
|
|
247
255
|
* @opts
|
|
248
256
|
* baseDelayMs: number, // initial backoff (default 100)
|
|
@@ -263,10 +271,16 @@ function backoffDelay(attempt, opts) {
|
|
|
263
271
|
opts = opts || DEFAULT_RETRY;
|
|
264
272
|
var base = opts.baseDelayMs * Math.pow(2, attempt - 1);
|
|
265
273
|
var capped = Math.min(base, opts.maxDelayMs);
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
274
|
+
// CRYPTO-12 — jitter exists to spread retry storms across the
|
|
275
|
+
// millisecond window so N peer clients waking from the same
|
|
276
|
+
// upstream outage don't all hit the recovering service at the same
|
|
277
|
+
// tick. The value is observable to every client by construction
|
|
278
|
+
// (the request that comes in carries its own timing); there's no
|
|
279
|
+
// confidentiality property to protect, so a CSPRNG would burn
|
|
280
|
+
// 30-50K randomInt/sec under a retry storm without any security
|
|
281
|
+
// payoff. Math.random's PRNG is the right tool for
|
|
282
|
+
// thundering-herd avoidance.
|
|
283
|
+
var jitter = capped * opts.jitterFactor * Math.random(); // allow:math-random-noncrypto — jitter for thundering-herd, not a confidentiality primitive
|
|
270
284
|
return Math.floor(capped - jitter);
|
|
271
285
|
}
|
|
272
286
|
|
package/lib/router.js
CHANGED
|
@@ -463,6 +463,14 @@ class Router {
|
|
|
463
463
|
}
|
|
464
464
|
|
|
465
465
|
_registerRoute(method, pattern, args) {
|
|
466
|
+
// CVE-2026-4923 — refuse pattern with more than 3 consecutive `*`
|
|
467
|
+
// metacharacters. The framework's segment matcher doesn't compile
|
|
468
|
+
// regex from operator input, but the policy stays crisp at the
|
|
469
|
+
// registration boundary.
|
|
470
|
+
if (typeof pattern === "string" && /\*{4,}/.test(pattern)) {
|
|
471
|
+
throw new Error(method + " " + pattern + ": route pattern refused " +
|
|
472
|
+
"(CVE-2026-4923) — more than 3 consecutive '*' metacharacters");
|
|
473
|
+
}
|
|
466
474
|
var split = this._splitArgs(args);
|
|
467
475
|
if (split.spec) _validateRouteSpec(split.spec, method, pattern);
|
|
468
476
|
var handlers = split.handlers;
|
|
@@ -656,7 +664,21 @@ class Router {
|
|
|
656
664
|
allowedProtocols: safeUrl.ALLOW_HTTP_ALL,
|
|
657
665
|
});
|
|
658
666
|
req.pathname = parsed.pathname;
|
|
659
|
-
|
|
667
|
+
// CVE-2026-21717 V8 HashDoS defense — cap distinct query keys
|
|
668
|
+
// before forming the dense object. Integer-shaped keys past 1000
|
|
669
|
+
// entries degrade V8 hidden-class transitions to O(n²).
|
|
670
|
+
var queryEntries = [];
|
|
671
|
+
var queryKeyCount = 0;
|
|
672
|
+
for (var pair of parsed.searchParams) {
|
|
673
|
+
queryKeyCount += 1;
|
|
674
|
+
if (queryKeyCount > 1000) { // allow:raw-byte-literal — CVE-2026-21717 V8 HashDoS query-key cap
|
|
675
|
+
res.statusCode = 400;
|
|
676
|
+
res.end("400 Bad Request: too many query keys");
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
queryEntries.push(pair);
|
|
680
|
+
}
|
|
681
|
+
req.query = Object.fromEntries(queryEntries);
|
|
660
682
|
|
|
661
683
|
// Run middleware
|
|
662
684
|
for (var mw of this.middleware) {
|