@blamejs/blamejs-shop 0.3.11 → 0.3.13
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/admin.js +157 -117
- package/lib/asset-manifest.json +1 -1
- package/lib/currency-rounding.js +2 -14
- package/lib/index.js +1 -0
- package/lib/text-guard.js +227 -0
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/README.md +3 -2
- package/lib/vendor/blamejs/SECURITY.md +3 -0
- package/lib/vendor/blamejs/api-snapshot.json +14 -2
- package/lib/vendor/blamejs/lib/agent-event-bus.js +4 -4
- package/lib/vendor/blamejs/lib/agent-idempotency.js +6 -6
- package/lib/vendor/blamejs/lib/agent-orchestrator.js +9 -9
- package/lib/vendor/blamejs/lib/agent-posture-chain.js +10 -10
- package/lib/vendor/blamejs/lib/agent-saga.js +6 -7
- package/lib/vendor/blamejs/lib/agent-snapshot.js +8 -8
- package/lib/vendor/blamejs/lib/agent-stream.js +3 -3
- package/lib/vendor/blamejs/lib/agent-tenant.js +4 -4
- package/lib/vendor/blamejs/lib/agent-trace.js +5 -5
- package/lib/vendor/blamejs/lib/ai-disclosure.js +3 -3
- package/lib/vendor/blamejs/lib/app.js +2 -2
- package/lib/vendor/blamejs/lib/archive-read.js +1 -1
- package/lib/vendor/blamejs/lib/archive-tar-read.js +1 -1
- package/lib/vendor/blamejs/lib/archive-wrap.js +5 -5
- package/lib/vendor/blamejs/lib/audit-tools.js +65 -5
- package/lib/vendor/blamejs/lib/audit.js +2 -2
- package/lib/vendor/blamejs/lib/auth/ciba.js +1 -1
- package/lib/vendor/blamejs/lib/auth/dpop.js +1 -1
- package/lib/vendor/blamejs/lib/auth/fal.js +1 -1
- package/lib/vendor/blamejs/lib/auth/fido-mds3.js +2 -3
- package/lib/vendor/blamejs/lib/auth/jwt-external.js +2 -2
- package/lib/vendor/blamejs/lib/auth/oauth.js +9 -9
- package/lib/vendor/blamejs/lib/auth/oid4vci.js +7 -7
- package/lib/vendor/blamejs/lib/auth/oid4vp.js +1 -1
- package/lib/vendor/blamejs/lib/auth/openid-federation.js +5 -5
- package/lib/vendor/blamejs/lib/auth/passkey.js +6 -6
- package/lib/vendor/blamejs/lib/auth/saml.js +1 -1
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +3 -6
- package/lib/vendor/blamejs/lib/backup/index.js +18 -18
- package/lib/vendor/blamejs/lib/cache.js +4 -4
- package/lib/vendor/blamejs/lib/calendar.js +5 -5
- package/lib/vendor/blamejs/lib/circuit-breaker.js +1 -1
- package/lib/vendor/blamejs/lib/cms-codec.js +2 -2
- package/lib/vendor/blamejs/lib/compliance.js +14 -14
- package/lib/vendor/blamejs/lib/crypto-field.js +58 -21
- package/lib/vendor/blamejs/lib/crypto.js +5 -6
- package/lib/vendor/blamejs/lib/db-query.js +131 -9
- package/lib/vendor/blamejs/lib/db.js +106 -22
- package/lib/vendor/blamejs/lib/external-db.js +64 -16
- package/lib/vendor/blamejs/lib/framework-schema.js +4 -4
- package/lib/vendor/blamejs/lib/guard-list-id.js +2 -2
- package/lib/vendor/blamejs/lib/guard-list-unsubscribe.js +1 -2
- package/lib/vendor/blamejs/lib/incident-report.js +150 -0
- package/lib/vendor/blamejs/lib/mail-crypto-smime.js +1 -1
- package/lib/vendor/blamejs/lib/mail-deploy.js +3 -3
- package/lib/vendor/blamejs/lib/mail-server-managesieve.js +2 -2
- package/lib/vendor/blamejs/lib/mail-server-pop3.js +2 -2
- package/lib/vendor/blamejs/lib/mail-store.js +1 -1
- package/lib/vendor/blamejs/lib/metrics.js +8 -8
- package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +1 -1
- package/lib/vendor/blamejs/lib/middleware/dpop.js +5 -5
- package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +21 -22
- package/lib/vendor/blamejs/lib/middleware/protected-resource-metadata.js +2 -2
- package/lib/vendor/blamejs/lib/network-dns-resolver.js +2 -2
- package/lib/vendor/blamejs/lib/network-dns.js +1 -2
- package/lib/vendor/blamejs/lib/network-tls.js +0 -1
- package/lib/vendor/blamejs/lib/outbox.js +1 -1
- package/lib/vendor/blamejs/lib/pqc-agent.js +1 -1
- package/lib/vendor/blamejs/lib/retention.js +1 -1
- package/lib/vendor/blamejs/lib/retry.js +1 -1
- package/lib/vendor/blamejs/lib/safe-archive.js +2 -2
- package/lib/vendor/blamejs/lib/safe-ical.js +2 -2
- package/lib/vendor/blamejs/lib/safe-mime.js +1 -1
- package/lib/vendor/blamejs/lib/self-update-standalone-verifier.js +1 -1
- package/lib/vendor/blamejs/lib/self-update.js +2 -2
- package/lib/vendor/blamejs/lib/static.js +1 -1
- package/lib/vendor/blamejs/lib/subject.js +2 -2
- package/lib/vendor/blamejs/lib/vault/index.js +64 -1
- package/lib/vendor/blamejs/lib/vault/rotate.js +19 -0
- package/lib/vendor/blamejs/lib/vendor-data.js +1 -1
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.14.7.json +77 -0
- package/lib/vendor/blamejs/scripts/release.js +28 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-tools-dual-control.test.js +115 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +163 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-derived-hash.test.js +102 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-column-gate.test.js +150 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-key-aad.test.js +109 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-hardening.test.js +53 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/incident-report.test.js +65 -0
- package/lib/webhook-subscriptions.js +11 -24
- package/lib/webhooks.js +12 -33
- package/package.json +1 -1
|
@@ -70,6 +70,7 @@ var safeJson = require("./safe-json");
|
|
|
70
70
|
var safeSql = require("./safe-sql");
|
|
71
71
|
var validateOpts = require("./validate-opts");
|
|
72
72
|
var vault = require("./vault");
|
|
73
|
+
var vaultAad = require("./vault-aad");
|
|
73
74
|
|
|
74
75
|
var DbError = defineClass("DbError", { alwaysPermanent: true });
|
|
75
76
|
var WormViolationError = require("./framework-error").WormViolationError;
|
|
@@ -153,12 +154,18 @@ var initialized = false;
|
|
|
153
154
|
var dataResidency = null; // operator's declared region config (validated by storage backends)
|
|
154
155
|
var subjectTables = []; // [{ name, subjectField, personalDataCategories }] — for subject.export/erase
|
|
155
156
|
var tableMetadata = {}; // table name → metadata snapshot (PK/FK/sealed/derived) for getTableMetadata
|
|
156
|
-
//
|
|
157
|
+
// StreamLimit ceiling. db.stream() / Query.stream() consult this
|
|
157
158
|
// (overridden per-call via opts.streamLimit). Default cap matches a
|
|
158
159
|
// generous-but-bounded 1M rows so an accidentally-unbounded export
|
|
159
160
|
// surfaces a thrown error instead of OOM. v0.7.67's maxRowsPerQuery
|
|
160
161
|
// bounds .all() / .first() — this is its streaming counterpart.
|
|
161
162
|
var streamLimit = C.BYTES.bytes(1000000); // row-count ceiling, not bytes
|
|
163
|
+
// Column-membership gate mode, set by db.init({ columnGate }). Default
|
|
164
|
+
// "reject" (security-on): a query that orders / selects / filters on a
|
|
165
|
+
// column that is not declared in the table's schema is refused before
|
|
166
|
+
// the identifier interpolates into SQL. "warn" audits + allows; "off"
|
|
167
|
+
// disables the gate.
|
|
168
|
+
var columnGateMode = "reject";
|
|
162
169
|
|
|
163
170
|
// ---- Framework-baked tables ----
|
|
164
171
|
//
|
|
@@ -287,7 +294,7 @@ var FRAMEWORK_SCHEMA = [
|
|
|
287
294
|
indexes: ["placedAt"],
|
|
288
295
|
},
|
|
289
296
|
{
|
|
290
|
-
// Per-row crypto-erasure key registry —
|
|
297
|
+
// Per-row crypto-erasure key registry — per-row keys.
|
|
291
298
|
// Each entry holds a sealed wrapped K_row keyed by (table,
|
|
292
299
|
// rowId). b.subject.eraseHard deletes the entry, leaving WAL /
|
|
293
300
|
// replica residuals undecryptable.
|
|
@@ -614,8 +621,8 @@ var FRAMEWORK_SCHEMA = [
|
|
|
614
621
|
{
|
|
615
622
|
// _blamejs_break_glass_grants — issued grants. Each successful
|
|
616
623
|
// step-up creates one row; each row read decrements rowsRemaining.
|
|
617
|
-
// Default maxRowsPerGrant=1 enforces "row by row" auth
|
|
618
|
-
//
|
|
624
|
+
// Default maxRowsPerGrant=1 enforces "row by row" auth
|
|
625
|
+
// (each row access = its own grant).
|
|
619
626
|
// Sealed columns hold reason + scopeColumns so audit-readable
|
|
620
627
|
// metadata doesn't leak in cleartext.
|
|
621
628
|
name: "_blamejs_break_glass_grants",
|
|
@@ -661,25 +668,58 @@ function resolveTmpDir(optsTmpDir) {
|
|
|
661
668
|
|
|
662
669
|
// ---- DB encryption key management ----
|
|
663
670
|
|
|
671
|
+
// AAD binds the sealed DB encryption key to the deployment's dataDir
|
|
672
|
+
// + key-file path, so a key file substituted from another deployment
|
|
673
|
+
// fails the AEAD tag check on unseal (cross-deployment ciphertext
|
|
674
|
+
// substitution / silent re-key — CWE-345 / CWE-441; the AAD itself is
|
|
675
|
+
// NIST SP 800-38D additional-authenticated-data over the XChaCha20-
|
|
676
|
+
// Poly1305 seal). nodePath.resolve (not realpathSync) — the key file
|
|
677
|
+
// may not exist yet at first-run seal.
|
|
678
|
+
function _dbKeyAad(dataDirPath, keyPath) {
|
|
679
|
+
return vaultAad.buildContextAad({
|
|
680
|
+
purpose: "blamejs/db-encryption-key/v1",
|
|
681
|
+
dataDir: nodePath.resolve(dataDirPath),
|
|
682
|
+
keyPath: nodePath.resolve(keyPath),
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
664
686
|
function loadOrCreateDbKey(dataDirPath, keyPathOverride) {
|
|
665
687
|
// Operator opt: `opts.dbKeyPath` — useful when the encryption key
|
|
666
688
|
// needs to live outside `dataDir` (e.g. a separate volume mounted
|
|
667
689
|
// from a KMS-fronted secret store). Default places it next to the
|
|
668
690
|
// encrypted DB so backup capture is one-tarball.
|
|
669
691
|
var keyPath = keyPathOverride || nodePath.join(dataDirPath, "db.key.enc");
|
|
692
|
+
var aad = _dbKeyAad(dataDirPath, keyPath);
|
|
670
693
|
if (nodeFs.existsSync(keyPath)) {
|
|
671
694
|
var sealed = atomicFile.readSync(keyPath, { encoding: "utf8" }).trim();
|
|
672
|
-
var b64
|
|
695
|
+
var b64;
|
|
696
|
+
// isAadSealed is checked FIRST and is load-bearing: AAD_PREFIX
|
|
697
|
+
// ("vault.aad:") is NOT a prefix of VAULT_PREFIX ("vault:"), so a
|
|
698
|
+
// plain vault.unseal would silently pass an AAD-sealed value
|
|
699
|
+
// through unchanged. AAD-bound keys verify the deployment context;
|
|
700
|
+
// a key file lifted from another deployment fails the tag check.
|
|
701
|
+
if (vaultAad.isAadSealed(sealed)) {
|
|
702
|
+
b64 = vaultAad.unseal(sealed, aad);
|
|
703
|
+
} else {
|
|
704
|
+
// Legacy plain-sealed key (pre-AAD): unseal with the classic
|
|
705
|
+
// path, then re-seal in place with the deployment-path binding so
|
|
706
|
+
// the next boot is AAD-verified. Read-migration preserves the key
|
|
707
|
+
// bytes — no re-key, no operator action.
|
|
708
|
+
b64 = vault.unseal(sealed);
|
|
709
|
+
if (b64) {
|
|
710
|
+
atomicFile.writeSync(keyPath, vaultAad.seal(b64, aad), { fileMode: 0o600 });
|
|
711
|
+
log("re-sealed DB encryption key with deployment-path binding at " + keyPath);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
673
714
|
if (!b64) {
|
|
674
715
|
throw _dbErr("db/key-unseal-empty",
|
|
675
716
|
"FATAL: db.key.enc unseal returned empty — vault may not be initialized or key file corrupted");
|
|
676
717
|
}
|
|
677
718
|
return Buffer.from(b64, "base64");
|
|
678
719
|
}
|
|
679
|
-
// First run — generate, seal, persist (atomic)
|
|
720
|
+
// First run — generate, AAD-seal, persist (atomic).
|
|
680
721
|
var raw = generateBytes(C.BYTES.bytes(32));
|
|
681
|
-
|
|
682
|
-
var sealedKey = vault.seal(raw.toString("base64"));
|
|
722
|
+
var sealedKey = vaultAad.seal(raw.toString("base64"), aad);
|
|
683
723
|
atomicFile.writeSync(keyPath, sealedKey, { fileMode: 0o600 });
|
|
684
724
|
log("generated DB encryption key at " + keyPath);
|
|
685
725
|
return raw;
|
|
@@ -918,6 +958,7 @@ function cleanStaleTmpDbs(tmpDir) {
|
|
|
918
958
|
* tmpDir: string, // override the encrypted-mode tmpfs path (default /dev/shm or BLAMEJS_TMPDIR)
|
|
919
959
|
* migrationDir: string, // optional — path to ./migrations/ (run-once each)
|
|
920
960
|
* streamLimit: number, // default 1_000_000 — db.stream row ceiling
|
|
961
|
+
* columnGate: "reject"|"warn"|"off", // default "reject" — refuse queries on columns not declared in the table schema
|
|
921
962
|
* skipBootIntegrityCheck: boolean, // default false — skip PRAGMA integrity_check
|
|
922
963
|
* skipIntegrityCheck: boolean, // default false — alias
|
|
923
964
|
* auditSigning: { mode, algorithm }, // default { mode: "wrapped" }
|
|
@@ -966,7 +1007,7 @@ async function init(opts) {
|
|
|
966
1007
|
throw new DbError("db/bad-at-rest",
|
|
967
1008
|
"db.init: atRest must be 'encrypted' or 'plain', got: " + opts.atRest);
|
|
968
1009
|
}
|
|
969
|
-
//
|
|
1010
|
+
// Operator-tunable streamLimit ceiling. Throw at config-time
|
|
970
1011
|
// on bad shape so a typo surfaces at boot rather than as an
|
|
971
1012
|
// unbounded stream at first export.
|
|
972
1013
|
if (opts.streamLimit !== undefined) {
|
|
@@ -978,6 +1019,15 @@ async function init(opts) {
|
|
|
978
1019
|
}
|
|
979
1020
|
streamLimit = opts.streamLimit;
|
|
980
1021
|
}
|
|
1022
|
+
// Column-membership gate mode — throw at config-time on a typo so it
|
|
1023
|
+
// surfaces at boot, not as a query that silently bypasses the gate.
|
|
1024
|
+
if (opts.columnGate !== undefined &&
|
|
1025
|
+
opts.columnGate !== "reject" && opts.columnGate !== "warn" && opts.columnGate !== "off") {
|
|
1026
|
+
throw new DbError("db/bad-init",
|
|
1027
|
+
"db.init: columnGate must be 'reject' (default), 'warn', or 'off'; got " +
|
|
1028
|
+
JSON.stringify(opts.columnGate));
|
|
1029
|
+
}
|
|
1030
|
+
columnGateMode = opts.columnGate || "reject";
|
|
981
1031
|
dataDir = opts.dataDir;
|
|
982
1032
|
if (!nodeFs.existsSync(dataDir)) nodeFs.mkdirSync(dataDir, { recursive: true });
|
|
983
1033
|
|
|
@@ -990,7 +1040,7 @@ async function init(opts) {
|
|
|
990
1040
|
}
|
|
991
1041
|
if (!nodeFs.existsSync(tmpDir)) nodeFs.mkdirSync(tmpDir, { recursive: true });
|
|
992
1042
|
|
|
993
|
-
//
|
|
1043
|
+
// If the resolved tmpDir is NOT actually tmpfs, the
|
|
994
1044
|
// plaintext DB file lives on persistent storage. We check that tmpDir
|
|
995
1045
|
// resolves under /dev/shm or /run/shm on Linux as a heuristic; on other
|
|
996
1046
|
// platforms we warn that the operator must verify tmpfs binding
|
|
@@ -1264,9 +1314,10 @@ async function init(opts) {
|
|
|
1264
1314
|
for (var i = 0; i < fullSchema.length; i++) {
|
|
1265
1315
|
var t = fullSchema[i];
|
|
1266
1316
|
cryptoField.registerTable(t.name, {
|
|
1267
|
-
sealedFields:
|
|
1268
|
-
derivedHashes:
|
|
1269
|
-
hashNamespaces:
|
|
1317
|
+
sealedFields: t.sealedFields,
|
|
1318
|
+
derivedHashes: t.derivedHashes,
|
|
1319
|
+
hashNamespaces: t.hashNamespaces,
|
|
1320
|
+
derivedHashMode: t.derivedHashMode,
|
|
1270
1321
|
});
|
|
1271
1322
|
tableMetadata[t.name] = {
|
|
1272
1323
|
primaryKey: _normalizePk(t),
|
|
@@ -1349,7 +1400,7 @@ async function init(opts) {
|
|
|
1349
1400
|
// is BELOW tip — the DB was rolled back to an older snapshot. Refuse boot.
|
|
1350
1401
|
_checkRollback(dataDir);
|
|
1351
1402
|
|
|
1352
|
-
// ----
|
|
1403
|
+
// ---- WORM posture assertion ----
|
|
1353
1404
|
// Under sec-17a-4 / finra-4511 / fda-21cfr11 postures the operator
|
|
1354
1405
|
// MUST have declared row-level WORM on at least one business-record
|
|
1355
1406
|
// table. Refuse boot otherwise so missing-declaration drift is
|
|
@@ -1491,10 +1542,41 @@ async function init(opts) {
|
|
|
1491
1542
|
*/
|
|
1492
1543
|
function from(tableName) {
|
|
1493
1544
|
_requireInit();
|
|
1494
|
-
return new Query(database, tableName
|
|
1545
|
+
return new Query(database, tableName, {
|
|
1546
|
+
declaredColumns: getDeclaredColumns(tableName),
|
|
1547
|
+
columnGateMode: columnGateMode,
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
/**
|
|
1552
|
+
* @primitive b.db.getDeclaredColumns
|
|
1553
|
+
* @signature b.db.getDeclaredColumns(tableName)
|
|
1554
|
+
* @since 0.14.7
|
|
1555
|
+
* @status stable
|
|
1556
|
+
* @related b.db.from, b.db.getTableMetadata
|
|
1557
|
+
*
|
|
1558
|
+
* Returns the declared column names for a table as an array, or `null`
|
|
1559
|
+
* when the table has no registered schema metadata (a cross- or
|
|
1560
|
+
* attached-schema table — the column-membership gate is a no-op for
|
|
1561
|
+
* those). The declared set includes `_id` and any derived-hash columns,
|
|
1562
|
+
* so sealed-field queries (which rewrite to the hash column) and `_id`
|
|
1563
|
+
* lookups pass the gate. Backs the `db.init({ columnGate })` gate that
|
|
1564
|
+
* refuses queries ordering / selecting / filtering on an undeclared
|
|
1565
|
+
* column before the identifier interpolates into SQL.
|
|
1566
|
+
*
|
|
1567
|
+
* @example
|
|
1568
|
+
* b.db.getDeclaredColumns("orders");
|
|
1569
|
+
* // → ["_id", "customerId", "total", "createdAt"]
|
|
1570
|
+
*/
|
|
1571
|
+
// The declared set includes `_id` and any derived-hash columns, so
|
|
1572
|
+
// sealed-field queries (which rewrite to the hash column) and `_id`
|
|
1573
|
+
// lookups pass membership.
|
|
1574
|
+
function getDeclaredColumns(tableName) {
|
|
1575
|
+
var md = tableMetadata[tableName];
|
|
1576
|
+
return (md && md.columns) ? Object.keys(md.columns) : null;
|
|
1495
1577
|
}
|
|
1496
1578
|
|
|
1497
|
-
//
|
|
1579
|
+
// Bounded prepared-statement cache for SQLite. Long-running
|
|
1498
1580
|
// daemons with diverse query shapes accumulate node:sqlite Statement
|
|
1499
1581
|
// handles indefinitely; the LRU here caps at PREPARE_CACHE_MAX (256)
|
|
1500
1582
|
// distinct SQL strings and finalizes the oldest when over. Reuse of
|
|
@@ -1611,7 +1693,7 @@ function stream(sql) {
|
|
|
1611
1693
|
var table = opts && typeof opts.table === "string" ? opts.table : null;
|
|
1612
1694
|
var unseal = table ? cryptoField : null;
|
|
1613
1695
|
|
|
1614
|
-
//
|
|
1696
|
+
// StreamLimit ceiling. Per-call opts.streamLimit overrides
|
|
1615
1697
|
// the module-level default; bad shape throws at call time so the
|
|
1616
1698
|
// typo surfaces instead of an unbounded stream.
|
|
1617
1699
|
var perCallLimit = streamLimit;
|
|
@@ -1661,10 +1743,10 @@ function stream(sql) {
|
|
|
1661
1743
|
|
|
1662
1744
|
// DDL_RE — case-insensitive prefix match for the eight statement
|
|
1663
1745
|
// shapes that MUTATE schema. Audited individually so a forensic
|
|
1664
|
-
// review can reconstruct schema evolution from the chain alone
|
|
1746
|
+
// review can reconstruct schema evolution from the chain alone.
|
|
1665
1747
|
var DDL_RE = /^\s*(CREATE|DROP|ALTER|TRUNCATE|RENAME|ATTACH|DETACH|REINDEX)\b/i;
|
|
1666
1748
|
|
|
1667
|
-
//
|
|
1749
|
+
// Slow-query observability buckets for the local SQLite nodePath.
|
|
1668
1750
|
// Highest matched bucket wins so the per-query emit is single-shot;
|
|
1669
1751
|
// operators dashboard on the `bucket` label.
|
|
1670
1752
|
var _SLOW_QUERY_BUCKETS_LOCAL = Object.freeze([
|
|
@@ -1712,7 +1794,7 @@ function execRaw(sql) {
|
|
|
1712
1794
|
action: "db.ddl.executed",
|
|
1713
1795
|
outcome: "success",
|
|
1714
1796
|
metadata: {
|
|
1715
|
-
// OTel db.* semconv
|
|
1797
|
+
// OTel db.* semconv — emit framework-conventional
|
|
1716
1798
|
// attributes alongside the audit row so dashboards built on
|
|
1717
1799
|
// OTel can correlate without an adapter.
|
|
1718
1800
|
"db.system": "sqlite",
|
|
@@ -2701,7 +2783,7 @@ function vacuumAfterErase(opts) {
|
|
|
2701
2783
|
} catch (_e) { /* audit best-effort */ }
|
|
2702
2784
|
}
|
|
2703
2785
|
|
|
2704
|
-
//
|
|
2786
|
+
// Cascade-installed posture name. b.compliance.set(p)
|
|
2705
2787
|
// calls applyPosture(p) which records the posture; the downstream
|
|
2706
2788
|
// cryptoField.eraseRow path consults this via getActivePosture() to
|
|
2707
2789
|
// auto-vacuum under postures whose POSTURE_DEFAULTS sets
|
|
@@ -3054,10 +3136,12 @@ module.exports = {
|
|
|
3054
3136
|
getActivePosture: getActivePosture,
|
|
3055
3137
|
vacuumAfterErase: vacuumAfterErase,
|
|
3056
3138
|
from: from,
|
|
3139
|
+
getDeclaredColumns: getDeclaredColumns,
|
|
3140
|
+
_checkDualControlGate: _checkDualControlGate,
|
|
3057
3141
|
collection: require("./db-collection").collection, // allow:inline-require — db-collection lazy-requires db.js back; the inline require here breaks the cycle without needing a stub
|
|
3058
3142
|
prepare: prepare,
|
|
3059
3143
|
stream: stream,
|
|
3060
|
-
//
|
|
3144
|
+
// Runtime read-only accessor so Query.stream picks up the
|
|
3061
3145
|
// configured ceiling without re-importing module state.
|
|
3062
3146
|
getStreamLimit: function () { return streamLimit; },
|
|
3063
3147
|
runSql: execRaw,
|
|
@@ -57,7 +57,7 @@ function _emitMetric(name, value, labels) {
|
|
|
57
57
|
catch (_e) { /* hot-path observability sink — drop silent by design */ }
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
// Statement-class classifier for auth-failure forensics
|
|
60
|
+
// Statement-class classifier for auth-failure forensics. Inspects
|
|
61
61
|
// the leading keyword only so an attacker-controlled trailing fragment
|
|
62
62
|
// can't smuggle a false classification. Skips leading whitespace plus
|
|
63
63
|
// SQL line / block comments before reading the keyword.
|
|
@@ -83,8 +83,49 @@ function _classifyStatement(sql) {
|
|
|
83
83
|
return _STATEMENT_CLASS_MAP[m[1].toUpperCase()] || "OTHER";
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// Best-effort target-relation extractor for auth-failure forensics: the
|
|
87
|
+
// table the denied role attempted to touch, so the audit row records
|
|
88
|
+
// the OBJECT (SOC2 CC7.2 / NIST SP 800-53 AU-3 "what was accessed"),
|
|
89
|
+
// not just the statement class. Defensive reader — returns null on
|
|
90
|
+
// anything unparseable and NEVER throws: it runs in the live failure
|
|
91
|
+
// path and must not mask the real 28000 / 42501 error. The extracted
|
|
92
|
+
// identifier is captured into audit METADATA (a JSON string, never
|
|
93
|
+
// re-executed as SQL), so the only sink risk is log-injection: any
|
|
94
|
+
// segment carrying a control / NUL character is refused. Spaces and
|
|
95
|
+
// ordinary punctuation inside a quoted identifier are kept so a
|
|
96
|
+
// legitimately-quoted relation name still surfaces in the audit row.
|
|
97
|
+
var _RELATION_RE = /\b(?:FROM|INTO|UPDATE|JOIN|TABLE|COPY)\s+((?:"[^"]+"|`[^`]+`|[A-Za-z_][\w$]*)(?:\.(?:"[^"]+"|`[^`]+`|[A-Za-z_][\w$]*))?)/ig;
|
|
98
|
+
function _hasControlChar(s) {
|
|
99
|
+
for (var i = 0; i < s.length; i += 1) {
|
|
100
|
+
var c = s.charCodeAt(i);
|
|
101
|
+
if (c < 0x20 || c === 0x7f) return true;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
function _extractTargetRelation(sql) {
|
|
106
|
+
if (typeof sql !== "string" || sql.length === 0) return null;
|
|
107
|
+
var clean = sql.replace(/--[^\n]*/g, " ").replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
108
|
+
_RELATION_RE.lastIndex = 0;
|
|
109
|
+
var m = _RELATION_RE.exec(clean);
|
|
110
|
+
if (!m) return null;
|
|
111
|
+
var segs = m[1].split(".").map(function (s) { return s.replace(/^["`]|["`]$/g, ""); });
|
|
112
|
+
for (var i = 0; i < segs.length; i += 1) {
|
|
113
|
+
if (segs[i].length === 0 || _hasControlChar(segs[i])) return null;
|
|
114
|
+
}
|
|
115
|
+
return segs.join(".");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function _countTargetRelations(sql) {
|
|
119
|
+
if (typeof sql !== "string") return 0;
|
|
120
|
+
var clean = sql.replace(/--[^\n]*/g, " ").replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
121
|
+
_RELATION_RE.lastIndex = 0;
|
|
122
|
+
var n = 0;
|
|
123
|
+
while (_RELATION_RE.exec(clean) !== null) n += 1;
|
|
124
|
+
return n;
|
|
125
|
+
}
|
|
126
|
+
|
|
86
127
|
// Postgres SQLSTATE classes that indicate authentication / authorization
|
|
87
|
-
// failure at the DB level. SOC2 forensic gap
|
|
128
|
+
// failure at the DB level. SOC2 forensic gap — every match emits
|
|
88
129
|
// db.auth.failed with the SQL identity attempted, the database, and
|
|
89
130
|
// the statement class.
|
|
90
131
|
var _AUTH_FAILURE_CODES = Object.freeze({
|
|
@@ -97,18 +138,24 @@ function _emitAuthFailureAudit(backend, role, sql, e) {
|
|
|
97
138
|
if (!e || !e.code) return;
|
|
98
139
|
var kind = _AUTH_FAILURE_CODES[e.code];
|
|
99
140
|
if (!kind) return;
|
|
141
|
+
var attemptedTable = _extractTargetRelation(sql);
|
|
142
|
+
var relationCount = _countTargetRelations(sql);
|
|
143
|
+
var resource = { kind: "db.backend", id: backend.name };
|
|
144
|
+
if (attemptedTable !== null) resource.attemptedTable = attemptedTable;
|
|
100
145
|
audit().safeEmit({
|
|
101
146
|
action: "db.auth.failed",
|
|
102
147
|
actor: {},
|
|
103
|
-
resource:
|
|
148
|
+
resource: resource,
|
|
104
149
|
outcome: "denied",
|
|
105
150
|
reason: kind,
|
|
106
151
|
metadata: {
|
|
107
|
-
backend:
|
|
108
|
-
dialect:
|
|
109
|
-
sqlIdentity:
|
|
110
|
-
sqlstate:
|
|
111
|
-
statementClass:
|
|
152
|
+
backend: backend.name,
|
|
153
|
+
dialect: backend.dialect,
|
|
154
|
+
sqlIdentity: role || null,
|
|
155
|
+
sqlstate: e.code,
|
|
156
|
+
statementClass: _classifyStatement(sql),
|
|
157
|
+
attemptedTable: attemptedTable,
|
|
158
|
+
attemptedRelationCount: relationCount,
|
|
112
159
|
},
|
|
113
160
|
});
|
|
114
161
|
_emitMetric("db.auth.failed", 1, {
|
|
@@ -118,7 +165,7 @@ function _emitAuthFailureAudit(backend, role, sql, e) {
|
|
|
118
165
|
});
|
|
119
166
|
}
|
|
120
167
|
|
|
121
|
-
// Slow-query bucket emitter
|
|
168
|
+
// Slow-query bucket emitter. Single-shot per query — highest
|
|
122
169
|
// matched bucket wins. Operators dashboard on the `bucket` label
|
|
123
170
|
// rather than separate counters per threshold.
|
|
124
171
|
var _SLOW_QUERY_BUCKETS = Object.freeze([
|
|
@@ -628,7 +675,7 @@ async function query(sql, params, opts) {
|
|
|
628
675
|
_emitMetric("db.role.denied", 1,
|
|
629
676
|
{ backend: b.name, role: role || "(none)" });
|
|
630
677
|
}
|
|
631
|
-
//
|
|
678
|
+
// DB-auth audit visibility. Every 28000 / 28P01 / 42501
|
|
632
679
|
// surfaces an auditable db.auth.failed row tagged with the SQL
|
|
633
680
|
// identity and the statement class so SOC2 reviewers can
|
|
634
681
|
// reconstruct the denial timeline.
|
|
@@ -693,13 +740,13 @@ async function transaction(fn, opts) {
|
|
|
693
740
|
var prebuiltGucs = _buildSessionGucsStatements(opts.sessionGucs);
|
|
694
741
|
|
|
695
742
|
var t0 = Date.now();
|
|
696
|
-
//
|
|
697
|
-
// the query-cancel ceiling to this transaction;
|
|
743
|
+
// Per-statement timeout. SET LOCAL statement_timeout binds
|
|
744
|
+
// the query-cancel ceiling to this transaction; this wires
|
|
698
745
|
// idle_in_transaction_session_timeout from the same opt. Both
|
|
699
746
|
// emit at SET LOCAL scope so the next pool checkout starts clean.
|
|
700
747
|
var stmtTimeoutMs = opts.statementTimeoutMs;
|
|
701
748
|
var idleTimeoutMs = opts.idleInTransactionTimeoutMs;
|
|
702
|
-
//
|
|
749
|
+
// Deadlock-retry policy. 40P01 (deadlock_detected) and 40001
|
|
703
750
|
// (serialization_failure) are transient — retry with capped attempts
|
|
704
751
|
// and a small jittered backoff. Operators tune retries via opts.deadlockRetries (default 3).
|
|
705
752
|
// numeric-bounds doesn't have a non-negative-int helper; use a
|
|
@@ -771,7 +818,7 @@ async function transaction(fn, opts) {
|
|
|
771
818
|
_emitMetric("db.role.denied", 1,
|
|
772
819
|
{ backend: b.name, role: role || "(none)" });
|
|
773
820
|
}
|
|
774
|
-
//
|
|
821
|
+
// DB-auth audit visibility on transaction-shaped denials.
|
|
775
822
|
// Statement class always reads as "TX" since the failure
|
|
776
823
|
// surface inside a transaction body could be any statement;
|
|
777
824
|
// operators correlate via the transaction's audit row.
|
|
@@ -1017,7 +1064,7 @@ function _requireInit() {
|
|
|
1017
1064
|
|
|
1018
1065
|
var REPLICA_UNHEALTHY_COOLDOWN_MS = C.TIME.seconds(30);
|
|
1019
1066
|
|
|
1020
|
-
//
|
|
1067
|
+
// Replica residency-tag compatibility.
|
|
1021
1068
|
//
|
|
1022
1069
|
// A primary tagged "EU" replicating to a "US" replica is a GDPR
|
|
1023
1070
|
// Article 46 cross-border transfer; without an explicit operator
|
|
@@ -1194,7 +1241,7 @@ async function _readQuery(sql, params, opts) {
|
|
|
1194
1241
|
_emitMetric("db.role.denied", 1,
|
|
1195
1242
|
{ backend: b.name, role: role || "(none)" });
|
|
1196
1243
|
}
|
|
1197
|
-
//
|
|
1244
|
+
// DB-auth audit visibility for read-replica denials too.
|
|
1198
1245
|
_emitAuthFailureAudit(b, role, sql, e);
|
|
1199
1246
|
// Fallback to primary on a failed replica read when allowed.
|
|
1200
1247
|
if (b.replicaFallbackToPrimary) {
|
|
@@ -1874,4 +1921,5 @@ module.exports = {
|
|
|
1874
1921
|
migrate: externalDbMigrate,
|
|
1875
1922
|
Pool: Pool,
|
|
1876
1923
|
_resetForTest: _resetForTest,
|
|
1924
|
+
_extractTargetRelation: _extractTargetRelation,
|
|
1877
1925
|
};
|
|
@@ -648,8 +648,8 @@ function _breakGlassPoliciesDDL(dialect) {
|
|
|
648
648
|
}
|
|
649
649
|
|
|
650
650
|
// _blamejs_break_glass_grants — issued grants. One row per successful
|
|
651
|
-
// step-up. Default maxRowsPerGrant=1 enforces row-by-row auth
|
|
652
|
-
//
|
|
651
|
+
// step-up. Default maxRowsPerGrant=1 enforces row-by-row auth
|
|
652
|
+
// ("each row access = its own grant").
|
|
653
653
|
function _breakGlassGrantsDDL(dialect) {
|
|
654
654
|
var t = _types(dialect);
|
|
655
655
|
var name = LOCAL_TO_EXTERNAL._blamejs_break_glass_grants;
|
|
@@ -766,7 +766,7 @@ async function ensureSchema(opts) {
|
|
|
766
766
|
created.push(d.create.match(/CREATE TABLE IF NOT EXISTS\s+(\S+)/)[1]);
|
|
767
767
|
}
|
|
768
768
|
|
|
769
|
-
//
|
|
769
|
+
// Append-only WORM enforcement on audit_log / consent_log /
|
|
770
770
|
// audit_checkpoints in cluster mode. Local-SQLite path already
|
|
771
771
|
// installs CREATE TRIGGER IF NOT EXISTS via lib/db.js's
|
|
772
772
|
// _installAppendOnlyTriggers; Postgres needs equivalent rules
|
|
@@ -779,7 +779,7 @@ async function ensureSchema(opts) {
|
|
|
779
779
|
return { tables: created };
|
|
780
780
|
}
|
|
781
781
|
|
|
782
|
-
//
|
|
782
|
+
// WORM enforcement helper. Idempotent: rebuilding triggers
|
|
783
783
|
// per boot is cheap and any operator-applied DROP TRIGGER is restored
|
|
784
784
|
// at the next ensureSchema pass.
|
|
785
785
|
async function _installWormTriggers(backend, dialect) {
|
|
@@ -203,8 +203,8 @@ function validate(headerValue, opts) {
|
|
|
203
203
|
// recover the boundary without Public Suffix List awareness
|
|
204
204
|
// (`team.example.com` could be label=team / ns=example.com OR
|
|
205
205
|
// label=team.example / ns=com). The earlier last-2-segment
|
|
206
|
-
// heuristic produced empty `label` for 2-label IDs
|
|
207
|
-
//
|
|
206
|
+
// heuristic produced empty `label` for 2-label IDs
|
|
207
|
+
// which violates RFC 2919 §2's required label "."
|
|
208
208
|
// namespace decomposition.
|
|
209
209
|
//
|
|
210
210
|
// Drop the heuristic split — surface only the raw `listId` (and
|
|
@@ -349,8 +349,7 @@ function _extractUris(raw, maxUris) {
|
|
|
349
349
|
// bracket pairs directly via String.matchAll so URIs containing
|
|
350
350
|
// commas (legitimate, e.g. `<https://x/u?tags=a,b>`) parse
|
|
351
351
|
// correctly. Earlier split(",")-based scan misclassified such
|
|
352
|
-
// URIs as "no <URI> elements" and refused legitimate mail
|
|
353
|
-
// (Codex P1 on PR #63).
|
|
352
|
+
// URIs as "no <URI> elements" and refused legitimate mail.
|
|
354
353
|
var matches = raw.matchAll(/<([^<>]*)>/g); // allow:regex-no-length-cap — input length-bounded by maxBytes check upstream
|
|
355
354
|
var uris = [];
|
|
356
355
|
for (var m of matches) {
|
|
@@ -305,8 +305,158 @@ function create(opts) {
|
|
|
305
305
|
};
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
// Breach detection -> notification running clock. The reporter
|
|
309
|
+
// (`create`) computes the static per-stage deadlines; this clock turns
|
|
310
|
+
// them into a live escalation loop: it tracks open incident records and,
|
|
311
|
+
// on each tick, fires "approaching" warnings as a stage's deadline nears
|
|
312
|
+
// and a "passed" alert when it elapses — once per (incident, stage,
|
|
313
|
+
// state) so a busy tick interval can't storm the operator. It re-uses
|
|
314
|
+
// the reporter's REGIME_DEADLINES / dueBy timestamps and re-encodes no
|
|
315
|
+
// jurisdiction hour-counts (GDPR Art.33 72h, NIS2 Art.23(4) 24h, DORA
|
|
316
|
+
// Art.19 + RTS 2024/1772 4h, CRA Art.14, HIPAA 45 CFR 164.404/408).
|
|
317
|
+
// `approachThresholds` are unitless proportions of detected-to-due.
|
|
318
|
+
function createDeadlineClock(opts) {
|
|
319
|
+
opts = opts || {};
|
|
320
|
+
validateOpts(opts, [
|
|
321
|
+
"audit", "notify", "approachThresholds", "intervalMs", "autoStart", "now",
|
|
322
|
+
], "incident.report.createDeadlineClock");
|
|
323
|
+
|
|
324
|
+
var auditOn = opts.audit !== false;
|
|
325
|
+
var notify = (opts.notify && typeof opts.notify.send === "function") ? opts.notify : null;
|
|
326
|
+
var thresholds = Array.isArray(opts.approachThresholds) ? opts.approachThresholds.slice() : [0.5, 0.75, 0.9];
|
|
327
|
+
for (var ti = 0; ti < thresholds.length; ti += 1) {
|
|
328
|
+
if (typeof thresholds[ti] !== "number" || !(thresholds[ti] > 0 && thresholds[ti] < 1)) {
|
|
329
|
+
throw new IncidentReportError("incident-report/bad-threshold",
|
|
330
|
+
"createDeadlineClock: approachThresholds must be numbers strictly between 0 and 1");
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
thresholds.sort(function (a, b) { return a - b; });
|
|
334
|
+
var now = typeof opts.now === "function" ? opts.now : function () { return Date.now(); };
|
|
335
|
+
var intervalMs = (typeof opts.intervalMs === "number" && isFinite(opts.intervalMs) && opts.intervalMs > 0)
|
|
336
|
+
? opts.intervalMs : C.TIME.minutes(1);
|
|
337
|
+
var autoStart = opts.autoStart !== false;
|
|
338
|
+
|
|
339
|
+
var tracked = new Map(); // incidentId -> { detectedAt, dueBy, regime, acked, fired }
|
|
340
|
+
var timer = null;
|
|
341
|
+
|
|
342
|
+
function _emit(action, outcome, metadata) {
|
|
343
|
+
if (!auditOn) return;
|
|
344
|
+
try {
|
|
345
|
+
audit().safeEmit({ action: "incident.report.clock." + action, outcome: outcome, metadata: metadata || {} });
|
|
346
|
+
} catch (_e) { /* drop-silent — by design */ }
|
|
347
|
+
}
|
|
348
|
+
function _notify(payload) {
|
|
349
|
+
if (!notify) return;
|
|
350
|
+
try {
|
|
351
|
+
var r = notify.send(payload);
|
|
352
|
+
if (r && typeof r.then === "function") r.then(null, function () {});
|
|
353
|
+
} catch (_e) { /* drop-silent — escalation is best-effort, never crashes a tick */ }
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function track(record) {
|
|
357
|
+
if (!record || typeof record !== "object" || typeof record.id !== "string" || record.id.length === 0) {
|
|
358
|
+
throw new IncidentReportError("incident-report/bad-record",
|
|
359
|
+
"createDeadlineClock.track: record must be an incident.report record with a string id");
|
|
360
|
+
}
|
|
361
|
+
if (!record.dueBy || typeof record.dueBy !== "object" ||
|
|
362
|
+
typeof record.detectedAt !== "number") {
|
|
363
|
+
throw new IncidentReportError("incident-report/bad-record",
|
|
364
|
+
"createDeadlineClock.track: record must carry detectedAt + dueBy { initial, intermediate, final }");
|
|
365
|
+
}
|
|
366
|
+
tracked.set(record.id, {
|
|
367
|
+
detectedAt: record.detectedAt,
|
|
368
|
+
dueBy: record.dueBy,
|
|
369
|
+
regime: record.regime || null,
|
|
370
|
+
acked: {},
|
|
371
|
+
fired: {},
|
|
372
|
+
});
|
|
373
|
+
return record.id;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function untrack(id) { return tracked.delete(id); }
|
|
377
|
+
|
|
378
|
+
function acknowledgeSubmission(id, stage, info) {
|
|
379
|
+
if (!VALID_STAGES[stage]) {
|
|
380
|
+
throw new IncidentReportError("incident-report/bad-stage",
|
|
381
|
+
"createDeadlineClock.acknowledgeSubmission: stage must be one of " + Object.keys(VALID_STAGES).join(", "));
|
|
382
|
+
}
|
|
383
|
+
var t = tracked.get(id);
|
|
384
|
+
if (!t) {
|
|
385
|
+
throw new IncidentReportError("incident-report/unknown-incident",
|
|
386
|
+
"createDeadlineClock.acknowledgeSubmission: no tracked incident '" + id + "'");
|
|
387
|
+
}
|
|
388
|
+
t.acked[stage] = true;
|
|
389
|
+
_emit("submission_acknowledged", "success", { incidentId: id, regime: t.regime, stage: stage, info: info || null });
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Pure evaluation seam — operators (and tests) can pass an explicit
|
|
394
|
+
// nowMs. Each (incident, stage, state) fires AT MOST once; a stage
|
|
395
|
+
// that has been acknowledged is skipped entirely.
|
|
396
|
+
function tick(nowMsArg) {
|
|
397
|
+
var nowMs = typeof nowMsArg === "number" ? nowMsArg : now();
|
|
398
|
+
tracked.forEach(function (t, id) {
|
|
399
|
+
var stages = ["initial", "intermediate", "final"];
|
|
400
|
+
for (var si = 0; si < stages.length; si += 1) {
|
|
401
|
+
var stage = stages[si];
|
|
402
|
+
if (t.acked[stage]) continue;
|
|
403
|
+
var due = t.dueBy[stage];
|
|
404
|
+
if (typeof due !== "number") continue;
|
|
405
|
+
var span = due - t.detectedAt;
|
|
406
|
+
if (span <= 0) continue;
|
|
407
|
+
if (nowMs >= due) {
|
|
408
|
+
var pk = stage + ":passed";
|
|
409
|
+
if (!t.fired[pk]) {
|
|
410
|
+
t.fired[pk] = true;
|
|
411
|
+
_emit("deadline_passed", "failure", { incidentId: id, regime: t.regime, stage: stage, dueBy: due });
|
|
412
|
+
_notify({ kind: "deadline_passed", incidentId: id, regime: t.regime, stage: stage, dueBy: due });
|
|
413
|
+
}
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
var proportion = (nowMs - t.detectedAt) / span;
|
|
417
|
+
for (var thi = thresholds.length - 1; thi >= 0; thi -= 1) {
|
|
418
|
+
if (proportion >= thresholds[thi]) {
|
|
419
|
+
var ak = stage + ":approaching:" + thresholds[thi];
|
|
420
|
+
if (!t.fired[ak]) {
|
|
421
|
+
t.fired[ak] = true;
|
|
422
|
+
_emit("deadline_approaching", "warning",
|
|
423
|
+
{ incidentId: id, regime: t.regime, stage: stage, dueBy: due, threshold: thresholds[thi] });
|
|
424
|
+
_notify({ kind: "deadline_approaching", incidentId: id, regime: t.regime, stage: stage, dueBy: due, threshold: thresholds[thi] });
|
|
425
|
+
}
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function start() {
|
|
434
|
+
if (timer) return;
|
|
435
|
+
timer = setInterval(function () { tick(); }, intervalMs);
|
|
436
|
+
if (timer && typeof timer.unref === "function") timer.unref();
|
|
437
|
+
}
|
|
438
|
+
function stop() {
|
|
439
|
+
if (timer) { clearInterval(timer); timer = null; }
|
|
440
|
+
}
|
|
441
|
+
function status() {
|
|
442
|
+
return { tracked: tracked.size, running: timer !== null, intervalMs: intervalMs };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (autoStart) start();
|
|
446
|
+
return {
|
|
447
|
+
track: track,
|
|
448
|
+
untrack: untrack,
|
|
449
|
+
acknowledgeSubmission: acknowledgeSubmission,
|
|
450
|
+
tick: tick,
|
|
451
|
+
start: start,
|
|
452
|
+
stop: stop,
|
|
453
|
+
status: status,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
308
457
|
module.exports = {
|
|
309
458
|
create: create,
|
|
459
|
+
createDeadlineClock: createDeadlineClock,
|
|
310
460
|
IncidentReportError: IncidentReportError,
|
|
311
461
|
REGIME_DEADLINES: REGIME_DEADLINES,
|
|
312
462
|
DEFAULT_DEADLINES: DEFAULT_DEADLINES,
|
|
@@ -763,7 +763,7 @@ function checkCert(opts) {
|
|
|
763
763
|
}
|
|
764
764
|
|
|
765
765
|
// Validity window — refuse certs outside their notBefore / notAfter
|
|
766
|
-
// window.
|
|
766
|
+
// window. checkCert's docstring promises this throws
|
|
767
767
|
// `mail-crypto/smime/expired-cert` but the impl was missing, letting
|
|
768
768
|
// expired or not-yet-valid signing certs pass boot-time preflight
|
|
769
769
|
// and fail interop later when peers verify signatures against the
|