@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/report.js
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/report.ts
|
|
3
|
+
var DEFAULT_MAILERY_API_URL = "http://localhost:3900";
|
|
4
|
+
var DEFAULT_TELEPHONY_API_URL = "http://localhost:19451";
|
|
5
|
+
var DEFAULT_LOGS_API_URL = "http://localhost:3460";
|
|
6
|
+
var DEFAULT_TIMEOUT_MS = 15000;
|
|
7
|
+
function buildUptimeReport(summary, options = {}) {
|
|
8
|
+
const subject = options.subject ?? defaultSubject(summary);
|
|
9
|
+
const lines = [
|
|
10
|
+
subject,
|
|
11
|
+
`Generated: ${summary.generatedAt}`,
|
|
12
|
+
`Monitors: ${summary.totals.monitors} total, ${summary.totals.enabled} enabled, ${summary.totals.up} up, ${summary.totals.down} down, ${summary.totals.openIncidents} open incidents`,
|
|
13
|
+
"",
|
|
14
|
+
...summary.monitors.map(renderMonitorLine)
|
|
15
|
+
];
|
|
16
|
+
const text = lines.join(`
|
|
17
|
+
`).trimEnd();
|
|
18
|
+
const json = {
|
|
19
|
+
kind: "open-uptime.report",
|
|
20
|
+
generated_at: summary.generatedAt,
|
|
21
|
+
subject,
|
|
22
|
+
totals: summary.totals,
|
|
23
|
+
monitors: summary.monitors
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
subject,
|
|
27
|
+
generatedAt: summary.generatedAt,
|
|
28
|
+
summary,
|
|
29
|
+
text,
|
|
30
|
+
html: `<pre>${escapeHtml(text)}</pre>`,
|
|
31
|
+
json
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async function sendUptimeReport(summary, options = {}) {
|
|
35
|
+
const report = buildUptimeReport(summary, options);
|
|
36
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
37
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
38
|
+
const deliveries = [];
|
|
39
|
+
if (options.email) {
|
|
40
|
+
deliveries.push(await sendEmailReport(report, resolveEmailTarget(options.email), fetchImpl, timeoutMs));
|
|
41
|
+
}
|
|
42
|
+
if (options.sms) {
|
|
43
|
+
const smsTarget = resolveSmsTarget(options.sms);
|
|
44
|
+
const recipients = splitTargets(smsTarget.to);
|
|
45
|
+
if (recipients.length === 0) {
|
|
46
|
+
deliveries.push(await sendSmsReport(report, smsTarget, fetchImpl, timeoutMs));
|
|
47
|
+
} else {
|
|
48
|
+
for (const target of recipients) {
|
|
49
|
+
deliveries.push(await sendSmsReport(report, { ...smsTarget, to: target }, fetchImpl, timeoutMs));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (options.logs) {
|
|
54
|
+
deliveries.push(await sendLogsReport(report, resolveLogsTarget(options.logs), fetchImpl, timeoutMs));
|
|
55
|
+
}
|
|
56
|
+
return deliveries;
|
|
57
|
+
}
|
|
58
|
+
function defaultSubject(summary) {
|
|
59
|
+
if (summary.totals.openIncidents > 0 || summary.totals.down > 0) {
|
|
60
|
+
return `Open Uptime alert: ${summary.totals.down} down, ${summary.totals.openIncidents} open incidents`;
|
|
61
|
+
}
|
|
62
|
+
return `Open Uptime report: ${summary.totals.up}/${summary.totals.enabled} enabled monitors up`;
|
|
63
|
+
}
|
|
64
|
+
function renderMonitorLine(item) {
|
|
65
|
+
const uptime = item.uptimePercent == null ? "-" : `${item.uptimePercent.toFixed(2)}%`;
|
|
66
|
+
const latency = item.averageLatencyMs == null ? "-" : `${item.averageLatencyMs}ms`;
|
|
67
|
+
const incident = item.openIncident ? ` open incident: ${item.openIncident.reason ?? "down"}` : "";
|
|
68
|
+
return `- ${item.monitor.status.toUpperCase()} ${item.monitor.name} (${targetLabel(item)}): uptime ${uptime}, latency ${latency}${incident}`;
|
|
69
|
+
}
|
|
70
|
+
function targetLabel(item) {
|
|
71
|
+
return item.monitor.kind === "http" ? item.monitor.url ?? "" : `${item.monitor.host}:${item.monitor.port}`;
|
|
72
|
+
}
|
|
73
|
+
function resolveEmailTarget(value) {
|
|
74
|
+
const target = typeof value === "boolean" ? {} : value;
|
|
75
|
+
return {
|
|
76
|
+
apiUrl: target.apiUrl ?? env("HASNA_MAILERY_API_URL", "MAILERY_API_URL") ?? DEFAULT_MAILERY_API_URL,
|
|
77
|
+
sendKey: target.sendKey ?? env("HASNA_MAILERY_SEND_KEY", "MAILERY_SEND_KEY", "ESK"),
|
|
78
|
+
from: target.from ?? env("HASNA_UPTIME_REPORT_EMAIL_FROM", "UPTIME_REPORT_EMAIL_FROM"),
|
|
79
|
+
to: target.to ?? env("HASNA_UPTIME_REPORT_EMAIL_TO", "UPTIME_REPORT_EMAIL_TO"),
|
|
80
|
+
subject: target.subject,
|
|
81
|
+
providerId: target.providerId ?? env("HASNA_MAILERY_PROVIDER_ID", "MAILERY_PROVIDER_ID")
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function resolveSmsTarget(value) {
|
|
85
|
+
const target = typeof value === "boolean" ? {} : value;
|
|
86
|
+
return {
|
|
87
|
+
apiUrl: target.apiUrl ?? env("HASNA_TELEPHONY_API_URL", "TELEPHONY_API_URL") ?? DEFAULT_TELEPHONY_API_URL,
|
|
88
|
+
from: target.from ?? env("HASNA_UPTIME_REPORT_SMS_FROM", "UPTIME_REPORT_SMS_FROM"),
|
|
89
|
+
to: target.to ?? env("HASNA_UPTIME_REPORT_PHONE_TO", "UPTIME_REPORT_PHONE_TO")
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function resolveLogsTarget(value) {
|
|
93
|
+
const target = typeof value === "boolean" ? {} : value;
|
|
94
|
+
return {
|
|
95
|
+
apiUrl: target.apiUrl ?? env("HASNA_LOGS_URL", "LOGS_URL") ?? DEFAULT_LOGS_API_URL,
|
|
96
|
+
apiKey: target.apiKey ?? env("HASNA_LOGS_API_TOKEN", "LOGS_API_TOKEN", "HASNA_LOGS_API_KEY", "LOGS_API_KEY"),
|
|
97
|
+
projectId: target.projectId ?? env("HASNA_LOGS_PROJECT_ID", "LOGS_PROJECT_ID") ?? "open-uptime",
|
|
98
|
+
environment: target.environment ?? env("HASNA_ENV", "NODE_ENV"),
|
|
99
|
+
service: target.service ?? "open-uptime"
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
async function sendEmailReport(report, target, fetchImpl, timeoutMs) {
|
|
103
|
+
if (!target.sendKey)
|
|
104
|
+
return { channel: "email", ok: false, error: "Mailery send key is required" };
|
|
105
|
+
if (!target.from)
|
|
106
|
+
return { channel: "email", ok: false, error: "Email from address is required" };
|
|
107
|
+
if (!hasTargets(target.to))
|
|
108
|
+
return { channel: "email", ok: false, error: "Email recipient is required" };
|
|
109
|
+
const body = {
|
|
110
|
+
from: target.from,
|
|
111
|
+
to: splitTargets(target.to),
|
|
112
|
+
subject: target.subject ?? report.subject,
|
|
113
|
+
text: report.text,
|
|
114
|
+
html: report.html,
|
|
115
|
+
provider_id: target.providerId
|
|
116
|
+
};
|
|
117
|
+
return requestJson("email", `${normalizeUrl(target.apiUrl ?? DEFAULT_MAILERY_API_URL)}/api/v1/send`, {
|
|
118
|
+
method: "POST",
|
|
119
|
+
headers: { authorization: `Bearer ${target.sendKey}` },
|
|
120
|
+
body
|
|
121
|
+
}, fetchImpl, timeoutMs, secretsForTarget(target));
|
|
122
|
+
}
|
|
123
|
+
async function sendSmsReport(report, target, fetchImpl, timeoutMs) {
|
|
124
|
+
if (!hasTargets(target.to))
|
|
125
|
+
return { channel: "sms", ok: false, error: "SMS recipient phone number is required" };
|
|
126
|
+
return requestJson("sms", `${normalizeUrl(target.apiUrl ?? DEFAULT_TELEPHONY_API_URL)}/api/sms/send`, {
|
|
127
|
+
method: "POST",
|
|
128
|
+
body: {
|
|
129
|
+
to: Array.isArray(target.to) ? target.to[0] : target.to,
|
|
130
|
+
from: target.from,
|
|
131
|
+
body: truncateSms(report.text)
|
|
132
|
+
}
|
|
133
|
+
}, fetchImpl, timeoutMs, secretsForTarget(target));
|
|
134
|
+
}
|
|
135
|
+
async function sendLogsReport(report, target, fetchImpl, timeoutMs) {
|
|
136
|
+
const params = new URLSearchParams({
|
|
137
|
+
format: "json",
|
|
138
|
+
source: "structured",
|
|
139
|
+
service: target.service ?? "open-uptime",
|
|
140
|
+
project_id: target.projectId ?? "open-uptime"
|
|
141
|
+
});
|
|
142
|
+
if (target.environment)
|
|
143
|
+
params.set("environment", target.environment);
|
|
144
|
+
return requestJson("logs", `${normalizeUrl(target.apiUrl ?? DEFAULT_LOGS_API_URL)}/api/logs/structured?${params}`, {
|
|
145
|
+
method: "POST",
|
|
146
|
+
headers: target.apiKey ? { authorization: `Bearer ${target.apiKey}` } : undefined,
|
|
147
|
+
body: {
|
|
148
|
+
timestamp: report.generatedAt,
|
|
149
|
+
level: report.summary.totals.down > 0 || report.summary.totals.openIncidents > 0 ? "warn" : "info",
|
|
150
|
+
message: report.subject,
|
|
151
|
+
report: report.json
|
|
152
|
+
}
|
|
153
|
+
}, fetchImpl, timeoutMs, secretsForTarget(target));
|
|
154
|
+
}
|
|
155
|
+
async function requestJson(channel, url, options, fetchImpl, timeoutMs, secrets = []) {
|
|
156
|
+
const controller = new AbortController;
|
|
157
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
158
|
+
try {
|
|
159
|
+
const response = await fetchImpl(url, {
|
|
160
|
+
method: options.method,
|
|
161
|
+
signal: controller.signal,
|
|
162
|
+
headers: {
|
|
163
|
+
"content-type": "application/json",
|
|
164
|
+
accept: "application/json",
|
|
165
|
+
...options.headers
|
|
166
|
+
},
|
|
167
|
+
body: JSON.stringify(options.body)
|
|
168
|
+
});
|
|
169
|
+
const text = await response.text();
|
|
170
|
+
const data = parseMaybeJson(text);
|
|
171
|
+
if (!response.ok) {
|
|
172
|
+
return { channel, ok: false, status: response.status, error: errorFromResponse(data, response.statusText, secrets) };
|
|
173
|
+
}
|
|
174
|
+
return { channel, ok: true, status: response.status, id: redactOptional(idFromResponse(data), secrets) };
|
|
175
|
+
} catch (error) {
|
|
176
|
+
const message = error instanceof Error && error.name === "AbortError" ? "request timed out" : error instanceof Error ? error.message : String(error);
|
|
177
|
+
return { channel, ok: false, error: redactSecrets(message, secrets) };
|
|
178
|
+
} finally {
|
|
179
|
+
clearTimeout(timer);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function hasTargets(value) {
|
|
183
|
+
return splitTargets(value).length > 0;
|
|
184
|
+
}
|
|
185
|
+
function splitTargets(value) {
|
|
186
|
+
if (!value)
|
|
187
|
+
return [];
|
|
188
|
+
const values = Array.isArray(value) ? value : value.split(",");
|
|
189
|
+
return values.map((item) => item.trim()).filter(Boolean);
|
|
190
|
+
}
|
|
191
|
+
function normalizeUrl(value) {
|
|
192
|
+
const parsed = new URL(value.trim());
|
|
193
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
194
|
+
throw new Error("Integration API URL must use http or https");
|
|
195
|
+
}
|
|
196
|
+
return parsed.toString().replace(/\/$/, "");
|
|
197
|
+
}
|
|
198
|
+
function truncateSms(value) {
|
|
199
|
+
return value.length > 1400 ? `${value.slice(0, 1397)}...` : value;
|
|
200
|
+
}
|
|
201
|
+
function parseMaybeJson(text) {
|
|
202
|
+
if (!text.trim())
|
|
203
|
+
return {};
|
|
204
|
+
try {
|
|
205
|
+
return JSON.parse(text);
|
|
206
|
+
} catch {
|
|
207
|
+
return { message: text };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function idFromResponse(data) {
|
|
211
|
+
if (!data || typeof data !== "object")
|
|
212
|
+
return;
|
|
213
|
+
const record = data;
|
|
214
|
+
for (const key of ["id", "message_id", "event_id"]) {
|
|
215
|
+
if (typeof record[key] === "string")
|
|
216
|
+
return record[key];
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
function errorFromResponse(data, fallback, secrets = []) {
|
|
221
|
+
if (data && typeof data === "object") {
|
|
222
|
+
const record = data;
|
|
223
|
+
if (typeof record.error === "string")
|
|
224
|
+
return redactSecrets(record.error, secrets);
|
|
225
|
+
if (typeof record.message === "string")
|
|
226
|
+
return redactSecrets(record.message, secrets);
|
|
227
|
+
}
|
|
228
|
+
return redactSecrets(fallback, secrets);
|
|
229
|
+
}
|
|
230
|
+
function env(...keys) {
|
|
231
|
+
for (const key of keys) {
|
|
232
|
+
const value = process.env[key]?.trim();
|
|
233
|
+
if (value)
|
|
234
|
+
return value;
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
function escapeHtml(value) {
|
|
239
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
240
|
+
}
|
|
241
|
+
function secretsForTarget(target) {
|
|
242
|
+
const values = new Set;
|
|
243
|
+
for (const key of ["sendKey", "apiKey"]) {
|
|
244
|
+
const value = target[key];
|
|
245
|
+
if (typeof value === "string" && value.trim())
|
|
246
|
+
values.add(value.trim());
|
|
247
|
+
}
|
|
248
|
+
const apiUrl = target.apiUrl;
|
|
249
|
+
if (apiUrl) {
|
|
250
|
+
try {
|
|
251
|
+
const parsed = new URL(apiUrl);
|
|
252
|
+
if (parsed.username)
|
|
253
|
+
values.add(decodeURIComponent(parsed.username));
|
|
254
|
+
if (parsed.password)
|
|
255
|
+
values.add(decodeURIComponent(parsed.password));
|
|
256
|
+
} catch {}
|
|
257
|
+
}
|
|
258
|
+
return [...values];
|
|
259
|
+
}
|
|
260
|
+
function redactSecrets(value, secrets = []) {
|
|
261
|
+
let redacted = value;
|
|
262
|
+
for (const secret of secrets) {
|
|
263
|
+
if (secret.length >= 3)
|
|
264
|
+
redacted = redacted.split(secret).join("[REDACTED]");
|
|
265
|
+
}
|
|
266
|
+
return redacted.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]").replace(/\besk_[A-Za-z0-9._~+/=-]+/g, "esk_[REDACTED]");
|
|
267
|
+
}
|
|
268
|
+
function redactOptional(value, secrets) {
|
|
269
|
+
return value === undefined ? undefined : redactSecrets(value, secrets);
|
|
270
|
+
}
|
|
271
|
+
export {
|
|
272
|
+
sendUptimeReport,
|
|
273
|
+
buildUptimeReport
|
|
274
|
+
};
|
package/dist/service.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { UptimeStore, type UptimeStoreOptions } from "./store.js";
|
|
2
|
+
import { type BuildUptimeReportOptions, type SendUptimeReportOptions, type UptimeReport, type UptimeReportDelivery } from "./report.js";
|
|
2
3
|
import type { CheckAttemptResult, CheckResult, CreateMonitorInput, Incident, ListResultsOptions, Monitor, SchedulerHandle, UpdateMonitorInput, UptimeSummary } from "./types.js";
|
|
3
4
|
export interface UptimeServiceOptions extends UptimeStoreOptions {
|
|
4
5
|
store?: UptimeStore;
|
|
@@ -7,6 +8,7 @@ export interface UptimeServiceOptions extends UptimeStoreOptions {
|
|
|
7
8
|
export declare class UptimeService {
|
|
8
9
|
readonly store: UptimeStore;
|
|
9
10
|
private readonly checkRunner;
|
|
11
|
+
private readonly leaseOwner;
|
|
10
12
|
private readonly inFlightChecks;
|
|
11
13
|
constructor(options?: UptimeServiceOptions);
|
|
12
14
|
close(): void;
|
|
@@ -24,6 +26,8 @@ export declare class UptimeService {
|
|
|
24
26
|
limit?: number;
|
|
25
27
|
}): Incident[];
|
|
26
28
|
summary(): UptimeSummary;
|
|
29
|
+
buildReport(options?: BuildUptimeReportOptions): UptimeReport;
|
|
30
|
+
sendReport(options?: SendUptimeReportOptions): Promise<UptimeReportDelivery[]>;
|
|
27
31
|
checkMonitor(idOrName: string): Promise<CheckResult>;
|
|
28
32
|
checkAll(): Promise<CheckResult[]>;
|
|
29
33
|
startScheduler(options?: {
|
|
@@ -33,4 +37,7 @@ export declare class UptimeService {
|
|
|
33
37
|
private isDue;
|
|
34
38
|
}
|
|
35
39
|
export declare function createUptimeClient(options?: UptimeServiceOptions): UptimeService;
|
|
40
|
+
export declare class MonitorCheckBusyError extends Error {
|
|
41
|
+
constructor(message: string);
|
|
42
|
+
}
|
|
36
43
|
//# sourceMappingURL=service.d.ts.map
|
package/dist/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAyB,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACzF,OAAO,EAAuC,KAAK,wBAAwB,EAAE,KAAK,uBAAuB,EAAE,KAAK,YAAY,EAAE,KAAK,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAC7K,OAAO,KAAK,EACV,kBAAkB,EAClB,WAAW,EACX,kBAAkB,EAClB,QAAQ,EACR,kBAAkB,EAClB,OAAO,EACP,eAAe,EACf,kBAAkB,EAClB,aAAa,EACd,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,oBAAqB,SAAQ,kBAAkB;IAC9D,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;CACjE;AAED,qBAAa,aAAa;IACxB,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAoD;IAChF,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAwD;IACnF,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAqB;gBAExC,OAAO,GAAE,oBAAyB;IAK9C,KAAK,IAAI,IAAI;IAIb,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO;IAIjD,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB,GAAG,OAAO;IAInE,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAIxC,YAAY,CAAC,OAAO,GAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,OAAO,EAAE;IAIpE,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI;IAI5C,WAAW,CAAC,OAAO,GAAE,kBAAuB,GAAG,WAAW,EAAE;IAI5D,aAAa,CAAC,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,QAAQ,EAAE;IAI3G,OAAO,IAAI,aAAa;IAIxB,WAAW,CAAC,OAAO,GAAE,wBAA6B,GAAG,YAAY;IAI3D,UAAU,CAAC,OAAO,GAAE,uBAA4B,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAIlF,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAkCpD,QAAQ,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IASxC,cAAc,CAAC,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,eAAe;IAY5D,YAAY,CAAC,GAAG,GAAE,IAAiB,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAgBlE,OAAO,CAAC,KAAK;CAOd;AAED,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,oBAAyB,GAAG,aAAa,CAEpF;AAED,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B"}
|