@hasna/uptime 0.1.3 → 0.1.4
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 +26 -0
- package/README.md +35 -3
- package/dist/api.js +626 -4
- package/dist/cli/index.js +757 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +626 -4
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +664 -4
- package/dist/report.d.ts +2 -7
- package/dist/report.d.ts.map +1 -1
- package/dist/service.d.ts +41 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +577 -4
- package/dist/store.d.ts +22 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +427 -4
- package/dist/types.d.ts +92 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -825,9 +825,24 @@ import { dirname, join as join2 } from "path";
|
|
|
825
825
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
826
826
|
import { Database } from "bun:sqlite";
|
|
827
827
|
var SECRET_URL_PARAM_PATTERN = /(token|secret|password|passwd|api[_-]?key|access[_-]?token|auth|credential|session)/i;
|
|
828
|
-
var REQUIRED_TABLES = [
|
|
828
|
+
var REQUIRED_TABLES = [
|
|
829
|
+
"schema_migrations",
|
|
830
|
+
"monitors",
|
|
831
|
+
"check_results",
|
|
832
|
+
"incidents",
|
|
833
|
+
"check_leases",
|
|
834
|
+
"monitor_provenance",
|
|
835
|
+
"import_batches",
|
|
836
|
+
"probe_identities",
|
|
837
|
+
"probe_check_jobs",
|
|
838
|
+
"probe_submissions",
|
|
839
|
+
"report_schedules",
|
|
840
|
+
"report_runs",
|
|
841
|
+
"audit_events"
|
|
842
|
+
];
|
|
829
843
|
var PROBE_TABLES = new Set(["probe_identities", "probe_check_jobs", "probe_submissions"]);
|
|
830
|
-
var
|
|
844
|
+
var REPORT_AUDIT_TABLES = new Set(["report_schedules", "report_runs", "audit_events"]);
|
|
845
|
+
var CURRENT_SCHEMA_VERSION = "3";
|
|
831
846
|
|
|
832
847
|
class StaleCheckResultError extends Error {
|
|
833
848
|
constructor(message) {
|
|
@@ -987,6 +1002,44 @@ class UptimeStore {
|
|
|
987
1002
|
acquired_at TEXT NOT NULL
|
|
988
1003
|
)
|
|
989
1004
|
`);
|
|
1005
|
+
this.db.run(`
|
|
1006
|
+
CREATE TABLE IF NOT EXISTS report_schedules (
|
|
1007
|
+
id TEXT PRIMARY KEY,
|
|
1008
|
+
name TEXT NOT NULL UNIQUE,
|
|
1009
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
1010
|
+
interval_seconds INTEGER NOT NULL,
|
|
1011
|
+
next_run_at TEXT NOT NULL,
|
|
1012
|
+
last_run_at TEXT,
|
|
1013
|
+
subject TEXT,
|
|
1014
|
+
channels_json TEXT NOT NULL,
|
|
1015
|
+
created_at TEXT NOT NULL,
|
|
1016
|
+
updated_at TEXT NOT NULL
|
|
1017
|
+
)
|
|
1018
|
+
`);
|
|
1019
|
+
this.db.run(`
|
|
1020
|
+
CREATE TABLE IF NOT EXISTS report_runs (
|
|
1021
|
+
id TEXT PRIMARY KEY,
|
|
1022
|
+
schedule_id TEXT REFERENCES report_schedules(id) ON DELETE SET NULL,
|
|
1023
|
+
status TEXT NOT NULL CHECK (status IN ('success', 'failed')),
|
|
1024
|
+
started_at TEXT NOT NULL,
|
|
1025
|
+
finished_at TEXT NOT NULL,
|
|
1026
|
+
deliveries_json TEXT NOT NULL,
|
|
1027
|
+
error TEXT,
|
|
1028
|
+
report_json TEXT
|
|
1029
|
+
)
|
|
1030
|
+
`);
|
|
1031
|
+
this.db.run(`
|
|
1032
|
+
CREATE TABLE IF NOT EXISTS audit_events (
|
|
1033
|
+
id TEXT PRIMARY KEY,
|
|
1034
|
+
action TEXT NOT NULL,
|
|
1035
|
+
resource_type TEXT,
|
|
1036
|
+
resource_id TEXT,
|
|
1037
|
+
message TEXT,
|
|
1038
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
1039
|
+
actor TEXT,
|
|
1040
|
+
created_at TEXT NOT NULL
|
|
1041
|
+
)
|
|
1042
|
+
`);
|
|
990
1043
|
this.db.run(`
|
|
991
1044
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
992
1045
|
key TEXT PRIMARY KEY,
|
|
@@ -1004,6 +1057,10 @@ class UptimeStore {
|
|
|
1004
1057
|
this.db.run("CREATE INDEX IF NOT EXISTS idx_probe_submissions_probe_time ON probe_submissions(probe_id, submitted_at DESC)");
|
|
1005
1058
|
this.db.run("CREATE INDEX IF NOT EXISTS idx_probe_submissions_monitor_time ON probe_submissions(monitor_id, checked_at DESC)");
|
|
1006
1059
|
this.db.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_probe_submissions_job ON probe_submissions(job_id) WHERE job_id IS NOT NULL AND job_id != ''");
|
|
1060
|
+
this.db.run("CREATE INDEX IF NOT EXISTS idx_report_schedules_due ON report_schedules(enabled, next_run_at)");
|
|
1061
|
+
this.db.run("CREATE INDEX IF NOT EXISTS idx_report_runs_schedule_time ON report_runs(schedule_id, started_at DESC)");
|
|
1062
|
+
this.db.run("CREATE INDEX IF NOT EXISTS idx_audit_events_resource_time ON audit_events(resource_type, resource_id, created_at DESC)");
|
|
1063
|
+
this.db.run("CREATE INDEX IF NOT EXISTS idx_audit_events_time ON audit_events(created_at DESC)");
|
|
1007
1064
|
}
|
|
1008
1065
|
backup(destinationPath) {
|
|
1009
1066
|
if (this.dbPath === ":memory:" && !destinationPath) {
|
|
@@ -1300,6 +1357,136 @@ class UptimeStore {
|
|
|
1300
1357
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(receipt.id, receipt.probeId, receipt.jobId, receipt.monitorId, receipt.checkResultId, receipt.nonce, receipt.checkedAt, receipt.submittedAt);
|
|
1301
1358
|
return receipt;
|
|
1302
1359
|
}
|
|
1360
|
+
createReportSchedule(input) {
|
|
1361
|
+
const normalized = normalizeReportScheduleInput(input);
|
|
1362
|
+
const now = new Date().toISOString();
|
|
1363
|
+
const schedule = {
|
|
1364
|
+
id: newId("rps"),
|
|
1365
|
+
name: normalized.name,
|
|
1366
|
+
enabled: normalized.enabled,
|
|
1367
|
+
intervalSeconds: normalized.intervalSeconds,
|
|
1368
|
+
nextRunAt: normalized.nextRunAt,
|
|
1369
|
+
lastRunAt: null,
|
|
1370
|
+
subject: normalized.subject,
|
|
1371
|
+
channels: normalized.channels,
|
|
1372
|
+
createdAt: now,
|
|
1373
|
+
updatedAt: now
|
|
1374
|
+
};
|
|
1375
|
+
this.db.query(`INSERT INTO report_schedules (
|
|
1376
|
+
id, name, enabled, interval_seconds, next_run_at, last_run_at,
|
|
1377
|
+
subject, channels_json, created_at, updated_at
|
|
1378
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(schedule.id, schedule.name, schedule.enabled ? 1 : 0, schedule.intervalSeconds, schedule.nextRunAt, schedule.lastRunAt, schedule.subject, JSON.stringify(schedule.channels), schedule.createdAt, schedule.updatedAt);
|
|
1379
|
+
return schedule;
|
|
1380
|
+
}
|
|
1381
|
+
listReportSchedules(options = {}) {
|
|
1382
|
+
const rows = options.includeDisabled ? this.db.query("SELECT * FROM report_schedules ORDER BY name ASC").all() : this.db.query("SELECT * FROM report_schedules WHERE enabled = 1 ORDER BY name ASC").all();
|
|
1383
|
+
return rows.map(reportScheduleFromRow);
|
|
1384
|
+
}
|
|
1385
|
+
listDueReportSchedules(nowIso = new Date().toISOString()) {
|
|
1386
|
+
assertIsoTimestamp(nowIso, "Report schedule due timestamp");
|
|
1387
|
+
const rows = this.db.query("SELECT * FROM report_schedules WHERE enabled = 1 AND next_run_at <= ? ORDER BY next_run_at ASC, name ASC").all(nowIso);
|
|
1388
|
+
return rows.map(reportScheduleFromRow);
|
|
1389
|
+
}
|
|
1390
|
+
getReportSchedule(idOrName) {
|
|
1391
|
+
const row = this.db.query("SELECT * FROM report_schedules WHERE id = ? OR name = ?").get(idOrName, idOrName);
|
|
1392
|
+
return row ? reportScheduleFromRow(row) : null;
|
|
1393
|
+
}
|
|
1394
|
+
updateReportSchedule(idOrName, input) {
|
|
1395
|
+
const current = this.getReportSchedule(idOrName);
|
|
1396
|
+
if (!current)
|
|
1397
|
+
throw new Error(`Report schedule not found: ${idOrName}`);
|
|
1398
|
+
const normalized = normalizeReportScheduleInput({
|
|
1399
|
+
name: input.name ?? current.name,
|
|
1400
|
+
intervalSeconds: input.intervalSeconds ?? current.intervalSeconds,
|
|
1401
|
+
nextRunAt: input.nextRunAt ?? current.nextRunAt,
|
|
1402
|
+
enabled: input.enabled ?? current.enabled,
|
|
1403
|
+
subject: input.subject === undefined ? current.subject : input.subject,
|
|
1404
|
+
channels: input.channels ?? current.channels
|
|
1405
|
+
});
|
|
1406
|
+
const updatedAt = new Date().toISOString();
|
|
1407
|
+
this.db.query(`UPDATE report_schedules SET
|
|
1408
|
+
name = ?, enabled = ?, interval_seconds = ?, next_run_at = ?,
|
|
1409
|
+
subject = ?, channels_json = ?, updated_at = ?
|
|
1410
|
+
WHERE id = ?`).run(normalized.name, normalized.enabled ? 1 : 0, normalized.intervalSeconds, normalized.nextRunAt, normalized.subject, JSON.stringify(normalized.channels), updatedAt, current.id);
|
|
1411
|
+
return this.getReportSchedule(current.id);
|
|
1412
|
+
}
|
|
1413
|
+
deleteReportSchedule(idOrName) {
|
|
1414
|
+
const current = this.getReportSchedule(idOrName);
|
|
1415
|
+
if (!current)
|
|
1416
|
+
return false;
|
|
1417
|
+
this.db.query("DELETE FROM report_schedules WHERE id = ?").run(current.id);
|
|
1418
|
+
return true;
|
|
1419
|
+
}
|
|
1420
|
+
recordReportRun(input) {
|
|
1421
|
+
const startedAt = input.startedAt ?? new Date().toISOString();
|
|
1422
|
+
const finishedAt = input.finishedAt ?? new Date().toISOString();
|
|
1423
|
+
assertIsoTimestamp(startedAt, "Report run startedAt");
|
|
1424
|
+
assertIsoTimestamp(finishedAt, "Report run finishedAt");
|
|
1425
|
+
if (input.status !== "success" && input.status !== "failed") {
|
|
1426
|
+
throw new Error("Report run status must be success or failed");
|
|
1427
|
+
}
|
|
1428
|
+
if (input.scheduleId && !this.getReportSchedule(input.scheduleId)) {
|
|
1429
|
+
throw new Error(`Report schedule not found: ${input.scheduleId}`);
|
|
1430
|
+
}
|
|
1431
|
+
const run = {
|
|
1432
|
+
id: newId("rpr"),
|
|
1433
|
+
scheduleId: input.scheduleId ?? null,
|
|
1434
|
+
status: input.status,
|
|
1435
|
+
startedAt,
|
|
1436
|
+
finishedAt,
|
|
1437
|
+
deliveries: normalizeReportDeliveries(input.deliveries ?? []),
|
|
1438
|
+
error: normalizeNullableRedactedText(input.error, "Report run error", 1000),
|
|
1439
|
+
reportJson: input.reportJson ?? null
|
|
1440
|
+
};
|
|
1441
|
+
this.db.query(`INSERT INTO report_runs (
|
|
1442
|
+
id, schedule_id, status, started_at, finished_at, deliveries_json,
|
|
1443
|
+
error, report_json
|
|
1444
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(run.id, run.scheduleId, run.status, run.startedAt, run.finishedAt, JSON.stringify(run.deliveries), run.error, run.reportJson ? JSON.stringify(run.reportJson) : null);
|
|
1445
|
+
if (run.scheduleId) {
|
|
1446
|
+
this.advanceReportSchedule(run.scheduleId, run.finishedAt);
|
|
1447
|
+
}
|
|
1448
|
+
return run;
|
|
1449
|
+
}
|
|
1450
|
+
listReportRuns(options = {}) {
|
|
1451
|
+
const limit = clampLimit(options.limit ?? 50);
|
|
1452
|
+
const rows = options.scheduleId ? this.db.query("SELECT * FROM report_runs WHERE schedule_id = ? ORDER BY started_at DESC, id DESC LIMIT ?").all(options.scheduleId, limit) : this.db.query("SELECT * FROM report_runs ORDER BY started_at DESC, id DESC LIMIT ?").all(limit);
|
|
1453
|
+
return rows.map(reportRunFromRow);
|
|
1454
|
+
}
|
|
1455
|
+
recordAuditEvent(input) {
|
|
1456
|
+
const action = normalizeAuditText(input.action, "Audit action", 160);
|
|
1457
|
+
const createdAt = input.createdAt ?? new Date().toISOString();
|
|
1458
|
+
assertIsoTimestamp(createdAt, "Audit event createdAt");
|
|
1459
|
+
const event = {
|
|
1460
|
+
id: newId("aud"),
|
|
1461
|
+
action,
|
|
1462
|
+
resourceType: normalizeNullableAuditText(input.resourceType, "Audit resourceType", 80),
|
|
1463
|
+
resourceId: normalizeNullableAuditText(input.resourceId, "Audit resourceId", 160),
|
|
1464
|
+
message: normalizeNullableAuditText(input.message, "Audit message", 500),
|
|
1465
|
+
metadata: normalizeAuditMetadata(input.metadata ?? {}),
|
|
1466
|
+
actor: normalizeNullableAuditText(input.actor, "Audit actor", 160),
|
|
1467
|
+
createdAt
|
|
1468
|
+
};
|
|
1469
|
+
this.db.query(`INSERT INTO audit_events (
|
|
1470
|
+
id, action, resource_type, resource_id, message, metadata_json, actor, created_at
|
|
1471
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(event.id, event.action, event.resourceType, event.resourceId, event.message, JSON.stringify(event.metadata), event.actor, event.createdAt);
|
|
1472
|
+
return event;
|
|
1473
|
+
}
|
|
1474
|
+
listAuditEvents(options = {}) {
|
|
1475
|
+
const clauses = [];
|
|
1476
|
+
const args = [];
|
|
1477
|
+
if (options.resourceType) {
|
|
1478
|
+
clauses.push("resource_type = ?");
|
|
1479
|
+
args.push(options.resourceType);
|
|
1480
|
+
}
|
|
1481
|
+
if (options.resourceId) {
|
|
1482
|
+
clauses.push("resource_id = ?");
|
|
1483
|
+
args.push(options.resourceId);
|
|
1484
|
+
}
|
|
1485
|
+
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
1486
|
+
args.push(clampLimit(options.limit ?? 50));
|
|
1487
|
+
const rows = this.db.query(`SELECT * FROM audit_events ${where} ORDER BY created_at DESC, id DESC LIMIT ?`).all(...args);
|
|
1488
|
+
return rows.map(auditEventFromRow);
|
|
1489
|
+
}
|
|
1303
1490
|
acquireCheckLease(monitorId, owner, ttlMs) {
|
|
1304
1491
|
const now = new Date;
|
|
1305
1492
|
const nowIso = now.toISOString();
|
|
@@ -1482,6 +1669,18 @@ class UptimeStore {
|
|
|
1482
1669
|
closeOpenIncident(monitorId, closedAt) {
|
|
1483
1670
|
this.db.query("UPDATE incidents SET status = 'closed', closed_at = ? WHERE monitor_id = ? AND status = 'open'").run(closedAt, monitorId);
|
|
1484
1671
|
}
|
|
1672
|
+
advanceReportSchedule(scheduleId, finishedAt) {
|
|
1673
|
+
const schedule = this.getReportSchedule(scheduleId);
|
|
1674
|
+
if (!schedule)
|
|
1675
|
+
throw new Error(`Report schedule not found: ${scheduleId}`);
|
|
1676
|
+
const finishedMs = Date.parse(finishedAt);
|
|
1677
|
+
let nextMs = Math.max(Date.parse(schedule.nextRunAt), finishedMs);
|
|
1678
|
+
do {
|
|
1679
|
+
nextMs += schedule.intervalSeconds * 1000;
|
|
1680
|
+
} while (nextMs <= finishedMs);
|
|
1681
|
+
const nextRunAt = new Date(nextMs).toISOString();
|
|
1682
|
+
this.db.query("UPDATE report_schedules SET last_run_at = ?, next_run_at = ?, updated_at = ? WHERE id = ?").run(finishedAt, nextRunAt, finishedAt, schedule.id);
|
|
1683
|
+
}
|
|
1485
1684
|
ensureColumn(table, name, definition) {
|
|
1486
1685
|
const columns = this.db.query(`PRAGMA table_info(${table})`).all();
|
|
1487
1686
|
if (!columns.some((column) => column.name === name)) {
|
|
@@ -1560,9 +1759,10 @@ function verifyBackupFile(backupPath) {
|
|
|
1560
1759
|
const missingTables = REQUIRED_TABLES.filter((table) => !tableExists(db, table));
|
|
1561
1760
|
const schemaVersion = missingTables.includes("schema_migrations") ? null : db.query("SELECT value FROM schema_migrations WHERE key = 'schema_version'").get()?.value ?? null;
|
|
1562
1761
|
const currentOk = missingTables.length === 0 && schemaVersion === CURRENT_SCHEMA_VERSION;
|
|
1563
|
-
const restorableV1 = schemaVersion === "1" && missingTables.every((table) => PROBE_TABLES.has(table));
|
|
1762
|
+
const restorableV1 = schemaVersion === "1" && missingTables.every((table) => PROBE_TABLES.has(table) || REPORT_AUDIT_TABLES.has(table));
|
|
1763
|
+
const restorableV2 = schemaVersion === "2" && missingTables.every((table) => REPORT_AUDIT_TABLES.has(table));
|
|
1564
1764
|
return {
|
|
1565
|
-
ok: integrity === "ok" && (currentOk || restorableV1),
|
|
1765
|
+
ok: integrity === "ok" && (currentOk || restorableV1 || restorableV2),
|
|
1566
1766
|
backupPath,
|
|
1567
1767
|
integrity,
|
|
1568
1768
|
schemaVersion,
|
|
@@ -1726,6 +1926,175 @@ function normalizeScheduleSlot(value) {
|
|
|
1726
1926
|
rejectControlCharacters2(slot, "Probe job scheduleSlot");
|
|
1727
1927
|
return slot;
|
|
1728
1928
|
}
|
|
1929
|
+
function normalizeReportScheduleInput(input) {
|
|
1930
|
+
const name = input.name?.trim();
|
|
1931
|
+
if (!name)
|
|
1932
|
+
throw new Error("Report schedule name is required");
|
|
1933
|
+
rejectControlCharacters2(name, "Report schedule name");
|
|
1934
|
+
const intervalSeconds = boundedInteger2(input.intervalSeconds, "intervalSeconds", MIN_INTERVAL_SECONDS, MAX_INTERVAL_SECONDS);
|
|
1935
|
+
const nextRunAt = input.nextRunAt ?? new Date().toISOString();
|
|
1936
|
+
assertIsoTimestamp(nextRunAt, "Report schedule nextRunAt");
|
|
1937
|
+
const enabled = normalizeEnabled(input.enabled);
|
|
1938
|
+
const subject = normalizeNullableBoundedText(input.subject, "Report schedule subject", 200);
|
|
1939
|
+
const channels = normalizeReportChannels(input.channels);
|
|
1940
|
+
return { name, intervalSeconds, nextRunAt, enabled, subject, channels };
|
|
1941
|
+
}
|
|
1942
|
+
function normalizeReportChannels(channels) {
|
|
1943
|
+
if (!channels || typeof channels !== "object")
|
|
1944
|
+
throw new Error("Report schedule channels are required");
|
|
1945
|
+
const normalized = {};
|
|
1946
|
+
if (channels.email !== undefined)
|
|
1947
|
+
normalized.email = normalizeChannelTarget(channels.email, "email", ["apiUrl", "from", "to", "subject", "providerId"]);
|
|
1948
|
+
if (channels.sms !== undefined)
|
|
1949
|
+
normalized.sms = normalizeChannelTarget(channels.sms, "sms", ["apiUrl", "from", "to"]);
|
|
1950
|
+
if (channels.logs !== undefined)
|
|
1951
|
+
normalized.logs = normalizeChannelTarget(channels.logs, "logs", ["apiUrl", "projectId", "environment", "service"]);
|
|
1952
|
+
if (!normalized.email && !normalized.sms && !normalized.logs) {
|
|
1953
|
+
throw new Error("Report schedule requires at least one channel");
|
|
1954
|
+
}
|
|
1955
|
+
return normalized;
|
|
1956
|
+
}
|
|
1957
|
+
function normalizeChannelTarget(value, channel, allowedKeys) {
|
|
1958
|
+
if (value === false || value == null)
|
|
1959
|
+
return false;
|
|
1960
|
+
if (value === true)
|
|
1961
|
+
return true;
|
|
1962
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1963
|
+
throw new Error(`Report schedule ${channel} channel must be true or an object`);
|
|
1964
|
+
}
|
|
1965
|
+
const record = value;
|
|
1966
|
+
const normalized = {};
|
|
1967
|
+
for (const [key, rawValue] of Object.entries(record)) {
|
|
1968
|
+
if (!allowedKeys.includes(key)) {
|
|
1969
|
+
if (/key|token|secret|password|credential|auth/i.test(key)) {
|
|
1970
|
+
throw new Error("Report schedules must not persist API keys or tokens; use environment variables or cloud channel refs");
|
|
1971
|
+
}
|
|
1972
|
+
throw new Error(`Unsupported report schedule ${channel} channel field: ${key}`);
|
|
1973
|
+
}
|
|
1974
|
+
if (rawValue === undefined || rawValue === null || rawValue === "")
|
|
1975
|
+
continue;
|
|
1976
|
+
if (key === "apiUrl" && Array.isArray(rawValue)) {
|
|
1977
|
+
throw new Error(`Report schedule ${channel}.${key} must be a string`);
|
|
1978
|
+
}
|
|
1979
|
+
if (Array.isArray(rawValue)) {
|
|
1980
|
+
const items = rawValue.map((item) => normalizeBoundedText(String(item), `Report schedule ${channel}.${key}`, 300));
|
|
1981
|
+
if (items.length > 0)
|
|
1982
|
+
normalized[key] = items;
|
|
1983
|
+
} else if (typeof rawValue === "string" || typeof rawValue === "number") {
|
|
1984
|
+
normalized[key] = key === "apiUrl" ? normalizeHttpIntegrationUrl(String(rawValue)) : normalizeBoundedText(String(rawValue), `Report schedule ${channel}.${key}`, 500);
|
|
1985
|
+
} else {
|
|
1986
|
+
throw new Error(`Report schedule ${channel}.${key} must be a string or string array`);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
return Object.keys(normalized).length > 0 ? normalized : true;
|
|
1990
|
+
}
|
|
1991
|
+
function normalizeHttpIntegrationUrl(value) {
|
|
1992
|
+
const parsed = new URL(value.trim());
|
|
1993
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
1994
|
+
throw new Error("Report schedule integration API URL must use http or https");
|
|
1995
|
+
}
|
|
1996
|
+
if (parsed.username || parsed.password) {
|
|
1997
|
+
throw new Error("Report schedule integration API URL must not include credentials");
|
|
1998
|
+
}
|
|
1999
|
+
for (const key of parsed.searchParams.keys()) {
|
|
2000
|
+
if (SECRET_URL_PARAM_PATTERN.test(key)) {
|
|
2001
|
+
throw new Error("Report schedule integration API URL must not include secret query parameters");
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
parsed.hash = "";
|
|
2005
|
+
return parsed.toString();
|
|
2006
|
+
}
|
|
2007
|
+
function normalizeReportDeliveries(deliveries) {
|
|
2008
|
+
return deliveries.map((delivery) => {
|
|
2009
|
+
if (delivery.channel !== "email" && delivery.channel !== "sms" && delivery.channel !== "logs") {
|
|
2010
|
+
throw new Error("Report delivery channel must be email, sms, or logs");
|
|
2011
|
+
}
|
|
2012
|
+
return {
|
|
2013
|
+
channel: delivery.channel,
|
|
2014
|
+
ok: Boolean(delivery.ok),
|
|
2015
|
+
status: delivery.status,
|
|
2016
|
+
id: delivery.id === undefined ? undefined : normalizeRedactedText(String(delivery.id), "Report delivery id", 300),
|
|
2017
|
+
error: delivery.error === undefined ? undefined : normalizeRedactedText(String(delivery.error), "Report delivery error", 1000)
|
|
2018
|
+
};
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
function normalizeAuditText(value, label, maxLength) {
|
|
2022
|
+
return normalizeBoundedText(value ?? "", label, maxLength);
|
|
2023
|
+
}
|
|
2024
|
+
function normalizeNullableAuditText(value, label, maxLength) {
|
|
2025
|
+
return normalizeNullableBoundedText(value, label, maxLength);
|
|
2026
|
+
}
|
|
2027
|
+
function normalizeNullableBoundedText(value, label, maxLength) {
|
|
2028
|
+
if (value == null)
|
|
2029
|
+
return null;
|
|
2030
|
+
const normalized = normalizeRedactedText(value, label, maxLength);
|
|
2031
|
+
return normalized || null;
|
|
2032
|
+
}
|
|
2033
|
+
function normalizeBoundedText(value, label, maxLength) {
|
|
2034
|
+
const normalized = value.trim();
|
|
2035
|
+
rejectControlCharacters2(normalized, label);
|
|
2036
|
+
if (normalized.length > maxLength)
|
|
2037
|
+
throw new Error(`${label} is too long`);
|
|
2038
|
+
return normalized;
|
|
2039
|
+
}
|
|
2040
|
+
function normalizeNullableRedactedText(value, label, maxLength) {
|
|
2041
|
+
if (value == null)
|
|
2042
|
+
return null;
|
|
2043
|
+
const normalized = normalizeRedactedText(value, label, maxLength);
|
|
2044
|
+
return normalized || null;
|
|
2045
|
+
}
|
|
2046
|
+
function normalizeRedactedText(value, label, maxLength) {
|
|
2047
|
+
return normalizeBoundedText(redactSecretString(value), label, maxLength);
|
|
2048
|
+
}
|
|
2049
|
+
function normalizeAuditMetadata(value) {
|
|
2050
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2051
|
+
throw new Error("Audit metadata must be an object");
|
|
2052
|
+
}
|
|
2053
|
+
return redactAuditSecrets(JSON.parse(JSON.stringify(value)));
|
|
2054
|
+
}
|
|
2055
|
+
function redactAuditSecrets(value) {
|
|
2056
|
+
if (Array.isArray(value))
|
|
2057
|
+
return value.map(redactAuditSecrets);
|
|
2058
|
+
if (typeof value === "string")
|
|
2059
|
+
return redactSecretString(value);
|
|
2060
|
+
if (!value || typeof value !== "object")
|
|
2061
|
+
return value;
|
|
2062
|
+
const output = {};
|
|
2063
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
2064
|
+
output[key] = /key|token|secret|password|credential|auth/i.test(key) ? "[REDACTED]" : redactAuditSecrets(nested);
|
|
2065
|
+
}
|
|
2066
|
+
return output;
|
|
2067
|
+
}
|
|
2068
|
+
function redactSecretString(value) {
|
|
2069
|
+
let output = value.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]");
|
|
2070
|
+
output = output.replace(/https?:\/\/[^\s"'<>]+/gi, (match) => redactUrlString(match));
|
|
2071
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(output))
|
|
2072
|
+
return output;
|
|
2073
|
+
return redactUrlString(output);
|
|
2074
|
+
}
|
|
2075
|
+
function redactUrlString(value) {
|
|
2076
|
+
let trailing = "";
|
|
2077
|
+
let candidate = value;
|
|
2078
|
+
while (/[),.;\]]$/.test(candidate)) {
|
|
2079
|
+
trailing = `${candidate.slice(-1)}${trailing}`;
|
|
2080
|
+
candidate = candidate.slice(0, -1);
|
|
2081
|
+
}
|
|
2082
|
+
try {
|
|
2083
|
+
const parsed = new URL(candidate);
|
|
2084
|
+
if (parsed.username)
|
|
2085
|
+
parsed.username = "[REDACTED]";
|
|
2086
|
+
if (parsed.password)
|
|
2087
|
+
parsed.password = "[REDACTED]";
|
|
2088
|
+
for (const key of [...parsed.searchParams.keys()]) {
|
|
2089
|
+
if (SECRET_URL_PARAM_PATTERN.test(key))
|
|
2090
|
+
parsed.searchParams.set(key, "[REDACTED]");
|
|
2091
|
+
}
|
|
2092
|
+
parsed.hash = "";
|
|
2093
|
+
return `${parsed.toString()}${trailing}`;
|
|
2094
|
+
} catch {
|
|
2095
|
+
return value;
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
1729
2098
|
function assertIsoTimestamp(value, label) {
|
|
1730
2099
|
if (!Number.isFinite(Date.parse(value))) {
|
|
1731
2100
|
throw new Error(`${label} must be an ISO timestamp`);
|
|
@@ -1825,12 +2194,66 @@ function probeCheckJobFromRow(row) {
|
|
|
1825
2194
|
updatedAt: row.updated_at
|
|
1826
2195
|
};
|
|
1827
2196
|
}
|
|
2197
|
+
function reportScheduleFromRow(row) {
|
|
2198
|
+
return {
|
|
2199
|
+
id: row.id,
|
|
2200
|
+
name: row.name,
|
|
2201
|
+
enabled: Boolean(row.enabled),
|
|
2202
|
+
intervalSeconds: row.interval_seconds,
|
|
2203
|
+
nextRunAt: row.next_run_at,
|
|
2204
|
+
lastRunAt: row.last_run_at,
|
|
2205
|
+
subject: row.subject,
|
|
2206
|
+
channels: parseReportChannels(row.channels_json),
|
|
2207
|
+
createdAt: row.created_at,
|
|
2208
|
+
updatedAt: row.updated_at
|
|
2209
|
+
};
|
|
2210
|
+
}
|
|
2211
|
+
function reportRunFromRow(row) {
|
|
2212
|
+
return {
|
|
2213
|
+
id: row.id,
|
|
2214
|
+
scheduleId: row.schedule_id,
|
|
2215
|
+
status: row.status,
|
|
2216
|
+
startedAt: row.started_at,
|
|
2217
|
+
finishedAt: row.finished_at,
|
|
2218
|
+
deliveries: parseReportDeliveries(row.deliveries_json),
|
|
2219
|
+
error: row.error,
|
|
2220
|
+
reportJson: parseRecord(row.report_json)
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
function auditEventFromRow(row) {
|
|
2224
|
+
return {
|
|
2225
|
+
id: row.id,
|
|
2226
|
+
action: row.action,
|
|
2227
|
+
resourceType: row.resource_type,
|
|
2228
|
+
resourceId: row.resource_id,
|
|
2229
|
+
message: row.message,
|
|
2230
|
+
metadata: parseRecord(row.metadata_json) ?? {},
|
|
2231
|
+
actor: row.actor,
|
|
2232
|
+
createdAt: row.created_at
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
1828
2235
|
function parseEvidence(value) {
|
|
1829
2236
|
if (!value)
|
|
1830
2237
|
return null;
|
|
1831
2238
|
const parsed = parseJson(value);
|
|
1832
2239
|
return parsed && typeof parsed === "object" ? parsed : null;
|
|
1833
2240
|
}
|
|
2241
|
+
function parseReportChannels(value) {
|
|
2242
|
+
const parsed = parseJson(value);
|
|
2243
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
2244
|
+
return {};
|
|
2245
|
+
return parsed;
|
|
2246
|
+
}
|
|
2247
|
+
function parseReportDeliveries(value) {
|
|
2248
|
+
const parsed = parseJson(value);
|
|
2249
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
2250
|
+
}
|
|
2251
|
+
function parseRecord(value) {
|
|
2252
|
+
if (!value)
|
|
2253
|
+
return null;
|
|
2254
|
+
const parsed = parseJson(value);
|
|
2255
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
2256
|
+
}
|
|
1834
2257
|
function parseJson(value) {
|
|
1835
2258
|
try {
|
|
1836
2259
|
return JSON.parse(value);
|
|
@@ -2153,6 +2576,7 @@ class UptimeService {
|
|
|
2153
2576
|
checkRunner;
|
|
2154
2577
|
leaseOwner = `svc_${randomUUID3().replace(/-/g, "").slice(0, 18)}`;
|
|
2155
2578
|
inFlightChecks = new Set;
|
|
2579
|
+
inFlightReportSchedules = new Set;
|
|
2156
2580
|
constructor(options = {}) {
|
|
2157
2581
|
this.store = options.store ?? new UptimeStore({ mode: "local", ...options });
|
|
2158
2582
|
this.checkRunner = options.checkRunner ?? runMonitorCheck;
|
|
@@ -2246,6 +2670,115 @@ class UptimeService {
|
|
|
2246
2670
|
}
|
|
2247
2671
|
return sendUptimeReport(this.summary(), options);
|
|
2248
2672
|
}
|
|
2673
|
+
createReportSchedule(input) {
|
|
2674
|
+
const store = this.reportStore();
|
|
2675
|
+
const schedule = store.createReportSchedule(input);
|
|
2676
|
+
this.audit("report_schedule.create", "report_schedule", schedule.id, `Created report schedule ${schedule.name}`, {
|
|
2677
|
+
name: schedule.name,
|
|
2678
|
+
enabled: schedule.enabled,
|
|
2679
|
+
intervalSeconds: schedule.intervalSeconds,
|
|
2680
|
+
channels: enabledReportChannels(schedule)
|
|
2681
|
+
});
|
|
2682
|
+
return schedule;
|
|
2683
|
+
}
|
|
2684
|
+
listReportSchedules(options = {}) {
|
|
2685
|
+
return this.reportStore().listReportSchedules(options);
|
|
2686
|
+
}
|
|
2687
|
+
getReportSchedule(idOrName) {
|
|
2688
|
+
return this.reportStore().getReportSchedule(idOrName);
|
|
2689
|
+
}
|
|
2690
|
+
updateReportSchedule(idOrName, input) {
|
|
2691
|
+
const store = this.reportStore();
|
|
2692
|
+
const schedule = store.updateReportSchedule(idOrName, input);
|
|
2693
|
+
this.audit("report_schedule.update", "report_schedule", schedule.id, `Updated report schedule ${schedule.name}`, {
|
|
2694
|
+
name: schedule.name,
|
|
2695
|
+
enabled: schedule.enabled,
|
|
2696
|
+
intervalSeconds: schedule.intervalSeconds,
|
|
2697
|
+
channels: enabledReportChannels(schedule)
|
|
2698
|
+
});
|
|
2699
|
+
return schedule;
|
|
2700
|
+
}
|
|
2701
|
+
deleteReportSchedule(idOrName) {
|
|
2702
|
+
const store = this.reportStore();
|
|
2703
|
+
const schedule = store.getReportSchedule(idOrName);
|
|
2704
|
+
const deleted = store.deleteReportSchedule(idOrName);
|
|
2705
|
+
if (deleted && schedule) {
|
|
2706
|
+
this.audit("report_schedule.delete", "report_schedule", schedule.id, `Deleted report schedule ${schedule.name}`, {
|
|
2707
|
+
name: schedule.name
|
|
2708
|
+
});
|
|
2709
|
+
}
|
|
2710
|
+
return deleted;
|
|
2711
|
+
}
|
|
2712
|
+
listReportRuns(options = {}) {
|
|
2713
|
+
return this.reportStore().listReportRuns(options);
|
|
2714
|
+
}
|
|
2715
|
+
listAuditEvents(options = {}) {
|
|
2716
|
+
return this.reportStore().listAuditEvents(options);
|
|
2717
|
+
}
|
|
2718
|
+
recordAuditEvent(input) {
|
|
2719
|
+
return this.reportStore().recordAuditEvent(input);
|
|
2720
|
+
}
|
|
2721
|
+
async runReportSchedule(idOrName, options = {}) {
|
|
2722
|
+
const store = this.reportStore();
|
|
2723
|
+
const schedule = store.getReportSchedule(idOrName);
|
|
2724
|
+
if (!schedule)
|
|
2725
|
+
throw new Error(`Report schedule not found: ${idOrName}`);
|
|
2726
|
+
if (!schedule.enabled)
|
|
2727
|
+
throw new Error(`Report schedule is disabled: ${schedule.name}`);
|
|
2728
|
+
if (this.inFlightReportSchedules.has(schedule.id))
|
|
2729
|
+
throw new Error(`Report schedule already running: ${schedule.name}`);
|
|
2730
|
+
this.inFlightReportSchedules.add(schedule.id);
|
|
2731
|
+
try {
|
|
2732
|
+
const startedAt = new Date().toISOString();
|
|
2733
|
+
let deliveries = [];
|
|
2734
|
+
let error = null;
|
|
2735
|
+
let reportJson = null;
|
|
2736
|
+
try {
|
|
2737
|
+
const report = this.buildReport({ subject: schedule.subject ?? undefined });
|
|
2738
|
+
reportJson = report.json;
|
|
2739
|
+
deliveries = await this.sendReport({
|
|
2740
|
+
subject: schedule.subject ?? undefined,
|
|
2741
|
+
email: schedule.channels.email,
|
|
2742
|
+
sms: schedule.channels.sms,
|
|
2743
|
+
logs: schedule.channels.logs,
|
|
2744
|
+
fetchImpl: options.fetchImpl
|
|
2745
|
+
});
|
|
2746
|
+
const failed = deliveries.filter((delivery) => !delivery.ok);
|
|
2747
|
+
if (failed.length > 0) {
|
|
2748
|
+
error = failed.map((delivery) => `${delivery.channel}: ${delivery.error ?? delivery.status ?? "failed"}`).join("; ");
|
|
2749
|
+
}
|
|
2750
|
+
} catch (caught) {
|
|
2751
|
+
error = caught instanceof Error ? caught.message : String(caught);
|
|
2752
|
+
}
|
|
2753
|
+
const finishedAt = new Date().toISOString();
|
|
2754
|
+
const run = store.recordReportRun({
|
|
2755
|
+
scheduleId: schedule.id,
|
|
2756
|
+
status: error ? "failed" : "success",
|
|
2757
|
+
startedAt,
|
|
2758
|
+
finishedAt,
|
|
2759
|
+
deliveries,
|
|
2760
|
+
error,
|
|
2761
|
+
reportJson
|
|
2762
|
+
});
|
|
2763
|
+
this.audit("report_schedule.run", "report_schedule", schedule.id, `Ran report schedule ${schedule.name}`, {
|
|
2764
|
+
runId: run.id,
|
|
2765
|
+
status: run.status,
|
|
2766
|
+
deliveryChannels: run.deliveries.map((delivery) => ({ channel: delivery.channel, ok: delivery.ok }))
|
|
2767
|
+
});
|
|
2768
|
+
return run;
|
|
2769
|
+
} finally {
|
|
2770
|
+
this.inFlightReportSchedules.delete(schedule.id);
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
async runDueReportSchedules(now = new Date, options = {}) {
|
|
2774
|
+
const store = this.reportStore();
|
|
2775
|
+
const schedules = store.listDueReportSchedules(now.toISOString());
|
|
2776
|
+
const runs = [];
|
|
2777
|
+
for (const schedule of schedules) {
|
|
2778
|
+
runs.push(await this.runReportSchedule(schedule.id, options));
|
|
2779
|
+
}
|
|
2780
|
+
return runs;
|
|
2781
|
+
}
|
|
2249
2782
|
async checkMonitor(idOrName) {
|
|
2250
2783
|
if (this.store.mode === "hosted")
|
|
2251
2784
|
throw new Error("hosted checks require check_jobs and probes");
|
|
@@ -2304,6 +2837,9 @@ class UptimeService {
|
|
|
2304
2837
|
this.runDueChecks().catch((error) => {
|
|
2305
2838
|
console.error(error instanceof Error ? error.message : String(error));
|
|
2306
2839
|
});
|
|
2840
|
+
this.runDueReportSchedules(new Date, { fetchImpl: options.reportFetchImpl }).catch((error) => {
|
|
2841
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
2842
|
+
});
|
|
2307
2843
|
}, tickMs);
|
|
2308
2844
|
return {
|
|
2309
2845
|
stop: () => clearInterval(timer)
|
|
@@ -2363,6 +2899,40 @@ class UptimeService {
|
|
|
2363
2899
|
}
|
|
2364
2900
|
return store;
|
|
2365
2901
|
}
|
|
2902
|
+
reportStore() {
|
|
2903
|
+
if (this.store.mode === "hosted") {
|
|
2904
|
+
throw new Error("hosted report schedules require cloud channel refs, workspace stores, and audit logging");
|
|
2905
|
+
}
|
|
2906
|
+
const store = this.store;
|
|
2907
|
+
const required = [
|
|
2908
|
+
"createReportSchedule",
|
|
2909
|
+
"listReportSchedules",
|
|
2910
|
+
"listDueReportSchedules",
|
|
2911
|
+
"getReportSchedule",
|
|
2912
|
+
"updateReportSchedule",
|
|
2913
|
+
"deleteReportSchedule",
|
|
2914
|
+
"recordReportRun",
|
|
2915
|
+
"listReportRuns",
|
|
2916
|
+
"recordAuditEvent",
|
|
2917
|
+
"listAuditEvents"
|
|
2918
|
+
];
|
|
2919
|
+
for (const method of required) {
|
|
2920
|
+
if (typeof store[method] !== "function") {
|
|
2921
|
+
throw new Error("report scheduling requires a report-capable store");
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
return store;
|
|
2925
|
+
}
|
|
2926
|
+
audit(action, resourceType, resourceId, message, metadata) {
|
|
2927
|
+
this.reportStore().recordAuditEvent({
|
|
2928
|
+
action,
|
|
2929
|
+
resourceType,
|
|
2930
|
+
resourceId,
|
|
2931
|
+
message,
|
|
2932
|
+
metadata,
|
|
2933
|
+
actor: "local"
|
|
2934
|
+
});
|
|
2935
|
+
}
|
|
2366
2936
|
submitProbeResultInTransaction(input) {
|
|
2367
2937
|
const store = this.probeStore();
|
|
2368
2938
|
const probe = store.getProbeIdentity(input.probeId);
|
|
@@ -2456,6 +3026,9 @@ class MonitorCheckBusyError extends Error {
|
|
|
2456
3026
|
this.name = "MonitorCheckBusyError";
|
|
2457
3027
|
}
|
|
2458
3028
|
}
|
|
3029
|
+
function enabledReportChannels(schedule) {
|
|
3030
|
+
return ["email", "sms", "logs"].filter((channel) => Boolean(schedule.channels[channel]));
|
|
3031
|
+
}
|
|
2459
3032
|
function validateProbeSubmission(input) {
|
|
2460
3033
|
if (!input.jobId.trim())
|
|
2461
3034
|
throw new Error("Probe submission jobId is required");
|
|
@@ -2986,9 +3559,54 @@ async function handleApiRoute(service, request, url, apiPath, options, hosted) {
|
|
|
2986
3559
|
const input = await jsonBody(request);
|
|
2987
3560
|
return json(await service.sendReport({ ...input, fetchImpl: options.fetchImpl }));
|
|
2988
3561
|
}
|
|
3562
|
+
if (hosted && (apiPath.startsWith("/api/report-schedules") || apiPath.startsWith("/api/report-runs") || apiPath.startsWith("/api/audit-events"))) {
|
|
3563
|
+
throw new ApiError("hosted report schedules require cloud channel refs, workspace stores, and audit logging", 501);
|
|
3564
|
+
}
|
|
2989
3565
|
if (hosted && apiPath.startsWith("/api/probes")) {
|
|
2990
3566
|
throw new ApiError("hosted probe APIs require cloud check_jobs, workspace stores, and audit logging", 501);
|
|
2991
3567
|
}
|
|
3568
|
+
if (request.method === "GET" && apiPath === "/api/report-schedules") {
|
|
3569
|
+
return json(service.listReportSchedules({ includeDisabled: url.searchParams.get("includeDisabled") === "true" }));
|
|
3570
|
+
}
|
|
3571
|
+
if (request.method === "POST" && apiPath === "/api/report-schedules") {
|
|
3572
|
+
return json(service.createReportSchedule(await jsonBody(request)), 201);
|
|
3573
|
+
}
|
|
3574
|
+
if (request.method === "POST" && apiPath === "/api/report-schedules/run-due") {
|
|
3575
|
+
const input = await jsonBody(request);
|
|
3576
|
+
const now = input.now ? new Date(input.now) : new Date;
|
|
3577
|
+
return json(await service.runDueReportSchedules(now, { fetchImpl: options.fetchImpl }));
|
|
3578
|
+
}
|
|
3579
|
+
const reportScheduleRunMatch = apiPath.match(/^\/api\/report-schedules\/([^/]+)\/run$/);
|
|
3580
|
+
if (request.method === "POST" && reportScheduleRunMatch) {
|
|
3581
|
+
return json(await service.runReportSchedule(decodeURIComponent(reportScheduleRunMatch[1]), { fetchImpl: options.fetchImpl }));
|
|
3582
|
+
}
|
|
3583
|
+
const reportScheduleMatch = apiPath.match(/^\/api\/report-schedules\/([^/]+)$/);
|
|
3584
|
+
if (reportScheduleMatch) {
|
|
3585
|
+
const id = decodeURIComponent(reportScheduleMatch[1]);
|
|
3586
|
+
if (request.method === "GET") {
|
|
3587
|
+
const schedule = service.getReportSchedule(id);
|
|
3588
|
+
return schedule ? json(schedule) : json({ error: "not found" }, 404);
|
|
3589
|
+
}
|
|
3590
|
+
if (request.method === "PATCH") {
|
|
3591
|
+
return json(service.updateReportSchedule(id, await jsonBody(request)));
|
|
3592
|
+
}
|
|
3593
|
+
if (request.method === "DELETE") {
|
|
3594
|
+
return json({ deleted: service.deleteReportSchedule(id) });
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
if (request.method === "GET" && apiPath === "/api/report-runs") {
|
|
3598
|
+
return json(service.listReportRuns({
|
|
3599
|
+
scheduleId: url.searchParams.get("scheduleId") ?? undefined,
|
|
3600
|
+
limit: numericParam(url, "limit", 50)
|
|
3601
|
+
}));
|
|
3602
|
+
}
|
|
3603
|
+
if (request.method === "GET" && apiPath === "/api/audit-events") {
|
|
3604
|
+
return json(service.listAuditEvents({
|
|
3605
|
+
resourceType: url.searchParams.get("resourceType") ?? undefined,
|
|
3606
|
+
resourceId: url.searchParams.get("resourceId") ?? undefined,
|
|
3607
|
+
limit: numericParam(url, "limit", 50)
|
|
3608
|
+
}));
|
|
3609
|
+
}
|
|
2992
3610
|
if (request.method === "GET" && apiPath === "/api/monitors") {
|
|
2993
3611
|
return json(service.listMonitors({ includeDisabled: url.searchParams.get("includeDisabled") === "true" }));
|
|
2994
3612
|
}
|
|
@@ -3084,6 +3702,10 @@ async function handleApiRoute(service, request, url, apiPath, options, hosted) {
|
|
|
3084
3702
|
function hostedScopeFor(method, apiPath) {
|
|
3085
3703
|
if (method === "POST" && apiPath === "/api/report")
|
|
3086
3704
|
return "uptime:report";
|
|
3705
|
+
if (apiPath.startsWith("/api/report-schedules") || apiPath.startsWith("/api/report-runs"))
|
|
3706
|
+
return method === "GET" ? "uptime:read" : "uptime:report";
|
|
3707
|
+
if (apiPath.startsWith("/api/audit-events"))
|
|
3708
|
+
return method === "GET" ? "uptime:read" : "uptime:admin";
|
|
3087
3709
|
if (apiPath.startsWith("/api/probes"))
|
|
3088
3710
|
return method === "GET" ? "uptime:read" : "uptime:probe";
|
|
3089
3711
|
if (method === "POST" && (apiPath === "/api/check-all" || /\/check$/.test(apiPath)))
|