@blamejs/core 0.6.13 → 0.6.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/NOTICE +16 -0
  3. package/README.md +27 -18
  4. package/index.js +12 -0
  5. package/lib/archive.js +8 -7
  6. package/lib/audit.js +4 -0
  7. package/lib/auth/password.js +449 -4
  8. package/lib/bundler.js +8 -8
  9. package/lib/cache.js +105 -20
  10. package/lib/cli.js +598 -4
  11. package/lib/config-drift.js +309 -0
  12. package/lib/crypto-field.js +37 -0
  13. package/lib/crypto.js +8 -0
  14. package/lib/db-query.js +21 -2
  15. package/lib/db.js +32 -2
  16. package/lib/dual-control.js +475 -0
  17. package/lib/file-type.js +265 -0
  18. package/lib/framework-schema.js +38 -6
  19. package/lib/http-client-cookie-jar.js +117 -17
  20. package/lib/http-client.js +81 -3
  21. package/lib/internal-sha1-hibp.js +34 -0
  22. package/lib/mail.js +5 -4
  23. package/lib/middleware/csp-nonce.js +7 -4
  24. package/lib/middleware/index.js +2 -0
  25. package/lib/middleware/network-allowlist.js +199 -0
  26. package/lib/network-dns.js +564 -0
  27. package/lib/network-heartbeat.js +290 -0
  28. package/lib/network-nts.js +552 -0
  29. package/lib/network-proxy.js +246 -0
  30. package/lib/network-tls.js +326 -0
  31. package/lib/network.js +233 -0
  32. package/lib/ntp-check.js +50 -4
  33. package/lib/object-store/azure-blob.js +16 -42
  34. package/lib/pagination.js +136 -76
  35. package/lib/parsers/index.js +16 -2
  36. package/lib/parsers/safe-ini.js +273 -0
  37. package/lib/permissions.js +223 -9
  38. package/lib/pqc-agent.js +4 -4
  39. package/lib/retention.js +439 -0
  40. package/lib/security-assert.js +368 -0
  41. package/lib/session.js +138 -8
  42. package/lib/ssrf-guard.js +9 -0
  43. package/lib/vault/index.js +3 -3
  44. package/lib/vendor/MANIFEST.json +12 -0
  45. package/lib/vendor/common-passwords-top-10000.txt +10000 -0
  46. package/package.json +3 -2
  47. package/sbom.cyclonedx.json +61 -0
@@ -0,0 +1,309 @@
1
+ "use strict";
2
+ /**
3
+ * config-drift — boot-time config-baseline capture + signed sidecar +
4
+ * next-boot drift detection.
5
+ *
6
+ * The framework's audit chain captures DATA writes (every row, every
7
+ * key change). It does NOT capture RUNTIME CONFIG (the operator
8
+ * silently changed `allowedOrigins` 3 weeks ago and we have no
9
+ * signal). config-drift fills that gap: at every boot, the operator
10
+ * passes the baseline config snapshot they want tracked. The
11
+ * primitive hashes it with SHA3-512, signs the digest with the audit-
12
+ * signing key, and writes the result to a sidecar at
13
+ * `<dataDir>/config-baseline.sig`. On the next boot, the sidecar is
14
+ * loaded + verified + diffed against the new snapshot. Drift surfaces
15
+ * as an audit event (`config.drift.detected`); no boot block — the
16
+ * operator may have a legitimate reason to change config and the
17
+ * framework's job is to make the change auditable, not to refuse to
18
+ * start.
19
+ *
20
+ * var configDrift = b.configDrift.create({
21
+ * dataDir: "/data",
22
+ * audit: b.audit,
23
+ * });
24
+ *
25
+ * await configDrift.checkpoint({
26
+ * // operator decides what's tracked. JSON-stringifiable.
27
+ * allowedOrigins: ["https://app.example.com"],
28
+ * csp: "default-src 'self'",
29
+ * auditMode: b.audit.getMode(),
30
+ * vaultMode: b.vault.getMode(),
31
+ * dbAtRest: b.db.getAtRestMode(),
32
+ * });
33
+ * // → { signed: true, drifted: false, previousAt: 1730000000000 }
34
+ *
35
+ * The signed sidecar uses b.auditSign — same SLH-DSA-SHAKE-256f keypair
36
+ * the audit chain anchors on. An attacker who flips a config value
37
+ * would also need to forge the signing key to update the sidecar
38
+ * cleanly; otherwise next-boot verify catches the tamper.
39
+ *
40
+ * Validation:
41
+ * - create() opts: throw at boot on bad shape
42
+ * - checkpoint() snapshot: must be a JSON-serialisable object
43
+ * - sidecar verify failure (tampered, key rotated, missing pubkey)
44
+ * surfaces as `config.baseline.tamper` audit event AND the call
45
+ * returns { tamper: true } so the operator can decide whether
46
+ * to refuse boot
47
+ */
48
+ var fs = require("node:fs");
49
+ var path = require("node:path");
50
+ var auditSign = require("./audit-sign");
51
+ var crypto = require("./crypto");
52
+ var lazyRequire = require("./lazy-require");
53
+ var validateOpts = require("./validate-opts");
54
+ var { defineClass } = require("./framework-error");
55
+
56
+ var audit = lazyRequire(function () { return require("./audit"); });
57
+
58
+ var ConfigDriftError = defineClass("ConfigDriftError", { alwaysPermanent: true });
59
+ var _err = ConfigDriftError.factory;
60
+
61
+ var SIDECAR_NAME = "config-baseline.sig";
62
+ var SIDECAR_VERSION = 1;
63
+
64
+ // Stable JSON serialization: deterministic key order so the same
65
+ // snapshot always hashes to the same digest. Without this, an object
66
+ // reordered between boots would falsely flag as drift.
67
+ function _stableStringify(value) {
68
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
69
+ if (Array.isArray(value)) {
70
+ return "[" + value.map(_stableStringify).join(",") + "]";
71
+ }
72
+ var keys = Object.keys(value).sort();
73
+ var pairs = [];
74
+ for (var i = 0; i < keys.length; i++) {
75
+ pairs.push(JSON.stringify(keys[i]) + ":" + _stableStringify(value[keys[i]]));
76
+ }
77
+ return "{" + pairs.join(",") + "}";
78
+ }
79
+
80
+ function _hashSnapshot(snapshot) {
81
+ return crypto.sha3Hash(_stableStringify(snapshot));
82
+ }
83
+
84
+ function _diffShallow(prev, next) {
85
+ // For drift reporting only — names the keys that changed without
86
+ // dumping the full snapshot (which may carry sensitive values like
87
+ // a CSP nonce-derivation key). Operators reading the drift event
88
+ // get "these keys changed" + a hash on each side.
89
+ var changed = [];
90
+ var added = [];
91
+ var removed = [];
92
+ var allKeys = {};
93
+ Object.keys(prev || {}).forEach(function (k) { allKeys[k] = true; });
94
+ Object.keys(next || {}).forEach(function (k) { allKeys[k] = true; });
95
+ Object.keys(allKeys).forEach(function (k) {
96
+ var inPrev = Object.prototype.hasOwnProperty.call(prev || {}, k);
97
+ var inNext = Object.prototype.hasOwnProperty.call(next || {}, k);
98
+ if (inPrev && !inNext) { removed.push(k); return; }
99
+ if (!inPrev && inNext) { added.push(k); return; }
100
+ if (_stableStringify(prev[k]) !== _stableStringify(next[k])) changed.push(k);
101
+ });
102
+ return { changed: changed, added: added, removed: removed };
103
+ }
104
+
105
+ function create(opts) {
106
+ opts = opts || {};
107
+ validateOpts(opts, [
108
+ "dataDir", "audit", "baseline", "criticalKeys", "ignoreKeys",
109
+ ], "configDrift");
110
+ if (typeof opts.dataDir !== "string" || opts.dataDir.length === 0) {
111
+ throw _err("BAD_OPT", "create: opts.dataDir is required");
112
+ }
113
+ var dataDir = opts.dataDir;
114
+ var auditOn = opts.audit !== false;
115
+ var auditInstance = (opts.audit && opts.audit !== true) ? opts.audit : null;
116
+ // Multi-baseline support — each operator-named baseline lives in its
117
+ // own sidecar so production / staging / disaster-recovery deploys
118
+ // each track their own drift independently.
119
+ var baselineName = (typeof opts.baseline === "string" && opts.baseline.length > 0)
120
+ ? opts.baseline : "default";
121
+ // Critical-keys allowlist: drift in these keys raises severity to
122
+ // "high" in the audit emission so SIEM rules can page on them
123
+ // separately from cosmetic drift. null = every key is treated as
124
+ // high severity (the safer default).
125
+ var criticalKeys = Array.isArray(opts.criticalKeys) ? opts.criticalKeys.slice() : null;
126
+ // Ignore-keys: drift in these keys is excluded from drift detection
127
+ // (e.g. operator-tracked metadata that legitimately changes per
128
+ // boot). Captured in the snapshot but never flagged.
129
+ var ignoreKeys = Array.isArray(opts.ignoreKeys) ? opts.ignoreKeys.slice() : [];
130
+ var sidecarPath = path.join(dataDir,
131
+ baselineName === "default" ? SIDECAR_NAME : ("config-baseline-" + baselineName + ".sig"));
132
+
133
+ function _emit(action, info, outcome) {
134
+ if (!auditOn) return;
135
+ var sink = auditInstance || audit();
136
+ try {
137
+ sink.safeEmit({
138
+ action: action,
139
+ outcome: outcome,
140
+ metadata: info || {},
141
+ reason: info && info.reason ? info.reason : null,
142
+ });
143
+ } catch (_e) { /* audit best-effort */ }
144
+ }
145
+
146
+ function _readSidecar() {
147
+ if (!fs.existsSync(sidecarPath)) return null;
148
+ var raw;
149
+ try { raw = fs.readFileSync(sidecarPath, "utf8"); }
150
+ catch (_e) { return null; }
151
+ var parsed;
152
+ try { parsed = JSON.parse(raw); }
153
+ catch (_e) { return { unreadable: true }; }
154
+ if (!parsed || parsed.version !== SIDECAR_VERSION) return { unreadable: true };
155
+ if (typeof parsed.digestHex !== "string" || typeof parsed.signatureBase64 !== "string" ||
156
+ typeof parsed.publicKeyPem !== "string" || typeof parsed.snapshot !== "object") {
157
+ return { unreadable: true };
158
+ }
159
+ return parsed;
160
+ }
161
+
162
+ function _writeSidecar(snapshot, digestHex) {
163
+ // Sign over the digest (not the snapshot bytes directly) so the
164
+ // sidecar stays small even when the snapshot is large.
165
+ var signature = auditSign.sign(digestHex);
166
+ var payload = {
167
+ version: SIDECAR_VERSION,
168
+ capturedAt: Date.now(),
169
+ digestHex: digestHex,
170
+ signatureBase64: Buffer.from(signature).toString("base64"),
171
+ publicKeyPem: auditSign.getPublicKey(),
172
+ snapshot: snapshot,
173
+ };
174
+ var tmp = sidecarPath + ".tmp";
175
+ fs.writeFileSync(tmp, JSON.stringify(payload, null, 2));
176
+ fs.renameSync(tmp, sidecarPath);
177
+ }
178
+
179
+ function _verifySidecar(parsed) {
180
+ // Verify the recorded signature against the recorded public key
181
+ // first — this catches tampering with the digest+sig pair on its
182
+ // own. Then verify the digest matches a re-hash of the recorded
183
+ // snapshot — catches tampering with the snapshot field.
184
+ var sigBuf = Buffer.from(parsed.signatureBase64, "base64");
185
+ if (!auditSign.verify(parsed.digestHex, sigBuf, parsed.publicKeyPem)) {
186
+ return { ok: false, reason: "signature-invalid" };
187
+ }
188
+ if (_hashSnapshot(parsed.snapshot) !== parsed.digestHex) {
189
+ return { ok: false, reason: "digest-mismatch" };
190
+ }
191
+ return { ok: true };
192
+ }
193
+
194
+ async function checkpoint(snapshot) {
195
+ if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) {
196
+ throw _err("BAD_OPT", "checkpoint: snapshot must be a plain object");
197
+ }
198
+ var newDigest = _hashSnapshot(snapshot);
199
+
200
+ var existing = _readSidecar();
201
+ if (existing && existing.unreadable) {
202
+ _emit("config.baseline.unreadable",
203
+ { sidecar: sidecarPath, reason: "sidecar present but malformed or wrong version" },
204
+ "warning");
205
+ _writeSidecar(snapshot, newDigest);
206
+ return { signed: true, drifted: false, tamper: false, previousAt: null, reason: "sidecar-unreadable-rewritten" };
207
+ }
208
+ if (!existing) {
209
+ _writeSidecar(snapshot, newDigest);
210
+ _emit("config.baseline.captured",
211
+ { digestHex: newDigest, capturedAt: Date.now() }, "success");
212
+ return { signed: true, drifted: false, tamper: false, previousAt: null };
213
+ }
214
+
215
+ var verified = _verifySidecar(existing);
216
+ if (!verified.ok) {
217
+ _emit("config.baseline.tamper",
218
+ { sidecar: sidecarPath, reason: verified.reason, previousAt: existing.capturedAt },
219
+ "failure");
220
+ // DO NOT auto-rewrite on tamper — let the operator inspect first.
221
+ return { signed: false, drifted: false, tamper: true, reason: verified.reason, previousAt: existing.capturedAt };
222
+ }
223
+
224
+ if (existing.digestHex === newDigest) {
225
+ // No drift; refresh capturedAt so the sidecar timestamp moves
226
+ // forward each successful boot (operator can see "last verified
227
+ // clean at <T>" in the file).
228
+ _writeSidecar(snapshot, newDigest);
229
+ return { signed: true, drifted: false, tamper: false, previousAt: existing.capturedAt };
230
+ }
231
+
232
+ var diff = _diffShallow(existing.snapshot, snapshot);
233
+ // Filter ignore-keys out of the drift report.
234
+ function _stripIgnored(arr) {
235
+ return arr.filter(function (k) { return ignoreKeys.indexOf(k) === -1; });
236
+ }
237
+ diff.changed = _stripIgnored(diff.changed);
238
+ diff.added = _stripIgnored(diff.added);
239
+ diff.removed = _stripIgnored(diff.removed);
240
+ if (diff.changed.length === 0 && diff.added.length === 0 && diff.removed.length === 0) {
241
+ // All drift was in ignore-keys — refresh sidecar without
242
+ // emitting a drift event.
243
+ _writeSidecar(snapshot, newDigest);
244
+ return { signed: true, drifted: false, tamper: false,
245
+ previousAt: existing.capturedAt, ignoredOnly: true };
246
+ }
247
+ // Severity classification: HIGH when any drifted key is in
248
+ // criticalKeys (or no allowlist is configured — every key is
249
+ // critical by default). LOW when criticalKeys is set and none
250
+ // of the drifted keys are in it.
251
+ var severity = "high";
252
+ if (criticalKeys !== null) {
253
+ var anyCritical = false;
254
+ var allDrifted = diff.changed.concat(diff.added, diff.removed);
255
+ for (var di = 0; di < allDrifted.length; di++) {
256
+ if (criticalKeys.indexOf(allDrifted[di]) !== -1) { anyCritical = true; break; }
257
+ }
258
+ severity = anyCritical ? "high" : "low";
259
+ }
260
+ _emit("config.drift.detected",
261
+ {
262
+ baseline: baselineName,
263
+ previousDigestHex: existing.digestHex,
264
+ currentDigestHex: newDigest,
265
+ previousAt: existing.capturedAt,
266
+ keysChanged: diff.changed,
267
+ keysAdded: diff.added,
268
+ keysRemoved: diff.removed,
269
+ severity: severity,
270
+ },
271
+ severity === "high" ? "failure" : "warning");
272
+ _writeSidecar(snapshot, newDigest);
273
+ return {
274
+ signed: true,
275
+ drifted: true,
276
+ tamper: false,
277
+ severity: severity,
278
+ previousAt: existing.capturedAt,
279
+ diff: diff,
280
+ };
281
+ }
282
+
283
+ function read() {
284
+ var existing = _readSidecar();
285
+ if (!existing || existing.unreadable) return null;
286
+ var verified = _verifySidecar(existing);
287
+ return {
288
+ capturedAt: existing.capturedAt,
289
+ digestHex: existing.digestHex,
290
+ snapshot: existing.snapshot,
291
+ verified: verified.ok,
292
+ tamperReason: verified.ok ? null : verified.reason,
293
+ };
294
+ }
295
+
296
+ return {
297
+ checkpoint: checkpoint,
298
+ read: read,
299
+ sidecarPath: sidecarPath,
300
+ };
301
+ }
302
+
303
+ module.exports = {
304
+ create: create,
305
+ ConfigDriftError: ConfigDriftError,
306
+ // Test-only export for hashing — operators don't need this directly.
307
+ _hashSnapshot: _hashSnapshot,
308
+ _stableStringify: _stableStringify,
309
+ };
@@ -126,6 +126,42 @@ function unsealRow(table, row) {
126
126
  return out;
127
127
  }
128
128
 
129
+ // ---- Erasure (GDPR Art. 17 / "right to be forgotten") ----
130
+ //
131
+ // eraseRow(table, row) returns a tombstoned copy of the row: every
132
+ // sealed column is replaced with NULL, every derived hash column
133
+ // (computed from a sealed source) is replaced with NULL, and a
134
+ // `__erasedAt` field is added carrying the erasure timestamp. The
135
+ // row itself stays in the table (referential integrity), but the
136
+ // sealed cleartext is unrecoverable — even with the vault key, NULL
137
+ // decrypts to NULL.
138
+ //
139
+ // Callers that need the row removed entirely should DELETE; eraseRow
140
+ // is for the case where downstream FKs / audit references make
141
+ // outright deletion infeasible.
142
+ function eraseRow(table, row) {
143
+ if (!row) return row;
144
+ var s = schemas[table];
145
+ if (!s) return row;
146
+ var out = Object.assign({}, row);
147
+ // Erase sealed columns — set to null. After this, unsealRow on the
148
+ // erased row returns null for these columns; no key recovers them
149
+ // because there's no ciphertext to decrypt.
150
+ for (var i = 0; i < s.sealedFields.length; i++) {
151
+ out[s.sealedFields[i]] = null;
152
+ }
153
+ // Erase derived hashes — they're indexed lookup mirrors of sealed
154
+ // sources and would otherwise let an attacker reverse the cleartext
155
+ // via dictionary enumeration of the hash.
156
+ if (s.derivedHashes) {
157
+ for (var derivedField in s.derivedHashes) {
158
+ out[derivedField] = null;
159
+ }
160
+ }
161
+ out.__erasedAt = Date.now();
162
+ return out;
163
+ }
164
+
129
165
  // ---- Lookup translation ----
130
166
 
131
167
  // where({ email: 'x' }) → where({ emailHash: hash(...) }).
@@ -153,6 +189,7 @@ module.exports = {
153
189
  getSealedFields: getSealedFields,
154
190
  sealRow: sealRow,
155
191
  unsealRow: unsealRow,
192
+ eraseRow: eraseRow,
156
193
  computeDerived: computeDerived,
157
194
  lookupHash: lookupHash,
158
195
  clearForTest: clearForTest,
package/lib/crypto.js CHANGED
@@ -80,6 +80,14 @@ function timingSafeEqual(a, b) {
80
80
  function sha3Hash(data) { return hash(data, "sha3-512").toString("hex"); }
81
81
  function hmacSha3(key, data) { return hmac(key, data, "sha3-512"); }
82
82
 
83
+ // (SHA-1 is intentionally NOT exported from b.crypto. The framework's
84
+ // only legitimate SHA-1 use is the HaveIBeenPwned k-anonymity API in
85
+ // lib/auth/password.js, which imports lib/internal-sha1-hibp.js
86
+ // directly. Public b.crypto.sha1* is permanently off the table — a
87
+ // future caller wanting SHA-1 for storage / signing / fingerprinting
88
+ // would re-introduce a broken primitive into the crypto surface this
89
+ // framework spent every other line keeping out.)
90
+
83
91
  // ---- KDF ----
84
92
  function kdf(input, outputLength) { return hash(input, "shake256", outputLength); }
85
93
 
package/lib/db-query.js CHANGED
@@ -148,7 +148,21 @@ class Query {
148
148
  if (direction !== "asc" && direction !== "desc") {
149
149
  throw new Error("orderBy direction must be 'asc' or 'desc'");
150
150
  }
151
- this._orderBy = { field: field, direction: direction.toUpperCase() };
151
+ var entry = { field: field, direction: direction.toUpperCase() };
152
+ if (this._orderBy === null) {
153
+ // First call — keep the back-compat single-object shape so any
154
+ // legacy reader that does `query._orderBy.field` keeps working.
155
+ this._orderBy = entry;
156
+ return this;
157
+ }
158
+ // Second-or-later call — promote to an array. Multi-column ORDER BY
159
+ // is the keyset-pagination tiebreaker pattern: ORDER BY createdAt
160
+ // DESC, _id DESC means same-second rows still have a total order.
161
+ if (Array.isArray(this._orderBy)) {
162
+ this._orderBy.push(entry);
163
+ } else {
164
+ this._orderBy = [this._orderBy, entry];
165
+ }
152
166
  return this;
153
167
  }
154
168
 
@@ -173,7 +187,12 @@ class Query {
173
187
  _orderLimitOffset() {
174
188
  var s = "";
175
189
  if (this._orderBy) {
176
- s += ' ORDER BY "' + this._orderBy.field + '" ' + this._orderBy.direction;
190
+ var entries = Array.isArray(this._orderBy) ? this._orderBy : [this._orderBy];
191
+ var fragments = [];
192
+ for (var i = 0; i < entries.length; i++) {
193
+ fragments.push('"' + entries[i].field + '" ' + entries[i].direction);
194
+ }
195
+ s += " ORDER BY " + fragments.join(", ");
177
196
  }
178
197
  if (this._limit !== null) s += " LIMIT " + this._limit;
179
198
  if (this._offset !== null) s += " OFFSET " + this._offset;
package/lib/db.js CHANGED
@@ -399,6 +399,21 @@ var FRAMEWORK_SCHEMA = [
399
399
  indexes: ["expiresAt"],
400
400
  sealedFields: [],
401
401
  },
402
+ {
403
+ // _blamejs_cache_tags — junction table for tag→cacheKey lookup
404
+ // backing b.cache.invalidateTag(t) on the cluster backend. Composite
405
+ // PK (cacheKey, tag) lets one cacheKey carry many tags; index on
406
+ // tag makes invalidation a single indexed scan. Cleared together
407
+ // with the matching _blamejs_cache rows on del / clear / sweep.
408
+ name: "_blamejs_cache_tags",
409
+ columns: {
410
+ cacheKey: "TEXT NOT NULL",
411
+ tag: "TEXT NOT NULL",
412
+ },
413
+ primaryKey: ["cacheKey", "tag"],
414
+ indexes: ["tag"],
415
+ sealedFields: [],
416
+ },
402
417
  {
403
418
  // _blamejs_seeders — registry of applied seed files for the
404
419
  // b.seeders primitive (lib/seeders.js). Composite PK (env, name)
@@ -1037,11 +1052,26 @@ async function _runNtpBootCheck(opts) {
1037
1052
  try { ntpCheck = require("./ntp-check"); }
1038
1053
  catch (_e) { return; /* module not present — skip silently */ }
1039
1054
 
1055
+ var envServersRaw = safeEnv.readVar("BLAMEJS_NTP_SERVERS", { default: "" });
1056
+ var envTimeout = safeEnv.readVar("BLAMEJS_NTP_TIMEOUT_MS", { default: "" });
1057
+ var envWarn = safeEnv.readVar("BLAMEJS_NTP_DRIFT_WARN_MS", { default: "" });
1058
+ var envFatal = safeEnv.readVar("BLAMEJS_NTP_DRIFT_FATAL_MS", { default: "" });
1059
+ var resolvedServers = (opts && opts.ntpServers) ||
1060
+ (envServersRaw ? envServersRaw.split(",").map(function (s) { return s.trim(); }).filter(Boolean) : undefined);
1061
+ var resolvedTimeout = (opts && opts.ntpTimeoutMs) ||
1062
+ (envTimeout ? parseInt(envTimeout, 10) : undefined);
1063
+ if (envWarn || envFatal) {
1064
+ var thr = {};
1065
+ if (envWarn) thr.warnMs = parseInt(envWarn, 10);
1066
+ if (envFatal) thr.fatalMs = parseInt(envFatal, 10);
1067
+ try { ntpCheck.setThresholds(thr); } catch (_e) {}
1068
+ }
1069
+
1040
1070
  var result;
1041
1071
  try {
1042
1072
  result = await ntpCheck.bootCheck({
1043
- servers: opts && opts.ntpServers,
1044
- timeoutMs: opts && opts.ntpTimeoutMs,
1073
+ servers: resolvedServers,
1074
+ timeoutMs: resolvedTimeout,
1045
1075
  });
1046
1076
  } catch (e) {
1047
1077
  log.error("ntp boot check threw unexpectedly: " + e.message + " (continuing)");