@blamejs/core 0.6.13 → 0.6.21

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 (47) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/NOTICE +16 -0
  3. package/README.md +27 -18
  4. package/index.js +12 -0
  5. package/lib/archive.js +8 -7
  6. package/lib/audit.js +4 -0
  7. package/lib/auth/password.js +449 -4
  8. package/lib/bundler.js +8 -8
  9. package/lib/cache.js +105 -20
  10. package/lib/cli.js +598 -4
  11. package/lib/config-drift.js +309 -0
  12. package/lib/crypto-field.js +37 -0
  13. package/lib/crypto.js +8 -0
  14. package/lib/db-query.js +21 -2
  15. package/lib/db.js +32 -2
  16. package/lib/dual-control.js +475 -0
  17. package/lib/file-type.js +265 -0
  18. package/lib/framework-schema.js +38 -6
  19. package/lib/http-client-cookie-jar.js +117 -17
  20. package/lib/http-client.js +81 -3
  21. package/lib/internal-sha1-hibp.js +34 -0
  22. package/lib/mail.js +5 -4
  23. package/lib/middleware/csp-nonce.js +7 -4
  24. package/lib/middleware/index.js +2 -0
  25. package/lib/middleware/network-allowlist.js +199 -0
  26. package/lib/network-dns.js +564 -0
  27. package/lib/network-heartbeat.js +290 -0
  28. package/lib/network-nts.js +552 -0
  29. package/lib/network-proxy.js +246 -0
  30. package/lib/network-tls.js +326 -0
  31. package/lib/network.js +233 -0
  32. package/lib/ntp-check.js +50 -4
  33. package/lib/object-store/azure-blob.js +16 -42
  34. package/lib/pagination.js +136 -76
  35. package/lib/parsers/index.js +16 -2
  36. package/lib/parsers/safe-ini.js +273 -0
  37. package/lib/permissions.js +223 -9
  38. package/lib/pqc-agent.js +4 -4
  39. package/lib/retention.js +439 -0
  40. package/lib/security-assert.js +368 -0
  41. package/lib/session.js +138 -8
  42. package/lib/ssrf-guard.js +9 -0
  43. package/lib/vault/index.js +3 -3
  44. package/lib/vendor/MANIFEST.json +12 -0
  45. package/lib/vendor/common-passwords-top-10000.txt +10000 -0
  46. package/package.json +3 -2
  47. package/sbom.cyclonedx.json +61 -0
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ /**
3
+ * internal-sha1-hibp — SHA-1 hex digest, RESTRICTED to the
4
+ * HaveIBeenPwned k-anonymity API caller (lib/auth/password.js policy).
5
+ *
6
+ * SHA-1 is broken for collision-resistance and trivially extendable;
7
+ * the framework MUST NOT use it for storage, signing, message
8
+ * authentication, key derivation, fingerprinting, or any other
9
+ * security-relevant path. The HIBP API mandates SHA-1 for backwards
10
+ * compatibility with leaked-password corpora; that's the only reason
11
+ * this exists.
12
+ *
13
+ * Why not export from b.crypto:
14
+ * - Public exports invite cargo-cult use. A future contributor
15
+ * would search `b.crypto.sha1*` and slot it into "I just need
16
+ * a quick hash" code — re-introducing SHA-1 into a crypto-
17
+ * relevant path the framework spent every other primitive
18
+ * keeping out.
19
+ * - The single legitimate caller (auth/password.js) requires it
20
+ * for HIBP interop only. Restricting it to that module via a
21
+ * filename + comment-block gate keeps the surface honest.
22
+ *
23
+ * If a SECOND legitimate use case for SHA-1 ever emerges (operator
24
+ * needs to interop with another SHA-1-mandated API), this module
25
+ * stays internal — the new caller imports it directly. Public
26
+ * `b.crypto.sha1*` is permanently off the table.
27
+ */
28
+ var nodeCrypto = require("node:crypto");
29
+
30
+ function sha1Hex(data) {
31
+ return nodeCrypto.createHash("sha1").update(data).digest("hex");
32
+ }
33
+
34
+ module.exports = { sha1Hex: sha1Hex };
package/lib/mail.js CHANGED
@@ -403,10 +403,11 @@ function _buildRfc822(message) {
403
403
  body = inner.body;
404
404
  } else {
405
405
  // multipart/mixed: first part is the body (single or alternative),
406
- // subsequent parts are the attachments. Inline disposition + Content-ID
407
- // is interpreted correctly by every major client even inside mixed;
408
- // operators with strict-RFC-2387 multipart/related needs subscribe
409
- // to a future patch when demand surfaces.
406
+ // subsequent parts are the attachments. Inline disposition +
407
+ // Content-ID is interpreted correctly by every major client even
408
+ // inside mixed. Operators needing strict-RFC-2387 multipart/related
409
+ // wrap the body via the mail.transports interface and pass a
410
+ // content-type override.
410
411
  var mixedBoundary = _newBoundary("mixed");
411
412
  headers.push('Content-Type: multipart/mixed; boundary="' + mixedBoundary + '"');
412
413
  var parts = [];
@@ -248,10 +248,13 @@ function create(opts) {
248
248
  var property = (typeof opts.property === "string" && opts.property.length > 0) ? opts.property : "cspNonce";
249
249
  var always = !!opts.always;
250
250
 
251
- // Placeholder for the cacheable-render pattern. Per-instance random
252
- // by default — multiple cspNonce instances in the same process get
253
- // different placeholders, and the literal string never appears in
254
- // source so operator content can't accidentally collide.
251
+ // Token string used by the cacheable-render pattern: templates render
252
+ // with `cspNonce: nonceMw.PLACEHOLDER`, the rendered HTML is cached,
253
+ // then nonceMw.substitute(html, req) swaps in the per-request nonce
254
+ // at serve time. Per-instance random by default — multiple cspNonce
255
+ // instances in the same process get different placeholders, and the
256
+ // literal string never appears in source so operator content can't
257
+ // accidentally collide.
255
258
  //
256
259
  // Operators with caches that persist across process restarts (Redis,
257
260
  // cluster backend) pass `opts.placeholder` to pin a stable token —
@@ -34,6 +34,7 @@ module.exports = {
34
34
  requestLog: require("./request-log").create,
35
35
  apiEncrypt: require("./api-encrypt"),
36
36
  dbRoleFor: require("./db-role-for").create,
37
+ networkAllowlist: require("./network-allowlist").create,
37
38
 
38
39
  // Module exports for advanced use (constants, raw factory access)
39
40
  _modules: {
@@ -54,5 +55,6 @@ module.exports = {
54
55
  requestLog: require("./request-log"),
55
56
  apiEncrypt: require("./api-encrypt"),
56
57
  dbRoleFor: require("./db-role-for"),
58
+ networkAllowlist: require("./network-allowlist"),
57
59
  },
58
60
  };
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ /**
3
+ * network-allowlist — restrict an opt-named set of paths to operator-
4
+ * approved CIDR ranges.
5
+ *
6
+ * Path-based admin gates (perms.require, requireAuth) prevent UNAUTHORIZED
7
+ * users from reaching sensitive routes. They do NOT prevent the route
8
+ * from being REACHABLE from the public internet — a credential leak +
9
+ * a path-only gate is full compromise. Operators with a clear admin/
10
+ * non-admin network split want a network-layer fence on top of the
11
+ * application-layer gate.
12
+ *
13
+ * The cleanest place for that fence is the reverse proxy / NACL. This
14
+ * middleware is the in-process equivalent for operators who don't have
15
+ * separate infrastructure for it (small deploys, single-process apps,
16
+ * the wiki example default).
17
+ *
18
+ * var fence = b.middleware.networkAllowlist({
19
+ * paths: ["/admin", "/admin/", "/healthz/internal"],
20
+ * allowedCidrs: ["10.0.0.0/8", "192.168.0.0/16", "::1/128"],
21
+ * trustProxy: true, // honour x-forwarded-for; default false
22
+ * denyStatus: 404, // default — reveal nothing about the gate
23
+ * denyBody: "Not Found", // default
24
+ * audit: b.audit, // default: null — emits network.gate.denied
25
+ * });
26
+ *
27
+ * router.use(fence);
28
+ *
29
+ * Behaviour:
30
+ * - The middleware is path-scoped: requests whose pathname doesn't
31
+ * start with any of `paths` pass through unchanged. Hot-path-cheap.
32
+ * - Requests on a gated path get their client IP resolved through
33
+ * b.requestHelpers.clientIp(req, { trustProxy }) — same trust
34
+ * model as every other middleware that reads client IP.
35
+ * - The IP is checked against the CIDR allowlist using
36
+ * b.ssrfGuard.cidrContains. A miss returns denyStatus + denyBody
37
+ * and audits the rejection. Default 404 hides the gate's
38
+ * existence from probes.
39
+ *
40
+ * Validation policy:
41
+ * - opts: throw at create() time on bad shape (not-array paths /
42
+ * allowedCidrs, non-CIDR strings, denyStatus outside 4xx/5xx).
43
+ * - Per-request: a request that looks malformed (no socket, no
44
+ * headers) gets denied — fail closed.
45
+ */
46
+
47
+ var lazyRequire = require("../lazy-require");
48
+ var requestHelpers = require("../request-helpers");
49
+ var ssrfGuard = require("../ssrf-guard");
50
+ var validateOpts = require("../validate-opts");
51
+ var { defineClass } = require("../framework-error");
52
+
53
+ var audit = lazyRequire(function () { return require("../audit"); });
54
+
55
+ var NetworkAllowlistError = defineClass("NetworkAllowlistError", { alwaysPermanent: true });
56
+ var _err = NetworkAllowlistError.factory;
57
+
58
+ function _validateCidr(cidr) {
59
+ // ssrfGuard.cidrContains tolerates any string at runtime (returns
60
+ // false on garbage), but the operator means business at config time
61
+ // — a typo'd CIDR silently disables that allow entry. Verify the
62
+ // shape now: <ipv4-or-ipv6>/<prefix-length>.
63
+ if (typeof cidr !== "string" || cidr.length === 0) return false;
64
+ var slash = cidr.indexOf("/");
65
+ if (slash < 1 || slash >= cidr.length - 1) return false;
66
+ var prefix = parseInt(cidr.slice(slash + 1), 10);
67
+ if (!isFinite(prefix) || prefix < 0) return false;
68
+ // Smoke-test the implementation against a known value — if it
69
+ // throws, the cidr is malformed.
70
+ try { ssrfGuard.cidrContains(cidr, "127.0.0.1"); return true; }
71
+ catch (_e) { return false; }
72
+ }
73
+
74
+ function create(opts) {
75
+ opts = opts || {};
76
+ validateOpts(opts, [
77
+ "paths", "allowedCidrs", "deniedCidrs", "trustProxy",
78
+ "denyStatus", "denyBody", "audit",
79
+ ], "middleware.networkAllowlist");
80
+
81
+ if (!Array.isArray(opts.paths) || opts.paths.length === 0) {
82
+ throw _err("BAD_OPT", "paths must be a non-empty array of pathname prefixes");
83
+ }
84
+ for (var pi = 0; pi < opts.paths.length; pi++) {
85
+ if (typeof opts.paths[pi] !== "string" || opts.paths[pi].charAt(0) !== "/") {
86
+ throw _err("BAD_OPT",
87
+ "paths[" + pi + "] must be a string starting with '/', got " + JSON.stringify(opts.paths[pi]));
88
+ }
89
+ }
90
+ if (!Array.isArray(opts.allowedCidrs) || opts.allowedCidrs.length === 0) {
91
+ throw _err("BAD_OPT", "allowedCidrs must be a non-empty array of CIDR strings");
92
+ }
93
+ for (var ci = 0; ci < opts.allowedCidrs.length; ci++) {
94
+ if (!_validateCidr(opts.allowedCidrs[ci])) {
95
+ throw _err("BAD_OPT",
96
+ "allowedCidrs[" + ci + "] is not a valid CIDR, got " + JSON.stringify(opts.allowedCidrs[ci]));
97
+ }
98
+ }
99
+ // deniedCidrs takes precedence over allowedCidrs (deny-then-allow)
100
+ // — useful when an operator wants "10.0.0.0/8 except 10.0.99.0/24".
101
+ // Default empty (no deny rules).
102
+ var deniedCidrs = Array.isArray(opts.deniedCidrs) ? opts.deniedCidrs.slice() : [];
103
+ for (var di = 0; di < deniedCidrs.length; di++) {
104
+ if (!_validateCidr(deniedCidrs[di])) {
105
+ throw _err("BAD_OPT",
106
+ "deniedCidrs[" + di + "] is not a valid CIDR, got " + JSON.stringify(deniedCidrs[di]));
107
+ }
108
+ }
109
+
110
+ var paths = opts.paths.slice();
111
+ var allowedCidrs = opts.allowedCidrs.slice();
112
+ var trustProxy = opts.trustProxy === true || typeof opts.trustProxy === "number"
113
+ ? opts.trustProxy : false;
114
+ var denyStatus = typeof opts.denyStatus === "number" ? opts.denyStatus : 404;
115
+ if (denyStatus < 400 || denyStatus >= 600 || Math.floor(denyStatus) !== denyStatus) {
116
+ throw _err("BAD_OPT", "denyStatus must be a 4xx or 5xx integer, got " + denyStatus);
117
+ }
118
+ var denyBody = typeof opts.denyBody === "string" ? opts.denyBody : "Not Found";
119
+ var auditOn = opts.audit !== false && opts.audit != null;
120
+ var auditInstance = opts.audit === true ? null : opts.audit; // null → use lazy-required default
121
+
122
+ function _emitDeny(req, ip, route) {
123
+ if (!auditOn) return;
124
+ var sink = auditInstance || audit();
125
+ try {
126
+ sink.safeEmit({
127
+ action: "network.gate.denied",
128
+ outcome: "denied",
129
+ actor: requestHelpers.extractActorContext(req),
130
+ resource: { kind: "http.path", id: route },
131
+ reason: "ip-not-in-allowlist",
132
+ metadata: { clientIp: ip, allowedCidrs: allowedCidrs },
133
+ });
134
+ } catch (_e) { /* audit best-effort */ }
135
+ }
136
+
137
+ function _matchesPath(pathname) {
138
+ for (var i = 0; i < paths.length; i++) {
139
+ var prefix = paths[i];
140
+ if (pathname === prefix) return true;
141
+ // Boundary-aware: "/admin" matches "/admin" + "/admin/..." but
142
+ // not "/administer" — prevents accidental shadowing of
143
+ // similarly-named public routes.
144
+ if (pathname.length > prefix.length &&
145
+ pathname.indexOf(prefix) === 0 &&
146
+ (prefix.charAt(prefix.length - 1) === "/" || pathname.charAt(prefix.length) === "/")) {
147
+ return true;
148
+ }
149
+ }
150
+ return false;
151
+ }
152
+
153
+ return function networkAllowlist(req, res, next) {
154
+ var pathname = req.pathname || (req.url || "").split("?")[0];
155
+ if (!_matchesPath(pathname)) return next();
156
+
157
+ var ip = requestHelpers.clientIp(req, { trustProxy: trustProxy });
158
+ if (!ip) {
159
+ // Fail closed: a request we can't even derive an IP for shouldn't
160
+ // bypass the gate.
161
+ _emitDeny(req, "<unknown>", pathname);
162
+ res.writeHead(denyStatus, { "Content-Type": "text/plain" });
163
+ res.end(denyBody);
164
+ return;
165
+ }
166
+
167
+ // Deny-then-allow precedence: an explicit deny entry beats any
168
+ // allow entry that would otherwise match. Operators use this for
169
+ // "10.0.0.0/8 EXCEPT 10.0.99.0/24" patterns.
170
+ for (var dii = 0; dii < deniedCidrs.length; dii++) {
171
+ try {
172
+ if (ssrfGuard.cidrContains(deniedCidrs[dii], ip)) {
173
+ _emitDeny(req, ip, pathname);
174
+ res.writeHead(denyStatus, { "Content-Type": "text/plain" });
175
+ res.end(denyBody);
176
+ return;
177
+ }
178
+ } catch (_e) { /* skip malformed at runtime — caught at config */ }
179
+ }
180
+ var allowed = false;
181
+ for (var i = 0; i < allowedCidrs.length; i++) {
182
+ try {
183
+ if (ssrfGuard.cidrContains(allowedCidrs[i], ip)) { allowed = true; break; }
184
+ } catch (_e) { /* skip malformed at runtime — caught at config */ }
185
+ }
186
+ if (!allowed) {
187
+ _emitDeny(req, ip, pathname);
188
+ res.writeHead(denyStatus, { "Content-Type": "text/plain" });
189
+ res.end(denyBody);
190
+ return;
191
+ }
192
+ return next();
193
+ };
194
+ }
195
+
196
+ module.exports = {
197
+ create: create,
198
+ NetworkAllowlistError: NetworkAllowlistError,
199
+ };