@blamejs/core 0.14.6 → 0.14.7
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 +2 -0
- package/README.md +3 -2
- package/lib/agent-event-bus.js +4 -4
- package/lib/agent-idempotency.js +6 -6
- package/lib/agent-orchestrator.js +9 -9
- package/lib/agent-posture-chain.js +10 -10
- package/lib/agent-saga.js +6 -7
- package/lib/agent-snapshot.js +8 -8
- package/lib/agent-stream.js +3 -3
- package/lib/agent-tenant.js +4 -4
- package/lib/agent-trace.js +5 -5
- package/lib/ai-disclosure.js +3 -3
- package/lib/app.js +2 -2
- package/lib/archive-read.js +1 -1
- package/lib/archive-tar-read.js +1 -1
- package/lib/archive-wrap.js +5 -5
- package/lib/audit-tools.js +65 -5
- package/lib/audit.js +2 -2
- package/lib/auth/ciba.js +1 -1
- package/lib/auth/dpop.js +1 -1
- package/lib/auth/fal.js +1 -1
- package/lib/auth/fido-mds3.js +2 -3
- package/lib/auth/jwt-external.js +2 -2
- package/lib/auth/oauth.js +9 -9
- package/lib/auth/oid4vci.js +7 -7
- package/lib/auth/oid4vp.js +1 -1
- package/lib/auth/openid-federation.js +5 -5
- package/lib/auth/passkey.js +6 -6
- package/lib/auth/saml.js +1 -1
- package/lib/auth/sd-jwt-vc.js +3 -6
- package/lib/backup/index.js +18 -18
- package/lib/cache.js +4 -4
- package/lib/calendar.js +5 -5
- package/lib/circuit-breaker.js +1 -1
- package/lib/cms-codec.js +2 -2
- package/lib/compliance.js +14 -14
- package/lib/crypto-field.js +58 -21
- package/lib/crypto.js +5 -6
- package/lib/db-query.js +131 -9
- package/lib/db.js +106 -22
- package/lib/external-db.js +64 -16
- package/lib/framework-schema.js +4 -4
- package/lib/guard-list-id.js +2 -2
- package/lib/guard-list-unsubscribe.js +1 -2
- package/lib/incident-report.js +150 -0
- package/lib/mail-crypto-smime.js +1 -1
- package/lib/mail-deploy.js +3 -3
- package/lib/mail-server-managesieve.js +2 -2
- package/lib/mail-server-pop3.js +2 -2
- package/lib/mail-store.js +1 -1
- package/lib/metrics.js +8 -8
- package/lib/middleware/csrf-protect.js +1 -1
- package/lib/middleware/dpop.js +5 -5
- package/lib/middleware/idempotency-key.js +21 -22
- package/lib/middleware/protected-resource-metadata.js +2 -2
- package/lib/network-dns-resolver.js +2 -2
- package/lib/network-dns.js +1 -2
- package/lib/network-tls.js +0 -1
- package/lib/outbox.js +1 -1
- package/lib/pqc-agent.js +1 -1
- package/lib/retention.js +1 -1
- package/lib/retry.js +1 -1
- package/lib/safe-archive.js +2 -2
- package/lib/safe-ical.js +2 -2
- package/lib/safe-mime.js +1 -1
- package/lib/self-update-standalone-verifier.js +1 -1
- package/lib/self-update.js +2 -2
- package/lib/static.js +1 -1
- package/lib/subject.js +2 -2
- package/lib/vault/index.js +64 -1
- package/lib/vault/rotate.js +19 -0
- package/lib/vendor-data.js +1 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/backup/index.js
CHANGED
|
@@ -81,7 +81,7 @@ var BackupError = defineClass("BackupError");
|
|
|
81
81
|
// compliance.js); list them here so bundleAdapterStorage's
|
|
82
82
|
// posture check refuses plaintext bundles under these regimes
|
|
83
83
|
// alongside the long-standing HIPAA + PCI-DSS pair.
|
|
84
|
-
//
|
|
84
|
+
// The legacy `ai-act` short
|
|
85
85
|
// name MUST appear in the backup encryption-required list too,
|
|
86
86
|
// otherwise a deployment pinned to `posture: "ai-act"` (the
|
|
87
87
|
// stated back-compat path) bypasses the cryptoStrategy refusal
|
|
@@ -339,7 +339,7 @@ function create(opts) {
|
|
|
339
339
|
"create: opts.vaultKeyJson is required (string or function returning string)");
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
-
// Posture-enforced backup encryption
|
|
342
|
+
// Posture-enforced backup encryption. HIPAA / PCI-DSS
|
|
343
343
|
// operators MUST keep encryption on. The framework's backup pipeline
|
|
344
344
|
// is encrypted-by-default — passphrase + per-file XChaCha20-Poly1305
|
|
345
345
|
// — but operators in third-party storage backends sometimes pass
|
|
@@ -361,7 +361,7 @@ function create(opts) {
|
|
|
361
361
|
}
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
-
//
|
|
364
|
+
// Backup destination residency posture. EU-tagged primary
|
|
365
365
|
// backing up to a US-region destination is a GDPR Article 46
|
|
366
366
|
// cross-border transfer; without an explicit operator opt-in the
|
|
367
367
|
// framework refuses to create the pipeline under gdpr / dpdp /
|
|
@@ -1110,7 +1110,7 @@ function bundleAdapterStorage(opts) {
|
|
|
1110
1110
|
// HIPAA + PCI-DSS recipe raises the floor to 128 bits (per
|
|
1111
1111
|
// BACKUP_ENCRYPTION_REQUIRED_POSTURES below); default 80 matches
|
|
1112
1112
|
// OWASP "strong password" guidance for generic deployments.
|
|
1113
|
-
//
|
|
1113
|
+
// Typeof NaN === "number" and
|
|
1114
1114
|
// typeof Infinity === "number" both pass the typeof gate but
|
|
1115
1115
|
// bypass downstream comparisons (NaN < 128 is false; estimated
|
|
1116
1116
|
// < NaN is false). Use Number.isFinite + a finite integer check
|
|
@@ -1136,7 +1136,7 @@ function bundleAdapterStorage(opts) {
|
|
|
1136
1136
|
"passphraseMinEntropyBits defaults to 80; HIPAA / PCI-DSS postures raise the floor to 128.");
|
|
1137
1137
|
}
|
|
1138
1138
|
}
|
|
1139
|
-
//
|
|
1139
|
+
// The wrap layers (recipient AND
|
|
1140
1140
|
// passphrase) compose only with the tar / tar.gz writeBundle
|
|
1141
1141
|
// branches. Pairing encryption strategy with format: "directory"
|
|
1142
1142
|
// would silently write plaintext per-file payloads. Refuse upfront
|
|
@@ -1168,7 +1168,7 @@ function bundleAdapterStorage(opts) {
|
|
|
1168
1168
|
passphraseMinEntropyBits = 128; // entropy-bits floor, not byte count
|
|
1169
1169
|
}
|
|
1170
1170
|
}
|
|
1171
|
-
//
|
|
1171
|
+
// Tar mode builds the whole archive
|
|
1172
1172
|
// in memory before adapter.writeFile because the v0.12.8 adapter
|
|
1173
1173
|
// contract is bytes-in (no writeStream method). The OOM-prevention
|
|
1174
1174
|
// gate is maxBundleBytes: writeBundle pre-walks the source tree,
|
|
@@ -1244,7 +1244,7 @@ function bundleAdapterStorage(opts) {
|
|
|
1244
1244
|
// wire). Bundle sizes drop ~3-5× on text-heavy backups
|
|
1245
1245
|
// (databases, JSON exports, mail spools) under tar.gz.
|
|
1246
1246
|
//
|
|
1247
|
-
//
|
|
1247
|
+
// Tar bytes are materialized in
|
|
1248
1248
|
// memory because the v0.12.8 adapter contract is bytes-in
|
|
1249
1249
|
// (writeFile takes a Buffer, no writeStream method). The
|
|
1250
1250
|
// maxBundleBytes pre-walk computes the uncompressed payload
|
|
@@ -1312,7 +1312,7 @@ function bundleAdapterStorage(opts) {
|
|
|
1312
1312
|
}
|
|
1313
1313
|
atomicFile.ensureDir(destDir);
|
|
1314
1314
|
if (hasTarGz) {
|
|
1315
|
-
//
|
|
1315
|
+
// Propagate maxBundleBytes
|
|
1316
1316
|
// to the gz restore path + disable the expansion-ratio cap.
|
|
1317
1317
|
// archive.read.gz defaults (1 GiB output / 100× ratio) are
|
|
1318
1318
|
// bomb-defense settings appropriate for adversarial input;
|
|
@@ -1386,7 +1386,7 @@ function bundleAdapterStorage(opts) {
|
|
|
1386
1386
|
// writeBundle path produced (rule §2 — the format is part
|
|
1387
1387
|
// of the storage layout, not behind a probe).
|
|
1388
1388
|
//
|
|
1389
|
-
//
|
|
1389
|
+
// Track WHICH suffixes a
|
|
1390
1390
|
// bundle carries (set of booleans) then apply explicit
|
|
1391
1391
|
// precedence at the end: tar.gz > tar > directory. Matches
|
|
1392
1392
|
// readBundle's preference (which checks hasTarGz first)
|
|
@@ -1595,7 +1595,7 @@ function bundleAdapterStorage(opts) {
|
|
|
1595
1595
|
"cloneBundle: dstBundleId '" + dstBundleId + "' already exists; " +
|
|
1596
1596
|
"pass opts.overwrite=true to replace");
|
|
1597
1597
|
}
|
|
1598
|
-
//
|
|
1598
|
+
// When overwrite=true, the
|
|
1599
1599
|
// existence guard was the only protection but it didn't
|
|
1600
1600
|
// delete the destination's existing keys before writing
|
|
1601
1601
|
// the source keys. Stale-format bundles (dst=tar, src=
|
|
@@ -1654,7 +1654,7 @@ function bundleAdapterStorage(opts) {
|
|
|
1654
1654
|
var rwKeySuffix = info.format === "tar.gz" ? TAR_GZ_KEY_SUFFIX : TAR_KEY_SUFFIX;
|
|
1655
1655
|
var sealed = await adapter.readFile(bundleId + rwKeySuffix);
|
|
1656
1656
|
var envelopeKind = info.envelopeKind;
|
|
1657
|
-
//
|
|
1657
|
+
// When the adapter has no
|
|
1658
1658
|
// readPartial capability, bundleInfo returns envelopeKind:
|
|
1659
1659
|
// "unknown" rather than risk a full payload load. For
|
|
1660
1660
|
// rewrap, we already have to load the payload (to unwrap),
|
|
@@ -1705,7 +1705,7 @@ function bundleAdapterStorage(opts) {
|
|
|
1705
1705
|
"rewrapBundle: opts.newPassphrase is required (string or Buffer) to re-seal");
|
|
1706
1706
|
}
|
|
1707
1707
|
inner = await archiveLazy().unwrapWithPassphrase(sealed, { passphrase: oldPass });
|
|
1708
|
-
//
|
|
1708
|
+
// Preserve the storage's
|
|
1709
1709
|
// configured entropy floor across rewrap. The
|
|
1710
1710
|
// writeBundle path raises the floor to 128 bits under
|
|
1711
1711
|
// HIPAA / PCI-DSS postures (per
|
|
@@ -1770,7 +1770,7 @@ function bundleAdapterStorage(opts) {
|
|
|
1770
1770
|
var inflight = [];
|
|
1771
1771
|
var aborted = false;
|
|
1772
1772
|
function _spawn() {
|
|
1773
|
-
//
|
|
1773
|
+
// Synchronously drain
|
|
1774
1774
|
// non-wrappable entries inside _spawn until we hit one
|
|
1775
1775
|
// that actually needs an async rewrap (or the pending
|
|
1776
1776
|
// queue empties). The prior implementation returned
|
|
@@ -1850,7 +1850,7 @@ function bundleAdapterStorage(opts) {
|
|
|
1850
1850
|
},
|
|
1851
1851
|
async verifyAllBundles(vOpts) {
|
|
1852
1852
|
vOpts = vOpts || {};
|
|
1853
|
-
//
|
|
1853
|
+
// Clamp fractional + zero
|
|
1854
1854
|
// floors so a stray `0.5` doesn't spawn zero workers + return
|
|
1855
1855
|
// a silent ok=0/failed=0 report on non-empty storage. Default
|
|
1856
1856
|
// 4; minimum 1; non-finite / non-positive falls back to
|
|
@@ -1876,7 +1876,7 @@ function bundleAdapterStorage(opts) {
|
|
|
1876
1876
|
if (aborted) return null;
|
|
1877
1877
|
if (pending.length === 0) return null;
|
|
1878
1878
|
var entry = pending.shift();
|
|
1879
|
-
//
|
|
1879
|
+
// Wrap each worker so any
|
|
1880
1880
|
// verifyBundle rejection becomes a failed-result entry
|
|
1881
1881
|
// rather than rejecting the whole batch. Without this, a
|
|
1882
1882
|
// mid-walk failure (payload disappeared between listBundles
|
|
@@ -2063,7 +2063,7 @@ function bundleAdapterStorage(opts) {
|
|
|
2063
2063
|
var envelopeKind = "none";
|
|
2064
2064
|
var sizeBytes = null;
|
|
2065
2065
|
var createdAt = null;
|
|
2066
|
-
//
|
|
2066
|
+
// Directory-format bundles
|
|
2067
2067
|
// leave payloadKey null but DO have a manifest.json that
|
|
2068
2068
|
// statKey can read. For createdAt parity with
|
|
2069
2069
|
// listBundles({ withStats }), stat the manifest in the
|
|
@@ -2085,7 +2085,7 @@ function bundleAdapterStorage(opts) {
|
|
|
2085
2085
|
}
|
|
2086
2086
|
}
|
|
2087
2087
|
if (payloadKey !== null) {
|
|
2088
|
-
//
|
|
2088
|
+
// Claim was a 5-byte magic
|
|
2089
2089
|
// probe; the implementation was reading the entire bundle
|
|
2090
2090
|
// into memory. For multi-GB bundles, an administrative
|
|
2091
2091
|
// metadata call would allocate the whole payload and put
|
|
@@ -2381,7 +2381,7 @@ bundleAdapterStorage.objectStoreAdapter = function (client, osOpts) {
|
|
|
2381
2381
|
// prefix manually so `listKeys("")` enumerates everything
|
|
2382
2382
|
// under the operator-supplied namespace.
|
|
2383
2383
|
var realScoped = prefix + (keyPrefix || "");
|
|
2384
|
-
//
|
|
2384
|
+
// Object-store backends page
|
|
2385
2385
|
// results (default 1000 keys). Without continuation, listKeys
|
|
2386
2386
|
// silently dropped bundles past page 1 — listBundles missed
|
|
2387
2387
|
// them, deleteBundle skipped them. Follow the
|
package/lib/cache.js
CHANGED
|
@@ -85,7 +85,7 @@ var { CacheError } = require("./framework-error");
|
|
|
85
85
|
|
|
86
86
|
var log = boot("cache");
|
|
87
87
|
var observability = lazyRequire(function () { return require("./observability"); });
|
|
88
|
-
//
|
|
88
|
+
// Opt-in vault seal for cluster-backend cache values. Lazy so
|
|
89
89
|
// vault-not-initialized in tests with a memory cache doesn't crash
|
|
90
90
|
// at module load.
|
|
91
91
|
var vault = lazyRequire(function () { return require("./vault"); });
|
|
@@ -501,7 +501,7 @@ function _clusterBackend(cfg) {
|
|
|
501
501
|
).catch(function () { /* best-effort */ });
|
|
502
502
|
}
|
|
503
503
|
var stored = row.valueJson;
|
|
504
|
-
//
|
|
504
|
+
// Sealed-row decode. Sealed entries are prefixed at write
|
|
505
505
|
// time so the unseal-on-read path is a strict opt-in: rows
|
|
506
506
|
// written without seal:true continue parsing as before.
|
|
507
507
|
if (typeof stored === "string" && stored.indexOf(CACHE_SEAL_PREFIX) === 0) {
|
|
@@ -521,7 +521,7 @@ function _clusterBackend(cfg) {
|
|
|
521
521
|
// changed value, app code treats it as the original" — a subtle
|
|
522
522
|
// freshness bug that's hard to debug.
|
|
523
523
|
var json = safeJson.stringify(value);
|
|
524
|
-
//
|
|
524
|
+
// Opt-in vault seal. When the caller passes seal: true,
|
|
525
525
|
// wrap the JSON via b.vault.seal (XChaCha20-Poly1305) before
|
|
526
526
|
// landing in _blamejs_cache.valueJson. The marker prefix is what
|
|
527
527
|
// get() looks for to know it must unseal on read.
|
|
@@ -1091,7 +1091,7 @@ function create(opts) {
|
|
|
1091
1091
|
}
|
|
1092
1092
|
}
|
|
1093
1093
|
}
|
|
1094
|
-
//
|
|
1094
|
+
// Opt-in vault seal. Strict-shape check: must be the literal
|
|
1095
1095
|
// boolean true, not just truthy. Backends that don't support seal
|
|
1096
1096
|
// (memory, custom) ignore the flag transparently; cluster backend
|
|
1097
1097
|
// wraps valueJson via b.vault.seal before INSERT.
|
package/lib/calendar.js
CHANGED
|
@@ -263,7 +263,7 @@ function validate(jsCal) {
|
|
|
263
263
|
"b.calendar.validate: Group.source MUST be a string URI when present (RFC 8984 §1.4.4)");
|
|
264
264
|
}
|
|
265
265
|
if (jsCal.categories !== undefined) {
|
|
266
|
-
//
|
|
266
|
+
// `typeof null === "object"` would let `categories:
|
|
267
267
|
// null` through this check, and the subsequent Object.keys
|
|
268
268
|
// throws a raw TypeError instead of a structured CalendarError.
|
|
269
269
|
// Refuse null explicitly so callers depending on the
|
|
@@ -687,7 +687,7 @@ function _expandSingleRule(rule, startMs, ctx) {
|
|
|
687
687
|
var n = parseInt(arr[i], 10);
|
|
688
688
|
if (isFinite(n) && n >= lo && n <= hi) { s[n] = true; hasAny = true; }
|
|
689
689
|
}
|
|
690
|
-
//
|
|
690
|
+
// When every value in the BY* list is out of range,
|
|
691
691
|
// return null instead of an empty set. An empty truthy set would
|
|
692
692
|
// cause `_matchesBy` to reject every candidate (`!set[n]` is
|
|
693
693
|
// true) and silently turn malformed input into a "match nothing"
|
|
@@ -728,7 +728,7 @@ function _expandSingleRule(rule, startMs, ctx) {
|
|
|
728
728
|
if (byMonthSet && !byMonthSet[d.getUTCMonth() + 1]) return false;
|
|
729
729
|
if (byMonthDaySet && !byMonthDaySet[d.getUTCDate()]) return false;
|
|
730
730
|
if (byWeekNoSet) {
|
|
731
|
-
//
|
|
731
|
+
// ISO week-year vs Gregorian year. 2021-01-01 is ISO
|
|
732
732
|
// week 53 of WEEK-YEAR 2020 (since 2021 only has 52 ISO weeks).
|
|
733
733
|
// Comparing only the numeric week would let a Jan 1 2021 date
|
|
734
734
|
// match a BYWEEKNO=53 rule whose implicit year is 2021. Refuse
|
|
@@ -884,7 +884,7 @@ function _expandWithBysetpos(ctx) {
|
|
|
884
884
|
// Emit picked candidates in ascending order, gated by window +
|
|
885
885
|
// untilMs + per-rule count cap.
|
|
886
886
|
//
|
|
887
|
-
//
|
|
887
|
+
// Recurrence instances MUST NOT precede DTSTART (per
|
|
888
888
|
// RFC 5545 §3.8.5.3). The period-boundary enumeration above
|
|
889
889
|
// includes candidates BEFORE startMs when the period containing
|
|
890
890
|
// startMs has earlier BY*-matching days (e.g. start = May 20
|
|
@@ -960,7 +960,7 @@ function _veventToJsCalEvent(ve) {
|
|
|
960
960
|
if (tzid) {
|
|
961
961
|
jsCal.timeZone = tzid;
|
|
962
962
|
} else if (typeof dtstart === "string" && /Z$/.test(dtstart)) {
|
|
963
|
-
//
|
|
963
|
+
// RFC 8984 §1.4.4: a UTC-suffix DTSTART (`...Z`) in
|
|
964
964
|
// iCalendar maps to a JSCalendar Event with `timeZone: "Etc/UTC"`.
|
|
965
965
|
// Without this, round-tripping `fromIcal` → `toIcal` would drop
|
|
966
966
|
// the UTC anchor + emit floating time, shifting the absolute
|
package/lib/circuit-breaker.js
CHANGED
|
@@ -82,7 +82,7 @@ function create(opts) {
|
|
|
82
82
|
// split the name out of opts before invoking the constructor.
|
|
83
83
|
// Caught by hermitstash-sync operator review against v0.9.12.
|
|
84
84
|
//
|
|
85
|
-
//
|
|
85
|
+
// The previous empty-string fallback was unreachable
|
|
86
86
|
// (retryHelper.CircuitBreaker validator throws on "" first) AND
|
|
87
87
|
// produced a confusing error message ("name must be a non-empty
|
|
88
88
|
// string, got string \"\"") that obscured the real opt-shape
|
package/lib/cms-codec.js
CHANGED
|
@@ -367,7 +367,7 @@ function decode(buf, opts) {
|
|
|
367
367
|
|
|
368
368
|
// OIDs whose AlgorithmIdentifier specifies ABSENT parameters per their
|
|
369
369
|
// publishing RFC — emitting NULL here would make the CMS structure
|
|
370
|
-
// non-conformant for strict validators
|
|
370
|
+
// non-conformant for strict validators.
|
|
371
371
|
// ML-DSA per RFC 9909 §3, SLH-DSA per RFC 9881 §3, ML-KEM per
|
|
372
372
|
// RFC 9936 §3. SHAKE-family per FIPS 202 (NIST registry — absent params).
|
|
373
373
|
var ABSENT_PARAM_OIDS = new Set([
|
|
@@ -407,7 +407,7 @@ function _writeImplicitPrimitive(tagNumber, value) {
|
|
|
407
407
|
// [N] IMPLICIT context-specific PRIMITIVE — for wrapping primitive
|
|
408
408
|
// ASN.1 types (OCTET STRING / INTEGER / OID) that have been IMPLICIT-
|
|
409
409
|
// tagged. The constructed bit MUST NOT be set or strict CMS parsers
|
|
410
|
-
// reject the structure (
|
|
410
|
+
// reject the structure (RecipientIdentifier
|
|
411
411
|
// CHOICE's SubjectKeyIdentifier alternative is `[0] IMPLICIT OCTET STRING`,
|
|
412
412
|
// a primitive type).
|
|
413
413
|
var tagByte = 0x80 | (tagNumber & 0x1f); // context-specific primitive mask
|
package/lib/compliance.js
CHANGED
|
@@ -280,7 +280,7 @@ var KNOWN_POSTURES = Object.freeze([
|
|
|
280
280
|
"nyc-ll144-2024", // NYC Local Law 144 — Automated Employment Decision Tool bias audits (2024 enforcement update) // statute identifier, not bytes
|
|
281
281
|
]);
|
|
282
282
|
|
|
283
|
-
//
|
|
283
|
+
// Artifact standards (SBOM / VEX format families) are NOT
|
|
284
284
|
// regulatory regimes. Pinning a posture like `cyclonedx-v1.6` to
|
|
285
285
|
// cascade audit + TLS floors conflates the act of EMITTING a SBOM
|
|
286
286
|
// format with the regulatory floor an operator needs. Operators who
|
|
@@ -385,7 +385,7 @@ function set(posture) {
|
|
|
385
385
|
STATE.setAt = Date.now();
|
|
386
386
|
_emitAudit("compliance.posture.set", { posture: posture });
|
|
387
387
|
|
|
388
|
-
//
|
|
388
|
+
// Emit a `format_as_regime` audit warning when an
|
|
389
389
|
// operator pins an artifact-standard format (cyclonedx-v1.6 /
|
|
390
390
|
// spdx-v3.0 / vex-csaf-2.1) as the regulatory posture. These names
|
|
391
391
|
// remain in KNOWN_POSTURES for back-compat but pinning them as the
|
|
@@ -400,7 +400,7 @@ function set(posture) {
|
|
|
400
400
|
"warning");
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
-
//
|
|
403
|
+
// Emit `fips_conflict` audit warning when posture is
|
|
404
404
|
// FedRAMP / CMMC L3 AND the framework's PQC-first crypto defaults
|
|
405
405
|
// are active without an explicit fipsMode opt-in. Operators see
|
|
406
406
|
// this in the audit chain and either (a) document the deviation
|
|
@@ -417,7 +417,7 @@ function set(posture) {
|
|
|
417
417
|
"warning");
|
|
418
418
|
}
|
|
419
419
|
|
|
420
|
-
//
|
|
420
|
+
// Cascade the posture into every primitive that owns a
|
|
421
421
|
// posture-conditioned default. Each primitive exposes an
|
|
422
422
|
// `applyPosture(name)` that merges the POSTURE_DEFAULTS entry for the
|
|
423
423
|
// posture into its own state and emits
|
|
@@ -429,7 +429,7 @@ function set(posture) {
|
|
|
429
429
|
// skipped rows surface in the audit chain so a forensic review can
|
|
430
430
|
// reconstruct the boot order.
|
|
431
431
|
_applyPostureCascade(posture);
|
|
432
|
-
//
|
|
432
|
+
// TZ awareness. Auditors expect timestamps in UTC.
|
|
433
433
|
// process.env.TZ controls Node's local-time conversion for any
|
|
434
434
|
// operator code that uses non-UTC formatters; under regulated
|
|
435
435
|
// postures (hipaa / pci-dss / sox / gdpr / soc2) emit a boot
|
|
@@ -447,7 +447,7 @@ function set(posture) {
|
|
|
447
447
|
}
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
-
// _applyPostureCascade —
|
|
450
|
+
// _applyPostureCascade — walks every primitive that
|
|
451
451
|
// participates in posture-conditioned defaults and asks it to merge
|
|
452
452
|
// the named posture into its state. Each step is best-effort at the
|
|
453
453
|
// audit-emission level (a primitive that isn't loaded yet emits
|
|
@@ -936,11 +936,11 @@ function describe(posture) {
|
|
|
936
936
|
// floors.
|
|
937
937
|
//
|
|
938
938
|
// Keys per posture:
|
|
939
|
-
// backupEncryptionRequired — backup.create refuses encrypt:false
|
|
939
|
+
// backupEncryptionRequired — backup.create refuses encrypt:false
|
|
940
940
|
// auditChainSignedRequired — audit emissions MUST be ML-DSA-87 chain-signed
|
|
941
941
|
// tlsMinVersion — minimum TLS version (string e.g. "TLSv1.3")
|
|
942
942
|
// sessionAbsoluteTimeoutMs — hard session expiry ceiling
|
|
943
|
-
// requireVacuumAfterErase —
|
|
943
|
+
// requireVacuumAfterErase — cryptoField.eraseRow must call
|
|
944
944
|
// b.db.vacuumAfterErase({ mode: "full" })
|
|
945
945
|
// so freed B-tree index pages don't linger
|
|
946
946
|
// with sealed-column ciphertext readable
|
|
@@ -1178,7 +1178,7 @@ var POSTURE_DEFAULTS = Object.freeze({
|
|
|
1178
1178
|
"circia": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1179
1179
|
// ---- v0.9.6 — exceptd framework-control-gap closure cascade ----
|
|
1180
1180
|
"nist-800-53": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
1181
|
-
//
|
|
1181
|
+
// NIST AI-RMF MANAGE.4.3 / ISO 23894 §6.5 / ISO 42001
|
|
1182
1182
|
// §A.6 require encrypted backups for AI system state (model
|
|
1183
1183
|
// weights, training data, prompt logs all contain regulated
|
|
1184
1184
|
// payload). All AI-domain postures now enforce backupEncryption.
|
|
@@ -1186,7 +1186,7 @@ var POSTURE_DEFAULTS = Object.freeze({
|
|
|
1186
1186
|
"iso-42001-2023": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
1187
1187
|
"iso-23894-2023": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
1188
1188
|
"owasp-llm-top-10-2025": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
1189
|
-
//
|
|
1189
|
+
// OWASP ASVS v5.0 §8.3.4 (sensitive-data deletion)
|
|
1190
1190
|
// requires post-delete storage reclamation. Set requireVacuumAfterErase
|
|
1191
1191
|
// so operators pinning ASVS v5.0 inherit the proper floor.
|
|
1192
1192
|
"owasp-asvs-v5.0": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
@@ -1194,7 +1194,7 @@ var POSTURE_DEFAULTS = Object.freeze({
|
|
|
1194
1194
|
"nist-800-82-r3": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1195
1195
|
"nist-800-63b-rev4": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1196
1196
|
"iec-62443-3-3": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1197
|
-
//
|
|
1197
|
+
// FedRAMP Rev 5 Moderate baseline references FIPS 140-3
|
|
1198
1198
|
// validated cryptography for protect-against-disclosure controls
|
|
1199
1199
|
// (SC-13, SC-28). The framework's PQC-first defaults (ML-KEM-1024,
|
|
1200
1200
|
// XChaCha20-Poly1305, SHA3-512) are NOT FIPS-140-3 validated as of
|
|
@@ -1230,7 +1230,7 @@ var POSTURE_DEFAULTS = Object.freeze({
|
|
|
1230
1230
|
"nist-800-115": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1231
1231
|
"cwe-top-25-2024": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1232
1232
|
"cis-controls-v8": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1233
|
-
//
|
|
1233
|
+
// CMMC 2.0 levels differ in control mapping:
|
|
1234
1234
|
// L1 (Foundational, 15 FAR controls, FCI data only) — encrypted
|
|
1235
1235
|
// backups NOT mandated; audit-chain encouraged.
|
|
1236
1236
|
// L2 (Advanced, 110 NIST 800-171 Rev 2 controls, CUI data) —
|
|
@@ -1325,7 +1325,7 @@ var POSTURE_DEFAULTS = Object.freeze({
|
|
|
1325
1325
|
// erased from a system's storage, the residual EXIF / metadata
|
|
1326
1326
|
// entries pointing at the model must be cleared too.
|
|
1327
1327
|
"eu-ai-act": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
1328
|
-
//
|
|
1328
|
+
// The legacy `ai-act` short
|
|
1329
1329
|
// name carries the SAME cascade as `eu-ai-act` so deployments
|
|
1330
1330
|
// pinned to the legacy alias get the new encryption / audit /
|
|
1331
1331
|
// TLS / vacuum floors instead of falling through to null. The
|
|
@@ -1526,7 +1526,7 @@ function list() {
|
|
|
1526
1526
|
* Return the set of SBOM / VEX artifact standards the framework can
|
|
1527
1527
|
* emit. These are FORMAT FAMILIES, not regulatory regimes — pinning
|
|
1528
1528
|
* one of these names as the deployment's compliance posture conflates
|
|
1529
|
-
* "format I emit" with "regulatory floor I meet"
|
|
1529
|
+
* "format I emit" with "regulatory floor I meet". Pin
|
|
1530
1530
|
* the regulatory regime (FedRAMP / SSDF / HIPAA / etc.) via
|
|
1531
1531
|
* `b.compliance.set()` and surface the emitted artifact standards via
|
|
1532
1532
|
* this read-only catalog.
|
package/lib/crypto-field.js
CHANGED
|
@@ -52,7 +52,7 @@ var compliance = lazyRequire(function () { return require("./compliance"); })
|
|
|
52
52
|
var db = lazyRequire(function () { return require("./db"); });
|
|
53
53
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
54
54
|
|
|
55
|
-
//
|
|
55
|
+
// Posture cascade hook + erase-vacuum integration. Recording the
|
|
56
56
|
// posture lets eraseRow call b.db.vacuumAfterErase({ mode: "full" })
|
|
57
57
|
// automatically under postures whose POSTURE_DEFAULTS sets
|
|
58
58
|
// requireVacuumAfterErase: true (gdpr / dpdp / pipl-cn / lgpd-br /
|
|
@@ -113,7 +113,7 @@ function getActivePosture() { return _activePosture; }
|
|
|
113
113
|
// Per-table registry, populated by db.init()
|
|
114
114
|
var schemas = Object.create(null);
|
|
115
115
|
|
|
116
|
-
//
|
|
116
|
+
// Per-COLUMN data residency registry. Real GDPR / DPDP
|
|
117
117
|
// deployments have row-level mixed residency: a `users.name` column
|
|
118
118
|
// may be global, but `users.addressLine1` must stay in EU storage.
|
|
119
119
|
// db.init({ schema }) carries the operator's residency declaration
|
|
@@ -123,7 +123,7 @@ var schemas = Object.create(null);
|
|
|
123
123
|
// { tableName: { columnName: "eu" | "us" | "global" | <tag> } }
|
|
124
124
|
var columnResidency = Object.create(null);
|
|
125
125
|
|
|
126
|
-
//
|
|
126
|
+
// Per-row key declaration registry. For tables that opt
|
|
127
127
|
// into per-row keying, b.subject.eraseHard deletes the wrapped K_row
|
|
128
128
|
// from _blamejs_per_row_keys, leaving WAL/replica residual ciphertext
|
|
129
129
|
// undecryptable.
|
|
@@ -152,7 +152,7 @@ var perRowKeyTables = Object.create(null);
|
|
|
152
152
|
* // b.vault.aad — AEAD-binds the ciphertext
|
|
153
153
|
* // to (table, rowIdField=primary key, column)
|
|
154
154
|
* // so a DB-write attacker can't copy a
|
|
155
|
-
* // sealed value between rows.
|
|
155
|
+
* // sealed value between rows.
|
|
156
156
|
* rowIdField: string, // when aad=true, the column name carrying
|
|
157
157
|
* // the row identity. Default "id". The row
|
|
158
158
|
* // passed to sealRow MUST already have this
|
|
@@ -173,7 +173,7 @@ var perRowKeyTables = Object.create(null);
|
|
|
173
173
|
* });
|
|
174
174
|
* b.cryptoField.getSealedFields("patients"); // → ["ssn", "diagnosis"]
|
|
175
175
|
*
|
|
176
|
-
* // AAD-bound table (recommended for new schemas
|
|
176
|
+
* // AAD-bound table (recommended for new schemas).
|
|
177
177
|
* b.cryptoField.registerTable("idempotency_keys", {
|
|
178
178
|
* sealedFields: ["headers", "body"],
|
|
179
179
|
* aad: true,
|
|
@@ -185,16 +185,56 @@ function registerTable(name, opts) {
|
|
|
185
185
|
var rowIdField = typeof opts.rowIdField === "string" && opts.rowIdField.length > 0
|
|
186
186
|
? opts.rowIdField : "id";
|
|
187
187
|
var schemaVersion = opts.schemaVersion != null ? String(opts.schemaVersion) : "1";
|
|
188
|
+
var derivedHashMode = opts.derivedHashMode || "salted-sha3";
|
|
189
|
+
if (derivedHashMode !== "salted-sha3" && derivedHashMode !== "hmac-shake256") {
|
|
190
|
+
throw new Error("registerTable: derivedHashMode must be 'salted-sha3' (default) or " +
|
|
191
|
+
"'hmac-shake256', got " + JSON.stringify(derivedHashMode));
|
|
192
|
+
}
|
|
193
|
+
var derivedHashes = Object.assign({}, opts.derivedHashes || {});
|
|
194
|
+
for (var col in derivedHashes) {
|
|
195
|
+
if (!Object.prototype.hasOwnProperty.call(derivedHashes, col)) continue;
|
|
196
|
+
var colMode = derivedHashes[col] && derivedHashes[col].mode;
|
|
197
|
+
if (colMode !== undefined && colMode !== "salted-sha3" && colMode !== "hmac-shake256") {
|
|
198
|
+
throw new Error("registerTable: derivedHashes." + col + ".mode must be " +
|
|
199
|
+
"'salted-sha3' or 'hmac-shake256', got " + JSON.stringify(colMode));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
188
202
|
schemas[name] = {
|
|
189
|
-
sealedFields:
|
|
190
|
-
derivedHashes:
|
|
191
|
-
hashNamespaces:
|
|
192
|
-
aad:
|
|
193
|
-
rowIdField:
|
|
194
|
-
schemaVersion:
|
|
203
|
+
sealedFields: Array.isArray(opts.sealedFields) ? opts.sealedFields.slice() : [],
|
|
204
|
+
derivedHashes: derivedHashes,
|
|
205
|
+
hashNamespaces: Object.assign({}, opts.hashNamespaces || {}),
|
|
206
|
+
aad: aadOn,
|
|
207
|
+
rowIdField: rowIdField,
|
|
208
|
+
schemaVersion: schemaVersion,
|
|
209
|
+
derivedHashMode: derivedHashMode,
|
|
195
210
|
};
|
|
196
211
|
}
|
|
197
212
|
|
|
213
|
+
// Derived-hash digest width for the keyed (hmac-shake256) mode: 32
|
|
214
|
+
// bytes -> 64 hex chars.
|
|
215
|
+
var DERIVED_HASH_BYTES = 32;
|
|
216
|
+
|
|
217
|
+
// Compute the indexed-lookup digest for a derived-hash column.
|
|
218
|
+
// - "salted-sha3" (default): SHA3-512 over <per-deployment salt> + ns
|
|
219
|
+
// + value (128 hex). Deterministic per deployment.
|
|
220
|
+
// - "hmac-shake256": SHAKE256(<vault-sealed MAC key> || ns + value)
|
|
221
|
+
// truncated to 32 bytes (64 hex). The key is a vault-derived secret,
|
|
222
|
+
// NOT a static salt, so an attacker who recovers the salt alone
|
|
223
|
+
// can't correlate two low-entropy plaintexts; the sponge has no
|
|
224
|
+
// length-extension weakness. (b.crypto.hmacSha3 (HMAC-SHA3-512) was
|
|
225
|
+
// considered; SHAKE256(key||msg) is chosen for the fixed-width keyed
|
|
226
|
+
// digest with the same MAC-grade guarantee.) FIPS 202; NIST SP
|
|
227
|
+
// 800-185; GDPR Art. 4(5) pseudonymisation; HIPAA 45 CFR 164.514(b).
|
|
228
|
+
function _computeDerivedHash(spec, tableMode, ns, normalized) {
|
|
229
|
+
var mode = (spec && spec.mode) || tableMode || "salted-sha3";
|
|
230
|
+
if (mode === "hmac-shake256") {
|
|
231
|
+
var macKey = vault.getDerivedHashMacKey();
|
|
232
|
+
return kdf(Buffer.concat([macKey, Buffer.from(ns + normalized, "utf8")]),
|
|
233
|
+
DERIVED_HASH_BYTES).toString("hex");
|
|
234
|
+
}
|
|
235
|
+
return sha3Hash(vault.getDerivedHashSalt().toString("hex") + ns + normalized);
|
|
236
|
+
}
|
|
237
|
+
|
|
198
238
|
/**
|
|
199
239
|
* @primitive b.cryptoField.getSchema
|
|
200
240
|
* @signature b.cryptoField.getSchema(table)
|
|
@@ -308,8 +348,7 @@ function computeDerived(table, sourceField, sourceValue) {
|
|
|
308
348
|
if (spec.from === sourceField) {
|
|
309
349
|
var ns = namespaceFor(table, sourceField, s.hashNamespaces);
|
|
310
350
|
var normalized = spec.normalize ? spec.normalize(sourceValue) : String(sourceValue);
|
|
311
|
-
|
|
312
|
-
return { field: derivedField, value: sha3Hash(saltHex + ns + normalized) };
|
|
351
|
+
return { field: derivedField, value: _computeDerivedHash(spec, s.derivedHashMode, ns, normalized) };
|
|
313
352
|
}
|
|
314
353
|
}
|
|
315
354
|
return null;
|
|
@@ -367,12 +406,11 @@ function sealRow(table, row) {
|
|
|
367
406
|
}
|
|
368
407
|
var ns = namespaceFor(table, spec.from, s.hashNamespaces);
|
|
369
408
|
var normalized = spec.normalize ? spec.normalize(plain) : String(plain);
|
|
370
|
-
|
|
371
|
-
out[derivedField] = sha3Hash(saltHex2 + ns + normalized);
|
|
409
|
+
out[derivedField] = _computeDerivedHash(spec, s.derivedHashMode, ns, normalized);
|
|
372
410
|
}
|
|
373
411
|
}
|
|
374
412
|
|
|
375
|
-
//
|
|
413
|
+
// AAD-bound table requires the row's identity column to
|
|
376
414
|
// be populated BEFORE sealRow runs. Sealing under a placeholder /
|
|
377
415
|
// missing rowId produces ciphertext that no later unseal can open
|
|
378
416
|
// because the AAD on read is computed against the row's actual id.
|
|
@@ -390,7 +428,7 @@ function sealRow(table, row) {
|
|
|
390
428
|
// Seal fields. Plain mode: vault.seal (idempotent — already-sealed
|
|
391
429
|
// values pass through). AAD mode: vault.aad.seal binds the AEAD tag
|
|
392
430
|
// to (table, rowId, column, schemaVersion) — cross-row copy of a
|
|
393
|
-
// ciphertext fails Poly1305 on read.
|
|
431
|
+
// ciphertext fails Poly1305 on read.
|
|
394
432
|
for (var i = 0; i < s.sealedFields.length; i++) {
|
|
395
433
|
var field = s.sealedFields[i];
|
|
396
434
|
if (out[field] !== undefined && out[field] !== null) {
|
|
@@ -573,7 +611,7 @@ function eraseRow(table, row) {
|
|
|
573
611
|
out[derivedField] = null;
|
|
574
612
|
}
|
|
575
613
|
}
|
|
576
|
-
//
|
|
614
|
+
// `__erasedAt` was previously a plaintext UTC ms integer.
|
|
577
615
|
// That value alone fingerprints the erasure event (audit-log
|
|
578
616
|
// exfiltration + cross-tenant correlation: "this row was erased
|
|
579
617
|
// 2.3s before that one"). Bucket the timestamp to a 1-day floor so
|
|
@@ -584,7 +622,7 @@ function eraseRow(table, row) {
|
|
|
584
622
|
var dayMs = TIME.days(1);
|
|
585
623
|
out.__erasedAt = Math.floor(Date.now() / dayMs) * dayMs;
|
|
586
624
|
|
|
587
|
-
//
|
|
625
|
+
// Under regulatory postures whose POSTURE_DEFAULTS sets
|
|
588
626
|
// requireVacuumAfterErase: true (gdpr / dpdp / pipl-cn / lgpd-br /
|
|
589
627
|
// hipaa), the B-tree index pages freed by the upcoming UPDATE/DELETE
|
|
590
628
|
// would otherwise linger with sealed-column ciphertext readable
|
|
@@ -662,8 +700,7 @@ function lookupHash(table, field, value) {
|
|
|
662
700
|
if (spec.from === field) {
|
|
663
701
|
var ns = namespaceFor(table, field, s.hashNamespaces);
|
|
664
702
|
var normalized = spec.normalize ? spec.normalize(value) : String(value);
|
|
665
|
-
|
|
666
|
-
return { field: derivedField, value: sha3Hash(saltHex + ns + normalized) };
|
|
703
|
+
return { field: derivedField, value: _computeDerivedHash(spec, s.derivedHashMode, ns, normalized) };
|
|
667
704
|
}
|
|
668
705
|
}
|
|
669
706
|
return null;
|
package/lib/crypto.js
CHANGED
|
@@ -168,7 +168,7 @@ function hashFile(filePath, algorithm) {
|
|
|
168
168
|
// `algorithms` entry. Used by hashFilesParallel below; not exported
|
|
169
169
|
// directly because the common case is the parallel-many shape.
|
|
170
170
|
//
|
|
171
|
-
//
|
|
171
|
+
// Hardening (v0.9.58):
|
|
172
172
|
// - lstat-then-stat so symlinks are detected before open; refused
|
|
173
173
|
// unless opts.followSymlinks === true (default false — a symlink-
|
|
174
174
|
// in-input-list attack lets a write-restricted caller hash files
|
|
@@ -269,9 +269,8 @@ function _hashFileMulti(filePath, algorithms, opts) {
|
|
|
269
269
|
* SBOM regeneration / vendor-data integrity sweeps / release-asset
|
|
270
270
|
* bundling — situations where N files each need both SHA-256 (legacy
|
|
271
271
|
* compat) and SHA-3-512 (PQC-first) digests and rolling a worker
|
|
272
|
-
* pool by hand
|
|
273
|
-
*
|
|
274
|
-
* every release.
|
|
272
|
+
* pool by hand means the same two-loop, capture-N-promises, settle-Q
|
|
273
|
+
* boilerplate every release.
|
|
275
274
|
*
|
|
276
275
|
* @opts
|
|
277
276
|
* algorithms?: string[], // default ["sha256", "sha3-512"]; any node:crypto-known digest
|
|
@@ -333,7 +332,7 @@ function hashFilesParallel(filePaths, opts) {
|
|
|
333
332
|
"crypto.hashFilesParallel: opts.onProgress must be a function when supplied"
|
|
334
333
|
));
|
|
335
334
|
}
|
|
336
|
-
//
|
|
335
|
+
// DoS cap. Default 1 GiB per file; operators with larger
|
|
337
336
|
// legitimate hashing workloads (firmware images, vendor packs)
|
|
338
337
|
// override per-call.
|
|
339
338
|
var maxBytesPerFile = opts.maxBytesPerFile !== undefined
|
|
@@ -1202,7 +1201,7 @@ function decrypt(ciphertext, privateKeys, opts) {
|
|
|
1202
1201
|
}
|
|
1203
1202
|
// Audit-emit every legacy decrypt so the migration window is
|
|
1204
1203
|
// visible. Emit success ONLY on actual decrypt success; emit
|
|
1205
|
-
// failure on throw.
|
|
1204
|
+
// failure on throw. Before this fix, the audit fired
|
|
1206
1205
|
// before decryptEnvelope() ran, so corrupted 0xE1 blobs / wrong
|
|
1207
1206
|
// private keys / unsupported KEMs got logged as successful legacy
|
|
1208
1207
|
// decrypts when the call actually threw, inflating real success
|