@hasna/uptime 0.1.9 → 0.1.11
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 +34 -0
- package/SECURITY.md +4 -2
- package/dist/api.js +179 -58
- package/dist/checks.d.ts +2 -1
- package/dist/checks.d.ts.map +1 -1
- package/dist/checks.js +2 -1
- package/dist/cli/index.js +180 -59
- package/dist/cloud-plan.js +1 -1
- package/dist/imports.d.ts +6 -2
- package/dist/imports.d.ts.map +1 -1
- package/dist/imports.js +72 -8
- package/dist/index.js +180 -59
- package/dist/mcp/index.js +166 -47
- package/dist/service.d.ts +36 -10
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +166 -47
- package/dist/store.d.ts +13 -3
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +140 -26
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/aws-deployment-runbook.md +327 -14
- package/infra/aws/outputs.tf +35 -0
- package/infra/aws/terraform.tfvars.example +1 -1
- package/infra/aws/variables.tf +1 -1
- package/package.json +1 -1
package/dist/imports.js
CHANGED
|
@@ -81,13 +81,74 @@ function isDeniedIpv4(ip) {
|
|
|
81
81
|
}
|
|
82
82
|
function isDeniedIpv6(ip) {
|
|
83
83
|
const normalized = ip.toLowerCase();
|
|
84
|
-
|
|
84
|
+
const mappedIpv4 = ipv4FromMappedIpv6(normalized);
|
|
85
|
+
if (mappedIpv4)
|
|
86
|
+
return isDeniedIpv4(mappedIpv4);
|
|
87
|
+
const words = parseIpv6Words(normalized);
|
|
88
|
+
return normalized === "::" || normalized === "::1" || words !== null && (words[0] & 65472) === 65152 || normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("ff");
|
|
89
|
+
}
|
|
90
|
+
function ipv4FromMappedIpv6(ip) {
|
|
91
|
+
const words = parseIpv6Words(ip);
|
|
92
|
+
if (!words)
|
|
93
|
+
return null;
|
|
94
|
+
if (words[0] !== 0 || words[1] !== 0 || words[2] !== 0 || words[3] !== 0 || words[4] !== 0 || words[5] !== 65535) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return [
|
|
98
|
+
words[6] >> 8,
|
|
99
|
+
words[6] & 255,
|
|
100
|
+
words[7] >> 8,
|
|
101
|
+
words[7] & 255
|
|
102
|
+
].join(".");
|
|
103
|
+
}
|
|
104
|
+
function parseIpv6Words(value) {
|
|
105
|
+
let ip = value.toLowerCase();
|
|
106
|
+
const zoneIndex = ip.indexOf("%");
|
|
107
|
+
if (zoneIndex >= 0)
|
|
108
|
+
ip = ip.slice(0, zoneIndex);
|
|
109
|
+
if (ip.includes(".")) {
|
|
110
|
+
const lastColon = ip.lastIndexOf(":");
|
|
111
|
+
if (lastColon < 0)
|
|
112
|
+
return null;
|
|
113
|
+
const ipv4 = parseIpv4Words(ip.slice(lastColon + 1));
|
|
114
|
+
if (!ipv4)
|
|
115
|
+
return null;
|
|
116
|
+
ip = `${ip.slice(0, lastColon)}:${(ipv4[0] << 8 | ipv4[1]).toString(16)}:${(ipv4[2] << 8 | ipv4[3]).toString(16)}`;
|
|
117
|
+
}
|
|
118
|
+
const compressed = ip.split("::");
|
|
119
|
+
if (compressed.length > 2)
|
|
120
|
+
return null;
|
|
121
|
+
const left = parseIpv6Side(compressed[0]);
|
|
122
|
+
const right = compressed.length === 2 ? parseIpv6Side(compressed[1]) : [];
|
|
123
|
+
if (!left || !right)
|
|
124
|
+
return null;
|
|
125
|
+
if (compressed.length === 1)
|
|
126
|
+
return left.length === 8 ? left : null;
|
|
127
|
+
const missing = 8 - left.length - right.length;
|
|
128
|
+
if (missing < 1)
|
|
129
|
+
return null;
|
|
130
|
+
return [...left, ...Array(missing).fill(0), ...right];
|
|
131
|
+
}
|
|
132
|
+
function parseIpv6Side(value) {
|
|
133
|
+
if (!value)
|
|
134
|
+
return [];
|
|
135
|
+
const words = value.split(":");
|
|
136
|
+
if (words.some((word) => !/^[0-9a-f]{1,4}$/.test(word)))
|
|
137
|
+
return null;
|
|
138
|
+
return words.map((word) => Number.parseInt(word, 16));
|
|
139
|
+
}
|
|
140
|
+
function parseIpv4Words(value) {
|
|
141
|
+
const words = value.split(".").map((part) => Number(part));
|
|
142
|
+
if (words.length !== 4 || words.some((word) => !Number.isInteger(word) || word < 0 || word > 255)) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
return words;
|
|
85
146
|
}
|
|
86
147
|
|
|
87
148
|
// src/imports.ts
|
|
88
|
-
function previewImport(store, request) {
|
|
149
|
+
function previewImport(store, request, options = {}) {
|
|
89
150
|
const source = normalizeSource(request.source);
|
|
90
|
-
const items = dedupePreviewItems(request.records.map((record) => previewRecord(store, source, record, request.defaults ?? {})));
|
|
151
|
+
const items = dedupePreviewItems(request.records.map((record) => previewRecord(store, source, record, request.defaults ?? {}, options)));
|
|
91
152
|
return {
|
|
92
153
|
source,
|
|
93
154
|
generatedAt: new Date().toISOString(),
|
|
@@ -161,7 +222,7 @@ function rollbackImport(store, batchId) {
|
|
|
161
222
|
items
|
|
162
223
|
};
|
|
163
224
|
}
|
|
164
|
-
function previewRecord(store, source, record, defaults) {
|
|
225
|
+
function previewRecord(store, source, record, defaults, options) {
|
|
165
226
|
const warnings = [];
|
|
166
227
|
let candidate;
|
|
167
228
|
try {
|
|
@@ -181,13 +242,16 @@ function previewRecord(store, source, record, defaults) {
|
|
|
181
242
|
reason: error instanceof Error ? error.message : String(error)
|
|
182
243
|
};
|
|
183
244
|
}
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
|
|
245
|
+
const monitorOptions = options.workspaceId ? { workspaceId: options.workspaceId } : undefined;
|
|
246
|
+
const rawProvenance = store.getProvenance(candidate.source, candidate.sourceId);
|
|
247
|
+
const provenanceMonitor = rawProvenance ? store.getMonitor(rawProvenance.monitorId, monitorOptions) : null;
|
|
248
|
+
const provenance = provenanceMonitor ? rawProvenance : null;
|
|
249
|
+
const monitor = provenanceMonitor ?? store.getMonitor(candidate.name, monitorOptions);
|
|
250
|
+
if (rawProvenance && !provenanceMonitor && !options.workspaceId) {
|
|
187
251
|
return { candidate, action: "create", monitor: null, provenance, warnings: ["source provenance points to a missing monitor"], reason: null };
|
|
188
252
|
}
|
|
189
253
|
if (provenance && monitor) {
|
|
190
|
-
const nameOwner = store.getMonitor(candidate.name);
|
|
254
|
+
const nameOwner = store.getMonitor(candidate.name, monitorOptions);
|
|
191
255
|
if (nameOwner && nameOwner.id !== monitor.id) {
|
|
192
256
|
return {
|
|
193
257
|
candidate,
|
package/dist/index.js
CHANGED
|
@@ -276,13 +276,74 @@ function isDeniedIpv4(ip) {
|
|
|
276
276
|
}
|
|
277
277
|
function isDeniedIpv6(ip) {
|
|
278
278
|
const normalized = ip.toLowerCase();
|
|
279
|
-
|
|
279
|
+
const mappedIpv4 = ipv4FromMappedIpv6(normalized);
|
|
280
|
+
if (mappedIpv4)
|
|
281
|
+
return isDeniedIpv4(mappedIpv4);
|
|
282
|
+
const words = parseIpv6Words(normalized);
|
|
283
|
+
return normalized === "::" || normalized === "::1" || words !== null && (words[0] & 65472) === 65152 || normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("ff");
|
|
284
|
+
}
|
|
285
|
+
function ipv4FromMappedIpv6(ip) {
|
|
286
|
+
const words = parseIpv6Words(ip);
|
|
287
|
+
if (!words)
|
|
288
|
+
return null;
|
|
289
|
+
if (words[0] !== 0 || words[1] !== 0 || words[2] !== 0 || words[3] !== 0 || words[4] !== 0 || words[5] !== 65535) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
return [
|
|
293
|
+
words[6] >> 8,
|
|
294
|
+
words[6] & 255,
|
|
295
|
+
words[7] >> 8,
|
|
296
|
+
words[7] & 255
|
|
297
|
+
].join(".");
|
|
298
|
+
}
|
|
299
|
+
function parseIpv6Words(value) {
|
|
300
|
+
let ip = value.toLowerCase();
|
|
301
|
+
const zoneIndex = ip.indexOf("%");
|
|
302
|
+
if (zoneIndex >= 0)
|
|
303
|
+
ip = ip.slice(0, zoneIndex);
|
|
304
|
+
if (ip.includes(".")) {
|
|
305
|
+
const lastColon = ip.lastIndexOf(":");
|
|
306
|
+
if (lastColon < 0)
|
|
307
|
+
return null;
|
|
308
|
+
const ipv4 = parseIpv4Words(ip.slice(lastColon + 1));
|
|
309
|
+
if (!ipv4)
|
|
310
|
+
return null;
|
|
311
|
+
ip = `${ip.slice(0, lastColon)}:${(ipv4[0] << 8 | ipv4[1]).toString(16)}:${(ipv4[2] << 8 | ipv4[3]).toString(16)}`;
|
|
312
|
+
}
|
|
313
|
+
const compressed = ip.split("::");
|
|
314
|
+
if (compressed.length > 2)
|
|
315
|
+
return null;
|
|
316
|
+
const left = parseIpv6Side(compressed[0]);
|
|
317
|
+
const right = compressed.length === 2 ? parseIpv6Side(compressed[1]) : [];
|
|
318
|
+
if (!left || !right)
|
|
319
|
+
return null;
|
|
320
|
+
if (compressed.length === 1)
|
|
321
|
+
return left.length === 8 ? left : null;
|
|
322
|
+
const missing = 8 - left.length - right.length;
|
|
323
|
+
if (missing < 1)
|
|
324
|
+
return null;
|
|
325
|
+
return [...left, ...Array(missing).fill(0), ...right];
|
|
326
|
+
}
|
|
327
|
+
function parseIpv6Side(value) {
|
|
328
|
+
if (!value)
|
|
329
|
+
return [];
|
|
330
|
+
const words = value.split(":");
|
|
331
|
+
if (words.some((word) => !/^[0-9a-f]{1,4}$/.test(word)))
|
|
332
|
+
return null;
|
|
333
|
+
return words.map((word) => Number.parseInt(word, 16));
|
|
334
|
+
}
|
|
335
|
+
function parseIpv4Words(value) {
|
|
336
|
+
const words = value.split(".").map((part) => Number(part));
|
|
337
|
+
if (words.length !== 4 || words.some((word) => !Number.isInteger(word) || word < 0 || word > 255)) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
return words;
|
|
280
341
|
}
|
|
281
342
|
|
|
282
343
|
// src/imports.ts
|
|
283
|
-
function previewImport(store, request) {
|
|
344
|
+
function previewImport(store, request, options = {}) {
|
|
284
345
|
const source = normalizeSource(request.source);
|
|
285
|
-
const items = dedupePreviewItems(request.records.map((record) => previewRecord(store, source, record, request.defaults ?? {})));
|
|
346
|
+
const items = dedupePreviewItems(request.records.map((record) => previewRecord(store, source, record, request.defaults ?? {}, options)));
|
|
286
347
|
return {
|
|
287
348
|
source,
|
|
288
349
|
generatedAt: new Date().toISOString(),
|
|
@@ -356,7 +417,7 @@ function rollbackImport(store, batchId) {
|
|
|
356
417
|
items
|
|
357
418
|
};
|
|
358
419
|
}
|
|
359
|
-
function previewRecord(store, source, record, defaults) {
|
|
420
|
+
function previewRecord(store, source, record, defaults, options) {
|
|
360
421
|
const warnings = [];
|
|
361
422
|
let candidate;
|
|
362
423
|
try {
|
|
@@ -376,13 +437,16 @@ function previewRecord(store, source, record, defaults) {
|
|
|
376
437
|
reason: error instanceof Error ? error.message : String(error)
|
|
377
438
|
};
|
|
378
439
|
}
|
|
379
|
-
const
|
|
380
|
-
const
|
|
381
|
-
|
|
440
|
+
const monitorOptions = options.workspaceId ? { workspaceId: options.workspaceId } : undefined;
|
|
441
|
+
const rawProvenance = store.getProvenance(candidate.source, candidate.sourceId);
|
|
442
|
+
const provenanceMonitor = rawProvenance ? store.getMonitor(rawProvenance.monitorId, monitorOptions) : null;
|
|
443
|
+
const provenance = provenanceMonitor ? rawProvenance : null;
|
|
444
|
+
const monitor = provenanceMonitor ?? store.getMonitor(candidate.name, monitorOptions);
|
|
445
|
+
if (rawProvenance && !provenanceMonitor && !options.workspaceId) {
|
|
382
446
|
return { candidate, action: "create", monitor: null, provenance, warnings: ["source provenance points to a missing monitor"], reason: null };
|
|
383
447
|
}
|
|
384
448
|
if (provenance && monitor) {
|
|
385
|
-
const nameOwner = store.getMonitor(candidate.name);
|
|
449
|
+
const nameOwner = store.getMonitor(candidate.name, monitorOptions);
|
|
386
450
|
if (nameOwner && nameOwner.id !== monitor.id) {
|
|
387
451
|
return {
|
|
388
452
|
candidate,
|
|
@@ -844,7 +908,7 @@ var REQUIRED_TABLES = [
|
|
|
844
908
|
];
|
|
845
909
|
var PROBE_TABLES = new Set(["probe_identities", "probe_check_jobs", "probe_submissions"]);
|
|
846
910
|
var REPORT_AUDIT_TABLES = new Set(["report_schedules", "report_runs", "audit_events"]);
|
|
847
|
-
var CURRENT_SCHEMA_VERSION = "
|
|
911
|
+
var CURRENT_SCHEMA_VERSION = "4";
|
|
848
912
|
|
|
849
913
|
class StaleCheckResultError extends Error {
|
|
850
914
|
constructor(message) {
|
|
@@ -905,7 +969,8 @@ class UptimeStore {
|
|
|
905
969
|
this.db.run(`
|
|
906
970
|
CREATE TABLE IF NOT EXISTS monitors (
|
|
907
971
|
id TEXT PRIMARY KEY,
|
|
908
|
-
|
|
972
|
+
workspace_id TEXT NOT NULL DEFAULT 'local',
|
|
973
|
+
name TEXT NOT NULL,
|
|
909
974
|
kind TEXT NOT NULL CHECK (kind IN ('http', 'tcp', 'browser_page')),
|
|
910
975
|
url TEXT,
|
|
911
976
|
host TEXT,
|
|
@@ -920,9 +985,11 @@ class UptimeStore {
|
|
|
920
985
|
last_checked_at TEXT,
|
|
921
986
|
revision INTEGER NOT NULL DEFAULT 1,
|
|
922
987
|
created_at TEXT NOT NULL,
|
|
923
|
-
updated_at TEXT NOT NULL
|
|
988
|
+
updated_at TEXT NOT NULL,
|
|
989
|
+
UNIQUE (workspace_id, name)
|
|
924
990
|
)
|
|
925
991
|
`);
|
|
992
|
+
this.ensureColumn("monitors", "workspace_id", "TEXT NOT NULL DEFAULT 'local'");
|
|
926
993
|
this.ensureColumn("monitors", "revision", "INTEGER NOT NULL DEFAULT 1");
|
|
927
994
|
this.ensureMonitorKindAllowsBrowserPage();
|
|
928
995
|
this.db.run(`
|
|
@@ -1072,6 +1139,7 @@ class UptimeStore {
|
|
|
1072
1139
|
`);
|
|
1073
1140
|
this.db.query("INSERT OR REPLACE INTO schema_migrations (key, value, updated_at) VALUES ('schema_version', ?, ?)").run(CURRENT_SCHEMA_VERSION, new Date().toISOString());
|
|
1074
1141
|
this.db.run("CREATE INDEX IF NOT EXISTS idx_results_monitor_time ON check_results(monitor_id, checked_at DESC)");
|
|
1142
|
+
this.db.run("CREATE INDEX IF NOT EXISTS idx_monitors_workspace_enabled_name ON monitors(workspace_id, enabled, name)");
|
|
1075
1143
|
this.db.run("CREATE INDEX IF NOT EXISTS idx_incidents_monitor_status ON incidents(monitor_id, status)");
|
|
1076
1144
|
this.db.run("CREATE INDEX IF NOT EXISTS idx_check_leases_until ON check_leases(leased_until)");
|
|
1077
1145
|
this.db.run("CREATE INDEX IF NOT EXISTS idx_monitor_provenance_monitor ON monitor_provenance(monitor_id)");
|
|
@@ -1133,8 +1201,10 @@ class UptimeStore {
|
|
|
1133
1201
|
if (this.mode === "hosted")
|
|
1134
1202
|
assertHostedTargetAllowed(normalized);
|
|
1135
1203
|
const now = new Date().toISOString();
|
|
1204
|
+
const workspaceId = normalizeWorkspaceId(options.workspaceId ?? input.workspaceId ?? "local");
|
|
1136
1205
|
const monitor = {
|
|
1137
1206
|
id: newId("mon"),
|
|
1207
|
+
workspaceId,
|
|
1138
1208
|
name: normalized.name,
|
|
1139
1209
|
kind: normalized.kind,
|
|
1140
1210
|
url: normalized.url ?? null,
|
|
@@ -1153,22 +1223,33 @@ class UptimeStore {
|
|
|
1153
1223
|
updatedAt: now
|
|
1154
1224
|
};
|
|
1155
1225
|
this.db.query(`INSERT INTO monitors (
|
|
1156
|
-
id, name, kind, url, host, port, method, expected_status,
|
|
1226
|
+
id, workspace_id, name, kind, url, host, port, method, expected_status,
|
|
1157
1227
|
interval_seconds, timeout_ms, retry_count, enabled, status,
|
|
1158
1228
|
last_checked_at, revision, created_at, updated_at
|
|
1159
|
-
) 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);
|
|
1229
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(monitor.id, monitor.workspaceId, 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);
|
|
1160
1230
|
return monitor;
|
|
1161
1231
|
}
|
|
1162
1232
|
listMonitors(options = {}) {
|
|
1163
|
-
const
|
|
1233
|
+
const workspaceId = options.workspaceId ? normalizeWorkspaceId(options.workspaceId) : undefined;
|
|
1234
|
+
const clauses = [];
|
|
1235
|
+
const args = [];
|
|
1236
|
+
if (workspaceId) {
|
|
1237
|
+
clauses.push("workspace_id = ?");
|
|
1238
|
+
args.push(workspaceId);
|
|
1239
|
+
}
|
|
1240
|
+
if (!options.includeDisabled)
|
|
1241
|
+
clauses.push("enabled = 1");
|
|
1242
|
+
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
1243
|
+
const rows = this.db.query(`SELECT * FROM monitors ${where} ORDER BY name ASC`).all(...args);
|
|
1164
1244
|
return rows.map(monitorFromRow);
|
|
1165
1245
|
}
|
|
1166
|
-
getMonitor(idOrName) {
|
|
1167
|
-
const
|
|
1246
|
+
getMonitor(idOrName, options = {}) {
|
|
1247
|
+
const workspaceId = options.workspaceId ? normalizeWorkspaceId(options.workspaceId) : undefined;
|
|
1248
|
+
const row = this.db.query(`SELECT * FROM monitors WHERE (id = ? OR name = ?)${workspaceId ? " AND workspace_id = ?" : ""}`).get(...workspaceId ? [idOrName, idOrName, workspaceId] : [idOrName, idOrName]);
|
|
1168
1249
|
return row ? monitorFromRow(row) : null;
|
|
1169
1250
|
}
|
|
1170
1251
|
updateMonitor(idOrName, input, options = {}) {
|
|
1171
|
-
const current = this.getMonitor(idOrName);
|
|
1252
|
+
const current = this.getMonitor(idOrName, { workspaceId: options.workspaceId });
|
|
1172
1253
|
if (!current)
|
|
1173
1254
|
throw new Error(`Monitor not found: ${idOrName}`);
|
|
1174
1255
|
if (this.mode === "hosted") {
|
|
@@ -1192,10 +1273,10 @@ class UptimeStore {
|
|
|
1192
1273
|
if (definitionChanged(current, next)) {
|
|
1193
1274
|
this.closeOpenIncident(current.id, updatedAt);
|
|
1194
1275
|
}
|
|
1195
|
-
return this.getMonitor(current.id);
|
|
1276
|
+
return this.getMonitor(current.id, { workspaceId: options.workspaceId });
|
|
1196
1277
|
}
|
|
1197
|
-
deleteMonitor(idOrName) {
|
|
1198
|
-
const current = this.getMonitor(idOrName);
|
|
1278
|
+
deleteMonitor(idOrName, options = {}) {
|
|
1279
|
+
const current = this.getMonitor(idOrName, { workspaceId: options.workspaceId });
|
|
1199
1280
|
if (!current)
|
|
1200
1281
|
return false;
|
|
1201
1282
|
this.db.query("DELETE FROM monitors WHERE id = ?").run(current.id);
|
|
@@ -1572,7 +1653,20 @@ class UptimeStore {
|
|
|
1572
1653
|
}
|
|
1573
1654
|
listResults(options = {}) {
|
|
1574
1655
|
const limit = clampLimit(options.limit ?? 50);
|
|
1575
|
-
const
|
|
1656
|
+
const workspaceId = options.workspaceId ? normalizeWorkspaceId(options.workspaceId) : undefined;
|
|
1657
|
+
const clauses = [];
|
|
1658
|
+
const args = [];
|
|
1659
|
+
if (options.monitorId) {
|
|
1660
|
+
clauses.push("check_results.monitor_id = ?");
|
|
1661
|
+
args.push(options.monitorId);
|
|
1662
|
+
}
|
|
1663
|
+
if (workspaceId) {
|
|
1664
|
+
clauses.push("monitors.workspace_id = ?");
|
|
1665
|
+
args.push(workspaceId);
|
|
1666
|
+
}
|
|
1667
|
+
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
1668
|
+
args.push(limit);
|
|
1669
|
+
const rows = this.db.query(`SELECT check_results.* FROM check_results JOIN monitors ON monitors.id = check_results.monitor_id ${where} ORDER BY checked_at DESC LIMIT ?`).all(...args);
|
|
1576
1670
|
return rows.map(checkResultFromRow);
|
|
1577
1671
|
}
|
|
1578
1672
|
getProvenance(source, sourceId) {
|
|
@@ -1615,24 +1709,28 @@ class UptimeStore {
|
|
|
1615
1709
|
const clauses = [];
|
|
1616
1710
|
const args = [];
|
|
1617
1711
|
if (options.status) {
|
|
1618
|
-
clauses.push("status = ?");
|
|
1712
|
+
clauses.push("incidents.status = ?");
|
|
1619
1713
|
args.push(options.status);
|
|
1620
1714
|
}
|
|
1621
1715
|
if (options.monitorId) {
|
|
1622
|
-
clauses.push("monitor_id = ?");
|
|
1716
|
+
clauses.push("incidents.monitor_id = ?");
|
|
1623
1717
|
args.push(options.monitorId);
|
|
1624
1718
|
}
|
|
1719
|
+
if (options.workspaceId) {
|
|
1720
|
+
clauses.push("monitors.workspace_id = ?");
|
|
1721
|
+
args.push(normalizeWorkspaceId(options.workspaceId));
|
|
1722
|
+
}
|
|
1625
1723
|
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
1626
1724
|
args.push(clampLimit(options.limit ?? 50));
|
|
1627
|
-
const rows = this.db.query(`SELECT
|
|
1725
|
+
const rows = this.db.query(`SELECT incidents.* FROM incidents JOIN monitors ON monitors.id = incidents.monitor_id ${where} ORDER BY opened_at DESC LIMIT ?`).all(...args);
|
|
1628
1726
|
return rows.map(incidentFromRow);
|
|
1629
1727
|
}
|
|
1630
1728
|
getOpenIncident(monitorId) {
|
|
1631
1729
|
const row = this.db.query("SELECT * FROM incidents WHERE monitor_id = ? AND status = 'open' ORDER BY opened_at DESC LIMIT 1").get(monitorId);
|
|
1632
1730
|
return row ? incidentFromRow(row) : null;
|
|
1633
1731
|
}
|
|
1634
|
-
summary() {
|
|
1635
|
-
const monitors = this.listMonitors({ includeDisabled: true });
|
|
1732
|
+
summary(options = {}) {
|
|
1733
|
+
const monitors = this.listMonitors({ includeDisabled: true, workspaceId: options.workspaceId });
|
|
1636
1734
|
const summaries = monitors.map((monitor) => this.monitorSummary(monitor));
|
|
1637
1735
|
return {
|
|
1638
1736
|
generatedAt: new Date().toISOString(),
|
|
@@ -1644,11 +1742,15 @@ class UptimeStore {
|
|
|
1644
1742
|
down: monitors.filter((m) => m.status === "down").length,
|
|
1645
1743
|
paused: monitors.filter((m) => !m.enabled || m.status === "paused").length,
|
|
1646
1744
|
unknown: monitors.filter((m) => m.status === "unknown").length,
|
|
1647
|
-
openIncidents: this.countOpenIncidents()
|
|
1745
|
+
openIncidents: this.countOpenIncidents(options.workspaceId)
|
|
1648
1746
|
}
|
|
1649
1747
|
};
|
|
1650
1748
|
}
|
|
1651
|
-
countOpenIncidents() {
|
|
1749
|
+
countOpenIncidents(workspaceId) {
|
|
1750
|
+
if (workspaceId) {
|
|
1751
|
+
const row2 = this.db.query("SELECT COUNT(*) AS count FROM incidents JOIN monitors ON monitors.id = incidents.monitor_id WHERE incidents.status = 'open' AND monitors.workspace_id = ?").get(normalizeWorkspaceId(workspaceId));
|
|
1752
|
+
return Number(row2?.count ?? 0);
|
|
1753
|
+
}
|
|
1652
1754
|
const row = this.db.query("SELECT COUNT(*) AS count FROM incidents WHERE status = 'open'").get();
|
|
1653
1755
|
return Number(row?.count ?? 0);
|
|
1654
1756
|
}
|
|
@@ -1712,7 +1814,9 @@ class UptimeStore {
|
|
|
1712
1814
|
}
|
|
1713
1815
|
ensureMonitorKindAllowsBrowserPage() {
|
|
1714
1816
|
const row = this.db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'monitors'").get();
|
|
1715
|
-
|
|
1817
|
+
const needsBrowserPage = !row?.sql?.includes("browser_page");
|
|
1818
|
+
const needsWorkspaceUnique = Boolean(row?.sql?.includes("name TEXT NOT NULL UNIQUE"));
|
|
1819
|
+
if (!row?.sql || !needsBrowserPage && !needsWorkspaceUnique)
|
|
1716
1820
|
return;
|
|
1717
1821
|
this.db.run("PRAGMA foreign_keys = OFF");
|
|
1718
1822
|
this.db.run("PRAGMA legacy_alter_table = ON");
|
|
@@ -1722,7 +1826,8 @@ class UptimeStore {
|
|
|
1722
1826
|
this.db.run(`
|
|
1723
1827
|
CREATE TABLE monitors (
|
|
1724
1828
|
id TEXT PRIMARY KEY,
|
|
1725
|
-
|
|
1829
|
+
workspace_id TEXT NOT NULL DEFAULT 'local',
|
|
1830
|
+
name TEXT NOT NULL,
|
|
1726
1831
|
kind TEXT NOT NULL CHECK (kind IN ('http', 'tcp', 'browser_page')),
|
|
1727
1832
|
url TEXT,
|
|
1728
1833
|
host TEXT,
|
|
@@ -1737,17 +1842,18 @@ class UptimeStore {
|
|
|
1737
1842
|
last_checked_at TEXT,
|
|
1738
1843
|
revision INTEGER NOT NULL DEFAULT 1,
|
|
1739
1844
|
created_at TEXT NOT NULL,
|
|
1740
|
-
updated_at TEXT NOT NULL
|
|
1845
|
+
updated_at TEXT NOT NULL,
|
|
1846
|
+
UNIQUE (workspace_id, name)
|
|
1741
1847
|
)
|
|
1742
1848
|
`);
|
|
1743
1849
|
this.db.run(`
|
|
1744
1850
|
INSERT INTO monitors (
|
|
1745
|
-
id, name, kind, url, host, port, method, expected_status,
|
|
1851
|
+
id, workspace_id, name, kind, url, host, port, method, expected_status,
|
|
1746
1852
|
interval_seconds, timeout_ms, retry_count, enabled, status,
|
|
1747
1853
|
last_checked_at, revision, created_at, updated_at
|
|
1748
1854
|
)
|
|
1749
1855
|
SELECT
|
|
1750
|
-
id, name, kind, url, host, port, method, expected_status,
|
|
1856
|
+
id, workspace_id, name, kind, url, host, port, method, expected_status,
|
|
1751
1857
|
interval_seconds, timeout_ms, retry_count, enabled, status,
|
|
1752
1858
|
last_checked_at, revision, created_at, updated_at
|
|
1753
1859
|
FROM monitors_old_kind
|
|
@@ -1947,6 +2053,16 @@ function rejectControlCharacters2(value, label) {
|
|
|
1947
2053
|
throw new Error(`${label} must not contain control characters`);
|
|
1948
2054
|
}
|
|
1949
2055
|
}
|
|
2056
|
+
function normalizeWorkspaceId(value) {
|
|
2057
|
+
const normalized = value.trim();
|
|
2058
|
+
if (!normalized)
|
|
2059
|
+
throw new Error("Workspace id is required");
|
|
2060
|
+
rejectControlCharacters2(normalized, "Workspace id");
|
|
2061
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/.test(normalized)) {
|
|
2062
|
+
throw new Error("Workspace id contains unsupported characters");
|
|
2063
|
+
}
|
|
2064
|
+
return normalized;
|
|
2065
|
+
}
|
|
1950
2066
|
function normalizeScheduleSlot(value) {
|
|
1951
2067
|
const slot = value.trim();
|
|
1952
2068
|
if (!slot)
|
|
@@ -2133,6 +2249,7 @@ function assertIsoTimestamp(value, label) {
|
|
|
2133
2249
|
function monitorFromRow(row) {
|
|
2134
2250
|
return {
|
|
2135
2251
|
id: row.id,
|
|
2252
|
+
workspaceId: row.workspace_id ?? "local",
|
|
2136
2253
|
name: row.name,
|
|
2137
2254
|
kind: row.kind,
|
|
2138
2255
|
url: row.url,
|
|
@@ -2614,20 +2731,20 @@ class UptimeService {
|
|
|
2614
2731
|
close() {
|
|
2615
2732
|
this.store.close();
|
|
2616
2733
|
}
|
|
2617
|
-
createMonitor(input) {
|
|
2618
|
-
return this.store.createMonitor(input);
|
|
2734
|
+
createMonitor(input, options = {}) {
|
|
2735
|
+
return this.store.createMonitor(input, options);
|
|
2619
2736
|
}
|
|
2620
|
-
updateMonitor(idOrName, input) {
|
|
2621
|
-
return this.store.updateMonitor(idOrName, input);
|
|
2737
|
+
updateMonitor(idOrName, input, options = {}) {
|
|
2738
|
+
return this.store.updateMonitor(idOrName, input, options);
|
|
2622
2739
|
}
|
|
2623
|
-
deleteMonitor(idOrName) {
|
|
2624
|
-
return this.store.deleteMonitor(idOrName);
|
|
2740
|
+
deleteMonitor(idOrName, options = {}) {
|
|
2741
|
+
return this.store.deleteMonitor(idOrName, options);
|
|
2625
2742
|
}
|
|
2626
2743
|
listMonitors(options = {}) {
|
|
2627
2744
|
return this.store.listMonitors(options);
|
|
2628
2745
|
}
|
|
2629
|
-
getMonitor(idOrName) {
|
|
2630
|
-
return this.store.getMonitor(idOrName);
|
|
2746
|
+
getMonitor(idOrName, options = {}) {
|
|
2747
|
+
return this.store.getMonitor(idOrName, options);
|
|
2631
2748
|
}
|
|
2632
2749
|
listResults(options = {}) {
|
|
2633
2750
|
return this.store.listResults(options);
|
|
@@ -2635,8 +2752,8 @@ class UptimeService {
|
|
|
2635
2752
|
listIncidents(options = {}) {
|
|
2636
2753
|
return this.store.listIncidents(options);
|
|
2637
2754
|
}
|
|
2638
|
-
summary() {
|
|
2639
|
-
return this.store.summary();
|
|
2755
|
+
summary(options = {}) {
|
|
2756
|
+
return this.store.summary(options);
|
|
2640
2757
|
}
|
|
2641
2758
|
createProbe(input) {
|
|
2642
2759
|
const store = this.probeStore();
|
|
@@ -2676,8 +2793,8 @@ class UptimeService {
|
|
|
2676
2793
|
const execute = () => this.submitProbeResultInTransaction(input);
|
|
2677
2794
|
return this.store.runInTransaction ? this.store.runInTransaction(execute) : execute();
|
|
2678
2795
|
}
|
|
2679
|
-
previewImport(request) {
|
|
2680
|
-
return previewImport(this.store, request);
|
|
2796
|
+
previewImport(request, options = {}) {
|
|
2797
|
+
return previewImport(this.store, request, options);
|
|
2681
2798
|
}
|
|
2682
2799
|
applyImport(request) {
|
|
2683
2800
|
return applyImport(this.store, request);
|
|
@@ -2692,7 +2809,8 @@ class UptimeService {
|
|
|
2692
2809
|
return this.store.verifyBackup(backupPath);
|
|
2693
2810
|
}
|
|
2694
2811
|
buildReport(options = {}) {
|
|
2695
|
-
|
|
2812
|
+
const { workspaceId, ...reportOptions } = options;
|
|
2813
|
+
return buildUptimeReport(this.summary({ workspaceId }), reportOptions);
|
|
2696
2814
|
}
|
|
2697
2815
|
async sendReport(options = {}) {
|
|
2698
2816
|
if (this.store.mode === "hosted" && (options.email || options.sms || options.logs)) {
|
|
@@ -3016,6 +3134,7 @@ class UptimeService {
|
|
|
3016
3134
|
throw new Error("Probe job fencing token is invalid");
|
|
3017
3135
|
if (!job.leaseExpiresAt || job.leaseExpiresAt <= new Date().toISOString())
|
|
3018
3136
|
throw new Error("Probe job lease expired");
|
|
3137
|
+
const evidence = input.evidence ? normalizeBrowserEvidence(monitor.url ?? monitor.host ?? "https://example.invalid", input.evidence) : null;
|
|
3019
3138
|
const result = this.store.recordCheckResult({
|
|
3020
3139
|
monitorId: monitor.id,
|
|
3021
3140
|
checkedAt: input.checkedAt,
|
|
@@ -3023,7 +3142,7 @@ class UptimeService {
|
|
|
3023
3142
|
latencyMs: input.latencyMs,
|
|
3024
3143
|
statusCode: input.statusCode ?? null,
|
|
3025
3144
|
error: input.error ?? null,
|
|
3026
|
-
evidence
|
|
3145
|
+
evidence,
|
|
3027
3146
|
attemptCount: input.attemptCount ?? 1,
|
|
3028
3147
|
expectedMonitorRevision: input.monitorRevision
|
|
3029
3148
|
});
|
|
@@ -3568,11 +3687,11 @@ async function handleHostedRequest(service, request, url, options) {
|
|
|
3568
3687
|
}
|
|
3569
3688
|
const apiPath = `/api${url.pathname.slice("/api/v1".length)}`;
|
|
3570
3689
|
const scope = hostedScopeFor(request.method, apiPath);
|
|
3571
|
-
requireHostedActor(request, url, options, scope);
|
|
3690
|
+
const actor = requireHostedActor(request, url, options, scope);
|
|
3572
3691
|
if (["POST", "PATCH", "DELETE"].includes(request.method)) {
|
|
3573
3692
|
validateHostedMutationOrigin(request, url, options);
|
|
3574
3693
|
}
|
|
3575
|
-
return handleApiRoute(service, request, url, apiPath, options, true);
|
|
3694
|
+
return handleApiRoute(service, request, url, apiPath, options, true, actor);
|
|
3576
3695
|
}
|
|
3577
3696
|
function validateHostedMutationOrigin(request, url, options) {
|
|
3578
3697
|
const rawOrigin = request.headers.get("origin");
|
|
@@ -3587,12 +3706,12 @@ function validateHostedMutationOrigin(request, url, options) {
|
|
|
3587
3706
|
throw new ApiError("cross-origin mutation rejected", 403);
|
|
3588
3707
|
}
|
|
3589
3708
|
}
|
|
3590
|
-
async function handleApiRoute(service, request, url, apiPath, options, hosted) {
|
|
3709
|
+
async function handleApiRoute(service, request, url, apiPath, options, hosted, actor) {
|
|
3591
3710
|
if (request.method === "GET" && apiPath === "/api/summary") {
|
|
3592
|
-
return json(service.summary());
|
|
3711
|
+
return json(service.summary({ workspaceId: actor?.workspaceId }));
|
|
3593
3712
|
}
|
|
3594
3713
|
if (request.method === "GET" && apiPath === "/api/report") {
|
|
3595
|
-
return json(service.buildReport());
|
|
3714
|
+
return json(service.buildReport({ workspaceId: actor?.workspaceId }));
|
|
3596
3715
|
}
|
|
3597
3716
|
if (request.method === "POST" && apiPath === "/api/report") {
|
|
3598
3717
|
if (hosted)
|
|
@@ -3649,22 +3768,24 @@ async function handleApiRoute(service, request, url, apiPath, options, hosted) {
|
|
|
3649
3768
|
}));
|
|
3650
3769
|
}
|
|
3651
3770
|
if (request.method === "GET" && apiPath === "/api/monitors") {
|
|
3652
|
-
return json(service.listMonitors({ includeDisabled: url.searchParams.get("includeDisabled") === "true" }));
|
|
3771
|
+
return json(service.listMonitors({ includeDisabled: url.searchParams.get("includeDisabled") === "true", workspaceId: actor?.workspaceId }));
|
|
3653
3772
|
}
|
|
3654
3773
|
if (request.method === "POST" && apiPath === "/api/monitors") {
|
|
3655
|
-
return json(service.createMonitor(await jsonBody(request)), 201);
|
|
3774
|
+
return json(service.createMonitor(await jsonBody(request), { workspaceId: actor?.workspaceId }), 201);
|
|
3656
3775
|
}
|
|
3657
3776
|
if (request.method === "GET" && apiPath === "/api/incidents") {
|
|
3658
3777
|
const status = url.searchParams.get("status");
|
|
3659
3778
|
return json(service.listIncidents({
|
|
3660
3779
|
status: status === "open" || status === "closed" ? status : undefined,
|
|
3661
3780
|
monitorId: url.searchParams.get("monitorId") ?? undefined,
|
|
3781
|
+
workspaceId: actor?.workspaceId,
|
|
3662
3782
|
limit: numericParam(url, "limit", 50)
|
|
3663
3783
|
}));
|
|
3664
3784
|
}
|
|
3665
3785
|
if (request.method === "GET" && apiPath === "/api/results") {
|
|
3666
3786
|
return json(service.listResults({
|
|
3667
3787
|
monitorId: url.searchParams.get("monitorId") ?? undefined,
|
|
3788
|
+
workspaceId: actor?.workspaceId,
|
|
3668
3789
|
limit: numericParam(url, "limit", 50)
|
|
3669
3790
|
}));
|
|
3670
3791
|
}
|
|
@@ -3701,7 +3822,7 @@ async function handleApiRoute(service, request, url, apiPath, options, hosted) {
|
|
|
3701
3822
|
return json(service.submitProbeResult(await jsonBody(request)), 201);
|
|
3702
3823
|
}
|
|
3703
3824
|
if (request.method === "POST" && apiPath === "/api/imports/preview") {
|
|
3704
|
-
return json(service.previewImport(await jsonBody(request)));
|
|
3825
|
+
return json(service.previewImport(await jsonBody(request), { workspaceId: actor?.workspaceId }));
|
|
3705
3826
|
}
|
|
3706
3827
|
if (request.method === "POST" && apiPath === "/api/imports/apply") {
|
|
3707
3828
|
if (hosted)
|
|
@@ -3723,14 +3844,14 @@ async function handleApiRoute(service, request, url, apiPath, options, hosted) {
|
|
|
3723
3844
|
if (monitorMatch) {
|
|
3724
3845
|
const id = decodeURIComponent(monitorMatch[1]);
|
|
3725
3846
|
if (request.method === "GET" && !monitorMatch[2]) {
|
|
3726
|
-
const monitor = service.getMonitor(id);
|
|
3847
|
+
const monitor = service.getMonitor(id, { workspaceId: actor?.workspaceId });
|
|
3727
3848
|
return monitor ? json(monitor) : json({ error: "not found" }, 404);
|
|
3728
3849
|
}
|
|
3729
3850
|
if (request.method === "PATCH" && !monitorMatch[2]) {
|
|
3730
|
-
return json(service.updateMonitor(id, await jsonBody(request)));
|
|
3851
|
+
return json(service.updateMonitor(id, await jsonBody(request), { workspaceId: actor?.workspaceId }));
|
|
3731
3852
|
}
|
|
3732
3853
|
if (request.method === "DELETE" && !monitorMatch[2]) {
|
|
3733
|
-
return json({ deleted: service.deleteMonitor(id) });
|
|
3854
|
+
return json({ deleted: service.deleteMonitor(id, { workspaceId: actor?.workspaceId }) });
|
|
3734
3855
|
}
|
|
3735
3856
|
if (request.method === "POST" && monitorMatch[2] === "check") {
|
|
3736
3857
|
if (hosted)
|
|
@@ -3881,7 +4002,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
3881
4002
|
const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
|
|
3882
4003
|
const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
|
|
3883
4004
|
const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
|
|
3884
|
-
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.
|
|
4005
|
+
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.11");
|
|
3885
4006
|
const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
|
|
3886
4007
|
const protectedAccessUrl = protectedAccessMode === "cloudfront_default_domain" ? "https://<cloudfront-domain>" : `https://${hostname}`;
|
|
3887
4008
|
const cluster = `${prefix}-${stage}`;
|