@blamejs/core 0.8.0 → 0.8.4
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 +24 -0
- package/lib/audit-sign.js +1 -1
- package/lib/audit.js +62 -2
- package/lib/auth/jwt.js +13 -0
- package/lib/auth/lockout.js +16 -3
- package/lib/auth/oauth.js +15 -1
- package/lib/auth/password.js +22 -2
- package/lib/auth/sd-jwt-vc-issuer.js +2 -2
- package/lib/auth/sd-jwt-vc.js +7 -2
- package/lib/break-glass.js +53 -14
- package/lib/cache-redis.js +1 -1
- package/lib/cache.js +6 -1
- package/lib/cli.js +3 -3
- package/lib/cluster.js +24 -1
- package/lib/compliance-ai-act-logging.js +7 -3
- package/lib/compliance.js +10 -2
- package/lib/config-drift.js +2 -2
- package/lib/crypto-field.js +21 -1
- package/lib/crypto.js +82 -1
- package/lib/db.js +35 -4
- package/lib/dev.js +30 -3
- package/lib/dual-control.js +19 -1
- package/lib/external-db.js +10 -0
- package/lib/file-upload.js +30 -3
- package/lib/flag.js +1 -1
- package/lib/guard-all.js +33 -16
- package/lib/guard-csv.js +16 -2
- package/lib/guard-html.js +35 -0
- package/lib/guard-svg.js +20 -0
- package/lib/http-client.js +57 -11
- package/lib/inbox.js +34 -10
- package/lib/log-stream-syslog.js +8 -0
- package/lib/log-stream.js +1 -1
- package/lib/mail.js +40 -0
- package/lib/middleware/attach-user.js +25 -2
- package/lib/middleware/bearer-auth.js +71 -6
- package/lib/middleware/body-parser.js +13 -0
- package/lib/middleware/cors.js +10 -0
- package/lib/middleware/csrf-protect.js +34 -3
- package/lib/middleware/dpop.js +3 -3
- package/lib/middleware/host-allowlist.js +1 -1
- package/lib/middleware/require-aal.js +2 -2
- package/lib/middleware/trace-propagate.js +1 -1
- package/lib/mtls-ca.js +23 -29
- package/lib/mtls-engine-default.js +21 -1
- package/lib/network-tls.js +21 -6
- package/lib/object-store/sigv4-bucket-ops.js +41 -0
- package/lib/observability-otlp-exporter.js +35 -2
- package/lib/outbox.js +3 -3
- package/lib/permissions.js +10 -1
- package/lib/pqc-agent.js +22 -1
- package/lib/pubsub.js +8 -4
- package/lib/redact.js +26 -1
- package/lib/retention.js +26 -0
- package/lib/router.js +1 -0
- package/lib/scheduler.js +57 -1
- package/lib/session.js +3 -3
- package/lib/ssrf-guard.js +19 -4
- package/lib/static.js +12 -0
- package/lib/totp.js +16 -0
- package/lib/ws-client.js +158 -9
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
package/lib/static.js
CHANGED
|
@@ -100,6 +100,12 @@ var DEFAULT_CONTENT_TYPES = {
|
|
|
100
100
|
var DEFAULTS = Object.freeze({
|
|
101
101
|
defaultMaxAge: DEFAULT_MAX_AGE_SEC,
|
|
102
102
|
acceptRanges: true,
|
|
103
|
+
// Per-range byte cap — slowloris-range defense. A single Range
|
|
104
|
+
// request that asks for 1 GiB pins a worker on a long read; many
|
|
105
|
+
// concurrent requests asking for the same exhaust the process pool.
|
|
106
|
+
// The cap rejects ranges larger than maxRangeBytes with 416. Set to
|
|
107
|
+
// Infinity to opt out (audited reason).
|
|
108
|
+
maxRangeBytes: C.BYTES.mib(64),
|
|
103
109
|
// Empty array = no MIME allowlist gate.
|
|
104
110
|
allowedFileTypes: Object.freeze([]),
|
|
105
111
|
// Bandwidth + concurrency caps default to 0 = "no cap". Operators opt
|
|
@@ -844,6 +850,12 @@ function create(opts) {
|
|
|
844
850
|
return _writeError(res, HTTP.RANGE_NOT_SATISFIABLE, "range_not_satisfiable",
|
|
845
851
|
"Range Not Satisfiable", { "Content-Range": "bytes */" + meta.size });
|
|
846
852
|
}
|
|
853
|
+
if (range && cfg.maxRangeBytes !== Infinity && range.length > cfg.maxRangeBytes) {
|
|
854
|
+
stats.failures += 1;
|
|
855
|
+
_emitObs("staticServe.range_too_large", 1, { route: urlPath });
|
|
856
|
+
return _writeError(res, HTTP.RANGE_NOT_SATISFIABLE, "range_too_large",
|
|
857
|
+
"Range Not Satisfiable", { "Content-Range": "bytes */" + meta.size });
|
|
858
|
+
}
|
|
847
859
|
if (range) {
|
|
848
860
|
stats.rangeRequests += 1;
|
|
849
861
|
_emitObs("staticServe.range_requests", 1, { route: urlPath });
|
package/lib/totp.js
CHANGED
|
@@ -134,6 +134,22 @@ function _resolveOpts(opts) {
|
|
|
134
134
|
throw new AuthError("auth-totp/bad-alg",
|
|
135
135
|
"algorithm must be one of " + SUPPORTED_ALGORITHMS.join(", ") + " (got: " + alg + ")");
|
|
136
136
|
}
|
|
137
|
+
// SHA-256 is supported for back-compat with authenticator apps that
|
|
138
|
+
// don't yet honor SHA-512. Emit an audit signal each time it's
|
|
139
|
+
// selected so operator compliance dashboards see which accounts run
|
|
140
|
+
// on the weaker hash and can plan the migration.
|
|
141
|
+
if (alg === "sha256") {
|
|
142
|
+
setImmediate(function () {
|
|
143
|
+
try {
|
|
144
|
+
var auditMod = require("./audit"); // allow:inline-require — circular-load defense
|
|
145
|
+
auditMod.safeEmit({
|
|
146
|
+
action: "auth.totp.algorithm_downgraded",
|
|
147
|
+
outcome: "success",
|
|
148
|
+
metadata: { algorithm: alg, frameworkDefault: DEFAULT_ALGORITHM },
|
|
149
|
+
});
|
|
150
|
+
} catch (_e) { /* drop-silent */ }
|
|
151
|
+
});
|
|
152
|
+
}
|
|
137
153
|
var digits = opts.digits != null ? opts.digits : DEFAULT_DIGITS;
|
|
138
154
|
if (typeof digits !== "number" || digits < 6 || digits > 10) {
|
|
139
155
|
throw new AuthError("auth-totp/bad-digits", "digits must be 6–10 (got: " + digits + ")");
|
package/lib/ws-client.js
CHANGED
|
@@ -58,6 +58,8 @@ var fwCrypto = lazyRequire(function () { return require("./crypto"); });
|
|
|
58
58
|
var websocket = lazyRequire(function () { return require("./websocket"); });
|
|
59
59
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
60
60
|
var networkTls = lazyRequire(function () { return require("./network-tls"); });
|
|
61
|
+
var safeJson = lazyRequire(function () { return require("./safe-json"); });
|
|
62
|
+
var ssrfGuard = require("./ssrf-guard");
|
|
61
63
|
var C = require("./constants");
|
|
62
64
|
var { defineClass } = require("./framework-error");
|
|
63
65
|
|
|
@@ -164,9 +166,27 @@ function connect(target, opts) {
|
|
|
164
166
|
"maxMessageBytes", "maxFrameBytes",
|
|
165
167
|
"handshakeTimeoutMs", "reconnect",
|
|
166
168
|
"permessageDeflate", "audit", "origin",
|
|
167
|
-
"handshakeGuid",
|
|
169
|
+
"handshakeGuid", "allowInternal",
|
|
170
|
+
"parse", "parser",
|
|
171
|
+
"urlFor", "tlsOptsFor",
|
|
168
172
|
], "wsClient.connect");
|
|
169
173
|
|
|
174
|
+
// Per-dial state-injection callbacks. Both fire on every connect
|
|
175
|
+
// attempt (initial + each reconnect) so operators can rotate
|
|
176
|
+
// bearer tokens, DNS targets, or TLS material between hops without
|
|
177
|
+
// tearing the client down.
|
|
178
|
+
// urlFor(attempt) -> string — overrides target URL
|
|
179
|
+
// tlsOptsFor(attempt) -> object — overrides TLS opts
|
|
180
|
+
// attempt is a 0-based hop index; 0 is the initial dial.
|
|
181
|
+
if (opts.urlFor != null && typeof opts.urlFor !== "function") {
|
|
182
|
+
throw new WsClientError("ws-client/bad-url-for",
|
|
183
|
+
"wsClient.connect: urlFor must be a function");
|
|
184
|
+
}
|
|
185
|
+
if (opts.tlsOptsFor != null && typeof opts.tlsOptsFor !== "function") {
|
|
186
|
+
throw new WsClientError("ws-client/bad-tls-opts-for",
|
|
187
|
+
"wsClient.connect: tlsOptsFor must be a function");
|
|
188
|
+
}
|
|
189
|
+
|
|
170
190
|
// Operators with a non-RFC-6455 GUID (private protocols on top of
|
|
171
191
|
// the WebSocket framing layer, framework-specific handshake variants)
|
|
172
192
|
// pass a custom handshakeGuid. The server-side b.websocket already
|
|
@@ -219,8 +239,29 @@ function connect(target, opts) {
|
|
|
219
239
|
permessageDeflate: permessageDeflate,
|
|
220
240
|
auditOn: auditOn,
|
|
221
241
|
handshakeGuid: handshakeGuid,
|
|
242
|
+
allowInternal: opts.allowInternal,
|
|
243
|
+
parse: opts.parse || null,
|
|
244
|
+
parser: typeof opts.parser === "function" ? opts.parser : null,
|
|
245
|
+
urlFor: typeof opts.urlFor === "function" ? opts.urlFor : null,
|
|
246
|
+
tlsOptsFor: typeof opts.tlsOptsFor === "function" ? opts.tlsOptsFor : null,
|
|
247
|
+
});
|
|
248
|
+
// SSRF gate — refuse private / loopback / link-local / cloud-metadata /
|
|
249
|
+
// reserved IP destinations by default. Symmetric to b.httpClient. The
|
|
250
|
+
// returned `ips` are pinned through tls.connect / net.connect so the
|
|
251
|
+
// actual TCP connect targets the validated address (closes the DNS-
|
|
252
|
+
// rebinding TOCTOU window). Cloud-metadata IPs are unconditional
|
|
253
|
+
// hard-deny — `allowInternal: true` does not bypass them.
|
|
254
|
+
var hostnameForUrl = parsed.protocol === "wss:" ? "https:" : "http:";
|
|
255
|
+
var probeUrl = new url.URL(hostnameForUrl + "//" + parsed.host + parsed.pathname + parsed.search);
|
|
256
|
+
ssrfGuard.checkUrl(probeUrl, {
|
|
257
|
+
allowInternal: opts.allowInternal,
|
|
258
|
+
errorClass: WsClientError,
|
|
259
|
+
}).then(function (result) {
|
|
260
|
+
client._ssrfPinnedIps = result && result.ips;
|
|
261
|
+
client._dial();
|
|
262
|
+
}).catch(function (e) {
|
|
263
|
+
setImmediate(function () { client._handleSocketError(e); });
|
|
222
264
|
});
|
|
223
|
-
client._dial();
|
|
224
265
|
return client;
|
|
225
266
|
}
|
|
226
267
|
|
|
@@ -278,13 +319,74 @@ class WsClient extends EventEmitter {
|
|
|
278
319
|
var opts = this._opts;
|
|
279
320
|
this._readyState = "connecting";
|
|
280
321
|
|
|
281
|
-
|
|
322
|
+
// Per-dial overrides — urlFor swaps the target URL, tlsOptsFor
|
|
323
|
+
// overrides TLS material. Both fire every dial including reconnects
|
|
324
|
+
// so callers can rotate state. urlFor's result is re-validated
|
|
325
|
+
// through ssrfGuard so a hostile upstream can't direct the client
|
|
326
|
+
// at a private address mid-reconnect.
|
|
327
|
+
var attempt = this._reconnectAttempt || 0;
|
|
328
|
+
var dialTarget = opts.target;
|
|
329
|
+
var dialParsed = opts.parsedUrl;
|
|
330
|
+
if (typeof opts.urlFor === "function") {
|
|
331
|
+
try {
|
|
332
|
+
var nextTarget = opts.urlFor(attempt);
|
|
333
|
+
if (typeof nextTarget === "string" && nextTarget.length > 0 && nextTarget !== dialTarget) {
|
|
334
|
+
dialParsed = _parseUrl(nextTarget);
|
|
335
|
+
dialTarget = nextTarget;
|
|
336
|
+
var probeProto = dialParsed.protocol === "wss:" ? "https:" : "http:";
|
|
337
|
+
var probeUrl = new url.URL(probeProto + "//" + dialParsed.host + dialParsed.pathname + dialParsed.search);
|
|
338
|
+
var probe = ssrfGuard.checkUrl(probeUrl, {
|
|
339
|
+
allowInternal: opts.allowInternal,
|
|
340
|
+
errorClass: WsClientError,
|
|
341
|
+
});
|
|
342
|
+
this._ssrfPinnedIps = probe && probe.ips ? probe.ips : null;
|
|
343
|
+
}
|
|
344
|
+
} catch (e) {
|
|
345
|
+
return self._handleSocketError(e);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
var dialTlsOpts = opts.tlsOpts;
|
|
350
|
+
if (typeof opts.tlsOptsFor === "function") {
|
|
351
|
+
try {
|
|
352
|
+
var override = opts.tlsOptsFor(attempt);
|
|
353
|
+
if (override && typeof override === "object") {
|
|
354
|
+
dialTlsOpts = Object.assign({}, opts.tlsOpts || {}, override);
|
|
355
|
+
}
|
|
356
|
+
} catch (e) {
|
|
357
|
+
return self._handleSocketError(e);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
var parsed = dialParsed;
|
|
282
362
|
var port = parsed.port ? parseInt(parsed.port, 10) :
|
|
283
363
|
(parsed.protocol === "wss:" ? 443 : 80); // allow:raw-byte-literal — TLS / HTTP default port
|
|
284
364
|
var host = parsed.hostname;
|
|
285
365
|
|
|
286
366
|
function _onError(err) { self._handleSocketError(err); }
|
|
287
367
|
|
|
368
|
+
// Pin the connect to the SSRF-validated IPs returned by
|
|
369
|
+
// ssrfGuard.checkUrl — closes the DNS-rebinding TOCTOU window where
|
|
370
|
+
// the gate resolves a public IP and the kernel re-resolves to a
|
|
371
|
+
// private one between the check and the connect.
|
|
372
|
+
var pinnedIps = self._ssrfPinnedIps || null;
|
|
373
|
+
var lookup = null;
|
|
374
|
+
if (pinnedIps && pinnedIps.length > 0) {
|
|
375
|
+
lookup = function (h, lookupOpts, cb) {
|
|
376
|
+
// Node's lookup callback signatures:
|
|
377
|
+
// (err, address, family) for legacy { all: false } (default)
|
|
378
|
+
// (err, addresses) for { all: true }
|
|
379
|
+
if (typeof lookupOpts === "function") { cb = lookupOpts; lookupOpts = {}; }
|
|
380
|
+
var first = pinnedIps[0];
|
|
381
|
+
if (lookupOpts && lookupOpts.all) {
|
|
382
|
+
return cb(null, pinnedIps.map(function (ip) {
|
|
383
|
+
return { address: ip.address, family: ip.family };
|
|
384
|
+
}));
|
|
385
|
+
}
|
|
386
|
+
return cb(null, first.address, first.family);
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
288
390
|
var socket;
|
|
289
391
|
if (parsed.protocol === "wss:") {
|
|
290
392
|
var tls = require("tls"); // allow:inline-require — node:tls only on TLS path
|
|
@@ -294,7 +396,8 @@ class WsClient extends EventEmitter {
|
|
|
294
396
|
servername: host,
|
|
295
397
|
rejectUnauthorized: true,
|
|
296
398
|
minVersion: "TLSv1.3",
|
|
297
|
-
},
|
|
399
|
+
}, dialTlsOpts || {});
|
|
400
|
+
if (lookup) tlsOpts.lookup = lookup;
|
|
298
401
|
try {
|
|
299
402
|
var pqcShares = networkTls().pqc.getKeyShares();
|
|
300
403
|
if (Array.isArray(pqcShares) && pqcShares.length > 0 && !tlsOpts.curves) {
|
|
@@ -303,7 +406,9 @@ class WsClient extends EventEmitter {
|
|
|
303
406
|
} catch (_e) { /* drop-silent — tls module pre-init or non-Node */ }
|
|
304
407
|
socket = tls.connect(tlsOpts);
|
|
305
408
|
} else {
|
|
306
|
-
|
|
409
|
+
var netOpts = { host: host, port: port };
|
|
410
|
+
if (lookup) netOpts.lookup = lookup;
|
|
411
|
+
socket = net.connect(netOpts);
|
|
307
412
|
}
|
|
308
413
|
this._socket = socket;
|
|
309
414
|
socket.on("error", _onError);
|
|
@@ -406,8 +511,17 @@ class WsClient extends EventEmitter {
|
|
|
406
511
|
}
|
|
407
512
|
var status = parseInt(match[1], 10);
|
|
408
513
|
if (status !== 101) { // allow:raw-byte-literal — HTTP 101
|
|
409
|
-
|
|
410
|
-
|
|
514
|
+
// Body bytes after the header section are the server's
|
|
515
|
+
// explanation. Surface them on the error so callers can branch
|
|
516
|
+
// on the status code and inspect the body without re-parsing
|
|
517
|
+
// the message string.
|
|
518
|
+
var bodyText = "";
|
|
519
|
+
try { bodyText = rest.toString("utf8"); } catch (_e) { /* drop-silent */ }
|
|
520
|
+
var statusErr = new WsClientError("ws-client/bad-status",
|
|
521
|
+
"handshake response status was " + status + " (expected 101 Switching Protocols)");
|
|
522
|
+
statusErr.status = status;
|
|
523
|
+
statusErr.body = bodyText;
|
|
524
|
+
this._handleSocketError(statusErr);
|
|
411
525
|
return;
|
|
412
526
|
}
|
|
413
527
|
|
|
@@ -624,7 +738,29 @@ class WsClient extends EventEmitter {
|
|
|
624
738
|
return;
|
|
625
739
|
}
|
|
626
740
|
}
|
|
627
|
-
|
|
741
|
+
// Auto-parse text frames as JSON when the operator opted in via
|
|
742
|
+
// `parse: "json"` or supplied `parser: fn`. JSON-only protocols
|
|
743
|
+
// (most modern WS APIs) get a typed message argument without a
|
|
744
|
+
// wrapper layer; parse failures surface as 'error' events
|
|
745
|
+
// rather than crashing the message handler.
|
|
746
|
+
var parsed = data;
|
|
747
|
+
var parsedOk = true;
|
|
748
|
+
if (!isBinary && this._opts.parse === "json") {
|
|
749
|
+
try { parsed = safeJson().parse(data, { maxBytes: this._opts.maxMessageBytes }); }
|
|
750
|
+
catch (e) {
|
|
751
|
+
parsedOk = false;
|
|
752
|
+
this.emit("error", new WsClientError("ws-client/json-parse",
|
|
753
|
+
"text frame is not valid JSON: " + ((e && e.message) || String(e))));
|
|
754
|
+
}
|
|
755
|
+
} else if (!isBinary && typeof this._opts.parser === "function") {
|
|
756
|
+
try { parsed = this._opts.parser(data); }
|
|
757
|
+
catch (e) {
|
|
758
|
+
parsedOk = false;
|
|
759
|
+
this.emit("error", new WsClientError("ws-client/parser-failed",
|
|
760
|
+
"operator parser threw: " + ((e && e.message) || String(e))));
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (parsedOk) this.emit("message", parsed, isBinary);
|
|
628
764
|
}
|
|
629
765
|
}
|
|
630
766
|
|
|
@@ -741,12 +877,25 @@ class WsClient extends EventEmitter {
|
|
|
741
877
|
}
|
|
742
878
|
|
|
743
879
|
_handleSocketError(err) {
|
|
880
|
+
// Swallow post-close socket errors. After a consumer-initiated
|
|
881
|
+
// close() the framework waits for the server to send back its
|
|
882
|
+
// close frame; if the server tears down its end with an
|
|
883
|
+
// ECONNRESET / EPIPE / "premature close" instead of a graceful
|
|
884
|
+
// close frame, the underlying socket emits an error that bubbles
|
|
885
|
+
// up here. From the consumer's perspective the close already
|
|
886
|
+
// happened — surfacing a "socket error" event after `close` was
|
|
887
|
+
// called is noise that hides real bugs.
|
|
888
|
+
if (this._closed && err && (
|
|
889
|
+
err.code === "ECONNRESET" || err.code === "EPIPE" ||
|
|
890
|
+
err.code === "ECONNABORTED" || err.code === "ERR_STREAM_PREMATURE_CLOSE")) {
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
744
893
|
var permanent = _isPermanentError(err);
|
|
745
894
|
if (this._opts.auditOn) {
|
|
746
895
|
try {
|
|
747
896
|
audit().safeEmit({
|
|
748
897
|
action: "wsclient.error",
|
|
749
|
-
outcome: "
|
|
898
|
+
outcome: "failure",
|
|
750
899
|
actor: null,
|
|
751
900
|
metadata: {
|
|
752
901
|
host: this._opts.parsedUrl.host,
|
package/package.json
CHANGED
package/sbom.cyclonedx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:91e8b760-7fe3-4fef-a317-8b1e4f8cc238",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-06T22:02:35.725Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.8.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.8.4",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.8.
|
|
25
|
+
"version": "0.8.4",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.8.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.8.4",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.8.
|
|
57
|
+
"ref": "@blamejs/core@0.8.4",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|