@hasna/uptime 0.1.0 → 0.1.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 +27 -0
- package/README.md +25 -4
- package/dist/api.d.ts +9 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +422 -12
- package/dist/cli/index.js +499 -22
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +424 -12
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +424 -8
- package/dist/report.d.ts +49 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +274 -0
- package/dist/service.d.ts +7 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +392 -9
- package/dist/store.d.ts +9 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +93 -7
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -2
package/dist/mcp/index.js
CHANGED
|
@@ -14362,6 +14362,9 @@ async function runTcpCheck(monitor) {
|
|
|
14362
14362
|
});
|
|
14363
14363
|
}
|
|
14364
14364
|
|
|
14365
|
+
// src/service.ts
|
|
14366
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
14367
|
+
|
|
14365
14368
|
// src/store.ts
|
|
14366
14369
|
import { mkdirSync } from "fs";
|
|
14367
14370
|
import { dirname } from "path";
|
|
@@ -14388,6 +14391,13 @@ var MAX_RETRY_COUNT = 10;
|
|
|
14388
14391
|
var MAX_RESULT_LIMIT = 1000;
|
|
14389
14392
|
|
|
14390
14393
|
// src/store.ts
|
|
14394
|
+
class StaleCheckResultError extends Error {
|
|
14395
|
+
constructor(message) {
|
|
14396
|
+
super(message);
|
|
14397
|
+
this.name = "StaleCheckResultError";
|
|
14398
|
+
}
|
|
14399
|
+
}
|
|
14400
|
+
|
|
14391
14401
|
class UptimeStore {
|
|
14392
14402
|
dbPath;
|
|
14393
14403
|
db;
|
|
@@ -14421,10 +14431,12 @@ class UptimeStore {
|
|
|
14421
14431
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
14422
14432
|
status TEXT NOT NULL DEFAULT 'unknown',
|
|
14423
14433
|
last_checked_at TEXT,
|
|
14434
|
+
revision INTEGER NOT NULL DEFAULT 1,
|
|
14424
14435
|
created_at TEXT NOT NULL,
|
|
14425
14436
|
updated_at TEXT NOT NULL
|
|
14426
14437
|
)
|
|
14427
14438
|
`);
|
|
14439
|
+
this.ensureColumn("monitors", "revision", "INTEGER NOT NULL DEFAULT 1");
|
|
14428
14440
|
this.db.run(`
|
|
14429
14441
|
CREATE TABLE IF NOT EXISTS check_results (
|
|
14430
14442
|
id TEXT PRIMARY KEY,
|
|
@@ -14450,8 +14462,17 @@ class UptimeStore {
|
|
|
14450
14462
|
reason TEXT
|
|
14451
14463
|
)
|
|
14452
14464
|
`);
|
|
14465
|
+
this.db.run(`
|
|
14466
|
+
CREATE TABLE IF NOT EXISTS check_leases (
|
|
14467
|
+
monitor_id TEXT PRIMARY KEY REFERENCES monitors(id) ON DELETE CASCADE,
|
|
14468
|
+
owner TEXT NOT NULL,
|
|
14469
|
+
leased_until TEXT NOT NULL,
|
|
14470
|
+
acquired_at TEXT NOT NULL
|
|
14471
|
+
)
|
|
14472
|
+
`);
|
|
14453
14473
|
this.db.run("CREATE INDEX IF NOT EXISTS idx_results_monitor_time ON check_results(monitor_id, checked_at DESC)");
|
|
14454
14474
|
this.db.run("CREATE INDEX IF NOT EXISTS idx_incidents_monitor_status ON incidents(monitor_id, status)");
|
|
14475
|
+
this.db.run("CREATE INDEX IF NOT EXISTS idx_check_leases_until ON check_leases(leased_until)");
|
|
14455
14476
|
}
|
|
14456
14477
|
createMonitor(input) {
|
|
14457
14478
|
const normalized = normalizeCreateMonitor(input);
|
|
@@ -14471,14 +14492,15 @@ class UptimeStore {
|
|
|
14471
14492
|
enabled: normalized.enabled ?? true,
|
|
14472
14493
|
status: normalized.enabled === false ? "paused" : "unknown",
|
|
14473
14494
|
lastCheckedAt: null,
|
|
14495
|
+
revision: 1,
|
|
14474
14496
|
createdAt: now,
|
|
14475
14497
|
updatedAt: now
|
|
14476
14498
|
};
|
|
14477
14499
|
this.db.query(`INSERT INTO monitors (
|
|
14478
14500
|
id, name, kind, url, host, port, method, expected_status,
|
|
14479
14501
|
interval_seconds, timeout_ms, retry_count, enabled, status,
|
|
14480
|
-
last_checked_at, created_at, updated_at
|
|
14481
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(monitor.id, monitor.name, monitor.kind, monitor.url, monitor.host, monitor.port, monitor.method, monitor.expectedStatus, monitor.intervalSeconds, monitor.timeoutMs, monitor.retryCount, monitor.enabled ? 1 : 0, monitor.status, monitor.lastCheckedAt, monitor.createdAt, monitor.updatedAt);
|
|
14502
|
+
last_checked_at, revision, created_at, updated_at
|
|
14503
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(monitor.id, monitor.name, monitor.kind, monitor.url, monitor.host, monitor.port, monitor.method, monitor.expectedStatus, monitor.intervalSeconds, monitor.timeoutMs, monitor.retryCount, monitor.enabled ? 1 : 0, monitor.status, monitor.lastCheckedAt, monitor.revision, monitor.createdAt, monitor.updatedAt);
|
|
14482
14504
|
return monitor;
|
|
14483
14505
|
}
|
|
14484
14506
|
listMonitors(options = {}) {
|
|
@@ -14498,8 +14520,12 @@ class UptimeStore {
|
|
|
14498
14520
|
this.db.query(`UPDATE monitors SET
|
|
14499
14521
|
name = ?, kind = ?, url = ?, host = ?, port = ?, method = ?,
|
|
14500
14522
|
expected_status = ?, interval_seconds = ?, timeout_ms = ?,
|
|
14501
|
-
retry_count = ?, enabled = ?, status = ?, last_checked_at = ?,
|
|
14523
|
+
retry_count = ?, enabled = ?, status = ?, last_checked_at = ?,
|
|
14524
|
+
revision = revision + 1, updated_at = ?
|
|
14502
14525
|
WHERE id = ?`).run(next.name, next.kind, next.url, next.host, next.port, next.method, next.expectedStatus, next.intervalSeconds, next.timeoutMs, next.retryCount, next.enabled ? 1 : 0, next.status, next.lastCheckedAt, updatedAt, current.id);
|
|
14526
|
+
if (definitionChanged(current, next)) {
|
|
14527
|
+
this.closeOpenIncident(current.id, updatedAt);
|
|
14528
|
+
}
|
|
14503
14529
|
return this.getMonitor(current.id);
|
|
14504
14530
|
}
|
|
14505
14531
|
deleteMonitor(idOrName) {
|
|
@@ -14509,10 +14535,31 @@ class UptimeStore {
|
|
|
14509
14535
|
this.db.query("DELETE FROM monitors WHERE id = ?").run(current.id);
|
|
14510
14536
|
return true;
|
|
14511
14537
|
}
|
|
14538
|
+
acquireCheckLease(monitorId, owner, ttlMs) {
|
|
14539
|
+
const now = new Date;
|
|
14540
|
+
const nowIso = now.toISOString();
|
|
14541
|
+
const leasedUntil = new Date(now.getTime() + Math.max(1000, ttlMs)).toISOString();
|
|
14542
|
+
const tx = this.db.transaction(() => {
|
|
14543
|
+
this.db.query("DELETE FROM check_leases WHERE monitor_id = ? AND leased_until <= ?").run(monitorId, nowIso);
|
|
14544
|
+
this.db.query("INSERT OR IGNORE INTO check_leases (monitor_id, owner, leased_until, acquired_at) VALUES (?, ?, ?, ?)").run(monitorId, owner, leasedUntil, nowIso);
|
|
14545
|
+
const row = this.db.query("SELECT * FROM check_leases WHERE monitor_id = ?").get(monitorId);
|
|
14546
|
+
return row?.owner === owner;
|
|
14547
|
+
});
|
|
14548
|
+
return tx();
|
|
14549
|
+
}
|
|
14550
|
+
releaseCheckLease(monitorId, owner) {
|
|
14551
|
+
this.db.query("DELETE FROM check_leases WHERE monitor_id = ? AND owner = ?").run(monitorId, owner);
|
|
14552
|
+
}
|
|
14512
14553
|
recordCheckResult(input) {
|
|
14513
14554
|
const monitor = this.getMonitor(input.monitorId);
|
|
14514
14555
|
if (!monitor)
|
|
14515
14556
|
throw new Error(`Monitor not found: ${input.monitorId}`);
|
|
14557
|
+
if (input.expectedMonitorRevision !== undefined && monitor.revision !== input.expectedMonitorRevision) {
|
|
14558
|
+
throw new StaleCheckResultError(`Monitor changed while check was in progress: ${monitor.name}`);
|
|
14559
|
+
}
|
|
14560
|
+
if (!monitor.enabled) {
|
|
14561
|
+
throw new StaleCheckResultError(`Monitor was disabled while check was in progress: ${monitor.name}`);
|
|
14562
|
+
}
|
|
14516
14563
|
const checkedAt = input.checkedAt ?? new Date().toISOString();
|
|
14517
14564
|
const result = {
|
|
14518
14565
|
id: newId("chk"),
|
|
@@ -14525,6 +14572,15 @@ class UptimeStore {
|
|
|
14525
14572
|
attemptCount: Math.max(1, input.attemptCount)
|
|
14526
14573
|
};
|
|
14527
14574
|
const tx = this.db.transaction(() => {
|
|
14575
|
+
const current = this.db.query("SELECT * FROM monitors WHERE id = ?").get(result.monitorId);
|
|
14576
|
+
if (!current)
|
|
14577
|
+
throw new Error(`Monitor not found: ${result.monitorId}`);
|
|
14578
|
+
if (input.expectedMonitorRevision !== undefined && current.revision !== input.expectedMonitorRevision) {
|
|
14579
|
+
throw new StaleCheckResultError(`Monitor changed while check was in progress: ${current.name}`);
|
|
14580
|
+
}
|
|
14581
|
+
if (!current.enabled) {
|
|
14582
|
+
throw new StaleCheckResultError(`Monitor was disabled while check was in progress: ${current.name}`);
|
|
14583
|
+
}
|
|
14528
14584
|
this.db.query(`INSERT INTO check_results (
|
|
14529
14585
|
id, monitor_id, checked_at, status, latency_ms, status_code, error, attempt_count
|
|
14530
14586
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(result.id, result.monitorId, result.checkedAt, result.status, result.latencyMs, result.statusCode, result.error, result.attemptCount);
|
|
@@ -14572,10 +14628,14 @@ class UptimeStore {
|
|
|
14572
14628
|
down: monitors.filter((m) => m.status === "down").length,
|
|
14573
14629
|
paused: monitors.filter((m) => !m.enabled || m.status === "paused").length,
|
|
14574
14630
|
unknown: monitors.filter((m) => m.status === "unknown").length,
|
|
14575
|
-
openIncidents: this.
|
|
14631
|
+
openIncidents: this.countOpenIncidents()
|
|
14576
14632
|
}
|
|
14577
14633
|
};
|
|
14578
14634
|
}
|
|
14635
|
+
countOpenIncidents() {
|
|
14636
|
+
const row = this.db.query("SELECT COUNT(*) AS count FROM incidents WHERE status = 'open'").get();
|
|
14637
|
+
return Number(row?.count ?? 0);
|
|
14638
|
+
}
|
|
14579
14639
|
monitorSummary(monitor) {
|
|
14580
14640
|
const row = this.db.query(`SELECT
|
|
14581
14641
|
COUNT(*) as total,
|
|
@@ -14613,13 +14673,24 @@ class UptimeStore {
|
|
|
14613
14673
|
this.db.query("UPDATE incidents SET status = 'closed', closed_at = ?, recovery_check_id = ? WHERE id = ?").run(result.checkedAt, result.id, open.id);
|
|
14614
14674
|
}
|
|
14615
14675
|
}
|
|
14676
|
+
closeOpenIncident(monitorId, closedAt) {
|
|
14677
|
+
this.db.query("UPDATE incidents SET status = 'closed', closed_at = ? WHERE monitor_id = ? AND status = 'open'").run(closedAt, monitorId);
|
|
14678
|
+
}
|
|
14679
|
+
ensureColumn(table, name, definition) {
|
|
14680
|
+
const columns = this.db.query(`PRAGMA table_info(${table})`).all();
|
|
14681
|
+
if (!columns.some((column) => column.name === name)) {
|
|
14682
|
+
this.db.run(`ALTER TABLE ${table} ADD COLUMN ${name} ${definition}`);
|
|
14683
|
+
}
|
|
14684
|
+
}
|
|
14616
14685
|
}
|
|
14617
14686
|
function normalizeCreateMonitor(input) {
|
|
14618
14687
|
const name = input.name?.trim();
|
|
14619
14688
|
if (!name)
|
|
14620
14689
|
throw new Error("Monitor name is required");
|
|
14690
|
+
rejectControlCharacters(name, "Monitor name");
|
|
14621
14691
|
const method = normalizeMethod(input.method ?? "GET");
|
|
14622
14692
|
const expectedStatus = normalizeExpectedStatus(input.expectedStatus);
|
|
14693
|
+
const enabled = normalizeEnabled(input.enabled);
|
|
14623
14694
|
if (input.kind === "http") {
|
|
14624
14695
|
const url2 = normalizeHttpUrl(input.url);
|
|
14625
14696
|
return {
|
|
@@ -14631,12 +14702,13 @@ function normalizeCreateMonitor(input) {
|
|
|
14631
14702
|
intervalSeconds: boundedInteger(input.intervalSeconds ?? 60, "intervalSeconds", MIN_INTERVAL_SECONDS, MAX_INTERVAL_SECONDS),
|
|
14632
14703
|
timeoutMs: boundedInteger(input.timeoutMs ?? 5000, "timeoutMs", MIN_TIMEOUT_MS, MAX_TIMEOUT_MS),
|
|
14633
14704
|
retryCount: boundedInteger(input.retryCount ?? 0, "retryCount", MIN_RETRY_COUNT, MAX_RETRY_COUNT),
|
|
14634
|
-
enabled
|
|
14705
|
+
enabled
|
|
14635
14706
|
};
|
|
14636
14707
|
} else if (input.kind === "tcp") {
|
|
14637
14708
|
const host = input.host?.trim();
|
|
14638
14709
|
if (!host)
|
|
14639
14710
|
throw new Error("TCP monitors require host");
|
|
14711
|
+
rejectControlCharacters(host, "TCP host");
|
|
14640
14712
|
if (!Number.isInteger(input.port) || input.port <= 0 || input.port > 65535) {
|
|
14641
14713
|
throw new Error("TCP monitors require a port from 1 to 65535");
|
|
14642
14714
|
}
|
|
@@ -14650,12 +14722,15 @@ function normalizeCreateMonitor(input) {
|
|
|
14650
14722
|
intervalSeconds: boundedInteger(input.intervalSeconds ?? 60, "intervalSeconds", MIN_INTERVAL_SECONDS, MAX_INTERVAL_SECONDS),
|
|
14651
14723
|
timeoutMs: boundedInteger(input.timeoutMs ?? 5000, "timeoutMs", MIN_TIMEOUT_MS, MAX_TIMEOUT_MS),
|
|
14652
14724
|
retryCount: boundedInteger(input.retryCount ?? 0, "retryCount", MIN_RETRY_COUNT, MAX_RETRY_COUNT),
|
|
14653
|
-
enabled
|
|
14725
|
+
enabled
|
|
14654
14726
|
};
|
|
14655
14727
|
} else {
|
|
14656
14728
|
throw new Error("Monitor kind must be http or tcp");
|
|
14657
14729
|
}
|
|
14658
14730
|
}
|
|
14731
|
+
function definitionChanged(current, next) {
|
|
14732
|
+
return next.kind !== current.kind || next.url !== current.url || next.host !== current.host || next.port !== current.port || next.method !== current.method || next.expectedStatus !== current.expectedStatus;
|
|
14733
|
+
}
|
|
14659
14734
|
function normalizeUpdateMonitor(current, input, updatedAt) {
|
|
14660
14735
|
const merged = {
|
|
14661
14736
|
...current,
|
|
@@ -14720,6 +14795,18 @@ function normalizeExpectedStatus(value) {
|
|
|
14720
14795
|
}
|
|
14721
14796
|
return value;
|
|
14722
14797
|
}
|
|
14798
|
+
function normalizeEnabled(value) {
|
|
14799
|
+
if (value === undefined)
|
|
14800
|
+
return true;
|
|
14801
|
+
if (typeof value !== "boolean")
|
|
14802
|
+
throw new Error("enabled must be a boolean");
|
|
14803
|
+
return value;
|
|
14804
|
+
}
|
|
14805
|
+
function rejectControlCharacters(value, label) {
|
|
14806
|
+
if (/[\x00-\x1f\x7f-\x9f]/.test(value)) {
|
|
14807
|
+
throw new Error(`${label} must not contain control characters`);
|
|
14808
|
+
}
|
|
14809
|
+
}
|
|
14723
14810
|
function monitorFromRow(row) {
|
|
14724
14811
|
return {
|
|
14725
14812
|
id: row.id,
|
|
@@ -14736,6 +14823,7 @@ function monitorFromRow(row) {
|
|
|
14736
14823
|
enabled: Boolean(row.enabled),
|
|
14737
14824
|
status: row.status,
|
|
14738
14825
|
lastCheckedAt: row.last_checked_at,
|
|
14826
|
+
revision: row.revision ?? 1,
|
|
14739
14827
|
createdAt: row.created_at,
|
|
14740
14828
|
updatedAt: row.updated_at
|
|
14741
14829
|
};
|
|
@@ -14784,10 +14872,281 @@ function round(value, places) {
|
|
|
14784
14872
|
return Math.round(value * factor) / factor;
|
|
14785
14873
|
}
|
|
14786
14874
|
|
|
14875
|
+
// src/report.ts
|
|
14876
|
+
var DEFAULT_MAILERY_API_URL = "http://localhost:3900";
|
|
14877
|
+
var DEFAULT_TELEPHONY_API_URL = "http://localhost:19451";
|
|
14878
|
+
var DEFAULT_LOGS_API_URL = "http://localhost:3460";
|
|
14879
|
+
var DEFAULT_TIMEOUT_MS = 15000;
|
|
14880
|
+
function buildUptimeReport(summary, options = {}) {
|
|
14881
|
+
const subject = options.subject ?? defaultSubject(summary);
|
|
14882
|
+
const lines = [
|
|
14883
|
+
subject,
|
|
14884
|
+
`Generated: ${summary.generatedAt}`,
|
|
14885
|
+
`Monitors: ${summary.totals.monitors} total, ${summary.totals.enabled} enabled, ${summary.totals.up} up, ${summary.totals.down} down, ${summary.totals.openIncidents} open incidents`,
|
|
14886
|
+
"",
|
|
14887
|
+
...summary.monitors.map(renderMonitorLine)
|
|
14888
|
+
];
|
|
14889
|
+
const text = lines.join(`
|
|
14890
|
+
`).trimEnd();
|
|
14891
|
+
const json2 = {
|
|
14892
|
+
kind: "open-uptime.report",
|
|
14893
|
+
generated_at: summary.generatedAt,
|
|
14894
|
+
subject,
|
|
14895
|
+
totals: summary.totals,
|
|
14896
|
+
monitors: summary.monitors
|
|
14897
|
+
};
|
|
14898
|
+
return {
|
|
14899
|
+
subject,
|
|
14900
|
+
generatedAt: summary.generatedAt,
|
|
14901
|
+
summary,
|
|
14902
|
+
text,
|
|
14903
|
+
html: `<pre>${escapeHtml(text)}</pre>`,
|
|
14904
|
+
json: json2
|
|
14905
|
+
};
|
|
14906
|
+
}
|
|
14907
|
+
async function sendUptimeReport(summary, options = {}) {
|
|
14908
|
+
const report = buildUptimeReport(summary, options);
|
|
14909
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
14910
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
14911
|
+
const deliveries = [];
|
|
14912
|
+
if (options.email) {
|
|
14913
|
+
deliveries.push(await sendEmailReport(report, resolveEmailTarget(options.email), fetchImpl, timeoutMs));
|
|
14914
|
+
}
|
|
14915
|
+
if (options.sms) {
|
|
14916
|
+
const smsTarget = resolveSmsTarget(options.sms);
|
|
14917
|
+
const recipients = splitTargets(smsTarget.to);
|
|
14918
|
+
if (recipients.length === 0) {
|
|
14919
|
+
deliveries.push(await sendSmsReport(report, smsTarget, fetchImpl, timeoutMs));
|
|
14920
|
+
} else {
|
|
14921
|
+
for (const target of recipients) {
|
|
14922
|
+
deliveries.push(await sendSmsReport(report, { ...smsTarget, to: target }, fetchImpl, timeoutMs));
|
|
14923
|
+
}
|
|
14924
|
+
}
|
|
14925
|
+
}
|
|
14926
|
+
if (options.logs) {
|
|
14927
|
+
deliveries.push(await sendLogsReport(report, resolveLogsTarget(options.logs), fetchImpl, timeoutMs));
|
|
14928
|
+
}
|
|
14929
|
+
return deliveries;
|
|
14930
|
+
}
|
|
14931
|
+
function defaultSubject(summary) {
|
|
14932
|
+
if (summary.totals.openIncidents > 0 || summary.totals.down > 0) {
|
|
14933
|
+
return `Open Uptime alert: ${summary.totals.down} down, ${summary.totals.openIncidents} open incidents`;
|
|
14934
|
+
}
|
|
14935
|
+
return `Open Uptime report: ${summary.totals.up}/${summary.totals.enabled} enabled monitors up`;
|
|
14936
|
+
}
|
|
14937
|
+
function renderMonitorLine(item) {
|
|
14938
|
+
const uptime = item.uptimePercent == null ? "-" : `${item.uptimePercent.toFixed(2)}%`;
|
|
14939
|
+
const latency = item.averageLatencyMs == null ? "-" : `${item.averageLatencyMs}ms`;
|
|
14940
|
+
const incident = item.openIncident ? ` open incident: ${item.openIncident.reason ?? "down"}` : "";
|
|
14941
|
+
return `- ${item.monitor.status.toUpperCase()} ${item.monitor.name} (${targetLabel(item)}): uptime ${uptime}, latency ${latency}${incident}`;
|
|
14942
|
+
}
|
|
14943
|
+
function targetLabel(item) {
|
|
14944
|
+
return item.monitor.kind === "http" ? item.monitor.url ?? "" : `${item.monitor.host}:${item.monitor.port}`;
|
|
14945
|
+
}
|
|
14946
|
+
function resolveEmailTarget(value) {
|
|
14947
|
+
const target = typeof value === "boolean" ? {} : value;
|
|
14948
|
+
return {
|
|
14949
|
+
apiUrl: target.apiUrl ?? env("HASNA_MAILERY_API_URL", "MAILERY_API_URL") ?? DEFAULT_MAILERY_API_URL,
|
|
14950
|
+
sendKey: target.sendKey ?? env("HASNA_MAILERY_SEND_KEY", "MAILERY_SEND_KEY", "ESK"),
|
|
14951
|
+
from: target.from ?? env("HASNA_UPTIME_REPORT_EMAIL_FROM", "UPTIME_REPORT_EMAIL_FROM"),
|
|
14952
|
+
to: target.to ?? env("HASNA_UPTIME_REPORT_EMAIL_TO", "UPTIME_REPORT_EMAIL_TO"),
|
|
14953
|
+
subject: target.subject,
|
|
14954
|
+
providerId: target.providerId ?? env("HASNA_MAILERY_PROVIDER_ID", "MAILERY_PROVIDER_ID")
|
|
14955
|
+
};
|
|
14956
|
+
}
|
|
14957
|
+
function resolveSmsTarget(value) {
|
|
14958
|
+
const target = typeof value === "boolean" ? {} : value;
|
|
14959
|
+
return {
|
|
14960
|
+
apiUrl: target.apiUrl ?? env("HASNA_TELEPHONY_API_URL", "TELEPHONY_API_URL") ?? DEFAULT_TELEPHONY_API_URL,
|
|
14961
|
+
from: target.from ?? env("HASNA_UPTIME_REPORT_SMS_FROM", "UPTIME_REPORT_SMS_FROM"),
|
|
14962
|
+
to: target.to ?? env("HASNA_UPTIME_REPORT_PHONE_TO", "UPTIME_REPORT_PHONE_TO")
|
|
14963
|
+
};
|
|
14964
|
+
}
|
|
14965
|
+
function resolveLogsTarget(value) {
|
|
14966
|
+
const target = typeof value === "boolean" ? {} : value;
|
|
14967
|
+
return {
|
|
14968
|
+
apiUrl: target.apiUrl ?? env("HASNA_LOGS_URL", "LOGS_URL") ?? DEFAULT_LOGS_API_URL,
|
|
14969
|
+
apiKey: target.apiKey ?? env("HASNA_LOGS_API_TOKEN", "LOGS_API_TOKEN", "HASNA_LOGS_API_KEY", "LOGS_API_KEY"),
|
|
14970
|
+
projectId: target.projectId ?? env("HASNA_LOGS_PROJECT_ID", "LOGS_PROJECT_ID") ?? "open-uptime",
|
|
14971
|
+
environment: target.environment ?? env("HASNA_ENV", "NODE_ENV"),
|
|
14972
|
+
service: target.service ?? "open-uptime"
|
|
14973
|
+
};
|
|
14974
|
+
}
|
|
14975
|
+
async function sendEmailReport(report, target, fetchImpl, timeoutMs) {
|
|
14976
|
+
if (!target.sendKey)
|
|
14977
|
+
return { channel: "email", ok: false, error: "Mailery send key is required" };
|
|
14978
|
+
if (!target.from)
|
|
14979
|
+
return { channel: "email", ok: false, error: "Email from address is required" };
|
|
14980
|
+
if (!hasTargets(target.to))
|
|
14981
|
+
return { channel: "email", ok: false, error: "Email recipient is required" };
|
|
14982
|
+
const body = {
|
|
14983
|
+
from: target.from,
|
|
14984
|
+
to: splitTargets(target.to),
|
|
14985
|
+
subject: target.subject ?? report.subject,
|
|
14986
|
+
text: report.text,
|
|
14987
|
+
html: report.html,
|
|
14988
|
+
provider_id: target.providerId
|
|
14989
|
+
};
|
|
14990
|
+
return requestJson("email", `${normalizeUrl(target.apiUrl ?? DEFAULT_MAILERY_API_URL)}/api/v1/send`, {
|
|
14991
|
+
method: "POST",
|
|
14992
|
+
headers: { authorization: `Bearer ${target.sendKey}` },
|
|
14993
|
+
body
|
|
14994
|
+
}, fetchImpl, timeoutMs, secretsForTarget(target));
|
|
14995
|
+
}
|
|
14996
|
+
async function sendSmsReport(report, target, fetchImpl, timeoutMs) {
|
|
14997
|
+
if (!hasTargets(target.to))
|
|
14998
|
+
return { channel: "sms", ok: false, error: "SMS recipient phone number is required" };
|
|
14999
|
+
return requestJson("sms", `${normalizeUrl(target.apiUrl ?? DEFAULT_TELEPHONY_API_URL)}/api/sms/send`, {
|
|
15000
|
+
method: "POST",
|
|
15001
|
+
body: {
|
|
15002
|
+
to: Array.isArray(target.to) ? target.to[0] : target.to,
|
|
15003
|
+
from: target.from,
|
|
15004
|
+
body: truncateSms(report.text)
|
|
15005
|
+
}
|
|
15006
|
+
}, fetchImpl, timeoutMs, secretsForTarget(target));
|
|
15007
|
+
}
|
|
15008
|
+
async function sendLogsReport(report, target, fetchImpl, timeoutMs) {
|
|
15009
|
+
const params = new URLSearchParams({
|
|
15010
|
+
format: "json",
|
|
15011
|
+
source: "structured",
|
|
15012
|
+
service: target.service ?? "open-uptime",
|
|
15013
|
+
project_id: target.projectId ?? "open-uptime"
|
|
15014
|
+
});
|
|
15015
|
+
if (target.environment)
|
|
15016
|
+
params.set("environment", target.environment);
|
|
15017
|
+
return requestJson("logs", `${normalizeUrl(target.apiUrl ?? DEFAULT_LOGS_API_URL)}/api/logs/structured?${params}`, {
|
|
15018
|
+
method: "POST",
|
|
15019
|
+
headers: target.apiKey ? { authorization: `Bearer ${target.apiKey}` } : undefined,
|
|
15020
|
+
body: {
|
|
15021
|
+
timestamp: report.generatedAt,
|
|
15022
|
+
level: report.summary.totals.down > 0 || report.summary.totals.openIncidents > 0 ? "warn" : "info",
|
|
15023
|
+
message: report.subject,
|
|
15024
|
+
report: report.json
|
|
15025
|
+
}
|
|
15026
|
+
}, fetchImpl, timeoutMs, secretsForTarget(target));
|
|
15027
|
+
}
|
|
15028
|
+
async function requestJson(channel, url2, options, fetchImpl, timeoutMs, secrets = []) {
|
|
15029
|
+
const controller = new AbortController;
|
|
15030
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
15031
|
+
try {
|
|
15032
|
+
const response = await fetchImpl(url2, {
|
|
15033
|
+
method: options.method,
|
|
15034
|
+
signal: controller.signal,
|
|
15035
|
+
headers: {
|
|
15036
|
+
"content-type": "application/json",
|
|
15037
|
+
accept: "application/json",
|
|
15038
|
+
...options.headers
|
|
15039
|
+
},
|
|
15040
|
+
body: JSON.stringify(options.body)
|
|
15041
|
+
});
|
|
15042
|
+
const text = await response.text();
|
|
15043
|
+
const data = parseMaybeJson(text);
|
|
15044
|
+
if (!response.ok) {
|
|
15045
|
+
return { channel, ok: false, status: response.status, error: errorFromResponse(data, response.statusText, secrets) };
|
|
15046
|
+
}
|
|
15047
|
+
return { channel, ok: true, status: response.status, id: redactOptional(idFromResponse(data), secrets) };
|
|
15048
|
+
} catch (error51) {
|
|
15049
|
+
const message = error51 instanceof Error && error51.name === "AbortError" ? "request timed out" : error51 instanceof Error ? error51.message : String(error51);
|
|
15050
|
+
return { channel, ok: false, error: redactSecrets(message, secrets) };
|
|
15051
|
+
} finally {
|
|
15052
|
+
clearTimeout(timer);
|
|
15053
|
+
}
|
|
15054
|
+
}
|
|
15055
|
+
function hasTargets(value) {
|
|
15056
|
+
return splitTargets(value).length > 0;
|
|
15057
|
+
}
|
|
15058
|
+
function splitTargets(value) {
|
|
15059
|
+
if (!value)
|
|
15060
|
+
return [];
|
|
15061
|
+
const values = Array.isArray(value) ? value : value.split(",");
|
|
15062
|
+
return values.map((item) => item.trim()).filter(Boolean);
|
|
15063
|
+
}
|
|
15064
|
+
function normalizeUrl(value) {
|
|
15065
|
+
const parsed = new URL(value.trim());
|
|
15066
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
15067
|
+
throw new Error("Integration API URL must use http or https");
|
|
15068
|
+
}
|
|
15069
|
+
return parsed.toString().replace(/\/$/, "");
|
|
15070
|
+
}
|
|
15071
|
+
function truncateSms(value) {
|
|
15072
|
+
return value.length > 1400 ? `${value.slice(0, 1397)}...` : value;
|
|
15073
|
+
}
|
|
15074
|
+
function parseMaybeJson(text) {
|
|
15075
|
+
if (!text.trim())
|
|
15076
|
+
return {};
|
|
15077
|
+
try {
|
|
15078
|
+
return JSON.parse(text);
|
|
15079
|
+
} catch {
|
|
15080
|
+
return { message: text };
|
|
15081
|
+
}
|
|
15082
|
+
}
|
|
15083
|
+
function idFromResponse(data) {
|
|
15084
|
+
if (!data || typeof data !== "object")
|
|
15085
|
+
return;
|
|
15086
|
+
const record2 = data;
|
|
15087
|
+
for (const key of ["id", "message_id", "event_id"]) {
|
|
15088
|
+
if (typeof record2[key] === "string")
|
|
15089
|
+
return record2[key];
|
|
15090
|
+
}
|
|
15091
|
+
return;
|
|
15092
|
+
}
|
|
15093
|
+
function errorFromResponse(data, fallback, secrets = []) {
|
|
15094
|
+
if (data && typeof data === "object") {
|
|
15095
|
+
const record2 = data;
|
|
15096
|
+
if (typeof record2.error === "string")
|
|
15097
|
+
return redactSecrets(record2.error, secrets);
|
|
15098
|
+
if (typeof record2.message === "string")
|
|
15099
|
+
return redactSecrets(record2.message, secrets);
|
|
15100
|
+
}
|
|
15101
|
+
return redactSecrets(fallback, secrets);
|
|
15102
|
+
}
|
|
15103
|
+
function env(...keys) {
|
|
15104
|
+
for (const key of keys) {
|
|
15105
|
+
const value = process.env[key]?.trim();
|
|
15106
|
+
if (value)
|
|
15107
|
+
return value;
|
|
15108
|
+
}
|
|
15109
|
+
return;
|
|
15110
|
+
}
|
|
15111
|
+
function escapeHtml(value) {
|
|
15112
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
15113
|
+
}
|
|
15114
|
+
function secretsForTarget(target) {
|
|
15115
|
+
const values = new Set;
|
|
15116
|
+
for (const key of ["sendKey", "apiKey"]) {
|
|
15117
|
+
const value = target[key];
|
|
15118
|
+
if (typeof value === "string" && value.trim())
|
|
15119
|
+
values.add(value.trim());
|
|
15120
|
+
}
|
|
15121
|
+
const apiUrl = target.apiUrl;
|
|
15122
|
+
if (apiUrl) {
|
|
15123
|
+
try {
|
|
15124
|
+
const parsed = new URL(apiUrl);
|
|
15125
|
+
if (parsed.username)
|
|
15126
|
+
values.add(decodeURIComponent(parsed.username));
|
|
15127
|
+
if (parsed.password)
|
|
15128
|
+
values.add(decodeURIComponent(parsed.password));
|
|
15129
|
+
} catch {}
|
|
15130
|
+
}
|
|
15131
|
+
return [...values];
|
|
15132
|
+
}
|
|
15133
|
+
function redactSecrets(value, secrets = []) {
|
|
15134
|
+
let redacted = value;
|
|
15135
|
+
for (const secret of secrets) {
|
|
15136
|
+
if (secret.length >= 3)
|
|
15137
|
+
redacted = redacted.split(secret).join("[REDACTED]");
|
|
15138
|
+
}
|
|
15139
|
+
return redacted.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]").replace(/\besk_[A-Za-z0-9._~+/=-]+/g, "esk_[REDACTED]");
|
|
15140
|
+
}
|
|
15141
|
+
function redactOptional(value, secrets) {
|
|
15142
|
+
return value === undefined ? undefined : redactSecrets(value, secrets);
|
|
15143
|
+
}
|
|
15144
|
+
|
|
14787
15145
|
// src/service.ts
|
|
14788
15146
|
class UptimeService {
|
|
14789
15147
|
store;
|
|
14790
15148
|
checkRunner;
|
|
15149
|
+
leaseOwner = `svc_${randomUUID2().replace(/-/g, "").slice(0, 18)}`;
|
|
14791
15150
|
inFlightChecks = new Set;
|
|
14792
15151
|
constructor(options = {}) {
|
|
14793
15152
|
this.store = options.store ?? new UptimeStore(options);
|
|
@@ -14820,6 +15179,12 @@ class UptimeService {
|
|
|
14820
15179
|
summary() {
|
|
14821
15180
|
return this.store.summary();
|
|
14822
15181
|
}
|
|
15182
|
+
buildReport(options = {}) {
|
|
15183
|
+
return buildUptimeReport(this.summary(), options);
|
|
15184
|
+
}
|
|
15185
|
+
async sendReport(options = {}) {
|
|
15186
|
+
return sendUptimeReport(this.summary(), options);
|
|
15187
|
+
}
|
|
14823
15188
|
async checkMonitor(idOrName) {
|
|
14824
15189
|
const monitor = this.store.getMonitor(idOrName);
|
|
14825
15190
|
if (!monitor)
|
|
@@ -14828,6 +15193,10 @@ class UptimeService {
|
|
|
14828
15193
|
throw new Error(`Monitor is disabled: ${monitor.name}`);
|
|
14829
15194
|
if (this.inFlightChecks.has(monitor.id))
|
|
14830
15195
|
throw new Error(`Monitor check already in progress: ${monitor.name}`);
|
|
15196
|
+
const leaseTtlMs = Math.max(60000, (monitor.retryCount + 1) * monitor.timeoutMs + 1e4);
|
|
15197
|
+
if (!this.store.acquireCheckLease(monitor.id, this.leaseOwner, leaseTtlMs)) {
|
|
15198
|
+
throw new MonitorCheckBusyError(`Monitor check already in progress: ${monitor.name}`);
|
|
15199
|
+
}
|
|
14831
15200
|
this.inFlightChecks.add(monitor.id);
|
|
14832
15201
|
try {
|
|
14833
15202
|
let attemptCount = 0;
|
|
@@ -14845,10 +15214,12 @@ class UptimeService {
|
|
|
14845
15214
|
latencyMs: last.latencyMs,
|
|
14846
15215
|
statusCode: last.statusCode ?? null,
|
|
14847
15216
|
error: last.error ?? null,
|
|
14848
|
-
attemptCount
|
|
15217
|
+
attemptCount,
|
|
15218
|
+
expectedMonitorRevision: monitor.revision
|
|
14849
15219
|
});
|
|
14850
15220
|
} finally {
|
|
14851
15221
|
this.inFlightChecks.delete(monitor.id);
|
|
15222
|
+
this.store.releaseCheckLease(monitor.id, this.leaseOwner);
|
|
14852
15223
|
}
|
|
14853
15224
|
}
|
|
14854
15225
|
async checkAll() {
|
|
@@ -14877,7 +15248,13 @@ class UptimeService {
|
|
|
14877
15248
|
const current = this.store.getMonitor(monitor.id);
|
|
14878
15249
|
if (!current || !this.isDue(current, now))
|
|
14879
15250
|
continue;
|
|
14880
|
-
|
|
15251
|
+
try {
|
|
15252
|
+
results.push(await this.checkMonitor(current.id));
|
|
15253
|
+
} catch (error51) {
|
|
15254
|
+
if (error51 instanceof MonitorCheckBusyError || error51 instanceof StaleCheckResultError)
|
|
15255
|
+
continue;
|
|
15256
|
+
throw error51;
|
|
15257
|
+
}
|
|
14881
15258
|
}
|
|
14882
15259
|
return results;
|
|
14883
15260
|
}
|
|
@@ -14892,6 +15269,12 @@ class UptimeService {
|
|
|
14892
15269
|
return now.getTime() - last >= monitor.intervalSeconds * 1000;
|
|
14893
15270
|
}
|
|
14894
15271
|
}
|
|
15272
|
+
class MonitorCheckBusyError extends Error {
|
|
15273
|
+
constructor(message) {
|
|
15274
|
+
super(message);
|
|
15275
|
+
this.name = "MonitorCheckBusyError";
|
|
15276
|
+
}
|
|
15277
|
+
}
|
|
14895
15278
|
|
|
14896
15279
|
// src/version.ts
|
|
14897
15280
|
import { readFileSync } from "fs";
|
|
@@ -15003,6 +15386,39 @@ function createMcpServer(options = {}) {
|
|
|
15003
15386
|
description: "Summarize monitor status, uptime percentages, latency, and open incidents.",
|
|
15004
15387
|
inputSchema: {}
|
|
15005
15388
|
}, async () => jsonResult(service.summary()));
|
|
15389
|
+
server.registerTool("uptime_send_report", {
|
|
15390
|
+
title: "Send an uptime report",
|
|
15391
|
+
description: "Build an uptime report and send it through Mailery email, Telephony SMS, and/or Open Logs structured logs.",
|
|
15392
|
+
inputSchema: {
|
|
15393
|
+
subject: exports_external.string().optional(),
|
|
15394
|
+
email: exports_external.object({
|
|
15395
|
+
apiUrl: exports_external.string().url().optional(),
|
|
15396
|
+
sendKey: exports_external.string().optional(),
|
|
15397
|
+
from: exports_external.string().optional(),
|
|
15398
|
+
to: exports_external.union([exports_external.string(), exports_external.array(exports_external.string())]).optional(),
|
|
15399
|
+
providerId: exports_external.string().optional()
|
|
15400
|
+
}).optional(),
|
|
15401
|
+
sms: exports_external.object({
|
|
15402
|
+
apiUrl: exports_external.string().url().optional(),
|
|
15403
|
+
from: exports_external.string().optional(),
|
|
15404
|
+
to: exports_external.union([exports_external.string(), exports_external.array(exports_external.string())]).optional()
|
|
15405
|
+
}).optional(),
|
|
15406
|
+
logs: exports_external.object({
|
|
15407
|
+
apiUrl: exports_external.string().url().optional(),
|
|
15408
|
+
apiKey: exports_external.string().optional(),
|
|
15409
|
+
projectId: exports_external.string().optional(),
|
|
15410
|
+
environment: exports_external.string().optional(),
|
|
15411
|
+
service: exports_external.string().optional()
|
|
15412
|
+
}).optional(),
|
|
15413
|
+
timeoutMs: exports_external.number().int().min(1000).max(60000).optional()
|
|
15414
|
+
}
|
|
15415
|
+
}, async (args) => jsonResult(await service.sendReport({
|
|
15416
|
+
subject: args.subject,
|
|
15417
|
+
email: args.email,
|
|
15418
|
+
sms: args.sms,
|
|
15419
|
+
logs: args.logs,
|
|
15420
|
+
timeoutMs: args.timeoutMs
|
|
15421
|
+
})));
|
|
15006
15422
|
server.registerTool("uptime_results", {
|
|
15007
15423
|
title: "List uptime check results",
|
|
15008
15424
|
description: "List recent check results.",
|
package/dist/report.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { UptimeSummary } from "./types.js";
|
|
2
|
+
export interface BuildUptimeReportOptions {
|
|
3
|
+
subject?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface UptimeReport {
|
|
6
|
+
subject: string;
|
|
7
|
+
generatedAt: string;
|
|
8
|
+
summary: UptimeSummary;
|
|
9
|
+
text: string;
|
|
10
|
+
html: string;
|
|
11
|
+
json: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
export interface SendUptimeReportOptions extends BuildUptimeReportOptions {
|
|
14
|
+
email?: boolean | UptimeEmailReportTarget;
|
|
15
|
+
sms?: boolean | UptimeSmsReportTarget;
|
|
16
|
+
logs?: boolean | UptimeLogsReportTarget;
|
|
17
|
+
fetchImpl?: typeof fetch;
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface UptimeEmailReportTarget {
|
|
21
|
+
apiUrl?: string;
|
|
22
|
+
sendKey?: string;
|
|
23
|
+
from?: string;
|
|
24
|
+
to?: string | string[];
|
|
25
|
+
subject?: string;
|
|
26
|
+
providerId?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface UptimeSmsReportTarget {
|
|
29
|
+
apiUrl?: string;
|
|
30
|
+
from?: string;
|
|
31
|
+
to?: string | string[];
|
|
32
|
+
}
|
|
33
|
+
export interface UptimeLogsReportTarget {
|
|
34
|
+
apiUrl?: string;
|
|
35
|
+
apiKey?: string;
|
|
36
|
+
projectId?: string;
|
|
37
|
+
environment?: string;
|
|
38
|
+
service?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface UptimeReportDelivery {
|
|
41
|
+
channel: "email" | "sms" | "logs";
|
|
42
|
+
ok: boolean;
|
|
43
|
+
status?: number;
|
|
44
|
+
id?: string;
|
|
45
|
+
error?: string;
|
|
46
|
+
}
|
|
47
|
+
export declare function buildUptimeReport(summary: UptimeSummary, options?: BuildUptimeReportOptions): UptimeReport;
|
|
48
|
+
export declare function sendUptimeReport(summary: UptimeSummary, options?: SendUptimeReportOptions): Promise<UptimeReportDelivery[]>;
|
|
49
|
+
//# sourceMappingURL=report.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAkB,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhE,MAAM,WAAW,wBAAwB;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,aAAa,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,uBAAwB,SAAQ,wBAAwB;IACvE,KAAK,CAAC,EAAE,OAAO,GAAG,uBAAuB,CAAC;IAC1C,GAAG,CAAC,EAAE,OAAO,GAAG,qBAAqB,CAAC;IACtC,IAAI,CAAC,EAAE,OAAO,GAAG,sBAAsB,CAAC;IACxC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;IAClC,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAOD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,aAAa,EAAE,OAAO,GAAE,wBAA6B,GAAG,YAAY,CAyB9G;AAED,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,aAAa,EAAE,OAAO,GAAE,uBAA4B,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAyBrI"}
|