@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.
Files changed (100) hide show
  1. package/CHANGELOG.md +41 -1
  2. package/NOTICE +17 -1
  3. package/README.md +4 -3
  4. package/index.js +15 -0
  5. package/lib/asyncapi-bindings.js +160 -0
  6. package/lib/asyncapi-traits.js +143 -0
  7. package/lib/asyncapi.js +531 -0
  8. package/lib/audit-sign.js +1 -1
  9. package/lib/audit.js +68 -2
  10. package/lib/auth/acr-vocabulary.js +265 -0
  11. package/lib/auth/auth-time-tracker.js +111 -0
  12. package/lib/auth/elevation-grant.js +306 -0
  13. package/lib/auth/jwt.js +13 -0
  14. package/lib/auth/lockout.js +16 -3
  15. package/lib/auth/oauth.js +15 -1
  16. package/lib/auth/password.js +22 -2
  17. package/lib/auth/sd-jwt-vc-issuer.js +2 -2
  18. package/lib/auth/sd-jwt-vc.js +7 -2
  19. package/lib/auth/step-up-policy.js +335 -0
  20. package/lib/auth/step-up.js +445 -0
  21. package/lib/break-glass.js +53 -14
  22. package/lib/cache-redis.js +1 -1
  23. package/lib/cache.js +6 -1
  24. package/lib/cli.js +3 -3
  25. package/lib/cluster.js +24 -1
  26. package/lib/compliance-ai-act-logging.js +190 -0
  27. package/lib/compliance-ai-act-prohibited.js +205 -0
  28. package/lib/compliance-ai-act-risk.js +189 -0
  29. package/lib/compliance-ai-act-transparency.js +200 -0
  30. package/lib/compliance-ai-act.js +558 -0
  31. package/lib/compliance.js +12 -2
  32. package/lib/config-drift.js +2 -2
  33. package/lib/crypto-field.js +21 -1
  34. package/lib/crypto.js +114 -1
  35. package/lib/db.js +35 -4
  36. package/lib/dev.js +30 -3
  37. package/lib/dual-control.js +19 -1
  38. package/lib/external-db.js +10 -0
  39. package/lib/file-upload.js +30 -3
  40. package/lib/flag-cache.js +136 -0
  41. package/lib/flag-evaluation-context.js +135 -0
  42. package/lib/flag-providers.js +279 -0
  43. package/lib/flag-targeting.js +210 -0
  44. package/lib/flag.js +284 -0
  45. package/lib/guard-all.js +33 -16
  46. package/lib/guard-csv.js +16 -2
  47. package/lib/guard-html.js +35 -0
  48. package/lib/guard-svg.js +20 -0
  49. package/lib/http-client.js +57 -11
  50. package/lib/inbox.js +391 -0
  51. package/lib/log-stream-syslog.js +8 -0
  52. package/lib/log-stream.js +1 -1
  53. package/lib/mail-arc-sign.js +372 -0
  54. package/lib/mail-auth.js +2 -0
  55. package/lib/mail.js +40 -0
  56. package/lib/middleware/ai-act-disclosure.js +166 -0
  57. package/lib/middleware/asyncapi-serve.js +136 -0
  58. package/lib/middleware/attach-user.js +25 -2
  59. package/lib/middleware/bearer-auth.js +71 -6
  60. package/lib/middleware/body-parser.js +13 -0
  61. package/lib/middleware/cors.js +10 -0
  62. package/lib/middleware/csrf-protect.js +34 -3
  63. package/lib/middleware/dpop.js +3 -3
  64. package/lib/middleware/flag-context.js +76 -0
  65. package/lib/middleware/host-allowlist.js +1 -1
  66. package/lib/middleware/index.js +15 -0
  67. package/lib/middleware/openapi-serve.js +143 -0
  68. package/lib/middleware/require-aal.js +2 -2
  69. package/lib/middleware/require-step-up.js +186 -0
  70. package/lib/middleware/trace-propagate.js +1 -1
  71. package/lib/mtls-ca.js +23 -29
  72. package/lib/mtls-engine-default.js +21 -1
  73. package/lib/network-tls.js +21 -6
  74. package/lib/object-store/sigv4-bucket-ops.js +41 -0
  75. package/lib/observability-otlp-exporter.js +35 -2
  76. package/lib/openapi-paths-builder.js +248 -0
  77. package/lib/openapi-schema-walk.js +192 -0
  78. package/lib/openapi-security.js +169 -0
  79. package/lib/openapi-yaml.js +154 -0
  80. package/lib/openapi.js +443 -0
  81. package/lib/outbox.js +3 -3
  82. package/lib/permissions.js +10 -1
  83. package/lib/pqc-agent.js +22 -1
  84. package/lib/pqc-software.js +195 -0
  85. package/lib/pubsub.js +8 -4
  86. package/lib/redact.js +26 -1
  87. package/lib/retention.js +26 -0
  88. package/lib/router.js +1 -0
  89. package/lib/scheduler.js +57 -1
  90. package/lib/session.js +3 -3
  91. package/lib/ssrf-guard.js +19 -4
  92. package/lib/static.js +12 -0
  93. package/lib/totp.js +16 -0
  94. package/lib/vault/index.js +3 -0
  95. package/lib/vault-aad.js +259 -0
  96. package/lib/vendor/MANIFEST.json +29 -0
  97. package/lib/vendor/noble-post-quantum.cjs +18 -0
  98. package/lib/ws-client.js +978 -0
  99. package/package.json +1 -1
  100. 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("system.pubsub.publish-failed", {
383
- channel: channel, error: (e && e.message) || String(e),
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("system.pubsub.publish", {
391
- channel: channel, localDispatched: local, remoteWritten: remote,
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)) return allDetectors[i].replacement;
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
- return {
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: "warning",
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: "warning",
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: "warning",
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
- return ((parts[0] | 0) << 24 >>> 0) +
144
- ((parts[1] | 0) << 16) +
145
- ((parts[2] | 0) << 8) +
146
- (parts[3] | 0);
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 + ")");
@@ -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,