@hasna/uptime 0.1.2 → 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/dist/dashboard.js CHANGED
@@ -215,7 +215,7 @@ function dashboardHtml() {
215
215
  clear(root);
216
216
  for (const item of summary.monitors) {
217
217
  const m = item.monitor;
218
- const target = m.kind === 'http' ? m.url : m.host + ':' + m.port;
218
+ const target = m.kind === 'tcp' ? m.host + ':' + m.port : m.url;
219
219
  const incident = item.openIncident ? 'open since ' + new Date(item.openIncident.openedAt).toLocaleString() : '-';
220
220
  const tr = document.createElement('tr');
221
221
  const name = document.createElement('td');
@@ -0,0 +1,90 @@
1
+ import type { MonitorProvenance, StoredImportBatch, UpsertMonitorProvenanceInput } from "./store.js";
2
+ import type { CreateMonitorInput, ImportedMonitorInput, ImportedUpdateMonitorInput, ListResultsOptions, Monitor, MonitorKind } from "./types.js";
3
+ export type ImportSource = "manual" | "projects" | "servers" | "domains" | "deployment";
4
+ export type ImportAction = "create" | "update" | "unchanged" | "blocked" | "conflict";
5
+ export interface ImportRequest {
6
+ source: ImportSource;
7
+ records: unknown[];
8
+ defaults?: Partial<CreateMonitorInput>;
9
+ }
10
+ export interface ImportCandidate {
11
+ source: ImportSource;
12
+ sourceId: string;
13
+ sourceLabel: string | null;
14
+ name: string;
15
+ kind: MonitorKind;
16
+ url?: string;
17
+ host?: string;
18
+ port?: number;
19
+ method?: string;
20
+ expectedStatus?: number | null;
21
+ intervalSeconds?: number;
22
+ timeoutMs?: number;
23
+ retryCount?: number;
24
+ enabled?: boolean;
25
+ snapshot: unknown;
26
+ }
27
+ export interface ImportPreviewItem {
28
+ candidate: ImportCandidate;
29
+ action: ImportAction;
30
+ monitor: Monitor | null;
31
+ provenance: MonitorProvenance | null;
32
+ warnings: string[];
33
+ reason: string | null;
34
+ }
35
+ export interface ImportPreview {
36
+ source: ImportSource;
37
+ generatedAt: string;
38
+ dryRun: true;
39
+ items: ImportPreviewItem[];
40
+ totals: Record<ImportAction, number>;
41
+ }
42
+ export interface ImportApplyItem extends ImportPreviewItem {
43
+ monitor: Monitor | null;
44
+ before: Monitor | null;
45
+ after: Monitor | null;
46
+ }
47
+ export interface ImportApplyResult {
48
+ batchId: string;
49
+ source: ImportSource;
50
+ appliedAt: string;
51
+ items: ImportApplyItem[];
52
+ totals: Record<ImportAction, number>;
53
+ }
54
+ export interface ImportRollbackItem {
55
+ monitorId: string | null;
56
+ action: "deleted" | "restored" | "disabled" | "skipped";
57
+ reason: string | null;
58
+ }
59
+ export interface ImportRollbackResult {
60
+ batchId: string;
61
+ source: string;
62
+ rolledBackAt: string;
63
+ items: ImportRollbackItem[];
64
+ }
65
+ export interface UptimeImportStore {
66
+ readonly mode: "local" | "hosted";
67
+ createMonitor(input: ImportedMonitorInput, options?: {
68
+ allowBrowserPage?: boolean;
69
+ }): Monitor;
70
+ updateMonitor(idOrName: string, input: ImportedUpdateMonitorInput, options?: {
71
+ allowBrowserPage?: boolean;
72
+ }): Monitor;
73
+ deleteMonitor(idOrName: string): boolean;
74
+ getMonitor(idOrName: string): Monitor | null;
75
+ listResults(options?: ListResultsOptions): unknown[];
76
+ getProvenance(source: string, sourceId: string): MonitorProvenance | null;
77
+ upsertMonitorProvenance(input: UpsertMonitorProvenanceInput): MonitorProvenance;
78
+ saveImportBatch(input: {
79
+ id: string;
80
+ source: string;
81
+ records: unknown[];
82
+ }): StoredImportBatch;
83
+ getImportBatch(batchId: string): StoredImportBatch | null;
84
+ markImportBatchRolledBack(batchId: string): StoredImportBatch;
85
+ runInTransaction?<T>(fn: () => T): T;
86
+ }
87
+ export declare function previewImport(store: UptimeImportStore, request: ImportRequest): ImportPreview;
88
+ export declare function applyImport(store: UptimeImportStore, request: ImportRequest): ImportApplyResult;
89
+ export declare function rollbackImport(store: UptimeImportStore, batchId: string): ImportRollbackResult;
90
+ //# sourceMappingURL=imports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"imports.d.ts","sourceRoot":"","sources":["../src/imports.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,556 @@
1
+ // @bun
2
+ // src/imports.ts
3
+ import { randomUUID } from "crypto";
4
+
5
+ // src/limits.ts
6
+ var MIN_INTERVAL_SECONDS = 1;
7
+ var MAX_INTERVAL_SECONDS = 86400;
8
+ var MIN_TIMEOUT_MS = 1;
9
+ var MAX_TIMEOUT_MS = 60000;
10
+ var MIN_RETRY_COUNT = 0;
11
+ var MAX_RETRY_COUNT = 10;
12
+ var MAX_RESULT_LIMIT = 1000;
13
+
14
+ // src/target-policy.ts
15
+ import net from "net";
16
+ var SECRET_PARAM_PATTERN = /(token|secret|password|passwd|api[_-]?key|access[_-]?token|auth|credential|session)/i;
17
+ function assertHostedTargetAllowed(target) {
18
+ if (target.kind === "http" || target.kind === "browser_page") {
19
+ if (!target.url)
20
+ throw new Error("HTTP monitors require url");
21
+ assertHostedHttpUrlAllowed(target.url);
22
+ return;
23
+ }
24
+ if (target.kind === "tcp") {
25
+ if (!target.host)
26
+ throw new Error("TCP monitors require host");
27
+ assertHostedHostAllowed(target.host, "TCP host");
28
+ if (!Number.isInteger(target.port) || target.port <= 0 || target.port > 65535) {
29
+ throw new Error("TCP monitors require a port from 1 to 65535");
30
+ }
31
+ return;
32
+ }
33
+ throw new Error("Monitor kind must be http, tcp, or browser_page");
34
+ }
35
+ function assertHostedHttpUrlAllowed(value) {
36
+ const parsed = new URL(value);
37
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
38
+ throw new Error("HTTP monitor url must use http or https");
39
+ }
40
+ if (parsed.username || parsed.password) {
41
+ throw new Error("hosted target URLs must not contain userinfo");
42
+ }
43
+ for (const key of parsed.searchParams.keys()) {
44
+ if (SECRET_PARAM_PATTERN.test(key)) {
45
+ throw new Error(`hosted target URL query parameter is not allowed: ${key}`);
46
+ }
47
+ }
48
+ if (parsed.hash && SECRET_PARAM_PATTERN.test(parsed.hash)) {
49
+ throw new Error("hosted target URL fragment contains secret-like data");
50
+ }
51
+ assertHostedHostAllowed(parsed.hostname, "HTTP host");
52
+ }
53
+ function assertHostedHostAllowed(hostname, label = "host") {
54
+ const host = normalizeHost(hostname);
55
+ if (!host)
56
+ throw new Error(`${label} is required`);
57
+ if (host === "localhost" || host.endsWith(".localhost")) {
58
+ throw new Error(`${label} is not allowed in hosted mode: localhost`);
59
+ }
60
+ if (host.endsWith(".local") || host.endsWith(".internal")) {
61
+ throw new Error(`${label} is not allowed in hosted mode: private DNS name`);
62
+ }
63
+ const ipVersion = net.isIP(host);
64
+ if (ipVersion === 4 && isDeniedIpv4(host)) {
65
+ throw new Error(`${label} is not allowed in hosted mode: private or reserved IPv4`);
66
+ }
67
+ if (ipVersion === 6 && isDeniedIpv6(host)) {
68
+ throw new Error(`${label} is not allowed in hosted mode: private or reserved IPv6`);
69
+ }
70
+ }
71
+ function normalizeHost(hostname) {
72
+ return hostname.trim().toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
73
+ }
74
+ function isDeniedIpv4(ip) {
75
+ const parts = ip.split(".").map((part) => Number(part));
76
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
77
+ return true;
78
+ }
79
+ const [a, b] = parts;
80
+ return a === 0 || a === 10 || a === 127 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a >= 224;
81
+ }
82
+ function isDeniedIpv6(ip) {
83
+ const normalized = ip.toLowerCase();
84
+ return normalized === "::" || normalized === "::1" || normalized.startsWith("fe80:") || normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("ff") || normalized.startsWith("::ffff:127.") || normalized.startsWith("::ffff:10.") || normalized.startsWith("::ffff:169.254.") || /^::ffff:172\.(1[6-9]|2\d|3[0-1])\./.test(normalized) || normalized.startsWith("::ffff:192.168.");
85
+ }
86
+
87
+ // src/imports.ts
88
+ function previewImport(store, request) {
89
+ const source = normalizeSource(request.source);
90
+ const items = dedupePreviewItems(request.records.map((record) => previewRecord(store, source, record, request.defaults ?? {})));
91
+ return {
92
+ source,
93
+ generatedAt: new Date().toISOString(),
94
+ dryRun: true,
95
+ items,
96
+ totals: countActions(items)
97
+ };
98
+ }
99
+ function dedupePreviewItems(items) {
100
+ const seenSources = new Set;
101
+ const seenNames = new Set;
102
+ return items.map((item) => {
103
+ if (item.action === "blocked")
104
+ return item;
105
+ const sourceKey = `${item.candidate.source}:${item.candidate.sourceId}`;
106
+ const nameKey = item.candidate.name.toLowerCase();
107
+ if (seenSources.has(sourceKey) || seenNames.has(nameKey)) {
108
+ return {
109
+ ...item,
110
+ action: "conflict",
111
+ monitor: item.monitor,
112
+ warnings: [...item.warnings, "duplicate import candidate in request"],
113
+ reason: "duplicate import candidate in request"
114
+ };
115
+ }
116
+ seenSources.add(sourceKey);
117
+ seenNames.add(nameKey);
118
+ return item;
119
+ });
120
+ }
121
+ function applyImport(store, request) {
122
+ if (store.mode === "hosted") {
123
+ throw new Error("hosted import apply requires cloud import_batches and audit");
124
+ }
125
+ const execute = () => {
126
+ const preview = previewImport(store, request);
127
+ const appliedAt = new Date().toISOString();
128
+ const items = preview.items.map((item) => applyPreviewItem(store, item));
129
+ const batchId = `imp_${randomUUID().replace(/-/g, "").slice(0, 18)}`;
130
+ store.saveImportBatch({
131
+ id: batchId,
132
+ source: preview.source,
133
+ records: items.map((item) => ({
134
+ action: item.action,
135
+ sourceId: item.candidate.sourceId,
136
+ monitorId: item.after?.id ?? item.monitor?.id ?? item.before?.id ?? null,
137
+ before: item.before,
138
+ after: item.after,
139
+ candidate: item.candidate
140
+ }))
141
+ });
142
+ return { batchId, source: preview.source, appliedAt, items, totals: countActions(items) };
143
+ };
144
+ return store.runInTransaction ? store.runInTransaction(execute) : execute();
145
+ }
146
+ function rollbackImport(store, batchId) {
147
+ if (store.mode === "hosted") {
148
+ throw new Error("hosted import rollback requires cloud import_batches and audit");
149
+ }
150
+ const batch = store.getImportBatch(batchId);
151
+ if (!batch)
152
+ throw new Error(`Import batch not found: ${batchId}`);
153
+ if (batch.status === "rolled_back")
154
+ throw new Error(`Import batch already rolled back: ${batchId}`);
155
+ const items = [...batch.records].reverse().map((record) => rollbackRecord(store, record));
156
+ const rolledBack = store.markImportBatchRolledBack(batchId);
157
+ return {
158
+ batchId,
159
+ source: rolledBack.source,
160
+ rolledBackAt: rolledBack.rolledBackAt ?? new Date().toISOString(),
161
+ items
162
+ };
163
+ }
164
+ function previewRecord(store, source, record, defaults) {
165
+ const warnings = [];
166
+ let candidate;
167
+ try {
168
+ if (store.mode === "hosted")
169
+ assertHostedTargetAllowed(rawTargetForHostedPolicy(source, record, defaults));
170
+ candidate = normalizeCandidate(source, record, defaults);
171
+ validateCandidate(candidate);
172
+ if (store.mode === "hosted")
173
+ assertHostedTargetAllowed(candidate);
174
+ } catch (error) {
175
+ return {
176
+ candidate: fallbackCandidate(source, record),
177
+ action: "blocked",
178
+ monitor: null,
179
+ provenance: null,
180
+ warnings,
181
+ reason: error instanceof Error ? error.message : String(error)
182
+ };
183
+ }
184
+ const provenance = store.getProvenance(candidate.source, candidate.sourceId);
185
+ const monitor = provenance ? store.getMonitor(provenance.monitorId) : store.getMonitor(candidate.name);
186
+ if (provenance && !monitor) {
187
+ return { candidate, action: "create", monitor: null, provenance, warnings: ["source provenance points to a missing monitor"], reason: null };
188
+ }
189
+ if (provenance && monitor) {
190
+ const nameOwner = store.getMonitor(candidate.name);
191
+ if (nameOwner && nameOwner.id !== monitor.id) {
192
+ return {
193
+ candidate,
194
+ action: "conflict",
195
+ monitor,
196
+ provenance,
197
+ warnings,
198
+ reason: "monitor name already exists on another monitor"
199
+ };
200
+ }
201
+ return {
202
+ candidate,
203
+ action: sameTarget(monitor, candidate) ? "unchanged" : "update",
204
+ monitor,
205
+ provenance,
206
+ warnings,
207
+ reason: null
208
+ };
209
+ }
210
+ if (monitor) {
211
+ return {
212
+ candidate,
213
+ action: "conflict",
214
+ monitor,
215
+ provenance: null,
216
+ warnings,
217
+ reason: "monitor name already exists without matching source provenance"
218
+ };
219
+ }
220
+ return { candidate, action: "create", monitor: null, provenance: null, warnings, reason: null };
221
+ }
222
+ function applyPreviewItem(store, item) {
223
+ if (item.action === "blocked" || item.action === "conflict") {
224
+ return { ...item, before: item.monitor, after: item.monitor };
225
+ }
226
+ const input = candidateToMonitorInput(item.candidate);
227
+ const before = item.monitor;
228
+ const after = item.action === "create" ? store.createMonitor(input, { allowBrowserPage: true }) : item.action === "update" ? store.updateMonitor(item.monitor.id, input, { allowBrowserPage: true }) : item.monitor;
229
+ if (after) {
230
+ store.upsertMonitorProvenance({
231
+ monitorId: after.id,
232
+ source: item.candidate.source,
233
+ sourceId: item.candidate.sourceId,
234
+ sourceLabel: item.candidate.sourceLabel,
235
+ snapshot: item.candidate.snapshot
236
+ });
237
+ }
238
+ return { ...item, before, after };
239
+ }
240
+ function rollbackRecord(store, record) {
241
+ const value = asRecord(record);
242
+ const action = stringValue(value.action);
243
+ const monitorId = stringValue(value.monitorId);
244
+ const before = isMonitor(value.before) ? value.before : null;
245
+ const after = isMonitor(value.after) ? value.after : null;
246
+ const targetId = after?.id ?? before?.id ?? monitorId;
247
+ if (!targetId)
248
+ return { monitorId: null, action: "skipped", reason: "batch record has no monitor id" };
249
+ if (action === "create") {
250
+ const hasHistory = store.listResults({ monitorId: targetId, limit: 1 }).length > 0;
251
+ if (hasHistory) {
252
+ store.updateMonitor(targetId, { enabled: false }, { allowBrowserPage: true });
253
+ return { monitorId: targetId, action: "disabled", reason: "created monitor has check history, so rollback preserved history and disabled it" };
254
+ }
255
+ return { monitorId: targetId, action: store.deleteMonitor(targetId) ? "deleted" : "skipped", reason: null };
256
+ }
257
+ if (action === "update" && before) {
258
+ store.updateMonitor(targetId, monitorToUpdateInput(before), { allowBrowserPage: true });
259
+ return { monitorId: targetId, action: "restored", reason: null };
260
+ }
261
+ return { monitorId: targetId, action: "skipped", reason: `no rollback needed for ${action || "unknown"} action` };
262
+ }
263
+ function normalizeCandidate(source, record, defaults) {
264
+ const value = asRecord(record);
265
+ const monitor = asRecord(value.monitor);
266
+ const sourceId = sanitizeIdentity(stringValue(value.sourceId) ?? stringValue(value.id) ?? stringValue(value.slug) ?? stringValue(value.name));
267
+ let url = stringValue(monitor.url) ?? stringValue(value.url) ?? stringValue(value.healthUrl) ?? stringValue(value.homepageUrl) ?? stringValue(value.environmentUrl);
268
+ if (source === "domains" && !url && stringValue(value.domain)) {
269
+ url = `https://${stringValue(value.domain)}`;
270
+ }
271
+ const rawHost = stringValue(monitor.host) ?? stringValue(value.host) ?? stringValue(value.hostname);
272
+ const rawKind = stringValue(monitor.kind) ?? stringValue(value.kind) ?? (url ? "http" : "tcp");
273
+ const kind = normalizeKind(rawKind);
274
+ const normalizedUrl = normalizeCandidateUrl(url ?? defaults.url);
275
+ const normalizedHost = kind === "tcp" ? rawHost ?? defaults.host : undefined;
276
+ const port = numberValue(monitor.port) ?? numberValue(value.port) ?? defaults.port;
277
+ const normalizedTargetKey = sanitizeGeneratedTargetKey(kind, normalizedUrl, normalizedHost, port);
278
+ const normalizedSourceId = sourceId ?? `${source}:${normalizedTargetKey}`;
279
+ const name = stringValue(monitor.name) ?? stringValue(value.monitorName) ?? stringValue(value.name) ?? stringValue(value.slug) ?? (source === "domains" ? stringValue(value.domain) : undefined) ?? (kind === "tcp" ? stringValue(value.hostname) : undefined) ?? `${source}-${normalizedTargetKey}`;
280
+ const expectedStatus = firstDefined(nullableNumberValue(monitor.expectedStatus), nullableNumberValue(value.expectedStatus), defaults.expectedStatus);
281
+ const candidate = {
282
+ source,
283
+ sourceId: normalizedSourceId,
284
+ sourceLabel: sanitizeIdentity(stringValue(value.label) ?? stringValue(value.name) ?? stringValue(value.slug)) ?? null,
285
+ name: sanitizeIdentity(name) ?? name,
286
+ kind,
287
+ url: normalizedUrl,
288
+ host: normalizedHost,
289
+ port,
290
+ method: normalizeCandidateMethod(stringValue(monitor.method) ?? stringValue(value.method) ?? defaults.method),
291
+ expectedStatus,
292
+ intervalSeconds: numberValue(monitor.intervalSeconds) ?? numberValue(value.intervalSeconds) ?? defaults.intervalSeconds,
293
+ timeoutMs: numberValue(monitor.timeoutMs) ?? numberValue(value.timeoutMs) ?? defaults.timeoutMs,
294
+ retryCount: numberValue(monitor.retryCount) ?? numberValue(value.retryCount) ?? defaults.retryCount,
295
+ enabled: booleanValue(monitor.enabled) ?? booleanValue(value.enabled) ?? defaults.enabled,
296
+ snapshot: sanitizeSnapshot(record)
297
+ };
298
+ return candidate;
299
+ }
300
+ function rawTargetForHostedPolicy(source, record, defaults) {
301
+ const value = asRecord(record);
302
+ const monitor = asRecord(value.monitor);
303
+ let url = stringValue(monitor.url) ?? stringValue(value.url) ?? stringValue(value.healthUrl) ?? stringValue(value.homepageUrl) ?? stringValue(value.environmentUrl);
304
+ if (source === "domains" && !url && stringValue(value.domain)) {
305
+ url = `https://${stringValue(value.domain)}`;
306
+ }
307
+ const host = stringValue(monitor.host) ?? stringValue(value.host) ?? stringValue(value.hostname) ?? defaults.host;
308
+ const rawKind = stringValue(monitor.kind) ?? stringValue(value.kind) ?? (url ? "http" : "tcp");
309
+ const kind = normalizeKind(rawKind);
310
+ return {
311
+ kind,
312
+ url: url ?? defaults.url,
313
+ host: kind === "tcp" ? host : undefined,
314
+ port: numberValue(monitor.port) ?? numberValue(value.port) ?? defaults.port
315
+ };
316
+ }
317
+ function validateCandidate(candidate) {
318
+ if (!candidate.name.trim())
319
+ throw new Error("import candidate requires name");
320
+ rejectControlCharacters(candidate.name.trim(), "Monitor name");
321
+ if (candidate.method !== undefined && !/^[A-Z]+$/.test(candidate.method)) {
322
+ throw new Error("HTTP method must contain only letters");
323
+ }
324
+ if (candidate.expectedStatus !== undefined && candidate.expectedStatus !== null) {
325
+ if (!Number.isInteger(candidate.expectedStatus) || candidate.expectedStatus < 100 || candidate.expectedStatus > 599) {
326
+ throw new Error("expectedStatus must be an HTTP status from 100 to 599");
327
+ }
328
+ }
329
+ if (candidate.intervalSeconds !== undefined) {
330
+ boundedInteger(candidate.intervalSeconds, "intervalSeconds", MIN_INTERVAL_SECONDS, MAX_INTERVAL_SECONDS);
331
+ }
332
+ if (candidate.timeoutMs !== undefined) {
333
+ boundedInteger(candidate.timeoutMs, "timeoutMs", MIN_TIMEOUT_MS, MAX_TIMEOUT_MS);
334
+ }
335
+ if (candidate.retryCount !== undefined) {
336
+ boundedInteger(candidate.retryCount, "retryCount", MIN_RETRY_COUNT, MAX_RETRY_COUNT);
337
+ }
338
+ if (candidate.kind === "http" || candidate.kind === "browser_page") {
339
+ if (!candidate.url)
340
+ throw new Error(`${candidate.kind} import candidate requires url`);
341
+ const parsed = new URL(candidate.url);
342
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
343
+ throw new Error(`${candidate.kind} import candidate URL must use http or https`);
344
+ }
345
+ if (parsed.username || parsed.password)
346
+ throw new Error(`${candidate.kind} import candidate URL must not contain userinfo`);
347
+ return;
348
+ }
349
+ if (candidate.kind === "tcp") {
350
+ if (!candidate.host)
351
+ throw new Error("tcp import candidate requires host");
352
+ rejectControlCharacters(candidate.host, "TCP host");
353
+ if (!Number.isInteger(candidate.port) || candidate.port <= 0 || candidate.port > 65535) {
354
+ throw new Error("tcp import candidate requires a port from 1 to 65535");
355
+ }
356
+ return;
357
+ }
358
+ throw new Error(`unsupported import candidate kind: ${candidate.kind}`);
359
+ }
360
+ function candidateToMonitorInput(candidate) {
361
+ return {
362
+ name: candidate.name,
363
+ kind: candidate.kind,
364
+ url: candidate.url,
365
+ host: candidate.host,
366
+ port: candidate.port,
367
+ method: candidate.method,
368
+ expectedStatus: candidate.expectedStatus,
369
+ intervalSeconds: candidate.intervalSeconds,
370
+ timeoutMs: candidate.timeoutMs,
371
+ retryCount: candidate.retryCount,
372
+ enabled: candidate.enabled
373
+ };
374
+ }
375
+ function monitorToUpdateInput(monitor) {
376
+ return {
377
+ name: monitor.name,
378
+ kind: monitor.kind,
379
+ url: monitor.url ?? undefined,
380
+ host: monitor.host ?? undefined,
381
+ port: monitor.port ?? undefined,
382
+ method: monitor.method,
383
+ expectedStatus: monitor.expectedStatus,
384
+ intervalSeconds: monitor.intervalSeconds,
385
+ timeoutMs: monitor.timeoutMs,
386
+ retryCount: monitor.retryCount,
387
+ enabled: monitor.enabled
388
+ };
389
+ }
390
+ function sameTarget(monitor, candidate) {
391
+ return monitor.kind === candidate.kind && monitor.name === candidate.name && monitor.url === (candidate.url ?? null) && monitor.host === (candidate.host ?? null) && monitor.port === (candidate.port ?? null) && monitor.method === (candidate.method ?? monitor.method) && (candidate.expectedStatus === undefined || monitor.expectedStatus === candidate.expectedStatus) && monitor.intervalSeconds === (candidate.intervalSeconds ?? monitor.intervalSeconds) && monitor.timeoutMs === (candidate.timeoutMs ?? monitor.timeoutMs) && monitor.retryCount === (candidate.retryCount ?? monitor.retryCount) && monitor.enabled === (candidate.enabled ?? monitor.enabled);
392
+ }
393
+ function countActions(items) {
394
+ return {
395
+ create: items.filter((item) => item.action === "create").length,
396
+ update: items.filter((item) => item.action === "update").length,
397
+ unchanged: items.filter((item) => item.action === "unchanged").length,
398
+ blocked: items.filter((item) => item.action === "blocked").length,
399
+ conflict: items.filter((item) => item.action === "conflict").length
400
+ };
401
+ }
402
+ function normalizeSource(source) {
403
+ if (["manual", "projects", "servers", "domains", "deployment"].includes(source))
404
+ return source;
405
+ throw new Error(`unsupported import source: ${source}`);
406
+ }
407
+ function normalizeKind(value) {
408
+ if (value === "http" || value === "tcp" || value === "browser_page")
409
+ return value;
410
+ return value === "browser" || value === "page" ? "browser_page" : "http";
411
+ }
412
+ function targetKey(kind, url, host, port) {
413
+ return kind === "tcp" ? `${host ?? "host"}:${port ?? "port"}` : url ?? "url";
414
+ }
415
+ function sanitizeGeneratedTargetKey(kind, url, host, port) {
416
+ const key = targetKey(kind, url, host, port);
417
+ return kind === "tcp" ? key : sanitizeIdentity(key) ?? key;
418
+ }
419
+ function normalizeCandidateUrl(value) {
420
+ if (!value)
421
+ return;
422
+ try {
423
+ const parsed = new URL(value);
424
+ for (const key of [...parsed.searchParams.keys()]) {
425
+ if (isSecretKey(key))
426
+ parsed.searchParams.set(key, "[redacted]");
427
+ }
428
+ parsed.hash = "";
429
+ return parsed.toString();
430
+ } catch {
431
+ return value;
432
+ }
433
+ }
434
+ function normalizeCandidateMethod(value) {
435
+ return value?.trim().toUpperCase();
436
+ }
437
+ function fallbackCandidate(source, record) {
438
+ const value = asRecord(record);
439
+ const monitor = asRecord(value.monitor);
440
+ const name = stringValue(monitor.name) ?? stringValue(value.name) ?? stringValue(value.domain) ?? "invalid import candidate";
441
+ const rawUrl = stringValue(monitor.url) ?? stringValue(value.url) ?? stringValue(value.domain);
442
+ const kind = normalizeKind(stringValue(monitor.kind) ?? stringValue(value.kind) ?? "http");
443
+ return {
444
+ source,
445
+ sourceId: sanitizeIdentity(stringValue(value.sourceId) ?? stringValue(value.id)) ?? `${source}:invalid`,
446
+ sourceLabel: sanitizeIdentity(stringValue(value.label)) ?? null,
447
+ name: sanitizeIdentity(name) ?? name,
448
+ kind,
449
+ url: redactUrlForDisplay(rawUrl),
450
+ host: kind === "tcp" ? sanitizeHost(stringValue(monitor.host) ?? stringValue(value.host) ?? stringValue(value.hostname)) : undefined,
451
+ port: numberValue(monitor.port) ?? numberValue(value.port),
452
+ snapshot: sanitizeSnapshot(record)
453
+ };
454
+ }
455
+ function asRecord(value) {
456
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
457
+ }
458
+ function stringValue(value) {
459
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
460
+ }
461
+ function numberValue(value) {
462
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
463
+ }
464
+ function nullableNumberValue(value) {
465
+ if (value === null)
466
+ return null;
467
+ return numberValue(value);
468
+ }
469
+ function booleanValue(value) {
470
+ return typeof value === "boolean" ? value : undefined;
471
+ }
472
+ function firstDefined(...values) {
473
+ return values.find((value) => value !== undefined);
474
+ }
475
+ function isMonitor(value) {
476
+ const row = asRecord(value);
477
+ return Boolean(stringValue(row.id) && stringValue(row.name) && stringValue(row.kind));
478
+ }
479
+ function sanitizeSnapshot(value) {
480
+ if (Array.isArray(value))
481
+ return value.map(sanitizeSnapshot);
482
+ if (!value || typeof value !== "object")
483
+ return sanitizeScalar(value);
484
+ const output = {};
485
+ for (const [key, entry] of Object.entries(value)) {
486
+ if (isSecretKey(key))
487
+ output[key] = "[redacted]";
488
+ else
489
+ output[key] = sanitizeSnapshot(entry);
490
+ }
491
+ return output;
492
+ }
493
+ function sanitizeScalar(value) {
494
+ if (typeof value !== "string")
495
+ return value;
496
+ return value.replace(/\/(?:home|Users)\/[^\s"'<>]+/g, "[local-path]").replace(/\b(?:localhost|(?:[a-z0-9-]+\.)+(?:local|internal))\b/gi, "[private-host]").replace(/(https?:\/\/)[^/?#\s"'<>]+:[^@/?#\s"'<>]+@/gi, "$1[redacted]@").replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]").replace(/((?:token|secret|password|passwd|api[_-]?key|access[_-]?token|auth|credential|session)[=:]\s*)[^\s&]+/gi, "$1[redacted]");
497
+ }
498
+ function isSecretKey(value) {
499
+ return /(token|secret|password|passwd|api[_-]?key|access[_-]?token|auth|credential|session)/i.test(value);
500
+ }
501
+ function rejectControlCharacters(value, label) {
502
+ if (/[\x00-\x1f\x7f-\x9f]/.test(value)) {
503
+ throw new Error(`${label} must not contain control characters`);
504
+ }
505
+ }
506
+ function boundedInteger(value, label, min, max) {
507
+ if (!Number.isInteger(value) || value < min || value > max) {
508
+ throw new Error(`${label} must be an integer from ${min} to ${max}`);
509
+ }
510
+ return value;
511
+ }
512
+ function redactUrlForDisplay(value) {
513
+ if (!value)
514
+ return;
515
+ try {
516
+ const parsed = new URL(value);
517
+ parsed.username = parsed.username ? "[redacted]" : "";
518
+ parsed.password = "";
519
+ for (const key of [...parsed.searchParams.keys()]) {
520
+ if (isSecretKey(key))
521
+ parsed.searchParams.set(key, "[redacted]");
522
+ }
523
+ if (parsed.hash && isSecretKey(parsed.hash))
524
+ parsed.hash = "#[redacted]";
525
+ return parsed.toString();
526
+ } catch {
527
+ return sanitizeScalar(value);
528
+ }
529
+ }
530
+ function sanitizeIdentity(value) {
531
+ if (!value)
532
+ return;
533
+ try {
534
+ const parsed = new URL(value);
535
+ parsed.username = parsed.username ? "[redacted]" : "";
536
+ parsed.password = "";
537
+ for (const key of [...parsed.searchParams.keys()]) {
538
+ if (isSecretKey(key))
539
+ parsed.searchParams.set(key, "[redacted]");
540
+ }
541
+ parsed.hash = "";
542
+ return parsed.toString();
543
+ } catch {
544
+ return sanitizeScalar(value);
545
+ }
546
+ }
547
+ function sanitizeHost(value) {
548
+ if (!value)
549
+ return;
550
+ return sanitizeScalar(value);
551
+ }
552
+ export {
553
+ rollbackImport,
554
+ previewImport,
555
+ applyImport
556
+ };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,15 @@
1
1
  export { createUptimeClient, UptimeService } from "./service.js";
2
2
  export { UptimeStore } from "./store.js";
3
- export { runMonitorCheck, runHttpCheck, runTcpCheck } from "./checks.js";
3
+ export { runBrowserPageCheck, runMonitorCheck, runHttpCheck, runTcpCheck } from "./checks.js";
4
4
  export { createApiHandler, serveUptime } from "./api.js";
5
+ export { applyImport, previewImport, rollbackImport } from "./imports.js";
5
6
  export { buildUptimeReport, sendUptimeReport } from "./report.js";
6
- export { uptimeHome, uptimeDbPath, ensureUptimeHome } from "./paths.js";
7
- export type { CheckAttemptResult, CheckResult, CheckStatus, CreateMonitorInput, Incident, IncidentStatus, ListResultsOptions, Monitor, MonitorKind, MonitorStatus, MonitorSummary, SchedulerHandle, UpdateMonitorInput, UptimeSummary, } from "./types.js";
7
+ export { generateProbeKeyPair, probePublicKeyFingerprint, probeResultSigningPayload, signProbeResult, verifyProbeResultSignature } from "./probes.js";
8
+ export { uptimeHome, uptimeDbPath, uptimeHostedFallbackDbPath, ensureUptimeHome } from "./paths.js";
9
+ export type { UptimeBackup, UptimeBackupCheck, UptimeRuntimeMode, UptimeStoreOptions, MonitorProvenance, SaveImportBatchInput, StoredImportBatch, UpsertMonitorProvenanceInput, } from "./store.js";
10
+ export type { BrowserPageRunner, BrowserPageRunnerResult, FetchLike, } from "./checks.js";
11
+ export type { ImportAction, ImportApplyItem, ImportApplyResult, ImportCandidate, ImportPreview, ImportPreviewItem, ImportRequest, ImportRollbackItem, ImportRollbackResult, ImportSource, } from "./imports.js";
12
+ export type { BrowserFailedRequest, BrowserPageEvidence, AuditEvent, CheckAttemptResult, CheckEvidence, CheckResult, CheckStatus, CreateMonitorKind, CreateMonitorInput, CreateReportScheduleInput, ImportedMonitorInput, ImportedUpdateMonitorInput, EvidenceArtifact, Incident, IncidentStatus, ListAuditEventsOptions, ListReportRunsOptions, ListResultsOptions, Monitor, MonitorKind, MonitorStatus, MonitorSummary, ProbeCheckJob, ProbeCheckJobStatus, ProbeIdentity, ProbeResultSubmission, ProbeSubmissionReceipt, RecordAuditEventInput, ReportDeliveryChannel, ReportDeliveryRecord, ReportEmailChannelConfig, ReportLogsChannelConfig, ReportRun, ReportRunStatus, ReportSchedule, ReportScheduleChannels, ReportScheduleStatus, ReportSmsChannelConfig, SchedulerHandle, UpdateMonitorInput, UpdateReportScheduleInput, UptimeSummary, } from "./types.js";
13
+ export type { ProbeKeyPair, ProbeSigningInput } from "./probes.js";
8
14
  export type { BuildUptimeReportOptions, SendUptimeReportOptions, UptimeEmailReportTarget, UptimeLogsReportTarget, UptimeReport, UptimeReportDelivery, UptimeSmsReportTarget, } from "./report.js";
9
15
  //# sourceMappingURL=index.d.ts.map