@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 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.
@@ -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: create,
438
- RetentionError: RetentionError,
476
+ create: create,
477
+ complianceFloor: complianceFloor,
478
+ COMPLIANCE_RETENTION_FLOOR_MS: COMPLIANCE_RETENTION_FLOOR_MS,
479
+ RetentionError: RetentionError,
439
480
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.7.22",
3
+ "version": "0.7.24",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -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:30b4f7c6-d648-478a-aa77-dbbfb61c7951",
5
+ "serialNumber": "urn:uuid:942606f7-1484-4ac3-9332-63f73e25256f",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-05T06:18:09.475Z",
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",
22
+ "bom-ref": "@blamejs/core@0.7.24",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.7.22",
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.22",
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.22",
57
+ "ref": "@blamejs/core@0.7.24",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]