@blamejs/core 0.7.22 → 0.7.24
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 +4 -0
- package/lib/network-dns.js +75 -4
- package/lib/retention.js +43 -2
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.7.x
|
|
10
10
|
|
|
11
|
+
- **0.7.24** (2026-05-05) — `b.retention.complianceFloor` accessor for regulatory minimum-retention windows. **Surface**: `b.retention.complianceFloor(posture, candidateTtlMs?)` returns the effective TTL that meets-or-exceeds the regulatory floor; throws `retention/unknown-posture` on unknown names. **`b.retention.COMPLIANCE_RETENTION_FLOOR_MS`** exposes the per-posture minimums for compliance audit visibility: `pci-dss` 365 days (PCI-DSS Req 10.7.1), `hipaa` 6 years (45 CFR §164.316(b)(2)), `sox` 7 years (Sarbanes-Oxley §802), `soc2` 1 year, `dora` 5 years (DORA Article 17). When `candidateTtlMs` exceeds the floor it wins; when it's below, the floor takes over. Smoke 8488 / wiki e2e 178 / Linux container smoke 8488 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
|
|
12
|
+
|
|
13
|
+
- **0.7.23** (2026-05-05) — DNS-over-HTTPS default-on. **Default-on DoH for outbound DNS** — `lib/network-dns.js` now uses Cloudflare DoH (`https://cloudflare-dns.com/dns-query`) for hostname resolution by default when neither `useDnsOverHttps()` nor `useDnsOverTls()` has been called explicitly. Privacy-respecting (encrypted DNS over TLS to a non-ISP resolver), aligned with Core Rule §3 ("security defaults are not opt-in"). **Local-form hosts route through node:dns** — RFC 6761 / 6762 special-form names (`localhost`, `*.localhost`, `*.local`, `*.test`, `*.invalid`, `*.internal`, `*.intranet`, `*.lan`, `*.home`, `*.corp`) AND IP literals skip DoH and use node:dns directly so `/etc/hosts` lookups + dev workflows continue to resolve correctly. **Operator opt-out via `b.network.dns.useSystemResolver()`** — split-horizon / internal-DNS deployments call this once at boot and every lookup routes through the OS resolver thereafter. **Env override `BLAMEJS_DNS_TRANSPORT`** — operators set `system` (force OS resolver), `dot` (force Cloudflare DNS-over-TLS at 1.1.1.1:853), or leave unset (default DoH). Smoke 8478 / wiki e2e 178 / Linux container smoke 8478 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
|
|
14
|
+
|
|
11
15
|
- **0.7.22** (2026-05-05) — DKIM dual-signer + soc2-cc7 → soc2 posture rename. **`b.mail.dkim.dualSigner`** — RFC 8463 §3 transition signer that produces messages with BOTH a legacy RSA-SHA-256 DKIM-Signature AND an Ed25519-SHA-256 DKIM-Signature header. Receivers without Ed25519 support validate the RSA signature; receivers that prefer Ed25519 validate the post-quantum-friendlier signature. Operators rolling off RSA-SHA-256 wire `b.mail.dkim.dualSigner({ domain, rsa: { selector, privateKey }, eddsa: { selector, privateKey } })` and pass the result anywhere a regular DKIM signer is accepted; `sign()` produces a wire with two `DKIM-Signature` headers. Both signers are constructed eagerly at create-time (configuration errors surface at boot, not at first send). **soc2-cc7 → soc2 posture rename** — every guard's compliance posture name changes from `"soc2-cc7"` to `"soc2"`. The CC7-specific scoping was misleading (SOC 2 controls span CC1–CC9; the existing posture wasn't CC7-specific). Operators with `compliancePosture: "soc2-cc7"` MUST update to `compliancePosture: "soc2"` — the old name now throws `unknown compliance posture`. Smoke 8478 / wiki e2e 178 / Linux container smoke 8478 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
|
|
12
16
|
|
|
13
17
|
- **0.7.21** (2026-05-05) — small primitive batch (5 fixes): TLS 1.3 framework-wide minimum, `b.safeRedirect`, `b.pick`, audit-sign legacy compat-shim removed, webhook PQC signatures emit base64url. **Framework-wide TLS 1.3 minimum** — `index.js` sets `tls.DEFAULT_MIN_VERSION = "TLSv1.3"` once at boot, before any framework module loads `node:tls`. Applies to every TLS socket (outbound `https.request` / mail SMTP+STARTTLS / Redis-Postgres-Mongo TLS / `b.httpClient`, AND inbound `https.createServer` when blamejs is the listener). Per-call override still works for legacy peers. **`b.safeRedirect`** — open-redirect (CWE-601) defense. `b.safeRedirect.resolve(rawTarget, { allowedOrigins, allowedHosts, fallback })` returns the safe URL (or fallback). Refuses protocol-relative (`//attacker.com`), backslash variants (`\\\\attacker.com`), control-char-laden (CRLF header injection), and `data:` / `javascript:` schemes; same-origin paths (`/dashboard`) and fragments (`#x`) pass through; full URLs require explicit allowlist. Operator drops the result straight into `res.writeHead(302, { Location: ... })`. **`b.pick`** — mass-assignment (CWE-915 / OWASP API3:2023) defense. `b.pick(req.body, ["a", "b", ["nested", ["sub1"]]])` returns a NEW object with only allowlisted keys; prototype-pollution keys (`__proto__` / `constructor` / `prototype`) ALWAYS stripped even if listed. `opts.onUnknown: "throw"` rejects unknown keys instead of silently dropping. Nested allowlist syntax for object-shaped fields. **Audit-sign legacy compat-shim removed** — `lib/audit-sign.js` no longer falls back to `ml-dsa-87` for key files missing the `algorithm` field. Throws `KEY_FILE_MISSING_ALG` / `UNWRAPPED_MISSING_ALG` at load time; operators with legacy files rotate the key (deletes + regenerates) or hand-edit to add `"algorithm": "slh-dsa-shake-256f"`. Pre-v1 compat-shim sweep per the no-pre-v1-compat rule. **Webhook PQC signatures emit base64url** — `b.webhook` now signs to base64url (was hex). SLH-DSA-SHAKE-256f signatures are ~29.5 KB binary → ~40 KB base64url vs ~59 KB hex; the hex form blew past nginx default 8 KB / Cloudflare default 16 KB / many CDN edge limits. Verification accepts EITHER encoding for a transition window — base64url-shaped sig values decode as base64url; hex-shaped values decode as hex. Smoke 8478 / wiki e2e 178 / Linux container smoke 8478 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
|
package/lib/network-dns.js
CHANGED
|
@@ -24,6 +24,7 @@ var IPV6_HEX_GROUPS = C.BYTES.bytes(8); // IPv6 16-bit hex groups
|
|
|
24
24
|
var HEX_RADIX = C.BYTES.bytes(16); // parseInt / toString radix-16
|
|
25
25
|
|
|
26
26
|
var observability = lazyRequire(function () { return require("./observability"); });
|
|
27
|
+
var safeEnv = require("./parsers/safe-env");
|
|
27
28
|
|
|
28
29
|
var STATE = {
|
|
29
30
|
servers: null,
|
|
@@ -34,8 +35,51 @@ var STATE = {
|
|
|
34
35
|
cacheNegativeTtlMs: 0,
|
|
35
36
|
doh: null,
|
|
36
37
|
dot: null,
|
|
38
|
+
// Default-on secure DNS (DoH via Cloudflare) when neither doh nor dot
|
|
39
|
+
// is operator-configured AND no opt-out env var is set. Operators
|
|
40
|
+
// who explicitly want the system resolver call useSystemResolver()
|
|
41
|
+
// or set BLAMEJS_DNS_TRANSPORT=system. Default-on per Core Rule §3
|
|
42
|
+
// ("security defaults are not opt-in").
|
|
43
|
+
systemResolver: false,
|
|
37
44
|
};
|
|
38
45
|
|
|
46
|
+
// Default DoH provider when nothing is configured. Cloudflare's
|
|
47
|
+
// 1.1.1.1 service is privacy-respecting, free, and global.
|
|
48
|
+
var DEFAULT_DOH_URL = "https://cloudflare-dns.com/dns-query";
|
|
49
|
+
|
|
50
|
+
// RFC 6761 + RFC 6762 + operator-internal special-form host names that
|
|
51
|
+
// public DoH/DoT providers don't resolve. Local lookups for these names
|
|
52
|
+
// route through node:dns which honours /etc/hosts + LDH locally.
|
|
53
|
+
var LOCAL_SUFFIXES = [".localhost", ".local", ".test", ".invalid",
|
|
54
|
+
".internal", ".intranet", ".lan", ".home", ".corp"];
|
|
55
|
+
function _isLocalFormHost(host) {
|
|
56
|
+
if (typeof host !== "string" || host.length === 0) return true;
|
|
57
|
+
if (host === "localhost") return true;
|
|
58
|
+
// IP literal — skip DNS resolution entirely (caller passes through).
|
|
59
|
+
if (net.isIP(host)) return true;
|
|
60
|
+
var lc = host.toLowerCase();
|
|
61
|
+
for (var i = 0; i < LOCAL_SUFFIXES.length; i += 1) {
|
|
62
|
+
if (lc.length > LOCAL_SUFFIXES[i].length &&
|
|
63
|
+
lc.slice(-LOCAL_SUFFIXES[i].length) === LOCAL_SUFFIXES[i]) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function _ensureSecureDefault() {
|
|
71
|
+
if (STATE.doh || STATE.dot || STATE.systemResolver) return;
|
|
72
|
+
var override = safeEnv.readVar("BLAMEJS_DNS_TRANSPORT");
|
|
73
|
+
if (override === "system") { STATE.systemResolver = true; return; }
|
|
74
|
+
if (override === "dot") {
|
|
75
|
+
// Cloudflare 1.1.1.1 over TLS, port 853.
|
|
76
|
+
STATE.dot = { host: "1.1.1.1", port: 853, servername: "1.1.1.1", ca: null }; // allow:raw-byte-literal — IANA-assigned DoT port
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// Default: DoH via Cloudflare.
|
|
80
|
+
STATE.doh = { url: DEFAULT_DOH_URL, method: null, ca: null };
|
|
81
|
+
}
|
|
82
|
+
|
|
39
83
|
var POSITIVE_CACHE = new Map();
|
|
40
84
|
var NEGATIVE_CACHE = new Map();
|
|
41
85
|
|
|
@@ -144,6 +188,20 @@ function setCacheTtlMs(ms, negativeMs) {
|
|
|
144
188
|
if (ms === 0) _clearCache();
|
|
145
189
|
}
|
|
146
190
|
|
|
191
|
+
// Operator opt-out from the default-on secure DNS. After calling this,
|
|
192
|
+
// every lookup routes through node:dns (the OS resolver). Equivalent
|
|
193
|
+
// to setting BLAMEJS_DNS_TRANSPORT=system. Operators on internal-DNS
|
|
194
|
+
// deployments (split-horizon, custom resolver appliances) need this
|
|
195
|
+
// to keep their hostnames resolving locally.
|
|
196
|
+
function useSystemResolver() {
|
|
197
|
+
STATE.doh = null;
|
|
198
|
+
STATE.dot = null;
|
|
199
|
+
STATE.systemResolver = true;
|
|
200
|
+
_resetDotPool();
|
|
201
|
+
_clearCache();
|
|
202
|
+
_emitObs("network.dns.system_resolver.set", {});
|
|
203
|
+
}
|
|
204
|
+
|
|
147
205
|
function useDnsOverHttps(opts) {
|
|
148
206
|
opts = opts || {};
|
|
149
207
|
validateOpts(opts, ["provider", "url", "method", "ca"], "dns.useDnsOverHttps");
|
|
@@ -514,15 +572,27 @@ async function lookup(host, opts) {
|
|
|
514
572
|
}
|
|
515
573
|
_emitObs("network.dns.lookup.requested", { family: cacheKey });
|
|
516
574
|
var startMs = _now();
|
|
575
|
+
// Resolve secure-DNS default on first use. Idempotent.
|
|
576
|
+
_ensureSecureDefault();
|
|
577
|
+
|
|
578
|
+
// Special-form hostnames per RFC 6761 (localhost / .test / .invalid /
|
|
579
|
+
// .localhost) and operator-internal suffixes never hit DoH/DoT —
|
|
580
|
+
// public DoH providers don't resolve these. Route through node:dns
|
|
581
|
+
// (which checks /etc/hosts and the OS resolver). This keeps tests +
|
|
582
|
+
// dev workflows working while public-internet resolution still goes
|
|
583
|
+
// through the secure transport.
|
|
584
|
+
var isLocalForm = _isLocalFormHost(host);
|
|
585
|
+
|
|
517
586
|
try {
|
|
518
587
|
var addrs;
|
|
519
|
-
if (STATE.doh) {
|
|
588
|
+
if (STATE.doh && !isLocalForm) {
|
|
520
589
|
addrs = await _dualStack(_dohLookup, host, family);
|
|
521
|
-
} else if (STATE.dot) {
|
|
590
|
+
} else if (STATE.dot && !isLocalForm) {
|
|
522
591
|
addrs = await _dualStack(_dotLookup, host, family);
|
|
523
592
|
} else {
|
|
593
|
+
// System resolver (operator explicit opt-out via useSystemResolver).
|
|
524
594
|
var nodeOpts = { all: true };
|
|
525
|
-
if (family === 4 || family === 6) nodeOpts.family = family;
|
|
595
|
+
if (family === 4 || family === 6) nodeOpts.family = family; // allow:raw-byte-literal — IPv4/IPv6 family literals
|
|
526
596
|
addrs = await _withTimeout(dnsPromises.lookup(host, nodeOpts), STATE.lookupTimeoutMs, host);
|
|
527
597
|
if (!Array.isArray(addrs)) addrs = [addrs];
|
|
528
598
|
}
|
|
@@ -606,7 +676,7 @@ function _stateForTest() { return STATE; }
|
|
|
606
676
|
function _resetForTest() {
|
|
607
677
|
STATE.servers = null; STATE.resultOrder = null; STATE.family = 0;
|
|
608
678
|
STATE.lookupTimeoutMs = 0; STATE.cacheTtlMs = 0; STATE.cacheNegativeTtlMs = 0;
|
|
609
|
-
STATE.doh = null; STATE.dot = null;
|
|
679
|
+
STATE.doh = null; STATE.dot = null; STATE.systemResolver = false;
|
|
610
680
|
_clearCache();
|
|
611
681
|
_resetDotPool();
|
|
612
682
|
}
|
|
@@ -620,6 +690,7 @@ module.exports = {
|
|
|
620
690
|
setCacheTtlMs: setCacheTtlMs,
|
|
621
691
|
useDnsOverHttps: useDnsOverHttps,
|
|
622
692
|
useDnsOverTls: useDnsOverTls,
|
|
693
|
+
useSystemResolver: useSystemResolver,
|
|
623
694
|
lookup: lookup,
|
|
624
695
|
resolve4: resolve4,
|
|
625
696
|
resolve6: resolve6,
|
package/lib/retention.js
CHANGED
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
* write. This is the escape hatch; the table+ageField+ttlMs shape
|
|
56
56
|
* covers the common case.
|
|
57
57
|
*/
|
|
58
|
+
var C = require("./constants");
|
|
58
59
|
var lazyRequire = require("./lazy-require");
|
|
59
60
|
var validateOpts = require("./validate-opts");
|
|
60
61
|
var { defineClass } = require("./framework-error");
|
|
@@ -433,7 +434,47 @@ function create(opts) {
|
|
|
433
434
|
return { declare: declare, run: run, runAll: runAll, preview: preview, list: list };
|
|
434
435
|
}
|
|
435
436
|
|
|
437
|
+
// Audit-log retention floors per regulatory posture. These are the
|
|
438
|
+
// MINIMUMS — operators may declare longer windows but cannot declare
|
|
439
|
+
// shorter ones. Operator-facing helper for compliance audit.
|
|
440
|
+
//
|
|
441
|
+
// PCI-DSS Requirement 10.7.1 — 12 months online (active accessible)
|
|
442
|
+
// HIPAA 45 CFR §164.316(b)(2) — 6 years from creation
|
|
443
|
+
// SOX (Sarbanes-Oxley) §802 — 7 years for audit-relevant records
|
|
444
|
+
// GDPR Art. 5(1)(e) — no fixed minimum; operator declares
|
|
445
|
+
// SOC 2 (CC1–CC9) — 1 year typical; auditor-driven
|
|
446
|
+
// DORA Art. 17 (incident logs) — 5 years
|
|
447
|
+
var COMPLIANCE_RETENTION_FLOOR_MS = Object.freeze({
|
|
448
|
+
"pci-dss": C.TIME.days(365), // 12 months
|
|
449
|
+
"hipaa": C.TIME.days(365 * 6), // 6 years
|
|
450
|
+
"sox": C.TIME.days(365 * 7), // 7 years
|
|
451
|
+
"soc2": C.TIME.days(365), // 1 year
|
|
452
|
+
"dora": C.TIME.days(365 * 5), // 5 years (Article 17 incident logs)
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// Operator passes a posture name + a candidate ttlMs; returns the
|
|
456
|
+
// effective ttl that meets-or-exceeds the floor. Throws if posture is
|
|
457
|
+
// unknown so typos surface at config time.
|
|
458
|
+
function complianceFloor(posture, candidateTtlMs) {
|
|
459
|
+
if (typeof posture !== "string") {
|
|
460
|
+
throw new RetentionError("retention/bad-posture",
|
|
461
|
+
"complianceFloor: posture must be a string, got " + JSON.stringify(posture));
|
|
462
|
+
}
|
|
463
|
+
var floor = COMPLIANCE_RETENTION_FLOOR_MS[posture];
|
|
464
|
+
if (floor === undefined) {
|
|
465
|
+
throw new RetentionError("retention/unknown-posture",
|
|
466
|
+
"complianceFloor: unknown posture '" + posture + "'; expected one of " +
|
|
467
|
+
Object.keys(COMPLIANCE_RETENTION_FLOOR_MS).join(", "));
|
|
468
|
+
}
|
|
469
|
+
if (typeof candidateTtlMs !== "number" || !isFinite(candidateTtlMs) || candidateTtlMs <= 0) {
|
|
470
|
+
return floor;
|
|
471
|
+
}
|
|
472
|
+
return candidateTtlMs > floor ? candidateTtlMs : floor;
|
|
473
|
+
}
|
|
474
|
+
|
|
436
475
|
module.exports = {
|
|
437
|
-
create:
|
|
438
|
-
|
|
476
|
+
create: create,
|
|
477
|
+
complianceFloor: complianceFloor,
|
|
478
|
+
COMPLIANCE_RETENTION_FLOOR_MS: COMPLIANCE_RETENTION_FLOOR_MS,
|
|
479
|
+
RetentionError: RetentionError,
|
|
439
480
|
};
|
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:942606f7-1484-4ac3-9332-63f73e25256f",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-05T06:
|
|
8
|
+
"timestamp": "2026-05-05T06:51:47.053Z",
|
|
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.7.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.7.24",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.7.
|
|
25
|
+
"version": "0.7.24",
|
|
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.7.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.7.24",
|
|
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.7.
|
|
57
|
+
"ref": "@blamejs/core@0.7.24",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|