@blamejs/core 0.7.18 → 0.7.19
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 +425 -423
- package/README.md +150 -150
- package/bin/blamejs.js +0 -0
- package/index.js +310 -308
- package/lib/api-key.js +660 -660
- 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-external.js +365 -0
- package/lib/auth/jwt.js +337 -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 +628 -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 -256
- package/lib/cache.js +1206 -1206
- 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/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 -275
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -420
- 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/forms.js +422 -422
- package/lib/framework-error.js +293 -293
- package/lib/framework-schema.js +717 -717
- 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 -369
- package/lib/mail.js +981 -981
- package/lib/metrics.js +683 -683
- package/lib/middleware/api-encrypt.js +936 -936
- package/lib/middleware/attach-user.js +157 -157
- package/lib/middleware/bearer-auth.js +152 -0
- package/lib/middleware/body-parser.js +1170 -1170
- 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 -264
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +82 -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 -612
- package/lib/ntp-check.js +229 -229
- package/lib/numeric-bounds.js +111 -111
- package/lib/object-store/azure-blob-bucket-ops.js +349 -349
- package/lib/object-store/azure-blob.js +488 -488
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -519
- 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 -903
- 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 -167
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -745
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -519
- package/lib/request-helpers.js +340 -340
- 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 -618
- package/lib/session.js +535 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- 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 -340
- 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 -595
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
package/lib/network-heartbeat.js
CHANGED
|
@@ -1,290 +1,290 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
var net = require("node:net");
|
|
4
|
-
|
|
5
|
-
var C = require("./constants");
|
|
6
|
-
var validateOpts = require("./validate-opts");
|
|
7
|
-
var lazyRequire = require("./lazy-require");
|
|
8
|
-
var { defineClass } = require("./framework-error");
|
|
9
|
-
|
|
10
|
-
var HeartbeatError = defineClass("HeartbeatError", { alwaysPermanent: true });
|
|
11
|
-
|
|
12
|
-
var observability = lazyRequire(function () { return require("./observability"); });
|
|
13
|
-
var audit = lazyRequire(function () { return require("./audit"); });
|
|
14
|
-
var httpClient = lazyRequire(function () { return require("./http-client"); });
|
|
15
|
-
var ntpCheck = lazyRequire(function () { return require("./ntp-check"); });
|
|
16
|
-
|
|
17
|
-
var DEFAULT_INTERVAL_MS = C.TIME.seconds(15);
|
|
18
|
-
var DEFAULT_TIMEOUT_MS = C.TIME.seconds(5);
|
|
19
|
-
var DEFAULT_THRESHOLD = 3;
|
|
20
|
-
|
|
21
|
-
var TARGETS = new Map();
|
|
22
|
-
|
|
23
|
-
function _validateTarget(t, idx) {
|
|
24
|
-
if (!t || typeof t !== "object") {
|
|
25
|
-
throw new HeartbeatError("heartbeat/bad-target", "targets[" + idx + "] must be an object");
|
|
26
|
-
}
|
|
27
|
-
if (typeof t.name !== "string" || t.name.length === 0) {
|
|
28
|
-
throw new HeartbeatError("heartbeat/bad-name", "targets[" + idx + "].name required (string)");
|
|
29
|
-
}
|
|
30
|
-
if (t.type !== "http" && t.type !== "tcp" && t.type !== "ntp") {
|
|
31
|
-
throw new HeartbeatError("heartbeat/bad-type",
|
|
32
|
-
"targets[" + idx + "].type must be 'http' | 'tcp' | 'ntp', got " + JSON.stringify(t.type));
|
|
33
|
-
}
|
|
34
|
-
if (t.type === "http" && (typeof t.url !== "string" || t.url.length === 0)) {
|
|
35
|
-
throw new HeartbeatError("heartbeat/bad-url", "targets[" + idx + "].url required for http heartbeat");
|
|
36
|
-
}
|
|
37
|
-
if (t.type === "tcp" && (typeof t.host !== "string" || typeof t.port !== "number")) {
|
|
38
|
-
throw new HeartbeatError("heartbeat/bad-tcp", "targets[" + idx + "] tcp heartbeat requires host (string) + port (number)");
|
|
39
|
-
}
|
|
40
|
-
if (t.type === "ntp" && typeof t.server !== "string") {
|
|
41
|
-
throw new HeartbeatError("heartbeat/bad-ntp", "targets[" + idx + "] ntp heartbeat requires server (string)");
|
|
42
|
-
}
|
|
43
|
-
if (t.intervalMs !== undefined && (typeof t.intervalMs !== "number" || !isFinite(t.intervalMs) || t.intervalMs <= 0)) {
|
|
44
|
-
throw new HeartbeatError("heartbeat/bad-interval", "targets[" + idx + "].intervalMs must be a positive number");
|
|
45
|
-
}
|
|
46
|
-
if (t.timeoutMs !== undefined && (typeof t.timeoutMs !== "number" || !isFinite(t.timeoutMs) || t.timeoutMs <= 0)) {
|
|
47
|
-
throw new HeartbeatError("heartbeat/bad-timeout", "targets[" + idx + "].timeoutMs must be a positive number");
|
|
48
|
-
}
|
|
49
|
-
if (t.threshold !== undefined && (typeof t.threshold !== "number" || t.threshold < 1 || Math.floor(t.threshold) !== t.threshold)) {
|
|
50
|
-
throw new HeartbeatError("heartbeat/bad-threshold", "targets[" + idx + "].threshold must be a positive integer");
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function _probeHttp(target, timeoutMs) {
|
|
55
|
-
return new Promise(function (resolve) {
|
|
56
|
-
var startMs = Date.now();
|
|
57
|
-
var settled = false;
|
|
58
|
-
var timer = setTimeout(function () {
|
|
59
|
-
if (settled) return;
|
|
60
|
-
settled = true;
|
|
61
|
-
resolve({ ok: false, latencyMs: Date.now() - startMs, error: "timeout" });
|
|
62
|
-
}, timeoutMs);
|
|
63
|
-
try {
|
|
64
|
-
var p = httpClient().request({
|
|
65
|
-
url: target.url,
|
|
66
|
-
method: target.method || "GET",
|
|
67
|
-
timeoutMs: timeoutMs,
|
|
68
|
-
allowInternal: target.allowInternal === true ? true : target.allowInternal,
|
|
69
|
-
});
|
|
70
|
-
p.then(function (res) {
|
|
71
|
-
if (settled) return;
|
|
72
|
-
settled = true;
|
|
73
|
-
clearTimeout(timer);
|
|
74
|
-
var ok = !target.expectStatus
|
|
75
|
-
? (res.statusCode >= 200 && res.statusCode < 400)
|
|
76
|
-
: res.statusCode === target.expectStatus;
|
|
77
|
-
resolve({ ok: ok, latencyMs: Date.now() - startMs, statusCode: res.statusCode });
|
|
78
|
-
}, function (err) {
|
|
79
|
-
if (settled) return;
|
|
80
|
-
settled = true;
|
|
81
|
-
clearTimeout(timer);
|
|
82
|
-
resolve({ ok: false, latencyMs: Date.now() - startMs, error: (err && err.message) || String(err) });
|
|
83
|
-
});
|
|
84
|
-
} catch (e) {
|
|
85
|
-
if (settled) return;
|
|
86
|
-
settled = true;
|
|
87
|
-
clearTimeout(timer);
|
|
88
|
-
resolve({ ok: false, latencyMs: Date.now() - startMs, error: e.message });
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function _probeTcp(target, timeoutMs) {
|
|
94
|
-
return new Promise(function (resolve) {
|
|
95
|
-
var startMs = Date.now();
|
|
96
|
-
var settled = false;
|
|
97
|
-
var sock = net.connect({ host: target.host, port: target.port });
|
|
98
|
-
sock.unref && sock.unref();
|
|
99
|
-
var timer = setTimeout(function () {
|
|
100
|
-
if (settled) return;
|
|
101
|
-
settled = true;
|
|
102
|
-
try { sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
|
|
103
|
-
resolve({ ok: false, latencyMs: Date.now() - startMs, error: "timeout" });
|
|
104
|
-
}, timeoutMs);
|
|
105
|
-
sock.once("connect", function () {
|
|
106
|
-
if (settled) return;
|
|
107
|
-
settled = true;
|
|
108
|
-
clearTimeout(timer);
|
|
109
|
-
try { sock.end(); } catch (_e) { /* best-effort socket close */ }
|
|
110
|
-
resolve({ ok: true, latencyMs: Date.now() - startMs });
|
|
111
|
-
});
|
|
112
|
-
sock.once("error", function (e) {
|
|
113
|
-
if (settled) return;
|
|
114
|
-
settled = true;
|
|
115
|
-
clearTimeout(timer);
|
|
116
|
-
resolve({ ok: false, latencyMs: Date.now() - startMs, error: e.message });
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async function _probeNtp(target, timeoutMs) {
|
|
122
|
-
var startMs = Date.now();
|
|
123
|
-
try {
|
|
124
|
-
var r = await ntpCheck().querySingle(target.server, { timeoutMs: timeoutMs });
|
|
125
|
-
return { ok: true, latencyMs: Date.now() - startMs, driftMs: r.driftMs };
|
|
126
|
-
} catch (e) {
|
|
127
|
-
return { ok: false, latencyMs: Date.now() - startMs, error: e.message || String(e) };
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function _probe(target, timeoutMs) {
|
|
132
|
-
if (target.type === "http") return _probeHttp(target, timeoutMs);
|
|
133
|
-
if (target.type === "tcp") return _probeTcp(target, timeoutMs);
|
|
134
|
-
return _probeNtp(target, timeoutMs);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async function _runProbe(entry) {
|
|
138
|
-
var result = await _probe(entry.target, entry.timeoutMs);
|
|
139
|
-
entry.lastResult = result;
|
|
140
|
-
entry.lastProbeAt = Date.now();
|
|
141
|
-
if (result.ok) {
|
|
142
|
-
entry.consecutiveFailures = 0;
|
|
143
|
-
entry.consecutiveSuccess += 1;
|
|
144
|
-
} else {
|
|
145
|
-
entry.consecutiveFailures += 1;
|
|
146
|
-
entry.consecutiveSuccess = 0;
|
|
147
|
-
}
|
|
148
|
-
var prevState = entry.state;
|
|
149
|
-
if (entry.consecutiveFailures >= entry.threshold) {
|
|
150
|
-
entry.state = "down";
|
|
151
|
-
} else if (entry.consecutiveFailures > 0) {
|
|
152
|
-
entry.state = "degraded";
|
|
153
|
-
} else if (entry.consecutiveSuccess >= entry.threshold) {
|
|
154
|
-
entry.state = "healthy";
|
|
155
|
-
}
|
|
156
|
-
_emitObsProbe(entry, result);
|
|
157
|
-
if (prevState !== entry.state) {
|
|
158
|
-
_emitAuditStateChange(entry, prevState);
|
|
159
|
-
if (typeof entry.onStateChange === "function") {
|
|
160
|
-
try { entry.onStateChange({ name: entry.target.name, prevState: prevState, state: entry.state, result: result }); }
|
|
161
|
-
catch (_e) { /* operator callback best-effort */ }
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
if (typeof entry.onResult === "function") {
|
|
165
|
-
try { entry.onResult({ name: entry.target.name, state: entry.state, result: result }); } catch (_e) { /* operator callback best-effort */ }
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function _scheduleNext(entry) {
|
|
170
|
-
if (entry.stopped) return;
|
|
171
|
-
entry.timer = setTimeout(function () {
|
|
172
|
-
_runProbe(entry).then(function () { _scheduleNext(entry); }, function () { _scheduleNext(entry); });
|
|
173
|
-
}, entry.intervalMs);
|
|
174
|
-
entry.timer.unref && entry.timer.unref();
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function start(opts) {
|
|
178
|
-
opts = opts || {};
|
|
179
|
-
validateOpts(opts, ["targets", "onResult", "onStateChange"], "heartbeat.start");
|
|
180
|
-
if (!Array.isArray(opts.targets) || opts.targets.length === 0) {
|
|
181
|
-
throw new HeartbeatError("heartbeat/no-targets", "heartbeat.start: targets required (non-empty array)");
|
|
182
|
-
}
|
|
183
|
-
for (var i = 0; i < opts.targets.length; i++) _validateTarget(opts.targets[i], i);
|
|
184
|
-
var started = [];
|
|
185
|
-
for (var j = 0; j < opts.targets.length; j++) {
|
|
186
|
-
var t = opts.targets[j];
|
|
187
|
-
if (TARGETS.has(t.name)) {
|
|
188
|
-
throw new HeartbeatError("heartbeat/duplicate", "heartbeat target '" + t.name + "' already started");
|
|
189
|
-
}
|
|
190
|
-
var entry = {
|
|
191
|
-
target: t,
|
|
192
|
-
intervalMs: t.intervalMs || DEFAULT_INTERVAL_MS,
|
|
193
|
-
timeoutMs: t.timeoutMs || DEFAULT_TIMEOUT_MS,
|
|
194
|
-
threshold: t.threshold || DEFAULT_THRESHOLD,
|
|
195
|
-
onResult: opts.onResult,
|
|
196
|
-
onStateChange: opts.onStateChange,
|
|
197
|
-
consecutiveFailures: 0,
|
|
198
|
-
consecutiveSuccess: 0,
|
|
199
|
-
state: "healthy",
|
|
200
|
-
lastResult: null,
|
|
201
|
-
lastProbeAt: null,
|
|
202
|
-
timer: null,
|
|
203
|
-
stopped: false,
|
|
204
|
-
};
|
|
205
|
-
TARGETS.set(t.name, entry);
|
|
206
|
-
_runProbe(entry).then(function (e) { return function () { _scheduleNext(e); }; }(entry),
|
|
207
|
-
function (e) { return function () { _scheduleNext(e); }; }(entry));
|
|
208
|
-
started.push(t.name);
|
|
209
|
-
}
|
|
210
|
-
return started;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function stop(name) {
|
|
214
|
-
var entry = TARGETS.get(name);
|
|
215
|
-
if (!entry) return false;
|
|
216
|
-
entry.stopped = true;
|
|
217
|
-
if (entry.timer) { try { clearTimeout(entry.timer); } catch (_e) { /* best-effort timer teardown */ } }
|
|
218
|
-
TARGETS.delete(name);
|
|
219
|
-
return true;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function stopAll() {
|
|
223
|
-
var names = Array.from(TARGETS.keys());
|
|
224
|
-
for (var i = 0; i < names.length; i++) stop(names[i]);
|
|
225
|
-
return names;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function status(name) {
|
|
229
|
-
var entry = TARGETS.get(name);
|
|
230
|
-
if (!entry) return null;
|
|
231
|
-
return {
|
|
232
|
-
name: name,
|
|
233
|
-
state: entry.state,
|
|
234
|
-
consecutiveFailures: entry.consecutiveFailures,
|
|
235
|
-
consecutiveSuccess: entry.consecutiveSuccess,
|
|
236
|
-
lastResult: entry.lastResult,
|
|
237
|
-
lastProbeAt: entry.lastProbeAt,
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function statuses() {
|
|
242
|
-
var names = Array.from(TARGETS.keys());
|
|
243
|
-
return names.map(function (n) { return status(n); });
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function _emitObsProbe(entry, result) {
|
|
247
|
-
try {
|
|
248
|
-
observability().emit("network.heartbeat.probe", {
|
|
249
|
-
name: entry.target.name,
|
|
250
|
-
type: entry.target.type,
|
|
251
|
-
ok: result.ok,
|
|
252
|
-
latencyMs: result.latencyMs,
|
|
253
|
-
});
|
|
254
|
-
} catch (_e) { /* obs best-effort */ }
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function _emitAuditStateChange(entry, prevState) {
|
|
258
|
-
var sink;
|
|
259
|
-
try { sink = audit(); } catch (_e) { return; }
|
|
260
|
-
if (!sink || typeof sink.safeEmit !== "function") return;
|
|
261
|
-
try {
|
|
262
|
-
sink.safeEmit({
|
|
263
|
-
action: "network.heartbeat.state_change",
|
|
264
|
-
outcome: entry.state === "healthy" ? "success" : "failure",
|
|
265
|
-
metadata: {
|
|
266
|
-
name: entry.target.name,
|
|
267
|
-
type: entry.target.type,
|
|
268
|
-
prevState: prevState,
|
|
269
|
-
state: entry.state,
|
|
270
|
-
consecutiveFailures: entry.consecutiveFailures,
|
|
271
|
-
consecutiveSuccess: entry.consecutiveSuccess,
|
|
272
|
-
lastResult: entry.lastResult,
|
|
273
|
-
},
|
|
274
|
-
});
|
|
275
|
-
} catch (_e) { /* audit best-effort — never break the caller */ }
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function _resetForTest() {
|
|
279
|
-
stopAll();
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
module.exports = {
|
|
283
|
-
start: start,
|
|
284
|
-
stop: stop,
|
|
285
|
-
stopAll: stopAll,
|
|
286
|
-
status: status,
|
|
287
|
-
statuses: statuses,
|
|
288
|
-
HeartbeatError: HeartbeatError,
|
|
289
|
-
_resetForTest: _resetForTest,
|
|
290
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var net = require("node:net");
|
|
4
|
+
|
|
5
|
+
var C = require("./constants");
|
|
6
|
+
var validateOpts = require("./validate-opts");
|
|
7
|
+
var lazyRequire = require("./lazy-require");
|
|
8
|
+
var { defineClass } = require("./framework-error");
|
|
9
|
+
|
|
10
|
+
var HeartbeatError = defineClass("HeartbeatError", { alwaysPermanent: true });
|
|
11
|
+
|
|
12
|
+
var observability = lazyRequire(function () { return require("./observability"); });
|
|
13
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
14
|
+
var httpClient = lazyRequire(function () { return require("./http-client"); });
|
|
15
|
+
var ntpCheck = lazyRequire(function () { return require("./ntp-check"); });
|
|
16
|
+
|
|
17
|
+
var DEFAULT_INTERVAL_MS = C.TIME.seconds(15);
|
|
18
|
+
var DEFAULT_TIMEOUT_MS = C.TIME.seconds(5);
|
|
19
|
+
var DEFAULT_THRESHOLD = 3;
|
|
20
|
+
|
|
21
|
+
var TARGETS = new Map();
|
|
22
|
+
|
|
23
|
+
function _validateTarget(t, idx) {
|
|
24
|
+
if (!t || typeof t !== "object") {
|
|
25
|
+
throw new HeartbeatError("heartbeat/bad-target", "targets[" + idx + "] must be an object");
|
|
26
|
+
}
|
|
27
|
+
if (typeof t.name !== "string" || t.name.length === 0) {
|
|
28
|
+
throw new HeartbeatError("heartbeat/bad-name", "targets[" + idx + "].name required (string)");
|
|
29
|
+
}
|
|
30
|
+
if (t.type !== "http" && t.type !== "tcp" && t.type !== "ntp") {
|
|
31
|
+
throw new HeartbeatError("heartbeat/bad-type",
|
|
32
|
+
"targets[" + idx + "].type must be 'http' | 'tcp' | 'ntp', got " + JSON.stringify(t.type));
|
|
33
|
+
}
|
|
34
|
+
if (t.type === "http" && (typeof t.url !== "string" || t.url.length === 0)) {
|
|
35
|
+
throw new HeartbeatError("heartbeat/bad-url", "targets[" + idx + "].url required for http heartbeat");
|
|
36
|
+
}
|
|
37
|
+
if (t.type === "tcp" && (typeof t.host !== "string" || typeof t.port !== "number")) {
|
|
38
|
+
throw new HeartbeatError("heartbeat/bad-tcp", "targets[" + idx + "] tcp heartbeat requires host (string) + port (number)");
|
|
39
|
+
}
|
|
40
|
+
if (t.type === "ntp" && typeof t.server !== "string") {
|
|
41
|
+
throw new HeartbeatError("heartbeat/bad-ntp", "targets[" + idx + "] ntp heartbeat requires server (string)");
|
|
42
|
+
}
|
|
43
|
+
if (t.intervalMs !== undefined && (typeof t.intervalMs !== "number" || !isFinite(t.intervalMs) || t.intervalMs <= 0)) {
|
|
44
|
+
throw new HeartbeatError("heartbeat/bad-interval", "targets[" + idx + "].intervalMs must be a positive number");
|
|
45
|
+
}
|
|
46
|
+
if (t.timeoutMs !== undefined && (typeof t.timeoutMs !== "number" || !isFinite(t.timeoutMs) || t.timeoutMs <= 0)) {
|
|
47
|
+
throw new HeartbeatError("heartbeat/bad-timeout", "targets[" + idx + "].timeoutMs must be a positive number");
|
|
48
|
+
}
|
|
49
|
+
if (t.threshold !== undefined && (typeof t.threshold !== "number" || t.threshold < 1 || Math.floor(t.threshold) !== t.threshold)) {
|
|
50
|
+
throw new HeartbeatError("heartbeat/bad-threshold", "targets[" + idx + "].threshold must be a positive integer");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function _probeHttp(target, timeoutMs) {
|
|
55
|
+
return new Promise(function (resolve) {
|
|
56
|
+
var startMs = Date.now();
|
|
57
|
+
var settled = false;
|
|
58
|
+
var timer = setTimeout(function () {
|
|
59
|
+
if (settled) return;
|
|
60
|
+
settled = true;
|
|
61
|
+
resolve({ ok: false, latencyMs: Date.now() - startMs, error: "timeout" });
|
|
62
|
+
}, timeoutMs);
|
|
63
|
+
try {
|
|
64
|
+
var p = httpClient().request({
|
|
65
|
+
url: target.url,
|
|
66
|
+
method: target.method || "GET",
|
|
67
|
+
timeoutMs: timeoutMs,
|
|
68
|
+
allowInternal: target.allowInternal === true ? true : target.allowInternal,
|
|
69
|
+
});
|
|
70
|
+
p.then(function (res) {
|
|
71
|
+
if (settled) return;
|
|
72
|
+
settled = true;
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
var ok = !target.expectStatus
|
|
75
|
+
? (res.statusCode >= 200 && res.statusCode < 400)
|
|
76
|
+
: res.statusCode === target.expectStatus;
|
|
77
|
+
resolve({ ok: ok, latencyMs: Date.now() - startMs, statusCode: res.statusCode });
|
|
78
|
+
}, function (err) {
|
|
79
|
+
if (settled) return;
|
|
80
|
+
settled = true;
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
resolve({ ok: false, latencyMs: Date.now() - startMs, error: (err && err.message) || String(err) });
|
|
83
|
+
});
|
|
84
|
+
} catch (e) {
|
|
85
|
+
if (settled) return;
|
|
86
|
+
settled = true;
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
resolve({ ok: false, latencyMs: Date.now() - startMs, error: e.message });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function _probeTcp(target, timeoutMs) {
|
|
94
|
+
return new Promise(function (resolve) {
|
|
95
|
+
var startMs = Date.now();
|
|
96
|
+
var settled = false;
|
|
97
|
+
var sock = net.connect({ host: target.host, port: target.port });
|
|
98
|
+
sock.unref && sock.unref();
|
|
99
|
+
var timer = setTimeout(function () {
|
|
100
|
+
if (settled) return;
|
|
101
|
+
settled = true;
|
|
102
|
+
try { sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
|
|
103
|
+
resolve({ ok: false, latencyMs: Date.now() - startMs, error: "timeout" });
|
|
104
|
+
}, timeoutMs);
|
|
105
|
+
sock.once("connect", function () {
|
|
106
|
+
if (settled) return;
|
|
107
|
+
settled = true;
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
try { sock.end(); } catch (_e) { /* best-effort socket close */ }
|
|
110
|
+
resolve({ ok: true, latencyMs: Date.now() - startMs });
|
|
111
|
+
});
|
|
112
|
+
sock.once("error", function (e) {
|
|
113
|
+
if (settled) return;
|
|
114
|
+
settled = true;
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
resolve({ ok: false, latencyMs: Date.now() - startMs, error: e.message });
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function _probeNtp(target, timeoutMs) {
|
|
122
|
+
var startMs = Date.now();
|
|
123
|
+
try {
|
|
124
|
+
var r = await ntpCheck().querySingle(target.server, { timeoutMs: timeoutMs });
|
|
125
|
+
return { ok: true, latencyMs: Date.now() - startMs, driftMs: r.driftMs };
|
|
126
|
+
} catch (e) {
|
|
127
|
+
return { ok: false, latencyMs: Date.now() - startMs, error: e.message || String(e) };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function _probe(target, timeoutMs) {
|
|
132
|
+
if (target.type === "http") return _probeHttp(target, timeoutMs);
|
|
133
|
+
if (target.type === "tcp") return _probeTcp(target, timeoutMs);
|
|
134
|
+
return _probeNtp(target, timeoutMs);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function _runProbe(entry) {
|
|
138
|
+
var result = await _probe(entry.target, entry.timeoutMs);
|
|
139
|
+
entry.lastResult = result;
|
|
140
|
+
entry.lastProbeAt = Date.now();
|
|
141
|
+
if (result.ok) {
|
|
142
|
+
entry.consecutiveFailures = 0;
|
|
143
|
+
entry.consecutiveSuccess += 1;
|
|
144
|
+
} else {
|
|
145
|
+
entry.consecutiveFailures += 1;
|
|
146
|
+
entry.consecutiveSuccess = 0;
|
|
147
|
+
}
|
|
148
|
+
var prevState = entry.state;
|
|
149
|
+
if (entry.consecutiveFailures >= entry.threshold) {
|
|
150
|
+
entry.state = "down";
|
|
151
|
+
} else if (entry.consecutiveFailures > 0) {
|
|
152
|
+
entry.state = "degraded";
|
|
153
|
+
} else if (entry.consecutiveSuccess >= entry.threshold) {
|
|
154
|
+
entry.state = "healthy";
|
|
155
|
+
}
|
|
156
|
+
_emitObsProbe(entry, result);
|
|
157
|
+
if (prevState !== entry.state) {
|
|
158
|
+
_emitAuditStateChange(entry, prevState);
|
|
159
|
+
if (typeof entry.onStateChange === "function") {
|
|
160
|
+
try { entry.onStateChange({ name: entry.target.name, prevState: prevState, state: entry.state, result: result }); }
|
|
161
|
+
catch (_e) { /* operator callback best-effort */ }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (typeof entry.onResult === "function") {
|
|
165
|
+
try { entry.onResult({ name: entry.target.name, state: entry.state, result: result }); } catch (_e) { /* operator callback best-effort */ }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function _scheduleNext(entry) {
|
|
170
|
+
if (entry.stopped) return;
|
|
171
|
+
entry.timer = setTimeout(function () {
|
|
172
|
+
_runProbe(entry).then(function () { _scheduleNext(entry); }, function () { _scheduleNext(entry); });
|
|
173
|
+
}, entry.intervalMs);
|
|
174
|
+
entry.timer.unref && entry.timer.unref();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function start(opts) {
|
|
178
|
+
opts = opts || {};
|
|
179
|
+
validateOpts(opts, ["targets", "onResult", "onStateChange"], "heartbeat.start");
|
|
180
|
+
if (!Array.isArray(opts.targets) || opts.targets.length === 0) {
|
|
181
|
+
throw new HeartbeatError("heartbeat/no-targets", "heartbeat.start: targets required (non-empty array)");
|
|
182
|
+
}
|
|
183
|
+
for (var i = 0; i < opts.targets.length; i++) _validateTarget(opts.targets[i], i);
|
|
184
|
+
var started = [];
|
|
185
|
+
for (var j = 0; j < opts.targets.length; j++) {
|
|
186
|
+
var t = opts.targets[j];
|
|
187
|
+
if (TARGETS.has(t.name)) {
|
|
188
|
+
throw new HeartbeatError("heartbeat/duplicate", "heartbeat target '" + t.name + "' already started");
|
|
189
|
+
}
|
|
190
|
+
var entry = {
|
|
191
|
+
target: t,
|
|
192
|
+
intervalMs: t.intervalMs || DEFAULT_INTERVAL_MS,
|
|
193
|
+
timeoutMs: t.timeoutMs || DEFAULT_TIMEOUT_MS,
|
|
194
|
+
threshold: t.threshold || DEFAULT_THRESHOLD,
|
|
195
|
+
onResult: opts.onResult,
|
|
196
|
+
onStateChange: opts.onStateChange,
|
|
197
|
+
consecutiveFailures: 0,
|
|
198
|
+
consecutiveSuccess: 0,
|
|
199
|
+
state: "healthy",
|
|
200
|
+
lastResult: null,
|
|
201
|
+
lastProbeAt: null,
|
|
202
|
+
timer: null,
|
|
203
|
+
stopped: false,
|
|
204
|
+
};
|
|
205
|
+
TARGETS.set(t.name, entry);
|
|
206
|
+
_runProbe(entry).then(function (e) { return function () { _scheduleNext(e); }; }(entry),
|
|
207
|
+
function (e) { return function () { _scheduleNext(e); }; }(entry));
|
|
208
|
+
started.push(t.name);
|
|
209
|
+
}
|
|
210
|
+
return started;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function stop(name) {
|
|
214
|
+
var entry = TARGETS.get(name);
|
|
215
|
+
if (!entry) return false;
|
|
216
|
+
entry.stopped = true;
|
|
217
|
+
if (entry.timer) { try { clearTimeout(entry.timer); } catch (_e) { /* best-effort timer teardown */ } }
|
|
218
|
+
TARGETS.delete(name);
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function stopAll() {
|
|
223
|
+
var names = Array.from(TARGETS.keys());
|
|
224
|
+
for (var i = 0; i < names.length; i++) stop(names[i]);
|
|
225
|
+
return names;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function status(name) {
|
|
229
|
+
var entry = TARGETS.get(name);
|
|
230
|
+
if (!entry) return null;
|
|
231
|
+
return {
|
|
232
|
+
name: name,
|
|
233
|
+
state: entry.state,
|
|
234
|
+
consecutiveFailures: entry.consecutiveFailures,
|
|
235
|
+
consecutiveSuccess: entry.consecutiveSuccess,
|
|
236
|
+
lastResult: entry.lastResult,
|
|
237
|
+
lastProbeAt: entry.lastProbeAt,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function statuses() {
|
|
242
|
+
var names = Array.from(TARGETS.keys());
|
|
243
|
+
return names.map(function (n) { return status(n); });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function _emitObsProbe(entry, result) {
|
|
247
|
+
try {
|
|
248
|
+
observability().emit("network.heartbeat.probe", {
|
|
249
|
+
name: entry.target.name,
|
|
250
|
+
type: entry.target.type,
|
|
251
|
+
ok: result.ok,
|
|
252
|
+
latencyMs: result.latencyMs,
|
|
253
|
+
});
|
|
254
|
+
} catch (_e) { /* obs best-effort */ }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function _emitAuditStateChange(entry, prevState) {
|
|
258
|
+
var sink;
|
|
259
|
+
try { sink = audit(); } catch (_e) { return; }
|
|
260
|
+
if (!sink || typeof sink.safeEmit !== "function") return;
|
|
261
|
+
try {
|
|
262
|
+
sink.safeEmit({
|
|
263
|
+
action: "network.heartbeat.state_change",
|
|
264
|
+
outcome: entry.state === "healthy" ? "success" : "failure",
|
|
265
|
+
metadata: {
|
|
266
|
+
name: entry.target.name,
|
|
267
|
+
type: entry.target.type,
|
|
268
|
+
prevState: prevState,
|
|
269
|
+
state: entry.state,
|
|
270
|
+
consecutiveFailures: entry.consecutiveFailures,
|
|
271
|
+
consecutiveSuccess: entry.consecutiveSuccess,
|
|
272
|
+
lastResult: entry.lastResult,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
} catch (_e) { /* audit best-effort — never break the caller */ }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function _resetForTest() {
|
|
279
|
+
stopAll();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
module.exports = {
|
|
283
|
+
start: start,
|
|
284
|
+
stop: stop,
|
|
285
|
+
stopAll: stopAll,
|
|
286
|
+
status: status,
|
|
287
|
+
statuses: statuses,
|
|
288
|
+
HeartbeatError: HeartbeatError,
|
|
289
|
+
_resetForTest: _resetForTest,
|
|
290
|
+
};
|