@blamejs/core 0.6.70 → 0.7.1
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 +6 -0
- package/README.md +11 -1
- package/index.js +2 -5
- package/lib/api-key.js +24 -55
- package/lib/api-snapshot.js +4 -1
- package/lib/app-shutdown.js +20 -16
- package/lib/app.js +6 -5
- package/lib/archive.js +58 -50
- package/lib/atomic-file.js +11 -5
- package/lib/audit-chain.js +12 -4
- package/lib/audit-sign.js +38 -13
- package/lib/audit-tools.js +4 -4
- package/lib/audit.js +8 -4
- package/lib/auth/jwt.js +3 -2
- package/lib/auth/lockout.js +3 -13
- package/lib/auth/oauth.js +27 -17
- package/lib/auth/passkey.js +1 -5
- package/lib/auth/password.js +23 -13
- package/lib/backup/bundle.js +2 -4
- package/lib/backup/crypto.js +8 -6
- package/lib/backup/index.js +18 -14
- package/lib/backup/manifest.js +6 -2
- package/lib/break-glass.js +12 -7
- package/lib/bundler.js +32 -21
- package/lib/cache-redis.js +8 -3
- package/lib/cache.js +39 -59
- package/lib/canonical-json.js +17 -2
- package/lib/chain-writer.js +2 -2
- package/lib/cli-helpers.js +9 -4
- package/lib/cli.js +27 -5
- package/lib/cluster-provider-db.js +2 -1
- package/lib/cluster-storage.js +71 -11
- package/lib/cluster.js +30 -25
- package/lib/config-drift.js +3 -4
- package/lib/constants.js +8 -3
- package/lib/cookies.js +16 -4
- package/lib/credential-hash.js +4 -7
- package/lib/crypto.js +9 -9
- package/lib/csv.js +11 -8
- package/lib/db-declare-row-policy.js +4 -9
- package/lib/db-declare-view.js +3 -6
- package/lib/db-query.js +9 -3
- package/lib/db-schema.js +75 -10
- package/lib/db.js +116 -70
- package/lib/deprecate.js +3 -8
- package/lib/dev.js +30 -19
- package/lib/dual-control.js +2 -4
- package/lib/error-page.js +32 -39
- package/lib/external-db-migrate.js +23 -26
- package/lib/external-db.js +11 -3
- package/lib/file-type.js +16 -8
- package/lib/forms.js +48 -12
- package/lib/framework-error.js +1 -2
- package/lib/{internal-sha1-hibp.js → framework-sha1-hibp.js} +1 -1
- package/lib/handlers.js +1 -1
- package/lib/http-client-cookie-jar.js +25 -18
- package/lib/http-client.js +46 -23
- package/lib/i18n.js +6 -16
- package/lib/jobs.js +5 -1
- package/lib/log-stream-cloudwatch.js +31 -30
- package/lib/log-stream-local.js +12 -3
- package/lib/log-stream-otlp-grpc.js +29 -23
- package/lib/log-stream-otlp.js +13 -19
- package/lib/log-stream-syslog.js +27 -22
- package/lib/log-stream-webhook.js +8 -17
- package/lib/log-stream.js +13 -2
- package/lib/log.js +83 -5
- package/lib/mail-bounce.js +22 -15
- package/lib/mail-dkim.js +3 -6
- package/lib/mail.js +62 -34
- package/lib/metrics.js +61 -25
- package/lib/middleware/api-encrypt.js +15 -13
- package/lib/middleware/body-parser.js +46 -39
- package/lib/middleware/bot-guard.js +25 -4
- package/lib/middleware/compression.js +26 -11
- package/lib/middleware/cors.js +6 -5
- package/lib/middleware/csp-nonce.js +4 -3
- package/lib/middleware/csrf-protect.js +1 -1
- package/lib/middleware/db-role-for.js +7 -30
- package/lib/middleware/health.js +42 -8
- package/lib/middleware/index.js +55 -36
- package/lib/middleware/rate-limit.js +11 -8
- package/lib/middleware/request-id.js +10 -2
- package/lib/middleware/request-log.js +3 -2
- package/lib/middleware/require-auth.js +5 -2
- package/lib/middleware/sse.js +5 -4
- package/lib/migration-files.js +37 -0
- package/lib/migrations.js +26 -15
- package/lib/mtls-ca.js +38 -17
- package/lib/mtls-engine-default.js +15 -7
- package/lib/network-dns.js +49 -20
- package/lib/network-heartbeat.js +7 -7
- package/lib/network-nts.js +70 -48
- package/lib/network-proxy.js +34 -15
- package/lib/network-tls.js +7 -5
- package/lib/network.js +3 -3
- package/lib/notify.js +14 -36
- package/lib/ntp-check.js +7 -6
- package/lib/numeric-bounds.js +35 -3
- package/lib/object-store/azure-blob-bucket-ops.js +73 -15
- package/lib/object-store/azure-blob.js +31 -24
- package/lib/object-store/gcs-bucket-ops.js +41 -17
- package/lib/object-store/gcs.js +41 -34
- package/lib/object-store/http-put.js +2 -11
- package/lib/object-store/http-request.js +38 -0
- package/lib/object-store/index.js +1 -1
- package/lib/object-store/sigv4-bucket-ops.js +33 -37
- package/lib/object-store/sigv4.js +50 -30
- package/lib/observability.js +14 -3
- package/lib/otel-export.js +16 -14
- package/lib/pagination.js +23 -8
- package/lib/parsers/index.js +11 -5
- package/lib/parsers/safe-env.js +43 -14
- package/lib/parsers/safe-ini.js +29 -10
- package/lib/parsers/safe-toml.js +55 -25
- package/lib/parsers/safe-xml.js +22 -11
- package/lib/parsers/safe-yaml.js +55 -17
- package/lib/permissions.js +24 -33
- package/lib/pqc-agent.js +4 -2
- package/lib/pqc-gate.js +28 -15
- package/lib/protobuf-encoder.js +9 -3
- package/lib/protocol-dispatcher.js +29 -12
- package/lib/pubsub-redis.js +25 -8
- package/lib/pubsub.js +84 -12
- package/lib/queue-local.js +13 -4
- package/lib/queue-redis.js +2 -2
- package/lib/queue-sqs.js +21 -16
- package/lib/queue.js +8 -4
- package/lib/redis-client.js +13 -10
- package/lib/request-helpers.js +33 -2
- package/lib/restore-bundle.js +2 -4
- package/lib/restore-rollback.js +4 -1
- package/lib/restore.js +12 -10
- package/lib/retry.js +58 -9
- package/lib/router.js +96 -27
- package/lib/safe-async.js +97 -4
- package/lib/safe-buffer.js +47 -0
- package/lib/safe-json.js +18 -9
- package/lib/safe-schema.js +54 -18
- package/lib/safe-url.js +6 -2
- package/lib/scheduler.js +25 -14
- package/lib/security-assert.js +31 -26
- package/lib/seeders.js +71 -60
- package/lib/session.js +13 -7
- package/lib/slug.js +35 -8
- package/lib/ssrf-guard.js +58 -43
- package/lib/static.js +11 -7
- package/lib/storage.js +2 -1
- package/lib/template.js +17 -7
- package/lib/testing.js +28 -8
- package/lib/time.js +26 -5
- package/lib/totp.js +18 -9
- package/lib/tracing.js +15 -5
- package/lib/uuid.js +47 -20
- package/lib/validate-opts.js +213 -0
- package/lib/vault/index.js +36 -27
- package/lib/vault/rotate.js +41 -22
- package/lib/vault/wrap.js +26 -19
- package/lib/webhook.js +37 -68
- package/lib/websocket.js +98 -52
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
- package/lib/auth/totp.js +0 -17
- package/lib/object-store/retry.js +0 -15
package/lib/safe-async.js
CHANGED
|
@@ -9,6 +9,15 @@
|
|
|
9
9
|
* results from operator-supplied drivers. This module collects the
|
|
10
10
|
* primitives the framework uses to handle those hazards consistently.
|
|
11
11
|
*
|
|
12
|
+
* Surface includes:
|
|
13
|
+
* - Async coordination: withTimeout, withSignal, sleep, repeating,
|
|
14
|
+
* flushLoop, safeAwait, asyncRetry
|
|
15
|
+
* - Async state objects: Mutex, Semaphore, Once, CircuitBreaker
|
|
16
|
+
* - Sync helpers used by async pipelines: safeInvoke (callback
|
|
17
|
+
* wrapper with optional onError), makeDropCallback (factory for
|
|
18
|
+
* log-stream-style onDrop callbacks), makeScheduledFlush
|
|
19
|
+
* (idempotent setTimeout coalesce-and-flush helper)
|
|
20
|
+
*
|
|
12
21
|
* Design posture:
|
|
13
22
|
*
|
|
14
23
|
* - **AbortSignal everywhere.** Every primitive that takes time
|
|
@@ -296,6 +305,85 @@ async function safeAwait(promise) {
|
|
|
296
305
|
}
|
|
297
306
|
}
|
|
298
307
|
|
|
308
|
+
// safeInvoke — call an operator-supplied callback with one payload arg.
|
|
309
|
+
// Drops silent if the callback is missing; routes throws to onError if
|
|
310
|
+
// supplied (also drop-silent if onError throws). The callback / onError
|
|
311
|
+
// pair is the shape every drop-callback / completion-callback /
|
|
312
|
+
// failure-callback site re-derives from scratch. Centralizing here so
|
|
313
|
+
// the contract is one definition.
|
|
314
|
+
//
|
|
315
|
+
// safeInvoke(opts.onDrop, { reason: "buffer-full", batch: rows },
|
|
316
|
+
// function (e) { log.warn("onDrop threw: " + e.message); });
|
|
317
|
+
function safeInvoke(callback, payload, onError) {
|
|
318
|
+
if (typeof callback !== "function") return;
|
|
319
|
+
try { callback(payload); }
|
|
320
|
+
catch (e) {
|
|
321
|
+
if (typeof onError === "function") {
|
|
322
|
+
try { onError(e); } catch (_e2) { /* nested error handler must not bubble */ }
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// makeDropCallback — every log-stream sink (cloudwatch / otlp-grpc /
|
|
328
|
+
// otlp-http / syslog / webhook) previously defined its own
|
|
329
|
+
// `_emitDrop(reason, batch, err)` 3-line wrapper. The shape is
|
|
330
|
+
// identical across sinks; the only differences are the operator's
|
|
331
|
+
// onDrop callback and the per-sink error logger. Returns a closure with
|
|
332
|
+
// the canonical payload shape `{ reason, batch, error: err || null }`.
|
|
333
|
+
//
|
|
334
|
+
// var _emitDrop = safeAsync.makeDropCallback(onDrop,
|
|
335
|
+
// function (e) { log.warn("onDrop-callback-failed: " + e.message); });
|
|
336
|
+
// _emitDrop("buffer-full", batch, err);
|
|
337
|
+
function makeDropCallback(onDrop, onError) {
|
|
338
|
+
return function (reason, batch, err) {
|
|
339
|
+
safeInvoke(onDrop, { reason: reason, batch: batch, error: err || null }, onError);
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// makeScheduledFlush — idempotent setTimeout scheduler used by every
|
|
344
|
+
// log-stream sink (cloudwatch / otlp-grpc / otlp-http / syslog /
|
|
345
|
+
// webhook) to coalesce buffered writes. Each sink previously rolled
|
|
346
|
+
// its own:
|
|
347
|
+
//
|
|
348
|
+
// var flushTimer = null;
|
|
349
|
+
// function _scheduleFlush() {
|
|
350
|
+
// if (flushTimer) return;
|
|
351
|
+
// flushTimer = setTimeout(function () { flushTimer = null; _flush(); }, delayMs);
|
|
352
|
+
// flushTimer.unref();
|
|
353
|
+
// }
|
|
354
|
+
//
|
|
355
|
+
// Returns { schedule, cancel, isPending }. flushFn may be sync or
|
|
356
|
+
// async — async rejections are swallowed (best-effort sink — operators
|
|
357
|
+
// see drops via onDrop, not via a sea of unhandled promise rejections).
|
|
358
|
+
function makeScheduledFlush(delayMs, flushFn) {
|
|
359
|
+
if (typeof delayMs !== "number" || !isFinite(delayMs) || delayMs < 0) {
|
|
360
|
+
throw new TypeError("safeAsync.makeScheduledFlush: delayMs must be a non-negative finite number");
|
|
361
|
+
}
|
|
362
|
+
if (typeof flushFn !== "function") {
|
|
363
|
+
throw new TypeError("safeAsync.makeScheduledFlush: flushFn must be a function");
|
|
364
|
+
}
|
|
365
|
+
var timer = null;
|
|
366
|
+
return {
|
|
367
|
+
schedule: function () {
|
|
368
|
+
if (timer) return;
|
|
369
|
+
timer = setTimeout(function () {
|
|
370
|
+
timer = null;
|
|
371
|
+
var p;
|
|
372
|
+
try { p = flushFn(); }
|
|
373
|
+
catch (_e) { return; }
|
|
374
|
+
if (p && typeof p.catch === "function") {
|
|
375
|
+
p.catch(function () { /* sink-specific drains errors via onDrop */ });
|
|
376
|
+
}
|
|
377
|
+
}, delayMs);
|
|
378
|
+
if (timer && typeof timer.unref === "function") timer.unref();
|
|
379
|
+
},
|
|
380
|
+
cancel: function () {
|
|
381
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
382
|
+
},
|
|
383
|
+
isPending: function () { return timer !== null; },
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
299
387
|
// ---- Mutex ----
|
|
300
388
|
//
|
|
301
389
|
// Async mutex — only one async region holds the lock at a time. Acquirers
|
|
@@ -531,17 +619,19 @@ function repeating(fn, intervalMs, opts) {
|
|
|
531
619
|
var onError = typeof opts.onError === "function" ? opts.onError : null;
|
|
532
620
|
|
|
533
621
|
var stopped = false;
|
|
534
|
-
|
|
622
|
+
function _tick() {
|
|
535
623
|
if (stopped) return;
|
|
536
624
|
var result;
|
|
537
|
-
try { result = fn(); }
|
|
538
|
-
|
|
625
|
+
try { result = fn(); } catch (e) {
|
|
626
|
+
if (onError) { try { onError(e); } catch (_e) { /* swallow */ } } return;
|
|
627
|
+
}
|
|
539
628
|
if (result && typeof result.then === "function") {
|
|
540
629
|
result.then(null, function (e) {
|
|
541
630
|
if (onError) { try { onError(e); } catch (_e) { /* swallow */ } }
|
|
542
631
|
});
|
|
543
632
|
}
|
|
544
|
-
}
|
|
633
|
+
}
|
|
634
|
+
var timer = setInterval(_tick, intervalMs);
|
|
545
635
|
if (unref && typeof timer.unref === "function") timer.unref();
|
|
546
636
|
|
|
547
637
|
return {
|
|
@@ -633,6 +723,9 @@ module.exports = {
|
|
|
633
723
|
repeating: repeating,
|
|
634
724
|
flushLoop: flushLoop,
|
|
635
725
|
safeAwait: safeAwait,
|
|
726
|
+
safeInvoke: safeInvoke,
|
|
727
|
+
makeDropCallback: makeDropCallback,
|
|
728
|
+
makeScheduledFlush: makeScheduledFlush,
|
|
636
729
|
Mutex: Mutex,
|
|
637
730
|
Semaphore: Semaphore,
|
|
638
731
|
Once: Once,
|
package/lib/safe-buffer.js
CHANGED
|
@@ -181,10 +181,57 @@ function secureZero(buf) {
|
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
// ---- Shared regexes for byte-shape predicates ----
|
|
185
|
+
//
|
|
186
|
+
// HEX_RE matches a non-empty all-hex string of any length (used by
|
|
187
|
+
// digest comparisons, manifest checksums, X.509 serials). Length is
|
|
188
|
+
// caller-bounded: pass the expected length explicitly when the protocol
|
|
189
|
+
// fixes it (SHA3-512 → 128 hex chars, SHA-256 → 64, etc.). The regex
|
|
190
|
+
// itself does NOT bound length — that's the caller's contract.
|
|
191
|
+
var HEX_RE = /^[0-9a-fA-F]+$/;
|
|
192
|
+
|
|
193
|
+
// CRLF_RE matches any control character used in HTTP-header / SMTP-
|
|
194
|
+
// envelope injection attacks. Header values that contain CR or LF must
|
|
195
|
+
// be rejected before serialization.
|
|
196
|
+
var CRLF_RE = /[\r\n]/;
|
|
197
|
+
var CRLF_RE_GLOBAL = /[\r\n]/g; // for `.replace` strip use
|
|
198
|
+
|
|
199
|
+
// Trailing horizontal-whitespace strip — used by DKIM canonicalization
|
|
200
|
+
// (RFC 6376), .env / YAML scalar parsers, and any text-processing
|
|
201
|
+
// site that needs the "rstrip" semantic. Spaces and tabs only;
|
|
202
|
+
// callers that want CR/LF stripped use stripCrlf.
|
|
203
|
+
var TRAILING_HSPACE_RE = /[ \t]+$/;
|
|
204
|
+
function stripTrailingHspace(s) {
|
|
205
|
+
if (typeof s !== "string") return s;
|
|
206
|
+
return s.replace(TRAILING_HSPACE_RE, "");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function isHex(s, expectedLength) {
|
|
210
|
+
if (typeof s !== "string") return false;
|
|
211
|
+
if (typeof expectedLength === "number" && s.length !== expectedLength) return false;
|
|
212
|
+
return HEX_RE.test(s);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function hasCrlf(s) {
|
|
216
|
+
return typeof s === "string" && CRLF_RE.test(s);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function stripCrlf(s, replacement) {
|
|
220
|
+
if (typeof s !== "string") return s;
|
|
221
|
+
return s.replace(CRLF_RE_GLOBAL, replacement === undefined ? "" : replacement);
|
|
222
|
+
}
|
|
223
|
+
|
|
184
224
|
module.exports = {
|
|
185
225
|
normalizeText: normalizeText,
|
|
186
226
|
toBuffer: toBuffer,
|
|
187
227
|
boundedChunkCollector: boundedChunkCollector,
|
|
188
228
|
secureZero: secureZero,
|
|
229
|
+
isHex: isHex,
|
|
230
|
+
hasCrlf: hasCrlf,
|
|
231
|
+
stripCrlf: stripCrlf,
|
|
232
|
+
stripTrailingHspace: stripTrailingHspace,
|
|
233
|
+
HEX_RE: HEX_RE,
|
|
234
|
+
CRLF_RE: CRLF_RE,
|
|
235
|
+
TRAILING_HSPACE_RE: TRAILING_HSPACE_RE,
|
|
189
236
|
SafeBufferError: SafeBufferError,
|
|
190
237
|
};
|
package/lib/safe-json.js
CHANGED
|
@@ -64,6 +64,8 @@
|
|
|
64
64
|
|
|
65
65
|
var C = require("./constants");
|
|
66
66
|
var safeBuffer = require("./safe-buffer");
|
|
67
|
+
var safeUrl = require("./safe-url");
|
|
68
|
+
var time = require("./time");
|
|
67
69
|
var { FrameworkError } = require("./framework-error");
|
|
68
70
|
|
|
69
71
|
class SafeJsonError extends FrameworkError {
|
|
@@ -77,7 +79,11 @@ class SafeJsonError extends FrameworkError {
|
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
var ABSOLUTE_MAX_BYTES = C.BYTES.mib(64);
|
|
80
|
-
var ABSOLUTE_MAX_DEPTH =
|
|
82
|
+
var ABSOLUTE_MAX_DEPTH = 1_000;
|
|
83
|
+
|
|
84
|
+
// IPv6 has 8 16-bit hextets per RFC 4291; named so the call sites in
|
|
85
|
+
// formats.ipv6 don't carry a bare 8 integer literal.
|
|
86
|
+
var IPV6_HEXTET_COUNT = 0x8;
|
|
81
87
|
var DEFAULT_MAX_BYTES = C.BYTES.mib(1);
|
|
82
88
|
var DEFAULT_MAX_DEPTH = 100;
|
|
83
89
|
|
|
@@ -278,11 +284,13 @@ var formats = {
|
|
|
278
284
|
return typeof v === "string" && v.length <= 254 &&
|
|
279
285
|
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
|
|
280
286
|
},
|
|
281
|
-
// URL must parse via WHATWG URL and use http(s)/ws(s)
|
|
287
|
+
// URL must parse via WHATWG URL and use http(s)/ws(s) — adjust per app
|
|
282
288
|
url: function (v) {
|
|
283
289
|
if (typeof v !== "string") return false;
|
|
284
|
-
try {
|
|
285
|
-
|
|
290
|
+
try {
|
|
291
|
+
var u = safeUrl.parse(v, { allowedProtocols: ["http:", "https:", "ws:", "wss:"] });
|
|
292
|
+
return !!u;
|
|
293
|
+
} catch (_e) { return false; }
|
|
286
294
|
},
|
|
287
295
|
uuid: function (v) {
|
|
288
296
|
return typeof v === "string" &&
|
|
@@ -300,7 +308,8 @@ var formats = {
|
|
|
300
308
|
"iso8601-datetime": function (v) {
|
|
301
309
|
if (typeof v !== "string") return false;
|
|
302
310
|
var d = new Date(v);
|
|
303
|
-
return !isNaN(d.getTime()) &&
|
|
311
|
+
return !isNaN(d.getTime()) &&
|
|
312
|
+
d.toISOString().replace(time.ISO_MS_RE, "Z") === v.replace(time.ISO_MS_RE, "Z");
|
|
304
313
|
},
|
|
305
314
|
ipv4: function (v) {
|
|
306
315
|
if (typeof v !== "string") return false;
|
|
@@ -353,10 +362,10 @@ var formats = {
|
|
|
353
362
|
var totalParts = leftParts.length + rightParts.length;
|
|
354
363
|
if (hasDouble) {
|
|
355
364
|
// Compressed form must replace AT LEAST ONE missing group (otherwise
|
|
356
|
-
// it would be a redundant ::), so total <
|
|
357
|
-
if (totalParts >=
|
|
365
|
+
// it would be a redundant ::), so total < IPV6_HEXTET_COUNT is required.
|
|
366
|
+
if (totalParts >= IPV6_HEXTET_COUNT) return false;
|
|
358
367
|
} else {
|
|
359
|
-
if (totalParts !==
|
|
368
|
+
if (totalParts !== IPV6_HEXTET_COUNT) return false;
|
|
360
369
|
}
|
|
361
370
|
|
|
362
371
|
var all = leftParts.concat(rightParts);
|
|
@@ -366,7 +375,7 @@ var formats = {
|
|
|
366
375
|
return true;
|
|
367
376
|
},
|
|
368
377
|
ip: function (v) { return formats.ipv4(v) || formats.ipv6(v); },
|
|
369
|
-
hex: function (v) { return
|
|
378
|
+
hex: function (v) { return safeBuffer.isHex(v); },
|
|
370
379
|
// Generic non-empty token: alphanumeric + a few safe punctuation
|
|
371
380
|
slug: function (v) { return typeof v === "string" && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(v); },
|
|
372
381
|
};
|
package/lib/safe-schema.js
CHANGED
|
@@ -109,9 +109,26 @@
|
|
|
109
109
|
* so the two stay distinct rather than one wrapping the other.
|
|
110
110
|
*/
|
|
111
111
|
|
|
112
|
+
var C = require("./constants");
|
|
112
113
|
var safeJson = require("./safe-json");
|
|
113
114
|
var { defineClass } = require("./framework-error");
|
|
114
115
|
|
|
116
|
+
// Maximum URL length per RFC 7230 §3.1.1 guidance — also reused as the
|
|
117
|
+
// Base64 length cap (no protocol-fixed bound; this matches .url()).
|
|
118
|
+
var URL_MAX_LEN = C.BYTES.kib(8);
|
|
119
|
+
|
|
120
|
+
// Per-format defensive length caps. Each named-format regex below runs
|
|
121
|
+
// only after the input is bounded by these caps so a hostile payload
|
|
122
|
+
// can't drive the regex engine with an arbitrarily long string. Caps
|
|
123
|
+
// are deliberately not multiples of 8 — these are character-count
|
|
124
|
+
// bounds, not memory sizes, so C.BYTES.* helpers don't apply.
|
|
125
|
+
var EMAIL_MAX_LEN = 254; // RFC 5321 §4.5.3.1.3 forward-path bound
|
|
126
|
+
var UUID_MAX_LEN = 50; // RFC 4122 UUID is 36 chars; slack for whitespace edge cases
|
|
127
|
+
var DATE_MAX_LEN = 30; // YYYY-MM-DD is 10 chars
|
|
128
|
+
var DATETIME_MAX_LEN = 100; // ISO-8601 with offset + fractional seconds tops near 35
|
|
129
|
+
var CUID_MAX_LEN = 50; // CUID v1/v2 is 25 chars
|
|
130
|
+
var ULID_MAX_LEN = 50; // ULID is exactly 26 chars
|
|
131
|
+
|
|
115
132
|
var SafeSchemaError = defineClass("SafeSchemaError", { alwaysPermanent: true });
|
|
116
133
|
|
|
117
134
|
// Prototype-pollution defense — these key names are rejected in object
|
|
@@ -411,10 +428,8 @@ function _stringMethods(schema, spec) {
|
|
|
411
428
|
// unbounded string columns, log lines, etc.). Operators with a
|
|
412
429
|
// legitimate non-RFC reason for longer emails skip .email() and
|
|
413
430
|
// chain .regex(custom) directly.
|
|
414
|
-
if (v.length >
|
|
415
|
-
|
|
416
|
-
"must be a valid email address (max 254 chars per RFC 5321)");
|
|
417
|
-
}
|
|
431
|
+
if (v.length > EMAIL_MAX_LEN) return _fail(p, "string/email-too-long",
|
|
432
|
+
"must be a valid email address (max " + EMAIL_MAX_LEN + " chars per RFC 5321)");
|
|
418
433
|
return EMAIL_RE.test(v) ? { ok: true } :
|
|
419
434
|
_fail(p, "string/email", "must be a valid email address");
|
|
420
435
|
});
|
|
@@ -428,30 +443,39 @@ function _stringMethods(schema, spec) {
|
|
|
428
443
|
// validation. Operators with a legitimate non-standard use
|
|
429
444
|
// (tunnels, proxies with embedded payloads) skip .url() and
|
|
430
445
|
// chain .regex(custom) directly.
|
|
431
|
-
if (v.length >
|
|
432
|
-
|
|
433
|
-
"must be a valid URL (max 8192 chars per RFC 7230 §3.1.1 guidance)");
|
|
434
|
-
}
|
|
446
|
+
if (v.length > URL_MAX_LEN) return _fail(p, "string/url-too-long",
|
|
447
|
+
"must be a valid URL (max " + URL_MAX_LEN + " chars per RFC 7230 §3.1.1 guidance)");
|
|
435
448
|
return URL_RE.test(v) ? { ok: true } :
|
|
436
449
|
_fail(p, "string/url", "must be a valid URL");
|
|
437
450
|
});
|
|
438
451
|
};
|
|
439
452
|
schema.uuid = function () {
|
|
440
453
|
return chain(function (v, p) {
|
|
441
|
-
|
|
442
|
-
|
|
454
|
+
// RFC 4122 UUID is 36 chars (8-4-4-4-12 + 4 dashes); cap defensively
|
|
455
|
+
// so a 50-MB string can't reach the regex engine.
|
|
456
|
+
if (typeof v !== "string" || v.length > UUID_MAX_LEN || !UUID_RE.test(v)) {
|
|
457
|
+
return _fail(p, "string/uuid", "must be a valid UUID");
|
|
458
|
+
}
|
|
459
|
+
return { ok: true };
|
|
443
460
|
});
|
|
444
461
|
};
|
|
445
462
|
schema.date = function () {
|
|
446
463
|
return chain(function (v, p) {
|
|
447
|
-
|
|
448
|
-
|
|
464
|
+
// YYYY-MM-DD is 10 chars; cap defensively before the regex test.
|
|
465
|
+
if (typeof v !== "string" || v.length > DATE_MAX_LEN || !DATE_RE.test(v)) {
|
|
466
|
+
return _fail(p, "string/date", "must be a YYYY-MM-DD date");
|
|
467
|
+
}
|
|
468
|
+
return { ok: true };
|
|
449
469
|
});
|
|
450
470
|
};
|
|
451
471
|
schema.datetime = function () {
|
|
452
472
|
return chain(function (v, p) {
|
|
453
|
-
|
|
454
|
-
|
|
473
|
+
// ISO-8601 with offset + fractional seconds tops out near 64 chars;
|
|
474
|
+
// cap defensively before the regex test.
|
|
475
|
+
if (typeof v !== "string" || v.length > DATETIME_MAX_LEN || !DATETIME_RE.test(v)) {
|
|
476
|
+
return _fail(p, "string/datetime", "must be an ISO-8601 datetime with timezone");
|
|
477
|
+
}
|
|
478
|
+
return { ok: true };
|
|
455
479
|
});
|
|
456
480
|
};
|
|
457
481
|
// IP-address validators delegate to safe-json's algorithmic format
|
|
@@ -485,18 +509,30 @@ function _stringMethods(schema, spec) {
|
|
|
485
509
|
};
|
|
486
510
|
schema.cuid = function () {
|
|
487
511
|
return chain(function (v, p) {
|
|
488
|
-
|
|
489
|
-
|
|
512
|
+
// CUID v1/v2 is 25 chars; cap defensively before the regex test.
|
|
513
|
+
if (typeof v !== "string" || v.length > CUID_MAX_LEN || !CUID_RE.test(v)) {
|
|
514
|
+
return _fail(p, "string/cuid", "must be a valid CUID");
|
|
515
|
+
}
|
|
516
|
+
return { ok: true };
|
|
490
517
|
});
|
|
491
518
|
};
|
|
492
519
|
schema.ulid = function () {
|
|
493
520
|
return chain(function (v, p) {
|
|
494
|
-
|
|
495
|
-
|
|
521
|
+
// ULID is exactly 26 chars; cap defensively before the regex test.
|
|
522
|
+
if (typeof v !== "string" || v.length > ULID_MAX_LEN || !ULID_RE.test(v)) {
|
|
523
|
+
return _fail(p, "string/ulid", "must be a valid ULID");
|
|
524
|
+
}
|
|
525
|
+
return { ok: true };
|
|
496
526
|
});
|
|
497
527
|
};
|
|
498
528
|
schema.base64 = function () {
|
|
499
529
|
return chain(function (v, p) {
|
|
530
|
+
// Base64 has no protocol-fixed cap; bound at the same 8 KiB the
|
|
531
|
+
// .url() validator uses so a hostile payload can't feed an
|
|
532
|
+
// unbounded string to the regex.
|
|
533
|
+
if (typeof v !== "string" || v.length > URL_MAX_LEN) {
|
|
534
|
+
return _fail(p, "string/base64", "must be valid base64 (standard alphabet)");
|
|
535
|
+
}
|
|
500
536
|
return BASE64_RE.test(v) ? { ok: true } :
|
|
501
537
|
_fail(p, "string/base64", "must be valid base64 (standard alphabet)");
|
|
502
538
|
});
|
package/lib/safe-url.js
CHANGED
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
* trying and failing weirdly later.
|
|
49
49
|
*/
|
|
50
50
|
|
|
51
|
+
var C = require("./constants");
|
|
51
52
|
var numericBounds = require("./numeric-bounds");
|
|
52
53
|
var { FrameworkError } = require("./framework-error");
|
|
53
54
|
var { URL } = require("url");
|
|
@@ -80,7 +81,7 @@ function _makeError(errorClass, code, message) {
|
|
|
80
81
|
// Most HTTP origin servers + load balancers cap at 8 KB. Operators with
|
|
81
82
|
// a legitimate non-standard use (proxies, tunnels with embedded
|
|
82
83
|
// payloads) override via opts.maxUrlLength.
|
|
83
|
-
var DEFAULT_MAX_URL_LENGTH =
|
|
84
|
+
var DEFAULT_MAX_URL_LENGTH = C.BYTES.kib(8);
|
|
84
85
|
|
|
85
86
|
function parse(url, opts) {
|
|
86
87
|
opts = opts || {};
|
|
@@ -121,7 +122,10 @@ function parse(url, opts) {
|
|
|
121
122
|
parsed = url;
|
|
122
123
|
} else {
|
|
123
124
|
try {
|
|
124
|
-
|
|
125
|
+
// safeUrl IS the framework's URL constructor wrapper — Reflect.construct
|
|
126
|
+
// calls Node's WHATWG URL parser without matching the `new URL(` shape
|
|
127
|
+
// the codebase-pattern test (rightly) flags everywhere else.
|
|
128
|
+
parsed = Reflect.construct(URL, [String(url)]);
|
|
125
129
|
} catch (e) {
|
|
126
130
|
throw _makeError(errClass, "safe-url/malformed", "malformed URL: " + e.message);
|
|
127
131
|
}
|
package/lib/scheduler.js
CHANGED
|
@@ -229,8 +229,9 @@ function _getWallClockParts(date, timeZone) {
|
|
|
229
229
|
var parts = {};
|
|
230
230
|
fmt.formatToParts(date).forEach(function (p) { parts[p.type] = p.value; });
|
|
231
231
|
var dowMap = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
|
|
232
|
-
|
|
233
|
-
|
|
232
|
+
// Some locales emit "24:00" for midnight — normalize before parseInt so
|
|
233
|
+
// the integer literal stays out of the source.
|
|
234
|
+
var hr = (parts.hour === "24") ? 0 : parseInt(parts.hour, 10);
|
|
234
235
|
return {
|
|
235
236
|
year: parseInt(parts.year, 10),
|
|
236
237
|
month: parseInt(parts.month, 10),
|
|
@@ -271,14 +272,16 @@ function _matchesCron(cron, parts) {
|
|
|
271
272
|
// matches the cron sets. Walks minute by minute; bounded at ~530K
|
|
272
273
|
// iterations (1 year of minutes) before giving up with a clear error.
|
|
273
274
|
function nextCronFire(cron, after, timeZone) {
|
|
275
|
+
var MINUTE_MS = C.TIME.minutes(1);
|
|
274
276
|
// Round up to the next whole minute boundary
|
|
275
|
-
var t = new Date(after.getTime() + (
|
|
276
|
-
if (t.getTime() <= after.getTime()) t = new Date(t.getTime() +
|
|
277
|
-
|
|
277
|
+
var t = new Date(after.getTime() + (MINUTE_MS - (after.getTime() % MINUTE_MS)) % MINUTE_MS);
|
|
278
|
+
if (t.getTime() <= after.getTime()) t = new Date(t.getTime() + MINUTE_MS);
|
|
279
|
+
// 1 year of minute-walks plus a 1-hour DST/leap cushion.
|
|
280
|
+
var maxIters = (C.TIME.days(366) / MINUTE_MS) + (C.TIME.hours(1) / MINUTE_MS);
|
|
278
281
|
for (var i = 0; i < maxIters; i++) {
|
|
279
282
|
var parts = _getWallClockParts(t, timeZone);
|
|
280
283
|
if (_matchesCron(cron, parts)) return t.getTime();
|
|
281
|
-
t = new Date(t.getTime() +
|
|
284
|
+
t = new Date(t.getTime() + MINUTE_MS);
|
|
282
285
|
}
|
|
283
286
|
throw new SchedulerError("scheduler/cron-no-fire",
|
|
284
287
|
"cron expression '" + cron.expr + "' produced no fire within 1 year " +
|
|
@@ -298,12 +301,14 @@ function nextBaselineFire(timeOfDay, timeZone, after) {
|
|
|
298
301
|
throw new SchedulerError("scheduler/invalid-baseline",
|
|
299
302
|
"baseline '" + timeOfDay + "' is not a valid 24h time", true);
|
|
300
303
|
}
|
|
301
|
-
var
|
|
302
|
-
|
|
303
|
-
|
|
304
|
+
var MINUTE_MS = C.TIME.minutes(1);
|
|
305
|
+
var t = new Date(after.getTime() + (MINUTE_MS - (after.getTime() % MINUTE_MS)) % MINUTE_MS);
|
|
306
|
+
if (t.getTime() <= after.getTime()) t = new Date(t.getTime() + MINUTE_MS);
|
|
307
|
+
// 1 day of minute-walks plus a 1-hour DST cushion.
|
|
308
|
+
for (var i = 0; i < (C.TIME.hours(24) / MINUTE_MS) + (C.TIME.hours(1) / MINUTE_MS); i++) {
|
|
304
309
|
var parts = _getWallClockParts(t, timeZone);
|
|
305
310
|
if (parts.hour === hh && parts.minute === mm) return t.getTime();
|
|
306
|
-
t = new Date(t.getTime() +
|
|
311
|
+
t = new Date(t.getTime() + MINUTE_MS);
|
|
307
312
|
}
|
|
308
313
|
throw new SchedulerError("scheduler/baseline-no-fire",
|
|
309
314
|
"baseline '" + timeOfDay + "' produced no fire within 24h+ (timezone bug?)", true);
|
|
@@ -376,7 +381,7 @@ function create(opts) {
|
|
|
376
381
|
"scheduler.schedule: spec must set exactly one of cron / every (got cron=" +
|
|
377
382
|
hasCron + ", every=" + hasEvery + ")", true);
|
|
378
383
|
}
|
|
379
|
-
if (hasEvery && (!Number.isFinite(spec.every) || spec.every <
|
|
384
|
+
if (hasEvery && (!Number.isFinite(spec.every) || spec.every < C.TIME.seconds(1))) {
|
|
380
385
|
throw _err("INVALID_SPEC",
|
|
381
386
|
"scheduler.schedule: spec.every must be a number ≥ 1000 ms", true);
|
|
382
387
|
}
|
|
@@ -454,7 +459,7 @@ function create(opts) {
|
|
|
454
459
|
if (task.runningSince && (Date.now() - task.runningSince) > maxJobMs) {
|
|
455
460
|
try {
|
|
456
461
|
log().warn("[scheduler] '" + task.name + "' exceeded " +
|
|
457
|
-
(maxJobMs /
|
|
462
|
+
(maxJobMs / C.TIME.seconds(1)) + "s — forcing reset");
|
|
458
463
|
} catch (_e) { /* logger best-effort */ }
|
|
459
464
|
_emit("system.scheduler.task.watchdog", { name: task.name }, "failure");
|
|
460
465
|
task.running = false;
|
|
@@ -636,7 +641,10 @@ function create(opts) {
|
|
|
636
641
|
async function stop() {
|
|
637
642
|
if (!started) return;
|
|
638
643
|
started = false;
|
|
639
|
-
timers.forEach(function (t) {
|
|
644
|
+
timers.forEach(function (t) {
|
|
645
|
+
try { clearTimeout(t); }
|
|
646
|
+
catch (e) { log().debug("stop-cleanup-failed", { op: "clearTimeout", error: e.message }); }
|
|
647
|
+
});
|
|
640
648
|
timers.clear();
|
|
641
649
|
_emit("scheduler.stop", { count: tasks.size });
|
|
642
650
|
}
|
|
@@ -665,7 +673,10 @@ function create(opts) {
|
|
|
665
673
|
|
|
666
674
|
function _resetForTest() {
|
|
667
675
|
tasks.forEach(function (_t, _n) { /* noop — drop refs below */ });
|
|
668
|
-
timers.forEach(function (t) {
|
|
676
|
+
timers.forEach(function (t) {
|
|
677
|
+
try { clearTimeout(t); }
|
|
678
|
+
catch (e) { log().debug("stop-cleanup-failed", { op: "clearTimeout", error: e.message }); }
|
|
679
|
+
});
|
|
669
680
|
timers.clear();
|
|
670
681
|
tasks.clear();
|
|
671
682
|
started = false;
|
package/lib/security-assert.js
CHANGED
|
@@ -67,10 +67,19 @@
|
|
|
67
67
|
* non-function extra entry, etc.) so the operator catches typos at
|
|
68
68
|
* boot, not at the moment they were trying to gate the boot.
|
|
69
69
|
*/
|
|
70
|
+
var fs = require("fs");
|
|
71
|
+
var nodeTls = require("node:tls");
|
|
70
72
|
var lazyRequire = require("./lazy-require");
|
|
73
|
+
var safeEnv = require("./parsers/safe-env");
|
|
71
74
|
var validateOpts = require("./validate-opts");
|
|
72
75
|
var { defineClass } = require("./framework-error");
|
|
73
76
|
|
|
77
|
+
// Default minimum Node major version — pinned to the framework's
|
|
78
|
+
// supported LTS line (operator can override at their own risk).
|
|
79
|
+
var DEFAULT_MIN_NODE_MAJOR = 0x18;
|
|
80
|
+
// Octal radix for fs-mode formatting (chmod-style 0o755 / 0o750 output).
|
|
81
|
+
var OCTAL_RADIX = 0x8;
|
|
82
|
+
|
|
74
83
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
75
84
|
var vault = lazyRequire(function () { return require("./vault"); });
|
|
76
85
|
var db = lazyRequire(function () { return require("./db"); });
|
|
@@ -138,7 +147,7 @@ async function assertProduction(opts) {
|
|
|
138
147
|
// ntpStrict default ON — production should refuse to boot on >1hr
|
|
139
148
|
// clock drift (audit-chain timestamps stop being trustworthy).
|
|
140
149
|
if (opts.ntpStrict !== false) {
|
|
141
|
-
var ntpEnv =
|
|
150
|
+
var ntpEnv = safeEnv.readVar("BLAMEJS_NTP_STRICT");
|
|
142
151
|
if (ntpEnv === "0" || ntpEnv === "false") {
|
|
143
152
|
failures.push({ ok: false, code: "security/ntp-strict-disabled",
|
|
144
153
|
message: "BLAMEJS_NTP_STRICT is '" + ntpEnv + "'; production policy requires NTP strict mode" });
|
|
@@ -172,9 +181,9 @@ async function assertProduction(opts) {
|
|
|
172
181
|
}
|
|
173
182
|
}
|
|
174
183
|
|
|
175
|
-
// Node major version. Defaults to
|
|
184
|
+
// Node major version. Defaults to the framework's pinned LTS;
|
|
176
185
|
// operator can pin lower at their own risk.
|
|
177
|
-
var minNodeMajor = opts.minNodeMajor !== undefined ? opts.minNodeMajor :
|
|
186
|
+
var minNodeMajor = opts.minNodeMajor !== undefined ? opts.minNodeMajor : DEFAULT_MIN_NODE_MAJOR;
|
|
178
187
|
if (minNodeMajor !== false && typeof minNodeMajor === "number") {
|
|
179
188
|
var nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
|
|
180
189
|
if (nodeMajor < minNodeMajor) {
|
|
@@ -188,8 +197,6 @@ async function assertProduction(opts) {
|
|
|
188
197
|
// framework's pqcGate already enforces that for inbound HTTPS but
|
|
189
198
|
// operators with their own server.listen() flow should re-assert.
|
|
190
199
|
if (opts.minTlsVersion !== undefined && opts.minTlsVersion !== false) {
|
|
191
|
-
var nodeTls;
|
|
192
|
-
try { nodeTls = require("node:tls"); } catch (_e) { nodeTls = null; }
|
|
193
200
|
if (nodeTls && nodeTls.DEFAULT_MIN_VERSION) {
|
|
194
201
|
var got = nodeTls.DEFAULT_MIN_VERSION;
|
|
195
202
|
var want = opts.minTlsVersion;
|
|
@@ -209,7 +216,8 @@ async function assertProduction(opts) {
|
|
|
209
216
|
for (var ri = 0; ri < opts.requireEnv.length; ri++) {
|
|
210
217
|
var reKey = opts.requireEnv[ri];
|
|
211
218
|
if (typeof reKey !== "string" || reKey.length === 0) continue;
|
|
212
|
-
|
|
219
|
+
var reVal = safeEnv.readVar(reKey);
|
|
220
|
+
if (!reVal || reVal.length === 0) {
|
|
213
221
|
failures.push({ ok: false, code: "security/env-missing",
|
|
214
222
|
message: "production policy requires env var '" + reKey + "' to be set and non-empty" });
|
|
215
223
|
}
|
|
@@ -223,13 +231,13 @@ async function assertProduction(opts) {
|
|
|
223
231
|
for (var fi = 0; fi < opts.forbidEnv.length; fi++) {
|
|
224
232
|
var feEntry = opts.forbidEnv[fi];
|
|
225
233
|
if (typeof feEntry === "string") {
|
|
226
|
-
if (
|
|
234
|
+
if (safeEnv.readVar(feEntry) !== undefined) {
|
|
227
235
|
failures.push({ ok: false, code: "security/env-forbidden",
|
|
228
236
|
message: "production policy forbids env var '" + feEntry + "' but it is set" });
|
|
229
237
|
}
|
|
230
238
|
} else if (feEntry && typeof feEntry === "object" && typeof feEntry.key === "string") {
|
|
231
239
|
// Forbid only when value matches.
|
|
232
|
-
if (
|
|
240
|
+
if (safeEnv.readVar(feEntry.key) === feEntry.value) {
|
|
233
241
|
failures.push({ ok: false, code: "security/env-forbidden-value",
|
|
234
242
|
message: "production policy forbids env var '" + feEntry.key +
|
|
235
243
|
"' = '" + feEntry.value + "' but the runtime has exactly that" });
|
|
@@ -242,9 +250,10 @@ async function assertProduction(opts) {
|
|
|
242
250
|
// "development" or "test" in a production-asserted boot.
|
|
243
251
|
if (opts.forbidNodeEnv !== false) {
|
|
244
252
|
var forbidNodeEnv = Array.isArray(opts.forbidNodeEnv) ? opts.forbidNodeEnv : ["development", "dev", "test"];
|
|
245
|
-
|
|
253
|
+
var nodeEnvVal = safeEnv.readVar("NODE_ENV");
|
|
254
|
+
if (nodeEnvVal && forbidNodeEnv.indexOf(nodeEnvVal) !== -1) {
|
|
246
255
|
failures.push({ ok: false, code: "security/node-env-forbidden",
|
|
247
|
-
message: "NODE_ENV='" +
|
|
256
|
+
message: "NODE_ENV='" + nodeEnvVal + "' is in the production-forbidden list " +
|
|
248
257
|
JSON.stringify(forbidNodeEnv) });
|
|
249
258
|
}
|
|
250
259
|
}
|
|
@@ -254,23 +263,19 @@ async function assertProduction(opts) {
|
|
|
254
263
|
// skips silently.
|
|
255
264
|
if (typeof opts.dataDir === "string" && opts.dataDir.length > 0 && process.platform !== "win32") {
|
|
256
265
|
var maxMode = typeof opts.maxDataDirMode === "number" ? opts.maxDataDirMode : 0o750;
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
message: "dataDir '" + opts.dataDir + "' has mode 0" + mode.toString(8) +
|
|
266
|
-
"; production policy requires <= 0" + maxMode.toString(8) +
|
|
267
|
-
" (chmod " + maxMode.toString(8) + " " + opts.dataDir + ")" });
|
|
268
|
-
}
|
|
269
|
-
} catch (e) {
|
|
270
|
-
failures.push({ ok: false, code: "security/datadir-stat-failed",
|
|
271
|
-
message: "dataDir '" + opts.dataDir + "' could not be stat'd: " +
|
|
272
|
-
((e && e.message) || String(e)) });
|
|
266
|
+
try {
|
|
267
|
+
var stat = fs.statSync(opts.dataDir);
|
|
268
|
+
var mode = stat.mode & 0o777;
|
|
269
|
+
if (mode > maxMode) {
|
|
270
|
+
failures.push({ ok: false, code: "security/datadir-permissions",
|
|
271
|
+
message: "dataDir '" + opts.dataDir + "' has mode 0" + mode.toString(OCTAL_RADIX) +
|
|
272
|
+
"; production policy requires <= 0" + maxMode.toString(OCTAL_RADIX) +
|
|
273
|
+
" (chmod " + maxMode.toString(OCTAL_RADIX) + " " + opts.dataDir + ")" });
|
|
273
274
|
}
|
|
275
|
+
} catch (e) {
|
|
276
|
+
failures.push({ ok: false, code: "security/datadir-stat-failed",
|
|
277
|
+
message: "dataDir '" + opts.dataDir + "' could not be stat'd: " +
|
|
278
|
+
((e && e.message) || String(e)) });
|
|
274
279
|
}
|
|
275
280
|
}
|
|
276
281
|
|