@blamejs/core 0.7.1 → 0.7.18
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 +423 -389
- package/README.md +150 -149
- package/bin/blamejs.js +0 -0
- package/index.js +308 -282
- package/lib/api-key.js +660 -672
- package/lib/api-snapshot.js +338 -338
- package/lib/app-shutdown.js +385 -385
- package/lib/app.js +365 -365
- package/lib/archive.js +250 -250
- package/lib/atomic-file.js +544 -544
- package/lib/audit-chain.js +177 -177
- package/lib/audit-sign.js +344 -344
- package/lib/audit-tools.js +677 -677
- package/lib/audit.js +766 -766
- package/lib/auth/jwt.js +311 -311
- package/lib/auth/lockout.js +436 -436
- package/lib/auth/oauth.js +721 -721
- package/lib/auth/passkey.js +181 -181
- package/lib/auth/password.js +594 -594
- package/lib/backup/bundle.js +217 -217
- package/lib/backup/crypto.js +176 -176
- package/lib/backup/index.js +515 -515
- package/lib/backup/manifest.js +282 -282
- package/lib/break-glass.js +1338 -1338
- package/lib/bundler.js +441 -441
- package/lib/cache-redis.js +256 -266
- package/lib/cache.js +1206 -1211
- package/lib/canonical-json.js +115 -115
- package/lib/chain-writer.js +234 -234
- package/lib/cli-helpers.js +206 -206
- package/lib/cli.js +2334 -2334
- package/lib/cluster-provider-db.js +317 -317
- package/lib/cluster-storage.js +226 -226
- package/lib/cluster.js +703 -703
- package/lib/codepoint-class.js +196 -0
- package/lib/config-drift.js +301 -301
- package/lib/consent.js +222 -222
- package/lib/constants.js +191 -191
- package/lib/cookies.js +315 -315
- package/lib/credential-hash.js +322 -322
- package/lib/crypto.js +266 -266
- package/lib/csv.js +275 -286
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -421
- package/lib/db-query.js +406 -406
- package/lib/db-schema.js +319 -319
- package/lib/db.js +1288 -1288
- package/lib/deprecate.js +222 -222
- package/lib/dev.js +335 -335
- package/lib/dual-control.js +473 -473
- package/lib/error-page.js +420 -420
- package/lib/external-db-migrate.js +441 -441
- package/lib/external-db.js +1061 -1061
- package/lib/file-type.js +273 -273
- package/lib/file-upload.js +1136 -0
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -202
- package/lib/framework-schema.js +717 -717
- package/lib/gate-contract.js +971 -0
- package/lib/guard-all.js +405 -0
- package/lib/guard-archive.js +739 -0
- package/lib/guard-csv.js +816 -0
- package/lib/guard-email.js +744 -0
- package/lib/guard-filename.js +724 -0
- package/lib/guard-html.js +976 -0
- package/lib/guard-json.js +729 -0
- package/lib/guard-markdown.js +586 -0
- package/lib/guard-svg.js +976 -0
- package/lib/guard-xml.js +405 -0
- package/lib/guard-yaml.js +529 -0
- package/lib/handlers.js +350 -350
- package/lib/http-client-cookie-jar.js +508 -508
- package/lib/http-client.js +1195 -1195
- package/lib/i18n.js +878 -878
- package/lib/jobs.js +185 -185
- package/lib/log-stream-cloudwatch.js +369 -369
- package/lib/log-stream-local.js +146 -146
- package/lib/log-stream-otlp-grpc.js +410 -410
- package/lib/log-stream-otlp.js +286 -286
- package/lib/log-stream-syslog.js +302 -302
- package/lib/log-stream-webhook.js +199 -199
- package/lib/log-stream.js +330 -330
- package/lib/log.js +500 -500
- package/lib/mail-bounce.js +528 -528
- package/lib/mail-dkim.js +369 -362
- package/lib/mail.js +981 -962
- package/lib/metrics.js +683 -683
- package/lib/middleware/api-encrypt.js +936 -573
- package/lib/middleware/attach-user.js +157 -157
- package/lib/middleware/body-parser.js +1170 -1091
- package/lib/middleware/bot-guard.js +178 -178
- package/lib/middleware/compression.js +452 -452
- package/lib/middleware/cors.js +314 -314
- package/lib/middleware/csp-nonce.js +348 -348
- package/lib/middleware/csrf-protect.js +316 -316
- package/lib/middleware/db-role-for.js +264 -269
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +79 -79
- package/lib/middleware/rate-limit.js +358 -358
- package/lib/middleware/request-id.js +61 -61
- package/lib/middleware/request-log.js +168 -168
- package/lib/middleware/require-auth.js +104 -104
- package/lib/middleware/security-headers.js +116 -116
- package/lib/middleware/sse.js +166 -166
- package/lib/migrations.js +383 -383
- package/lib/mtls-ca.js +518 -518
- package/lib/mtls-engine-default.js +481 -481
- package/lib/network-dns.js +632 -632
- package/lib/network-heartbeat.js +290 -290
- package/lib/network-nts.js +574 -574
- package/lib/network-proxy.js +265 -265
- package/lib/network-tls.js +328 -328
- package/lib/network.js +233 -233
- package/lib/notify.js +612 -614
- package/lib/ntp-check.js +229 -229
- package/lib/numeric-bounds.js +111 -91
- package/lib/object-store/azure-blob-bucket-ops.js +349 -349
- package/lib/object-store/azure-blob.js +488 -451
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -479
- package/lib/object-store/http-put.js +153 -153
- package/lib/object-store/index.js +197 -197
- package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
- package/lib/object-store/sigv4.js +903 -855
- package/lib/observability.js +151 -151
- package/lib/otel-export.js +269 -269
- package/lib/pagination.js +464 -464
- package/lib/parsers/index.js +80 -80
- package/lib/parsers/safe-env.js +642 -642
- package/lib/parsers/safe-ini.js +292 -292
- package/lib/parsers/safe-toml.js +784 -784
- package/lib/parsers/safe-xml.js +390 -390
- package/lib/parsers/safe-yaml.js +1015 -1015
- package/lib/permissions.js +708 -708
- package/lib/pqc-agent.js +87 -87
- package/lib/pqc-gate.js +279 -279
- package/lib/protobuf-encoder.js +190 -190
- package/lib/protocol-dispatcher.js +161 -161
- package/lib/pubsub-redis.js +167 -177
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -752
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -489
- package/lib/request-helpers.js +340 -336
- package/lib/restore-bundle.js +237 -237
- package/lib/restore-rollback.js +259 -259
- package/lib/restore.js +409 -409
- package/lib/retry.js +376 -376
- package/lib/router.js +748 -748
- package/lib/safe-async.js +735 -735
- package/lib/safe-buffer.js +237 -237
- package/lib/safe-json.js +541 -541
- package/lib/safe-schema.js +1266 -1266
- package/lib/safe-url.js +159 -159
- package/lib/scheduler.js +706 -706
- package/lib/security-assert.js +373 -373
- package/lib/seeders.js +618 -630
- package/lib/session.js +478 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/static.js +879 -114
- package/lib/storage.js +471 -471
- package/lib/subject.js +281 -281
- package/lib/template.js +791 -791
- package/lib/testing.js +798 -798
- package/lib/time.js +310 -310
- package/lib/totp.js +302 -302
- package/lib/tracing.js +494 -494
- package/lib/uuid.js +132 -132
- package/lib/validate-opts.js +340 -270
- package/lib/vault/index.js +308 -308
- package/lib/vault/rotate.js +784 -784
- package/lib/vault/wrap.js +296 -296
- package/lib/vendor/noble-ciphers.cjs +9 -9
- package/lib/webhook.js +595 -598
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
package/lib/ntp-check.js
CHANGED
|
@@ -1,229 +1,229 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Minimal SNTP client for boot-time clock-drift verification.
|
|
4
|
-
*
|
|
5
|
-
* Why: the audit chain's monotonicCounter orders events deterministically
|
|
6
|
-
* even if the wall clock jumps, but recordedAt is the human-readable
|
|
7
|
-
* timestamp auditors will rely on. A clock that's silently off by hours
|
|
8
|
-
* (container with no RTC sync, NTP daemon stopped) makes the audit trail
|
|
9
|
-
* misleading.
|
|
10
|
-
*
|
|
11
|
-
* What this does:
|
|
12
|
-
* - Sends a single SNTPv4 query to a configured server (default
|
|
13
|
-
* pool.ntp.org) over UDP port 123.
|
|
14
|
-
* - Computes drift = (server's transmit timestamp) - (local clock).
|
|
15
|
-
* - Returns the drift in milliseconds.
|
|
16
|
-
*
|
|
17
|
-
* What this does NOT do:
|
|
18
|
-
* - Continuous synchronization (use the OS NTP daemon for that).
|
|
19
|
-
* - Authenticated NTP (NTS, autokey).
|
|
20
|
-
* - Querying multiple servers and taking median (single-shot only).
|
|
21
|
-
*
|
|
22
|
-
* The framework's policy in db.init():
|
|
23
|
-
* - drift |x| < 5min → log info, continue
|
|
24
|
-
* - drift |x| in [5min,1hr) → log warning, continue
|
|
25
|
-
* - drift |x| >= 1hr → log fatal, exit (BLAMEJS_NTP_STRICT=1) or warn
|
|
26
|
-
* - NTP unreachable → log warning, continue (network may not allow UDP/123)
|
|
27
|
-
*/
|
|
28
|
-
var dgram = require("dgram");
|
|
29
|
-
var C = require("./constants");
|
|
30
|
-
|
|
31
|
-
// NTP epoch: 1900-01-01. Unix epoch: 1970-01-01. Offset: 70 years incl. 17
|
|
32
|
-
// leap days = 2,208,988,800 seconds.
|
|
33
|
-
var NTP_TO_UNIX_OFFSET_SECONDS = 2208988800;
|
|
34
|
-
|
|
35
|
-
var DEFAULT_SERVERS = ["pool.ntp.org", "time.cloudflare.com"];
|
|
36
|
-
var DEFAULT_PORT = 123;
|
|
37
|
-
var DEFAULT_TIMEOUT_MS = C.TIME.seconds(3);
|
|
38
|
-
var NTP_PACKET_BYTES = C.BYTES.bytes(48); // RFC 5905 §7.3
|
|
39
|
-
|
|
40
|
-
var DEFAULT_DRIFT_WARN_MS = C.TIME.minutes(5);
|
|
41
|
-
var DEFAULT_DRIFT_FATAL_MS = C.TIME.hours(1);
|
|
42
|
-
|
|
43
|
-
var thresholds = {
|
|
44
|
-
warnMs: DEFAULT_DRIFT_WARN_MS,
|
|
45
|
-
fatalMs: DEFAULT_DRIFT_FATAL_MS,
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
function setThresholds(opts) {
|
|
49
|
-
opts = opts || {};
|
|
50
|
-
if (opts.warnMs !== undefined) {
|
|
51
|
-
if (typeof opts.warnMs !== "number" || !isFinite(opts.warnMs) || opts.warnMs < 0) {
|
|
52
|
-
throw new TypeError("ntpCheck.setThresholds: warnMs must be non-negative finite number, got " + JSON.stringify(opts.warnMs));
|
|
53
|
-
}
|
|
54
|
-
thresholds.warnMs = opts.warnMs;
|
|
55
|
-
}
|
|
56
|
-
if (opts.fatalMs !== undefined) {
|
|
57
|
-
if (typeof opts.fatalMs !== "number" || !isFinite(opts.fatalMs) || opts.fatalMs < 0) {
|
|
58
|
-
throw new TypeError("ntpCheck.setThresholds: fatalMs must be non-negative finite number, got " + JSON.stringify(opts.fatalMs));
|
|
59
|
-
}
|
|
60
|
-
thresholds.fatalMs = opts.fatalMs;
|
|
61
|
-
}
|
|
62
|
-
if (thresholds.warnMs > thresholds.fatalMs && thresholds.fatalMs > 0) {
|
|
63
|
-
throw new RangeError("ntpCheck.setThresholds: warnMs (" + thresholds.warnMs +
|
|
64
|
-
") must be <= fatalMs (" + thresholds.fatalMs + ")");
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function getThresholds() {
|
|
69
|
-
return { warnMs: thresholds.warnMs, fatalMs: thresholds.fatalMs };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function _resetThresholdsForTest() {
|
|
73
|
-
thresholds.warnMs = DEFAULT_DRIFT_WARN_MS;
|
|
74
|
-
thresholds.fatalMs = DEFAULT_DRIFT_FATAL_MS;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Query an NTP server once. Resolves with { driftMs, serverTimeMs } or
|
|
79
|
-
* rejects with { code, message } where code is one of:
|
|
80
|
-
* 'ntp/timeout' — server didn't reply within timeoutMs
|
|
81
|
-
* 'ntp/refused' — DNS/connection error
|
|
82
|
-
* 'ntp/bad-reply' — packet structure wrong
|
|
83
|
-
*/
|
|
84
|
-
function querySingle(server, opts) {
|
|
85
|
-
opts = opts || {};
|
|
86
|
-
var port = opts.port || DEFAULT_PORT;
|
|
87
|
-
var timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
88
|
-
|
|
89
|
-
return new Promise(function (resolve, reject) {
|
|
90
|
-
// udp6 for IPv6 literals (`::1`, `fd00::…`), udp4 otherwise. Without
|
|
91
|
-
// this branch a query to an IPv6 NTP host fails with EINVAL because
|
|
92
|
-
// you can't send IPv6 packets through a udp4 socket.
|
|
93
|
-
var family = server.indexOf(":") !== -1 ? "udp6" : "udp4";
|
|
94
|
-
var socket = dgram.createSocket(family);
|
|
95
|
-
var settled = false;
|
|
96
|
-
|
|
97
|
-
function done(err, result) {
|
|
98
|
-
if (settled) return;
|
|
99
|
-
settled = true;
|
|
100
|
-
try { socket.close(); } catch (_e) { /* ignored */ }
|
|
101
|
-
if (err) reject(err); else resolve(result);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
var timer = setTimeout(function () {
|
|
105
|
-
done({ code: "ntp/timeout", message: "no reply from " + server + " within " + timeoutMs + "ms" });
|
|
106
|
-
}, timeoutMs);
|
|
107
|
-
timer.unref();
|
|
108
|
-
|
|
109
|
-
// SNTPv4 client request: NTP_PACKET_BYTES buffer, byte 0 = 0b00_100_011 = 0x23
|
|
110
|
-
// LI=0 (no warning), VN=4, Mode=3 (client). Other bytes zero.
|
|
111
|
-
var req = Buffer.alloc(NTP_PACKET_BYTES);
|
|
112
|
-
req[0] = 0x23;
|
|
113
|
-
var sendTimeMs = Date.now();
|
|
114
|
-
|
|
115
|
-
socket.on("error", function (e) {
|
|
116
|
-
clearTimeout(timer);
|
|
117
|
-
done({ code: "ntp/refused", message: server + ": " + e.message });
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
socket.on("message", function (msg) {
|
|
121
|
-
clearTimeout(timer);
|
|
122
|
-
var receiveTimeMs = Date.now();
|
|
123
|
-
if (!Buffer.isBuffer(msg) || msg.length < NTP_PACKET_BYTES) {
|
|
124
|
-
return done({ code: "ntp/bad-reply", message: "reply too short (" + (msg && msg.length) + " bytes)" });
|
|
125
|
-
}
|
|
126
|
-
// Bytes 40-47 = Transmit Timestamp (NTP epoch seconds.fraction)
|
|
127
|
-
var ntpSeconds = msg.readUInt32BE(40);
|
|
128
|
-
var ntpFraction = msg.readUInt32BE(44);
|
|
129
|
-
var serverUnixSeconds = ntpSeconds - NTP_TO_UNIX_OFFSET_SECONDS;
|
|
130
|
-
var fracMs = Math.round(C.TIME.seconds(ntpFraction / 0x100000000));
|
|
131
|
-
var serverTimeMs = C.TIME.seconds(serverUnixSeconds) + fracMs;
|
|
132
|
-
|
|
133
|
-
// Round-trip-corrected drift: assume the server's reply transmit
|
|
134
|
-
// timestamp is approximately at the midpoint of our send/receive.
|
|
135
|
-
var midpointMs = sendTimeMs + (receiveTimeMs - sendTimeMs) / 2;
|
|
136
|
-
var driftMs = serverTimeMs - midpointMs;
|
|
137
|
-
|
|
138
|
-
done(null, { driftMs: driftMs, serverTimeMs: serverTimeMs, server: server });
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
socket.send(req, 0, req.length, port, server, function (err) {
|
|
142
|
-
if (err) {
|
|
143
|
-
clearTimeout(timer);
|
|
144
|
-
done({ code: "ntp/refused", message: "send to " + server + ": " + err.message });
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Try each server in turn; return the first successful drift measurement.
|
|
152
|
-
* Resolves null if all servers fail (caller decides whether that's fatal).
|
|
153
|
-
*/
|
|
154
|
-
async function checkDrift(opts) {
|
|
155
|
-
opts = opts || {};
|
|
156
|
-
var servers = opts.servers || DEFAULT_SERVERS;
|
|
157
|
-
var lastError = null;
|
|
158
|
-
for (var i = 0; i < servers.length; i++) {
|
|
159
|
-
try {
|
|
160
|
-
return await querySingle(servers[i], opts);
|
|
161
|
-
} catch (e) {
|
|
162
|
-
lastError = e;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return { driftMs: null, error: lastError };
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Boot-time check that integrates with the framework's logging policy.
|
|
170
|
-
* Returns a result object with { ok, driftMs, severity, message }.
|
|
171
|
-
* Caller (db.init) decides whether to exit.
|
|
172
|
-
*/
|
|
173
|
-
async function bootCheck(opts) {
|
|
174
|
-
opts = opts || {};
|
|
175
|
-
var result = await checkDrift(opts);
|
|
176
|
-
if (result.driftMs === null) {
|
|
177
|
-
return {
|
|
178
|
-
ok: true, // unreachable NTP isn't a hard failure
|
|
179
|
-
severity: "warning",
|
|
180
|
-
driftMs: null,
|
|
181
|
-
message: "NTP unreachable: " + (result.error && result.error.message) +
|
|
182
|
-
" (continuing — set BLAMEJS_NTP_STRICT=1 to fail closed)",
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
var absMs = Math.abs(result.driftMs);
|
|
186
|
-
var driftStr = (result.driftMs >= 0 ? "+" : "") + result.driftMs + "ms";
|
|
187
|
-
var fatalMs = (opts && typeof opts.driftFatalMs === "number") ? opts.driftFatalMs : thresholds.fatalMs;
|
|
188
|
-
var warnMs = (opts && typeof opts.driftWarnMs === "number") ? opts.driftWarnMs : thresholds.warnMs;
|
|
189
|
-
if (fatalMs > 0 && absMs >= fatalMs) {
|
|
190
|
-
return {
|
|
191
|
-
ok: false,
|
|
192
|
-
severity: "fatal",
|
|
193
|
-
driftMs: result.driftMs,
|
|
194
|
-
server: result.server,
|
|
195
|
-
message: "clock drift " + driftStr + " from " + result.server +
|
|
196
|
-
" (>= " + fatalMs + "ms) — refuse to boot",
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
if (warnMs > 0 && absMs >= warnMs) {
|
|
200
|
-
return {
|
|
201
|
-
ok: true,
|
|
202
|
-
severity: "warning",
|
|
203
|
-
driftMs: result.driftMs,
|
|
204
|
-
server: result.server,
|
|
205
|
-
message: "clock drift " + driftStr + " from " + result.server +
|
|
206
|
-
" (>= " + warnMs + "ms) — investigate",
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
return {
|
|
210
|
-
ok: true,
|
|
211
|
-
severity: "info",
|
|
212
|
-
driftMs: result.driftMs,
|
|
213
|
-
server: result.server,
|
|
214
|
-
message: "clock drift " + driftStr + " from " + result.server,
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
module.exports = {
|
|
219
|
-
querySingle: querySingle,
|
|
220
|
-
checkDrift: checkDrift,
|
|
221
|
-
bootCheck: bootCheck,
|
|
222
|
-
setThresholds: setThresholds,
|
|
223
|
-
getThresholds: getThresholds,
|
|
224
|
-
DEFAULT_SERVERS: DEFAULT_SERVERS,
|
|
225
|
-
DEFAULT_DRIFT_WARN_MS: DEFAULT_DRIFT_WARN_MS,
|
|
226
|
-
DEFAULT_DRIFT_FATAL_MS: DEFAULT_DRIFT_FATAL_MS,
|
|
227
|
-
NTP_TO_UNIX_OFFSET_SECONDS: NTP_TO_UNIX_OFFSET_SECONDS,
|
|
228
|
-
_resetThresholdsForTest: _resetThresholdsForTest,
|
|
229
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal SNTP client for boot-time clock-drift verification.
|
|
4
|
+
*
|
|
5
|
+
* Why: the audit chain's monotonicCounter orders events deterministically
|
|
6
|
+
* even if the wall clock jumps, but recordedAt is the human-readable
|
|
7
|
+
* timestamp auditors will rely on. A clock that's silently off by hours
|
|
8
|
+
* (container with no RTC sync, NTP daemon stopped) makes the audit trail
|
|
9
|
+
* misleading.
|
|
10
|
+
*
|
|
11
|
+
* What this does:
|
|
12
|
+
* - Sends a single SNTPv4 query to a configured server (default
|
|
13
|
+
* pool.ntp.org) over UDP port 123.
|
|
14
|
+
* - Computes drift = (server's transmit timestamp) - (local clock).
|
|
15
|
+
* - Returns the drift in milliseconds.
|
|
16
|
+
*
|
|
17
|
+
* What this does NOT do:
|
|
18
|
+
* - Continuous synchronization (use the OS NTP daemon for that).
|
|
19
|
+
* - Authenticated NTP (NTS, autokey).
|
|
20
|
+
* - Querying multiple servers and taking median (single-shot only).
|
|
21
|
+
*
|
|
22
|
+
* The framework's policy in db.init():
|
|
23
|
+
* - drift |x| < 5min → log info, continue
|
|
24
|
+
* - drift |x| in [5min,1hr) → log warning, continue
|
|
25
|
+
* - drift |x| >= 1hr → log fatal, exit (BLAMEJS_NTP_STRICT=1) or warn
|
|
26
|
+
* - NTP unreachable → log warning, continue (network may not allow UDP/123)
|
|
27
|
+
*/
|
|
28
|
+
var dgram = require("dgram");
|
|
29
|
+
var C = require("./constants");
|
|
30
|
+
|
|
31
|
+
// NTP epoch: 1900-01-01. Unix epoch: 1970-01-01. Offset: 70 years incl. 17
|
|
32
|
+
// leap days = 2,208,988,800 seconds.
|
|
33
|
+
var NTP_TO_UNIX_OFFSET_SECONDS = 2208988800;
|
|
34
|
+
|
|
35
|
+
var DEFAULT_SERVERS = ["pool.ntp.org", "time.cloudflare.com"];
|
|
36
|
+
var DEFAULT_PORT = 123;
|
|
37
|
+
var DEFAULT_TIMEOUT_MS = C.TIME.seconds(3);
|
|
38
|
+
var NTP_PACKET_BYTES = C.BYTES.bytes(48); // RFC 5905 §7.3
|
|
39
|
+
|
|
40
|
+
var DEFAULT_DRIFT_WARN_MS = C.TIME.minutes(5);
|
|
41
|
+
var DEFAULT_DRIFT_FATAL_MS = C.TIME.hours(1);
|
|
42
|
+
|
|
43
|
+
var thresholds = {
|
|
44
|
+
warnMs: DEFAULT_DRIFT_WARN_MS,
|
|
45
|
+
fatalMs: DEFAULT_DRIFT_FATAL_MS,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function setThresholds(opts) {
|
|
49
|
+
opts = opts || {};
|
|
50
|
+
if (opts.warnMs !== undefined) {
|
|
51
|
+
if (typeof opts.warnMs !== "number" || !isFinite(opts.warnMs) || opts.warnMs < 0) {
|
|
52
|
+
throw new TypeError("ntpCheck.setThresholds: warnMs must be non-negative finite number, got " + JSON.stringify(opts.warnMs));
|
|
53
|
+
}
|
|
54
|
+
thresholds.warnMs = opts.warnMs;
|
|
55
|
+
}
|
|
56
|
+
if (opts.fatalMs !== undefined) {
|
|
57
|
+
if (typeof opts.fatalMs !== "number" || !isFinite(opts.fatalMs) || opts.fatalMs < 0) {
|
|
58
|
+
throw new TypeError("ntpCheck.setThresholds: fatalMs must be non-negative finite number, got " + JSON.stringify(opts.fatalMs));
|
|
59
|
+
}
|
|
60
|
+
thresholds.fatalMs = opts.fatalMs;
|
|
61
|
+
}
|
|
62
|
+
if (thresholds.warnMs > thresholds.fatalMs && thresholds.fatalMs > 0) {
|
|
63
|
+
throw new RangeError("ntpCheck.setThresholds: warnMs (" + thresholds.warnMs +
|
|
64
|
+
") must be <= fatalMs (" + thresholds.fatalMs + ")");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getThresholds() {
|
|
69
|
+
return { warnMs: thresholds.warnMs, fatalMs: thresholds.fatalMs };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function _resetThresholdsForTest() {
|
|
73
|
+
thresholds.warnMs = DEFAULT_DRIFT_WARN_MS;
|
|
74
|
+
thresholds.fatalMs = DEFAULT_DRIFT_FATAL_MS;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Query an NTP server once. Resolves with { driftMs, serverTimeMs } or
|
|
79
|
+
* rejects with { code, message } where code is one of:
|
|
80
|
+
* 'ntp/timeout' — server didn't reply within timeoutMs
|
|
81
|
+
* 'ntp/refused' — DNS/connection error
|
|
82
|
+
* 'ntp/bad-reply' — packet structure wrong
|
|
83
|
+
*/
|
|
84
|
+
function querySingle(server, opts) {
|
|
85
|
+
opts = opts || {};
|
|
86
|
+
var port = opts.port || DEFAULT_PORT;
|
|
87
|
+
var timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
88
|
+
|
|
89
|
+
return new Promise(function (resolve, reject) {
|
|
90
|
+
// udp6 for IPv6 literals (`::1`, `fd00::…`), udp4 otherwise. Without
|
|
91
|
+
// this branch a query to an IPv6 NTP host fails with EINVAL because
|
|
92
|
+
// you can't send IPv6 packets through a udp4 socket.
|
|
93
|
+
var family = server.indexOf(":") !== -1 ? "udp6" : "udp4";
|
|
94
|
+
var socket = dgram.createSocket(family);
|
|
95
|
+
var settled = false;
|
|
96
|
+
|
|
97
|
+
function done(err, result) {
|
|
98
|
+
if (settled) return;
|
|
99
|
+
settled = true;
|
|
100
|
+
try { socket.close(); } catch (_e) { /* ignored */ }
|
|
101
|
+
if (err) reject(err); else resolve(result);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
var timer = setTimeout(function () {
|
|
105
|
+
done({ code: "ntp/timeout", message: "no reply from " + server + " within " + timeoutMs + "ms" });
|
|
106
|
+
}, timeoutMs);
|
|
107
|
+
timer.unref();
|
|
108
|
+
|
|
109
|
+
// SNTPv4 client request: NTP_PACKET_BYTES buffer, byte 0 = 0b00_100_011 = 0x23
|
|
110
|
+
// LI=0 (no warning), VN=4, Mode=3 (client). Other bytes zero.
|
|
111
|
+
var req = Buffer.alloc(NTP_PACKET_BYTES);
|
|
112
|
+
req[0] = 0x23;
|
|
113
|
+
var sendTimeMs = Date.now();
|
|
114
|
+
|
|
115
|
+
socket.on("error", function (e) {
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
done({ code: "ntp/refused", message: server + ": " + e.message });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
socket.on("message", function (msg) {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
var receiveTimeMs = Date.now();
|
|
123
|
+
if (!Buffer.isBuffer(msg) || msg.length < NTP_PACKET_BYTES) {
|
|
124
|
+
return done({ code: "ntp/bad-reply", message: "reply too short (" + (msg && msg.length) + " bytes)" });
|
|
125
|
+
}
|
|
126
|
+
// Bytes 40-47 = Transmit Timestamp (NTP epoch seconds.fraction)
|
|
127
|
+
var ntpSeconds = msg.readUInt32BE(40);
|
|
128
|
+
var ntpFraction = msg.readUInt32BE(44);
|
|
129
|
+
var serverUnixSeconds = ntpSeconds - NTP_TO_UNIX_OFFSET_SECONDS;
|
|
130
|
+
var fracMs = Math.round(C.TIME.seconds(ntpFraction / 0x100000000));
|
|
131
|
+
var serverTimeMs = C.TIME.seconds(serverUnixSeconds) + fracMs;
|
|
132
|
+
|
|
133
|
+
// Round-trip-corrected drift: assume the server's reply transmit
|
|
134
|
+
// timestamp is approximately at the midpoint of our send/receive.
|
|
135
|
+
var midpointMs = sendTimeMs + (receiveTimeMs - sendTimeMs) / 2;
|
|
136
|
+
var driftMs = serverTimeMs - midpointMs;
|
|
137
|
+
|
|
138
|
+
done(null, { driftMs: driftMs, serverTimeMs: serverTimeMs, server: server });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
socket.send(req, 0, req.length, port, server, function (err) {
|
|
142
|
+
if (err) {
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
done({ code: "ntp/refused", message: "send to " + server + ": " + err.message });
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Try each server in turn; return the first successful drift measurement.
|
|
152
|
+
* Resolves null if all servers fail (caller decides whether that's fatal).
|
|
153
|
+
*/
|
|
154
|
+
async function checkDrift(opts) {
|
|
155
|
+
opts = opts || {};
|
|
156
|
+
var servers = opts.servers || DEFAULT_SERVERS;
|
|
157
|
+
var lastError = null;
|
|
158
|
+
for (var i = 0; i < servers.length; i++) {
|
|
159
|
+
try {
|
|
160
|
+
return await querySingle(servers[i], opts);
|
|
161
|
+
} catch (e) {
|
|
162
|
+
lastError = e;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { driftMs: null, error: lastError };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Boot-time check that integrates with the framework's logging policy.
|
|
170
|
+
* Returns a result object with { ok, driftMs, severity, message }.
|
|
171
|
+
* Caller (db.init) decides whether to exit.
|
|
172
|
+
*/
|
|
173
|
+
async function bootCheck(opts) {
|
|
174
|
+
opts = opts || {};
|
|
175
|
+
var result = await checkDrift(opts);
|
|
176
|
+
if (result.driftMs === null) {
|
|
177
|
+
return {
|
|
178
|
+
ok: true, // unreachable NTP isn't a hard failure
|
|
179
|
+
severity: "warning",
|
|
180
|
+
driftMs: null,
|
|
181
|
+
message: "NTP unreachable: " + (result.error && result.error.message) +
|
|
182
|
+
" (continuing — set BLAMEJS_NTP_STRICT=1 to fail closed)",
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
var absMs = Math.abs(result.driftMs);
|
|
186
|
+
var driftStr = (result.driftMs >= 0 ? "+" : "") + result.driftMs + "ms";
|
|
187
|
+
var fatalMs = (opts && typeof opts.driftFatalMs === "number") ? opts.driftFatalMs : thresholds.fatalMs;
|
|
188
|
+
var warnMs = (opts && typeof opts.driftWarnMs === "number") ? opts.driftWarnMs : thresholds.warnMs;
|
|
189
|
+
if (fatalMs > 0 && absMs >= fatalMs) {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
severity: "fatal",
|
|
193
|
+
driftMs: result.driftMs,
|
|
194
|
+
server: result.server,
|
|
195
|
+
message: "clock drift " + driftStr + " from " + result.server +
|
|
196
|
+
" (>= " + fatalMs + "ms) — refuse to boot",
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (warnMs > 0 && absMs >= warnMs) {
|
|
200
|
+
return {
|
|
201
|
+
ok: true,
|
|
202
|
+
severity: "warning",
|
|
203
|
+
driftMs: result.driftMs,
|
|
204
|
+
server: result.server,
|
|
205
|
+
message: "clock drift " + driftStr + " from " + result.server +
|
|
206
|
+
" (>= " + warnMs + "ms) — investigate",
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
ok: true,
|
|
211
|
+
severity: "info",
|
|
212
|
+
driftMs: result.driftMs,
|
|
213
|
+
server: result.server,
|
|
214
|
+
message: "clock drift " + driftStr + " from " + result.server,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = {
|
|
219
|
+
querySingle: querySingle,
|
|
220
|
+
checkDrift: checkDrift,
|
|
221
|
+
bootCheck: bootCheck,
|
|
222
|
+
setThresholds: setThresholds,
|
|
223
|
+
getThresholds: getThresholds,
|
|
224
|
+
DEFAULT_SERVERS: DEFAULT_SERVERS,
|
|
225
|
+
DEFAULT_DRIFT_WARN_MS: DEFAULT_DRIFT_WARN_MS,
|
|
226
|
+
DEFAULT_DRIFT_FATAL_MS: DEFAULT_DRIFT_FATAL_MS,
|
|
227
|
+
NTP_TO_UNIX_OFFSET_SECONDS: NTP_TO_UNIX_OFFSET_SECONDS,
|
|
228
|
+
_resetThresholdsForTest: _resetThresholdsForTest,
|
|
229
|
+
};
|
package/lib/numeric-bounds.js
CHANGED
|
@@ -1,91 +1,111 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* numeric-bounds — shared validators for operator-tunable numeric opts.
|
|
4
|
-
*
|
|
5
|
-
* Replaces the recurring `typeof opts.X === "number" && opts.X > 0`
|
|
6
|
-
* pattern that ships across the framework. Pre-v0.6.69 each site had
|
|
7
|
-
* its own slightly-different inline check; the bug they all shared was
|
|
8
|
-
* accepting `Infinity` (silently bypassing OOM caps, body size limits,
|
|
9
|
-
* URL caps, depth limits) and `NaN` (also bypassing because every
|
|
10
|
-
* comparison with NaN is false). v0.6.57 fixed boundedChunkCollector,
|
|
11
|
-
* v0.6.68 fixed atomicFile, v0.6.69 sweeps the rest.
|
|
12
|
-
*
|
|
13
|
-
* var nb = require("./numeric-bounds");
|
|
14
|
-
*
|
|
15
|
-
* if (!nb.isPositiveFiniteInt(opts.maxBytes)) {
|
|
16
|
-
* throw new CsvError("csv/bad-opt",
|
|
17
|
-
* "csv.parse: maxBytes must be a positive finite integer; got " +
|
|
18
|
-
* nb.shape(opts.maxBytes));
|
|
19
|
-
* }
|
|
20
|
-
*
|
|
21
|
-
* The helper returns a predicate + a shape-formatter rather than
|
|
22
|
-
* throwing itself, because the framework's error classes use two
|
|
23
|
-
* different constructor conventions:
|
|
24
|
-
*
|
|
25
|
-
* AtomicFileError, SafeBufferError, SafeUrlError (message, code)
|
|
26
|
-
* defineClass(...)-built (CsvError, MailBounceError…) (code, message, permanent[, statusCode])
|
|
27
|
-
*
|
|
28
|
-
* Each call site throws its own class, with whatever ctor shape that
|
|
29
|
-
* class wants. The helper just owns the validation rule + the consistent
|
|
30
|
-
* shape format ("number Infinity" / "number NaN" / "string \"100\"") so
|
|
31
|
-
* the typo / coercion is visible in the error message — `JSON.stringify`
|
|
32
|
-
* collapses Infinity / NaN to "null" which obscures what went wrong.
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
// Shape formatter — typeof + String preserves "number Infinity" /
|
|
36
|
-
// "number NaN" / "string foo". Strings get JSON-quoted so trailing
|
|
37
|
-
// whitespace / control chars are visible.
|
|
38
|
-
function shape(value) {
|
|
39
|
-
if (typeof value === "string") {
|
|
40
|
-
return "string " + JSON.stringify(value);
|
|
41
|
-
}
|
|
42
|
-
return (typeof value) + " " + String(value);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function isPositiveFiniteInt(value) {
|
|
46
|
-
return typeof value === "number" && Number.isFinite(value) &&
|
|
47
|
-
Number.isInteger(value) && value > 0;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function isNonNegativeFiniteInt(value) {
|
|
51
|
-
return typeof value === "number" && Number.isFinite(value) &&
|
|
52
|
-
Number.isInteger(value) && value >= 0;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// requirePositiveFiniteIntIfPresent / requireNonNegativeFiniteIntIfPresent —
|
|
56
|
-
// optional-shape gates that throw via the caller's framework-error class
|
|
57
|
-
// when the value is present but invalid. Replaces the per-file
|
|
58
|
-
// `if (opts.X !== undefined) { if (!nb.isYFiniteInt(opts.X)) throw new
|
|
59
|
-
// XError(...); }` cascade that app-shutdown / bundler / dev /
|
|
60
|
-
// http-client-cookie-jar / external-db-migrate / restore-rollback
|
|
61
|
-
// previously rolled by hand.
|
|
62
|
-
//
|
|
63
|
-
// var nb = require("./numeric-bounds");
|
|
64
|
-
// nb.requirePositiveFiniteIntIfPresent(opts.graceMs,
|
|
65
|
-
// "app-shutdown.create: opts.graceMs", AppShutdownError,
|
|
66
|
-
// "app-shutdown/bad-grace-ms");
|
|
67
|
-
function requirePositiveFiniteIntIfPresent(value, label, errorClass, code) {
|
|
68
|
-
if (value === undefined) return value;
|
|
69
|
-
if (!isPositiveFiniteInt(value)) {
|
|
70
|
-
throw new errorClass(code,
|
|
71
|
-
(label || "value") + " must be a positive finite integer; got " + shape(value));
|
|
72
|
-
}
|
|
73
|
-
return value;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function requireNonNegativeFiniteIntIfPresent(value, label, errorClass, code) {
|
|
77
|
-
if (value === undefined) return value;
|
|
78
|
-
if (!isNonNegativeFiniteInt(value)) {
|
|
79
|
-
throw new errorClass(code,
|
|
80
|
-
(label || "value") + " must be a non-negative finite integer; got " + shape(value));
|
|
81
|
-
}
|
|
82
|
-
return value;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* numeric-bounds — shared validators for operator-tunable numeric opts.
|
|
4
|
+
*
|
|
5
|
+
* Replaces the recurring `typeof opts.X === "number" && opts.X > 0`
|
|
6
|
+
* pattern that ships across the framework. Pre-v0.6.69 each site had
|
|
7
|
+
* its own slightly-different inline check; the bug they all shared was
|
|
8
|
+
* accepting `Infinity` (silently bypassing OOM caps, body size limits,
|
|
9
|
+
* URL caps, depth limits) and `NaN` (also bypassing because every
|
|
10
|
+
* comparison with NaN is false). v0.6.57 fixed boundedChunkCollector,
|
|
11
|
+
* v0.6.68 fixed atomicFile, v0.6.69 sweeps the rest.
|
|
12
|
+
*
|
|
13
|
+
* var nb = require("./numeric-bounds");
|
|
14
|
+
*
|
|
15
|
+
* if (!nb.isPositiveFiniteInt(opts.maxBytes)) {
|
|
16
|
+
* throw new CsvError("csv/bad-opt",
|
|
17
|
+
* "csv.parse: maxBytes must be a positive finite integer; got " +
|
|
18
|
+
* nb.shape(opts.maxBytes));
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* The helper returns a predicate + a shape-formatter rather than
|
|
22
|
+
* throwing itself, because the framework's error classes use two
|
|
23
|
+
* different constructor conventions:
|
|
24
|
+
*
|
|
25
|
+
* AtomicFileError, SafeBufferError, SafeUrlError (message, code)
|
|
26
|
+
* defineClass(...)-built (CsvError, MailBounceError…) (code, message, permanent[, statusCode])
|
|
27
|
+
*
|
|
28
|
+
* Each call site throws its own class, with whatever ctor shape that
|
|
29
|
+
* class wants. The helper just owns the validation rule + the consistent
|
|
30
|
+
* shape format ("number Infinity" / "number NaN" / "string \"100\"") so
|
|
31
|
+
* the typo / coercion is visible in the error message — `JSON.stringify`
|
|
32
|
+
* collapses Infinity / NaN to "null" which obscures what went wrong.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
// Shape formatter — typeof + String preserves "number Infinity" /
|
|
36
|
+
// "number NaN" / "string foo". Strings get JSON-quoted so trailing
|
|
37
|
+
// whitespace / control chars are visible.
|
|
38
|
+
function shape(value) {
|
|
39
|
+
if (typeof value === "string") {
|
|
40
|
+
return "string " + JSON.stringify(value);
|
|
41
|
+
}
|
|
42
|
+
return (typeof value) + " " + String(value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isPositiveFiniteInt(value) {
|
|
46
|
+
return typeof value === "number" && Number.isFinite(value) &&
|
|
47
|
+
Number.isInteger(value) && value > 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isNonNegativeFiniteInt(value) {
|
|
51
|
+
return typeof value === "number" && Number.isFinite(value) &&
|
|
52
|
+
Number.isInteger(value) && value >= 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// requirePositiveFiniteIntIfPresent / requireNonNegativeFiniteIntIfPresent —
|
|
56
|
+
// optional-shape gates that throw via the caller's framework-error class
|
|
57
|
+
// when the value is present but invalid. Replaces the per-file
|
|
58
|
+
// `if (opts.X !== undefined) { if (!nb.isYFiniteInt(opts.X)) throw new
|
|
59
|
+
// XError(...); }` cascade that app-shutdown / bundler / dev /
|
|
60
|
+
// http-client-cookie-jar / external-db-migrate / restore-rollback
|
|
61
|
+
// previously rolled by hand.
|
|
62
|
+
//
|
|
63
|
+
// var nb = require("./numeric-bounds");
|
|
64
|
+
// nb.requirePositiveFiniteIntIfPresent(opts.graceMs,
|
|
65
|
+
// "app-shutdown.create: opts.graceMs", AppShutdownError,
|
|
66
|
+
// "app-shutdown/bad-grace-ms");
|
|
67
|
+
function requirePositiveFiniteIntIfPresent(value, label, errorClass, code) {
|
|
68
|
+
if (value === undefined) return value;
|
|
69
|
+
if (!isPositiveFiniteInt(value)) {
|
|
70
|
+
throw new errorClass(code,
|
|
71
|
+
(label || "value") + " must be a positive finite integer; got " + shape(value));
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function requireNonNegativeFiniteIntIfPresent(value, label, errorClass, code) {
|
|
77
|
+
if (value === undefined) return value;
|
|
78
|
+
if (!isNonNegativeFiniteInt(value)) {
|
|
79
|
+
throw new errorClass(code,
|
|
80
|
+
(label || "value") + " must be a non-negative finite integer; got " + shape(value));
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// requireAllPositiveFiniteIntIfPresent — batch validator. Walk each
|
|
86
|
+
// opt-name in the list; for any that is present in opts, require it to
|
|
87
|
+
// be a positive finite integer (otherwise throw via errorClass with the
|
|
88
|
+
// shared code). Used by primitives whose entry points have a 3-6
|
|
89
|
+
// numeric opts that all share the same shape constraint, so the inline
|
|
90
|
+
// call sequence doesn't repeat per primitive.
|
|
91
|
+
//
|
|
92
|
+
// nb.requireAllPositiveFiniteIntIfPresent(opts,
|
|
93
|
+
// ["maxBytes", "maxAttrValueBytes", "maxTagDepth", "maxAttrsPerTag"],
|
|
94
|
+
// "guardHtml.validate", GuardHtmlError, "html.bad-opt");
|
|
95
|
+
function requireAllPositiveFiniteIntIfPresent(opts, names, labelPrefix, errorClass, code) {
|
|
96
|
+
if (!opts || !Array.isArray(names)) return;
|
|
97
|
+
for (var i = 0; i < names.length; i += 1) {
|
|
98
|
+
var n = names[i];
|
|
99
|
+
requirePositiveFiniteIntIfPresent(opts[n],
|
|
100
|
+
(labelPrefix || "") + ": " + n, errorClass, code);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
shape: shape,
|
|
106
|
+
isPositiveFiniteInt: isPositiveFiniteInt,
|
|
107
|
+
isNonNegativeFiniteInt: isNonNegativeFiniteInt,
|
|
108
|
+
requirePositiveFiniteIntIfPresent: requirePositiveFiniteIntIfPresent,
|
|
109
|
+
requireNonNegativeFiniteIntIfPresent: requireNonNegativeFiniteIntIfPresent,
|
|
110
|
+
requireAllPositiveFiniteIntIfPresent: requireAllPositiveFiniteIntIfPresent,
|
|
111
|
+
};
|