@blamejs/core 0.7.107 → 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 +41 -1
- package/NOTICE +17 -1
- package/README.md +4 -3
- package/index.js +15 -0
- package/lib/asyncapi-bindings.js +160 -0
- package/lib/asyncapi-traits.js +143 -0
- package/lib/asyncapi.js +531 -0
- package/lib/audit-sign.js +1 -1
- package/lib/audit.js +68 -2
- package/lib/auth/acr-vocabulary.js +265 -0
- package/lib/auth/auth-time-tracker.js +111 -0
- package/lib/auth/elevation-grant.js +306 -0
- 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/auth/step-up-policy.js +335 -0
- package/lib/auth/step-up.js +445 -0
- 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 +190 -0
- package/lib/compliance-ai-act-prohibited.js +205 -0
- package/lib/compliance-ai-act-risk.js +189 -0
- package/lib/compliance-ai-act-transparency.js +200 -0
- package/lib/compliance-ai-act.js +558 -0
- package/lib/compliance.js +12 -2
- package/lib/config-drift.js +2 -2
- package/lib/crypto-field.js +21 -1
- package/lib/crypto.js +114 -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-cache.js +136 -0
- package/lib/flag-evaluation-context.js +135 -0
- package/lib/flag-providers.js +279 -0
- package/lib/flag-targeting.js +210 -0
- package/lib/flag.js +284 -0
- 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 +391 -0
- package/lib/log-stream-syslog.js +8 -0
- package/lib/log-stream.js +1 -1
- package/lib/mail-arc-sign.js +372 -0
- package/lib/mail-auth.js +2 -0
- package/lib/mail.js +40 -0
- package/lib/middleware/ai-act-disclosure.js +166 -0
- package/lib/middleware/asyncapi-serve.js +136 -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/flag-context.js +76 -0
- package/lib/middleware/host-allowlist.js +1 -1
- package/lib/middleware/index.js +15 -0
- package/lib/middleware/openapi-serve.js +143 -0
- package/lib/middleware/require-aal.js +2 -2
- package/lib/middleware/require-step-up.js +186 -0
- 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/openapi-paths-builder.js +248 -0
- package/lib/openapi-schema-walk.js +192 -0
- package/lib/openapi-security.js +169 -0
- package/lib/openapi-yaml.js +154 -0
- package/lib/openapi.js +443 -0
- package/lib/outbox.js +3 -3
- package/lib/permissions.js +10 -1
- package/lib/pqc-agent.js +22 -1
- package/lib/pqc-software.js +195 -0
- 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/vault/index.js +3 -0
- package/lib/vault-aad.js +259 -0
- package/lib/vendor/MANIFEST.json +29 -0
- package/lib/vendor/noble-post-quantum.cjs +18 -0
- package/lib/ws-client.js +978 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.pqcSoftware — pure-JS post-quantum primitives sourced from the
|
|
4
|
+
* vendored @noble/post-quantum bundle (lib/vendor/noble-post-quantum.cjs).
|
|
5
|
+
*
|
|
6
|
+
* Usable server-side and client-side. Ciphertexts are FIPS 203
|
|
7
|
+
* conformant in both directions — encapsulating with Node's WebCrypto
|
|
8
|
+
* ML-KEM-1024 (used by b.crypto.encrypt / b.middleware.apiEncrypt)
|
|
9
|
+
* decapsulates with b.pqcSoftware.ml_kem_1024 and vice versa.
|
|
10
|
+
*
|
|
11
|
+
* Operator wiring:
|
|
12
|
+
*
|
|
13
|
+
* - Server-side: import b.pqcSoftware directly. Use it as the
|
|
14
|
+
* primary PQC path on Node releases without the experimental
|
|
15
|
+
* WebCrypto ML-KEM extension, or for reference-implementation
|
|
16
|
+
* interop testing against Node WebCrypto / hardware HSMs.
|
|
17
|
+
*
|
|
18
|
+
* - Client-side: re-bundle this module or import @noble/post-quantum
|
|
19
|
+
* directly into the build that ships b.middleware.apiEncrypt.client.
|
|
20
|
+
*
|
|
21
|
+
* Defaults pin to the highest cat-5 level:
|
|
22
|
+
*
|
|
23
|
+
* - DEFAULT_KEM = ML-KEM-1024 (FIPS 203)
|
|
24
|
+
* - DEFAULT_LATTICE_SIG = ML-DSA-87 (FIPS 204)
|
|
25
|
+
* - DEFAULT_HASH_SIG = SLH-DSA-SHAKE-256f (FIPS 205)
|
|
26
|
+
*
|
|
27
|
+
* Public surface (b.pqcSoftware.*):
|
|
28
|
+
*
|
|
29
|
+
* .ml_kem_1024 / .ml_kem_768 / .ml_kem_512 — FIPS 203 KEM objects
|
|
30
|
+
* .ml_dsa_87 / .ml_dsa_65 / .ml_dsa_44 — FIPS 204 lattice sig
|
|
31
|
+
* .slh_dsa_shake_256f / 192f / 128f — FIPS 205 (SHAKE)
|
|
32
|
+
* .slh_dsa_sha2_256f / 192f / 128f — FIPS 205 (SHA-2)
|
|
33
|
+
*
|
|
34
|
+
* .DEFAULT_KEM — alias to ml_kem_1024
|
|
35
|
+
* .DEFAULT_LATTICE_SIG — alias to ml_dsa_87
|
|
36
|
+
* .DEFAULT_HASH_SIG — alias to slh_dsa_shake_256f
|
|
37
|
+
*
|
|
38
|
+
* .isAvailable() — boolean: is the vendored bundle loadable?
|
|
39
|
+
* .listAlgorithms() — string[] of algorithm names
|
|
40
|
+
*
|
|
41
|
+
* Each KEM / signature object exposes `keygen()` / `encapsulate()` /
|
|
42
|
+
* `decapsulate()` (KEMs) or `keygen()` / `sign()` / `verify()`
|
|
43
|
+
* (signatures), matching the @noble/post-quantum API directly.
|
|
44
|
+
*
|
|
45
|
+
* Operators chaining this into other primitives:
|
|
46
|
+
*
|
|
47
|
+
* var pqc = b.pqcSoftware;
|
|
48
|
+
* var kp = pqc.DEFAULT_KEM.keygen();
|
|
49
|
+
* var enc = pqc.DEFAULT_KEM.encapsulate(kp.publicKey);
|
|
50
|
+
* // enc.cipherText / enc.sharedSecret
|
|
51
|
+
*
|
|
52
|
+
* Note on availability: the bundle is a build artifact in
|
|
53
|
+
* lib/vendor/noble-post-quantum.cjs. In tightly-locked deployments
|
|
54
|
+
* where operators stripped the vendor directory, .isAvailable()
|
|
55
|
+
* returns false and the module exposes a stub that throws on every
|
|
56
|
+
* primitive call.
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
var { defineClass } = require("./framework-error");
|
|
60
|
+
var PqcError = defineClass("PqcError", { alwaysPermanent: true });
|
|
61
|
+
|
|
62
|
+
var _vendoredOnce = null;
|
|
63
|
+
var _loadError = null;
|
|
64
|
+
|
|
65
|
+
function _load() {
|
|
66
|
+
if (_vendoredOnce !== null || _loadError !== null) return _vendoredOnce;
|
|
67
|
+
try {
|
|
68
|
+
// Inline-require: deliberate — the vendored bundle is loaded
|
|
69
|
+
// on-demand so deployments that strip lib/vendor/ still boot
|
|
70
|
+
// without crashing on first import of this module. Stub fallback
|
|
71
|
+
// is below.
|
|
72
|
+
_vendoredOnce = require("./vendor/noble-post-quantum.cjs"); // allow:inline-require — graceful-fallback shim
|
|
73
|
+
return _vendoredOnce;
|
|
74
|
+
} catch (e) {
|
|
75
|
+
_loadError = e;
|
|
76
|
+
_vendoredOnce = null;
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function _stubFor(name) {
|
|
82
|
+
return {
|
|
83
|
+
info: { type: name, available: false },
|
|
84
|
+
keygen: function () { _throwUnavailable(name); },
|
|
85
|
+
encapsulate: function () { _throwUnavailable(name); },
|
|
86
|
+
decapsulate: function () { _throwUnavailable(name); },
|
|
87
|
+
sign: function () { _throwUnavailable(name); },
|
|
88
|
+
verify: function () { _throwUnavailable(name); },
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function _throwUnavailable(name) {
|
|
93
|
+
throw new PqcError("pqc-software/unavailable",
|
|
94
|
+
"b.pqcSoftware." + name + ": vendored bundle lib/vendor/noble-post-quantum.cjs " +
|
|
95
|
+
"could not be loaded (" + (_loadError && _loadError.message || "unknown") + ") — " +
|
|
96
|
+
"if this is a deliberately-stripped deployment, use Node WebCrypto ML-KEM " +
|
|
97
|
+
"(b.crypto.encrypt / decrypt) instead. To restore: " +
|
|
98
|
+
"scripts/vendor-update.sh @noble/post-quantum");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function _accessor(name) {
|
|
102
|
+
var bundle = _load();
|
|
103
|
+
if (!bundle) return _stubFor(name);
|
|
104
|
+
var algo = bundle[name];
|
|
105
|
+
if (!algo) return _stubFor(name);
|
|
106
|
+
return algo;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isAvailable() {
|
|
110
|
+
return _load() !== null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function listAlgorithms() {
|
|
114
|
+
if (!isAvailable()) return [];
|
|
115
|
+
return [
|
|
116
|
+
"ml_kem_512", "ml_kem_768", "ml_kem_1024",
|
|
117
|
+
"ml_dsa_44", "ml_dsa_65", "ml_dsa_87",
|
|
118
|
+
"slh_dsa_sha2_128f", "slh_dsa_sha2_192f", "slh_dsa_sha2_256f",
|
|
119
|
+
"slh_dsa_shake_128f", "slh_dsa_shake_192f", "slh_dsa_shake_256f",
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Each accessor delegates to the bundle on demand. Naming follows
|
|
124
|
+
// the framework's underscore-separated convention; the bundle uses
|
|
125
|
+
// the same names with `_` already, so they map 1:1.
|
|
126
|
+
var pqc = {
|
|
127
|
+
PqcError: PqcError,
|
|
128
|
+
isAvailable: isAvailable,
|
|
129
|
+
listAlgorithms: listAlgorithms,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
Object.defineProperty(pqc, "ml_kem_512", {
|
|
133
|
+
enumerable: true,
|
|
134
|
+
get: function () { return _accessor("ml_kem512"); },
|
|
135
|
+
});
|
|
136
|
+
Object.defineProperty(pqc, "ml_kem_768", {
|
|
137
|
+
enumerable: true,
|
|
138
|
+
get: function () { return _accessor("ml_kem768"); },
|
|
139
|
+
});
|
|
140
|
+
Object.defineProperty(pqc, "ml_kem_1024", {
|
|
141
|
+
enumerable: true,
|
|
142
|
+
get: function () { return _accessor("ml_kem1024"); },
|
|
143
|
+
});
|
|
144
|
+
Object.defineProperty(pqc, "ml_dsa_44", {
|
|
145
|
+
enumerable: true,
|
|
146
|
+
get: function () { return _accessor("ml_dsa44"); },
|
|
147
|
+
});
|
|
148
|
+
Object.defineProperty(pqc, "ml_dsa_65", {
|
|
149
|
+
enumerable: true,
|
|
150
|
+
get: function () { return _accessor("ml_dsa65"); },
|
|
151
|
+
});
|
|
152
|
+
Object.defineProperty(pqc, "ml_dsa_87", {
|
|
153
|
+
enumerable: true,
|
|
154
|
+
get: function () { return _accessor("ml_dsa87"); },
|
|
155
|
+
});
|
|
156
|
+
Object.defineProperty(pqc, "slh_dsa_sha2_128f", {
|
|
157
|
+
enumerable: true,
|
|
158
|
+
get: function () { return _accessor("slh_dsa_sha2_128f"); },
|
|
159
|
+
});
|
|
160
|
+
Object.defineProperty(pqc, "slh_dsa_sha2_192f", {
|
|
161
|
+
enumerable: true,
|
|
162
|
+
get: function () { return _accessor("slh_dsa_sha2_192f"); },
|
|
163
|
+
});
|
|
164
|
+
Object.defineProperty(pqc, "slh_dsa_sha2_256f", {
|
|
165
|
+
enumerable: true,
|
|
166
|
+
get: function () { return _accessor("slh_dsa_sha2_256f"); },
|
|
167
|
+
});
|
|
168
|
+
Object.defineProperty(pqc, "slh_dsa_shake_128f", {
|
|
169
|
+
enumerable: true,
|
|
170
|
+
get: function () { return _accessor("slh_dsa_shake_128f"); },
|
|
171
|
+
});
|
|
172
|
+
Object.defineProperty(pqc, "slh_dsa_shake_192f", {
|
|
173
|
+
enumerable: true,
|
|
174
|
+
get: function () { return _accessor("slh_dsa_shake_192f"); },
|
|
175
|
+
});
|
|
176
|
+
Object.defineProperty(pqc, "slh_dsa_shake_256f", {
|
|
177
|
+
enumerable: true,
|
|
178
|
+
get: function () { return _accessor("slh_dsa_shake_256f"); },
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Security-first defaults — highest level wins.
|
|
182
|
+
Object.defineProperty(pqc, "DEFAULT_KEM", {
|
|
183
|
+
enumerable: true,
|
|
184
|
+
get: function () { return _accessor("ml_kem1024"); },
|
|
185
|
+
});
|
|
186
|
+
Object.defineProperty(pqc, "DEFAULT_LATTICE_SIG", {
|
|
187
|
+
enumerable: true,
|
|
188
|
+
get: function () { return _accessor("ml_dsa87"); },
|
|
189
|
+
});
|
|
190
|
+
Object.defineProperty(pqc, "DEFAULT_HASH_SIG", {
|
|
191
|
+
enumerable: true,
|
|
192
|
+
get: function () { return _accessor("slh_dsa_shake_256f"); },
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
module.exports = pqc;
|
package/lib/pubsub.js
CHANGED
|
@@ -379,16 +379,20 @@ function create(opts) {
|
|
|
379
379
|
? rv.remote : 1;
|
|
380
380
|
} catch (e) {
|
|
381
381
|
if (auditOn) {
|
|
382
|
-
try { audit().safeEmit(
|
|
383
|
-
|
|
382
|
+
try { audit().safeEmit({
|
|
383
|
+
action: "system.pubsub.publish_failed",
|
|
384
|
+
outcome: "failure",
|
|
385
|
+
metadata: { channel: channel, error: (e && e.message) || String(e) },
|
|
384
386
|
}); } catch (_e) { /* */ }
|
|
385
387
|
}
|
|
386
388
|
throw e;
|
|
387
389
|
}
|
|
388
390
|
}
|
|
389
391
|
if (auditOn) {
|
|
390
|
-
try { audit().safeEmit(
|
|
391
|
-
|
|
392
|
+
try { audit().safeEmit({
|
|
393
|
+
action: "system.pubsub.publish",
|
|
394
|
+
outcome: "success",
|
|
395
|
+
metadata: { channel: channel, localDispatched: local, remoteWritten: remote },
|
|
392
396
|
}); } catch (_e) { /* */ }
|
|
393
397
|
}
|
|
394
398
|
return { local: local, remote: remote };
|
package/lib/redact.js
CHANGED
|
@@ -35,6 +35,14 @@ var SENSITIVE_FIELDS = [
|
|
|
35
35
|
"card_number", "cardnumber", "cvc", "cvv", "pin",
|
|
36
36
|
"privatekey", "private_key", "passphrase", "session", "sid",
|
|
37
37
|
"_authtoken", "auth_token", "bearer", "cookie",
|
|
38
|
+
// Header-shaped variants of api-key — substring matching against a
|
|
39
|
+
// lowercased field name treats hyphen + underscore + dot as
|
|
40
|
+
// literal, so each header form needs its own entry.
|
|
41
|
+
"x-api-key", "x_api_key", "x-apikey", "api-key",
|
|
42
|
+
// DPoP / OAuth 2.1 / OIDC proof-of-possession + selective-disclosure
|
|
43
|
+
// fields — operator-error metadata logging often carries these.
|
|
44
|
+
"jwk", "dpop", "proof", "assertion", "client_assertion", "id_token_hint",
|
|
45
|
+
"code_verifier", "client_secret", "refresh_token", "access_token",
|
|
38
46
|
// Vault-sealed values (don't log even though they're encrypted —
|
|
39
47
|
// operational logs aren't a place to leak ciphertext shape either)
|
|
40
48
|
// matched separately by value detector below
|
|
@@ -74,6 +82,20 @@ var VALUE_DETECTORS = [
|
|
|
74
82
|
},
|
|
75
83
|
replacement: "[REDACTED-JWT]",
|
|
76
84
|
},
|
|
85
|
+
{
|
|
86
|
+
// URL with bearer-shaped query parameter — the parent field is
|
|
87
|
+
// typically `url` or `referer`, neither of which the field-name
|
|
88
|
+
// pass redacts. Replace the whole querystring after the marker
|
|
89
|
+
// so the path stays useful for log triage.
|
|
90
|
+
name: "url-bearer-query",
|
|
91
|
+
test: function (v) {
|
|
92
|
+
return typeof v === "string" &&
|
|
93
|
+
/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s]*[?&#](?:access_token|id_token|token|api_key|apikey)=/.test(v);
|
|
94
|
+
},
|
|
95
|
+
replacement: function (v) {
|
|
96
|
+
return String(v).replace(/(access_token|id_token|token|api_key|apikey)=[^&#]*/g, "$1=[REDACTED]");
|
|
97
|
+
},
|
|
98
|
+
},
|
|
77
99
|
{
|
|
78
100
|
name: "pem",
|
|
79
101
|
test: function (v) { return typeof v === "string" && /-----BEGIN [A-Z ]+-----/.test(v); },
|
|
@@ -151,7 +173,10 @@ function _redactValue(value) {
|
|
|
151
173
|
if (typeof value !== "string") return value;
|
|
152
174
|
var allDetectors = VALUE_DETECTORS.concat(customDetectors);
|
|
153
175
|
for (var i = 0; i < allDetectors.length; i++) {
|
|
154
|
-
if (allDetectors[i].test(value))
|
|
176
|
+
if (allDetectors[i].test(value)) {
|
|
177
|
+
var rep = allDetectors[i].replacement;
|
|
178
|
+
return typeof rep === "function" ? rep(value) : rep;
|
|
179
|
+
}
|
|
155
180
|
}
|
|
156
181
|
return value;
|
|
157
182
|
}
|
package/lib/retention.js
CHANGED
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
var C = require("./constants");
|
|
59
59
|
var lazyRequire = require("./lazy-require");
|
|
60
60
|
var validateOpts = require("./validate-opts");
|
|
61
|
+
var safeSql = require("./safe-sql");
|
|
61
62
|
var { defineClass } = require("./framework-error");
|
|
62
63
|
|
|
63
64
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
@@ -66,6 +67,21 @@ var cryptoField = require("./crypto-field");
|
|
|
66
67
|
var RetentionError = defineClass("RetentionError", { alwaysPermanent: true });
|
|
67
68
|
var _err = RetentionError.factory;
|
|
68
69
|
|
|
70
|
+
// Identifier-level SQLi defense: every operator-supplied table name,
|
|
71
|
+
// column name, and cascade FK must pass safeSql.validateIdentifier
|
|
72
|
+
// before reaching SQL string concatenation. Without this gate a
|
|
73
|
+
// rule registered with `table: 'users"; DROP TABLE audit_log;--'`
|
|
74
|
+
// would break out of the quoted-identifier wrap and execute the
|
|
75
|
+
// embedded statement.
|
|
76
|
+
function _validateRuleIdentifier(value, label) {
|
|
77
|
+
try {
|
|
78
|
+
safeSql.validateIdentifier(value, { allowReserved: true });
|
|
79
|
+
} catch (e) {
|
|
80
|
+
throw _err("BAD_RULE",
|
|
81
|
+
label + " is not a safe SQL identifier: " + (e && e.message || String(e)));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
69
85
|
function _validateRule(rule) {
|
|
70
86
|
if (!rule || typeof rule !== "object") {
|
|
71
87
|
throw _err("BAD_RULE", "rule must be an object");
|
|
@@ -76,9 +92,11 @@ function _validateRule(rule) {
|
|
|
76
92
|
if (typeof rule.table !== "string" || rule.table.length === 0) {
|
|
77
93
|
throw _err("BAD_RULE", "rule.table (string) is required");
|
|
78
94
|
}
|
|
95
|
+
_validateRuleIdentifier(rule.table, "rule.table");
|
|
79
96
|
if (typeof rule.ageField !== "string" || rule.ageField.length === 0) {
|
|
80
97
|
throw _err("BAD_RULE", "rule.ageField (string) is required");
|
|
81
98
|
}
|
|
99
|
+
_validateRuleIdentifier(rule.ageField, "rule.ageField");
|
|
82
100
|
if (typeof rule.ttlMs !== "number" || !isFinite(rule.ttlMs) || rule.ttlMs <= 0) {
|
|
83
101
|
throw _err("BAD_RULE", "rule.ttlMs must be a positive finite number");
|
|
84
102
|
}
|
|
@@ -99,10 +117,16 @@ function _validateRule(rule) {
|
|
|
99
117
|
(typeof rule.softDeleteField !== "string" || rule.softDeleteField.length === 0)) {
|
|
100
118
|
throw _err("BAD_RULE", "rule.softDeleteField must be a non-empty string");
|
|
101
119
|
}
|
|
120
|
+
if (rule.softDeleteField !== undefined) {
|
|
121
|
+
_validateRuleIdentifier(rule.softDeleteField, "rule.softDeleteField");
|
|
122
|
+
}
|
|
102
123
|
if (rule.legalHoldField !== undefined &&
|
|
103
124
|
(typeof rule.legalHoldField !== "string" || rule.legalHoldField.length === 0)) {
|
|
104
125
|
throw _err("BAD_RULE", "rule.legalHoldField must be a non-empty string");
|
|
105
126
|
}
|
|
127
|
+
if (rule.legalHoldField !== undefined) {
|
|
128
|
+
_validateRuleIdentifier(rule.legalHoldField, "rule.legalHoldField");
|
|
129
|
+
}
|
|
106
130
|
if (rule.cascade !== undefined) {
|
|
107
131
|
if (!Array.isArray(rule.cascade) || rule.cascade.length === 0) {
|
|
108
132
|
throw _err("BAD_RULE", "rule.cascade must be a non-empty array of { table, foreignKey } entries");
|
|
@@ -113,6 +137,8 @@ function _validateRule(rule) {
|
|
|
113
137
|
typeof c.foreignKey !== "string" || c.foreignKey.length === 0) {
|
|
114
138
|
throw _err("BAD_RULE", "rule.cascade[" + ci + "] must be { table: string, foreignKey: string }");
|
|
115
139
|
}
|
|
140
|
+
_validateRuleIdentifier(c.table, "rule.cascade[" + ci + "].table");
|
|
141
|
+
_validateRuleIdentifier(c.foreignKey, "rule.cascade[" + ci + "].foreignKey");
|
|
116
142
|
}
|
|
117
143
|
}
|
|
118
144
|
if (rule.stages !== undefined) {
|
package/lib/router.js
CHANGED
|
@@ -651,6 +651,7 @@ class Router {
|
|
|
651
651
|
maxHeaderListPairs: 100, // allow:raw-byte-literal — CVE-2024-27983 CONTINUATION-flood cap
|
|
652
652
|
maxSettings: 32, // allow:raw-byte-literal — SETTINGS-frame entry ceiling
|
|
653
653
|
peerMaxConcurrentStreams: 100, // allow:raw-byte-literal — peer-side stream cap
|
|
654
|
+
maxOutstandingPings: 10, // allow:raw-byte-literal — CVE-2019-9512 ping-flood cap (pin to Node default rather than letting it drift)
|
|
654
655
|
unknownProtocolTimeout: C.TIME.seconds(10),
|
|
655
656
|
}, tlsOptions), requestHandler);
|
|
656
657
|
} else {
|
package/lib/scheduler.js
CHANGED
|
@@ -682,8 +682,63 @@ function create(opts) {
|
|
|
682
682
|
started = false;
|
|
683
683
|
}
|
|
684
684
|
|
|
685
|
-
|
|
685
|
+
// Shorthand for the common interval-based registration shape:
|
|
686
|
+
// register("rotate-keys", C.TIME.minutes(5), runFn)
|
|
687
|
+
// is equivalent to schedule({ name, every: 300000, run: runFn }).
|
|
688
|
+
// Operators wanting cron expressions or job-queue dispatch keep
|
|
689
|
+
// using schedule() — register() is the every-N-ms direct-function
|
|
690
|
+
// path. Returns the scheduler instance for method chaining.
|
|
691
|
+
function register(name, intervalMs, fn) {
|
|
692
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
693
|
+
throw _err("INVALID_NAME", "scheduler.register: name must be a non-empty string", true);
|
|
694
|
+
}
|
|
695
|
+
if (typeof intervalMs !== "number" || !Number.isFinite(intervalMs) || intervalMs < C.TIME.seconds(1)) {
|
|
696
|
+
throw _err("INVALID_SPEC",
|
|
697
|
+
"scheduler.register: intervalMs must be a finite number ≥ 1000", true);
|
|
698
|
+
}
|
|
699
|
+
if (typeof fn !== "function") {
|
|
700
|
+
throw _err("INVALID_SPEC", "scheduler.register: fn must be a function", true);
|
|
701
|
+
}
|
|
702
|
+
schedule({ name: name, every: intervalMs, run: fn });
|
|
703
|
+
return facade;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Operator-facing health surface — every task with its lifecycle
|
|
707
|
+
// counters plus an aggregate. Probes / dashboards / readiness gates
|
|
708
|
+
// get a single object they can serialize. This is `list()` plus
|
|
709
|
+
// started state and aggregate stats.
|
|
710
|
+
function getStatus() {
|
|
711
|
+
var taskList = list();
|
|
712
|
+
var aggregate = {
|
|
713
|
+
total: taskList.length,
|
|
714
|
+
running: 0,
|
|
715
|
+
withErrors: 0,
|
|
716
|
+
totalFires: 0,
|
|
717
|
+
totalMisses: 0,
|
|
718
|
+
nonLeaderSkips: 0,
|
|
719
|
+
tickClaimLost: 0,
|
|
720
|
+
};
|
|
721
|
+
for (var i = 0; i < taskList.length; i++) {
|
|
722
|
+
var t = taskList[i];
|
|
723
|
+
if (t.running) aggregate.running += 1;
|
|
724
|
+
if (t.lastError) aggregate.withErrors += 1;
|
|
725
|
+
aggregate.totalFires += t.fires || 0;
|
|
726
|
+
aggregate.totalMisses += t.misses || 0;
|
|
727
|
+
aggregate.nonLeaderSkips += t.nonLeaderSkips || 0;
|
|
728
|
+
aggregate.tickClaimLost += t.tickClaimLost || 0;
|
|
729
|
+
}
|
|
730
|
+
return {
|
|
731
|
+
started: started,
|
|
732
|
+
isLeader: _isLeaderHere(),
|
|
733
|
+
tasks: taskList,
|
|
734
|
+
aggregate: aggregate,
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
var facade = {
|
|
686
739
|
schedule: schedule,
|
|
740
|
+
register: register,
|
|
741
|
+
getStatus: getStatus,
|
|
687
742
|
start: start,
|
|
688
743
|
stop: stop,
|
|
689
744
|
list: list,
|
|
@@ -695,6 +750,7 @@ function create(opts) {
|
|
|
695
750
|
},
|
|
696
751
|
_resetForTest: _resetForTest,
|
|
697
752
|
};
|
|
753
|
+
return facade;
|
|
698
754
|
}
|
|
699
755
|
|
|
700
756
|
module.exports = {
|
package/lib/session.js
CHANGED
|
@@ -238,7 +238,7 @@ async function verify(token, verifyOpts) {
|
|
|
238
238
|
if ((nowMs - lastActivity) > idleMs) {
|
|
239
239
|
try {
|
|
240
240
|
audit.safeEmit({
|
|
241
|
-
action: "auth.session.expired_idle", outcome: "
|
|
241
|
+
action: "auth.session.expired_idle", outcome: "success",
|
|
242
242
|
metadata: { idleMs: nowMs - lastActivity, threshold: idleMs },
|
|
243
243
|
});
|
|
244
244
|
} catch (_ignored) { /* audit best-effort */ }
|
|
@@ -253,7 +253,7 @@ async function verify(token, verifyOpts) {
|
|
|
253
253
|
if ((nowMs - createdAt) > absMs) {
|
|
254
254
|
try {
|
|
255
255
|
audit.safeEmit({
|
|
256
|
-
action: "auth.session.expired_absolute", outcome: "
|
|
256
|
+
action: "auth.session.expired_absolute", outcome: "success",
|
|
257
257
|
metadata: { ageMs: nowMs - createdAt, threshold: absMs },
|
|
258
258
|
});
|
|
259
259
|
} catch (_ignored) { /* audit best-effort */ }
|
|
@@ -334,7 +334,7 @@ async function verify(token, verifyOpts) {
|
|
|
334
334
|
try {
|
|
335
335
|
audit.safeEmit({
|
|
336
336
|
action: "auth.session.fingerprint_drift",
|
|
337
|
-
outcome: "
|
|
337
|
+
outcome: "success",
|
|
338
338
|
metadata: { hasUserId: !!unsealed.userId,
|
|
339
339
|
anomalyScore: fingerprintAnomalyScore },
|
|
340
340
|
});
|
package/lib/ssrf-guard.js
CHANGED
|
@@ -140,10 +140,25 @@ var CLOUD_METADATA_IPS = [
|
|
|
140
140
|
|
|
141
141
|
function _ipv4ToInt(ip) {
|
|
142
142
|
var parts = ip.split(".");
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
143
|
+
if (parts.length !== 4) return NaN;
|
|
144
|
+
var nums = [0, 0, 0, 0];
|
|
145
|
+
for (var i = 0; i < 4; i += 1) {
|
|
146
|
+
var s = parts[i];
|
|
147
|
+
// Strict octet validation: each segment must be 1-3 ASCII digits
|
|
148
|
+
// representing 0-255. The previous `parts[i] | 0` coerced
|
|
149
|
+
// anything non-numeric to 0 silently — exposed via cidrContains
|
|
150
|
+
// (network-allowlist) where a typo'd CIDR could collapse to
|
|
151
|
+
// 0.0.0.0/16 with no signal.
|
|
152
|
+
if (typeof s !== "string" || s.length === 0 || s.length > 3) return NaN;
|
|
153
|
+
if (!/^\d{1,3}$/.test(s)) return NaN;
|
|
154
|
+
var n = parseInt(s, 10);
|
|
155
|
+
if (n < 0 || n > 255) return NaN;
|
|
156
|
+
nums[i] = n;
|
|
157
|
+
}
|
|
158
|
+
return ((nums[0] << 24) >>> 0) +
|
|
159
|
+
(nums[1] << 16) +
|
|
160
|
+
(nums[2] << 8) +
|
|
161
|
+
nums[3];
|
|
147
162
|
}
|
|
148
163
|
|
|
149
164
|
function _ipv6ToBytes(ip) {
|
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/vault/index.js
CHANGED
|
@@ -290,10 +290,13 @@ function getMode() {
|
|
|
290
290
|
return currentMode;
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
var vaultAad = require("../vault-aad");
|
|
294
|
+
|
|
293
295
|
module.exports = {
|
|
294
296
|
init: init,
|
|
295
297
|
seal: seal,
|
|
296
298
|
unseal: unseal,
|
|
299
|
+
aad: vaultAad,
|
|
297
300
|
getKeysJson: getKeysJson,
|
|
298
301
|
getCurrentPassphrase: getCurrentPassphrase,
|
|
299
302
|
getMode: getMode,
|