@hasna/uptime 0.1.22 → 0.1.24

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/index.js CHANGED
@@ -236,8 +236,9 @@ async function runMonitorCheck(monitor, options = {}) {
236
236
  }
237
237
  if (monitor.kind === "browser_page")
238
238
  return runBrowserPageCheck(monitor, { fetch: options.fetch, runner: options.browserPage });
239
- if (monitor.kind === "tcp")
240
- return runTcpCheck(monitor);
239
+ if (monitor.kind === "tcp") {
240
+ return options.hostedTargetPolicy ? runHostedTcpCheck(monitor, { resolveHost: options.resolveHost }) : runTcpCheck(monitor);
241
+ }
241
242
  return { status: "down", latencyMs: null, error: `unsupported monitor kind: ${monitor.kind ?? "unknown"}` };
242
243
  }
243
244
  async function runHttpCheck(monitor, fetchImpl = fetch) {
@@ -349,9 +350,30 @@ async function runHostedHttpCheck(monitor, options = {}) {
349
350
  async function runTcpCheck(monitor) {
350
351
  if (!monitor.host || !monitor.port)
351
352
  return { status: "down", latencyMs: null, error: "missing host or port" };
353
+ return runTcpSocket(monitor.host, monitor.port, monitor.timeoutMs);
354
+ }
355
+ async function runHostedTcpCheck(monitor, options = {}) {
356
+ if (!monitor.host || !monitor.port)
357
+ return { status: "down", latencyMs: null, error: "missing host or port" };
358
+ const resolver = options.resolveHost ?? resolveHostedHost;
359
+ try {
360
+ const addresses = normalizeResolvedAddresses(await resolver(normalizeHostedHost(monitor.host)));
361
+ assertHostedResolvedAddressesAllowed(monitor.host, addresses, "TCP resolved address");
362
+ const address = addresses[0];
363
+ return runTcpSocket(address.address, monitor.port, monitor.timeoutMs, address.family);
364
+ } catch (error) {
365
+ return {
366
+ status: "down",
367
+ latencyMs: null,
368
+ statusCode: null,
369
+ error: error instanceof Error ? error.message : String(error)
370
+ };
371
+ }
372
+ }
373
+ async function runTcpSocket(host, port, timeoutMs, family) {
352
374
  const started = performance.now();
353
375
  return new Promise((resolve) => {
354
- const socket = net2.createConnection({ host: monitor.host, port: monitor.port, timeout: monitor.timeoutMs });
376
+ const socket = net2.createConnection({ host, port, timeout: timeoutMs, family });
355
377
  let settled = false;
356
378
  const finish = (result) => {
357
379
  if (settled)
@@ -767,7 +789,7 @@ function previewRecord(store, source, record, defaults, options) {
767
789
  };
768
790
  }
769
791
  const monitorOptions = options.workspaceId ? { workspaceId: options.workspaceId } : undefined;
770
- const rawProvenance = store.getProvenance(candidate.source, candidate.sourceId);
792
+ const rawProvenance = store.getProvenance(candidate.source, candidate.sourceId, monitorOptions);
771
793
  const provenanceMonitor = rawProvenance ? store.getMonitor(rawProvenance.monitorId, monitorOptions) : null;
772
794
  const provenance = provenanceMonitor ? rawProvenance : null;
773
795
  const monitor = provenanceMonitor ?? store.getMonitor(candidate.name, monitorOptions);
@@ -1237,7 +1259,7 @@ var REQUIRED_TABLES = [
1237
1259
  ];
1238
1260
  var PROBE_TABLES = new Set(["probe_identities", "probe_check_jobs", "probe_submissions"]);
1239
1261
  var REPORT_AUDIT_TABLES = new Set(["report_schedules", "report_runs", "audit_events"]);
1240
- var CURRENT_SCHEMA_VERSION = "4";
1262
+ var CURRENT_SCHEMA_VERSION = "5";
1241
1263
 
1242
1264
  class StaleCheckResultError extends Error {
1243
1265
  constructor(message) {
@@ -1350,15 +1372,17 @@ class UptimeStore {
1350
1372
  `);
1351
1373
  this.db.run(`
1352
1374
  CREATE TABLE IF NOT EXISTS monitor_provenance (
1375
+ workspace_id TEXT NOT NULL DEFAULT 'local',
1353
1376
  monitor_id TEXT NOT NULL REFERENCES monitors(id) ON DELETE CASCADE,
1354
1377
  source TEXT NOT NULL,
1355
1378
  source_id TEXT NOT NULL,
1356
1379
  source_label TEXT,
1357
1380
  imported_at TEXT NOT NULL,
1358
1381
  snapshot_json TEXT NOT NULL,
1359
- PRIMARY KEY (source, source_id)
1382
+ PRIMARY KEY (workspace_id, source, source_id)
1360
1383
  )
1361
1384
  `);
1385
+ this.ensureMonitorProvenanceWorkspaceScoped();
1362
1386
  this.db.run(`
1363
1387
  CREATE TABLE IF NOT EXISTS import_batches (
1364
1388
  id TEXT PRIMARY KEY,
@@ -1450,6 +1474,7 @@ class UptimeStore {
1450
1474
  this.db.run(`
1451
1475
  CREATE TABLE IF NOT EXISTS audit_events (
1452
1476
  id TEXT PRIMARY KEY,
1477
+ workspace_id TEXT,
1453
1478
  action TEXT NOT NULL,
1454
1479
  resource_type TEXT,
1455
1480
  resource_id TEXT,
@@ -1459,6 +1484,7 @@ class UptimeStore {
1459
1484
  created_at TEXT NOT NULL
1460
1485
  )
1461
1486
  `);
1487
+ this.ensureColumn("audit_events", "workspace_id", "TEXT");
1462
1488
  this.db.run(`
1463
1489
  CREATE TABLE IF NOT EXISTS schema_migrations (
1464
1490
  key TEXT PRIMARY KEY,
@@ -1472,6 +1498,7 @@ class UptimeStore {
1472
1498
  this.db.run("CREATE INDEX IF NOT EXISTS idx_incidents_monitor_status ON incidents(monitor_id, status)");
1473
1499
  this.db.run("CREATE INDEX IF NOT EXISTS idx_check_leases_until ON check_leases(leased_until)");
1474
1500
  this.db.run("CREATE INDEX IF NOT EXISTS idx_monitor_provenance_monitor ON monitor_provenance(monitor_id)");
1501
+ this.db.run("CREATE INDEX IF NOT EXISTS idx_monitor_provenance_workspace_source ON monitor_provenance(workspace_id, source, source_id)");
1475
1502
  this.db.run("CREATE INDEX IF NOT EXISTS idx_probe_jobs_status_due ON probe_check_jobs(status, due_at)");
1476
1503
  this.db.run("CREATE INDEX IF NOT EXISTS idx_probe_jobs_probe_status ON probe_check_jobs(claimed_by_probe_id, status)");
1477
1504
  this.db.run("CREATE INDEX IF NOT EXISTS idx_probe_submissions_probe_time ON probe_submissions(probe_id, submitted_at DESC)");
@@ -1480,6 +1507,7 @@ class UptimeStore {
1480
1507
  this.db.run("CREATE INDEX IF NOT EXISTS idx_report_schedules_due ON report_schedules(enabled, next_run_at)");
1481
1508
  this.db.run("CREATE INDEX IF NOT EXISTS idx_report_runs_schedule_time ON report_runs(schedule_id, started_at DESC)");
1482
1509
  this.db.run("CREATE INDEX IF NOT EXISTS idx_audit_events_resource_time ON audit_events(resource_type, resource_id, created_at DESC)");
1510
+ this.db.run("CREATE INDEX IF NOT EXISTS idx_audit_events_workspace_time ON audit_events(workspace_id, created_at DESC)");
1483
1511
  this.db.run("CREATE INDEX IF NOT EXISTS idx_audit_events_time ON audit_events(created_at DESC)");
1484
1512
  }
1485
1513
  backup(destinationPath) {
@@ -1529,6 +1557,9 @@ class UptimeStore {
1529
1557
  const normalized = normalizeCreateMonitor(input, options.allowBrowserPage === true);
1530
1558
  if (this.mode === "hosted")
1531
1559
  assertHostedTargetAllowed(normalized);
1560
+ if (this.mode === "hosted" && normalized.kind === "browser_page" && normalized.enabled !== false) {
1561
+ throw new Error("hosted browser_page monitors must remain disabled until browser evidence workers are configured");
1562
+ }
1532
1563
  const now = new Date().toISOString();
1533
1564
  const workspaceId = normalizeWorkspaceId(options.workspaceId ?? input.workspaceId ?? "local");
1534
1565
  const monitor = {
@@ -1593,6 +1624,9 @@ class UptimeStore {
1593
1624
  const next = normalizeUpdateMonitor(current, input, updatedAt, options.allowBrowserPage === true);
1594
1625
  if (this.mode === "hosted")
1595
1626
  assertHostedTargetAllowed(next);
1627
+ if (this.mode === "hosted" && next.kind === "browser_page" && next.enabled) {
1628
+ throw new Error("hosted browser_page monitors must remain disabled until browser evidence workers are configured");
1629
+ }
1596
1630
  this.db.query(`UPDATE monitors SET
1597
1631
  name = ?, kind = ?, url = ?, host = ?, port = ?, method = ?,
1598
1632
  expected_status = ?, interval_seconds = ?, timeout_ms = ?,
@@ -1889,8 +1923,10 @@ class UptimeStore {
1889
1923
  const action = normalizeAuditText(input.action, "Audit action", 160);
1890
1924
  const createdAt = input.createdAt ?? new Date().toISOString();
1891
1925
  assertIsoTimestamp(createdAt, "Audit event createdAt");
1926
+ const workspaceId = input.workspaceId == null ? null : normalizeWorkspaceId(input.workspaceId);
1892
1927
  const event = {
1893
1928
  id: newId("aud"),
1929
+ workspaceId,
1894
1930
  action,
1895
1931
  resourceType: normalizeNullableAuditText(input.resourceType, "Audit resourceType", 80),
1896
1932
  resourceId: normalizeNullableAuditText(input.resourceId, "Audit resourceId", 160),
@@ -1900,13 +1936,17 @@ class UptimeStore {
1900
1936
  createdAt
1901
1937
  };
1902
1938
  this.db.query(`INSERT INTO audit_events (
1903
- id, action, resource_type, resource_id, message, metadata_json, actor, created_at
1904
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(event.id, event.action, event.resourceType, event.resourceId, event.message, JSON.stringify(event.metadata), event.actor, event.createdAt);
1939
+ id, workspace_id, action, resource_type, resource_id, message, metadata_json, actor, created_at
1940
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(event.id, event.workspaceId, event.action, event.resourceType, event.resourceId, event.message, JSON.stringify(event.metadata), event.actor, event.createdAt);
1905
1941
  return event;
1906
1942
  }
1907
1943
  listAuditEvents(options = {}) {
1908
1944
  const clauses = [];
1909
1945
  const args = [];
1946
+ if (options.workspaceId) {
1947
+ clauses.push("workspace_id = ?");
1948
+ args.push(normalizeWorkspaceId(options.workspaceId));
1949
+ }
1910
1950
  if (options.resourceType) {
1911
1951
  clauses.push("resource_type = ?");
1912
1952
  args.push(options.resourceType);
@@ -1998,21 +2038,24 @@ class UptimeStore {
1998
2038
  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);
1999
2039
  return rows.map(checkResultFromRow);
2000
2040
  }
2001
- getProvenance(source, sourceId) {
2002
- const row = this.db.query("SELECT * FROM monitor_provenance WHERE source = ? AND source_id = ?").get(source, sourceId);
2041
+ getProvenance(source, sourceId, options = {}) {
2042
+ const workspaceId = options.workspaceId ? normalizeWorkspaceId(options.workspaceId) : undefined;
2043
+ const row = workspaceId ? this.db.query("SELECT * FROM monitor_provenance WHERE workspace_id = ? AND source = ? AND source_id = ?").get(workspaceId, source, sourceId) : this.db.query("SELECT * FROM monitor_provenance WHERE source = ? AND source_id = ? ORDER BY imported_at DESC LIMIT 1").get(source, sourceId);
2003
2044
  return row ? provenanceFromRow(row) : null;
2004
2045
  }
2005
2046
  upsertMonitorProvenance(input) {
2006
2047
  const importedAt = new Date().toISOString();
2048
+ const monitor = this.getMonitor(input.monitorId);
2049
+ const workspaceId = normalizeWorkspaceId(input.workspaceId ?? monitor?.workspaceId ?? "local");
2007
2050
  this.db.query(`INSERT INTO monitor_provenance (
2008
- monitor_id, source, source_id, source_label, imported_at, snapshot_json
2009
- ) VALUES (?, ?, ?, ?, ?, ?)
2010
- ON CONFLICT(source, source_id) DO UPDATE SET
2051
+ workspace_id, monitor_id, source, source_id, source_label, imported_at, snapshot_json
2052
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
2053
+ ON CONFLICT(workspace_id, source, source_id) DO UPDATE SET
2011
2054
  monitor_id = excluded.monitor_id,
2012
2055
  source_label = excluded.source_label,
2013
2056
  imported_at = excluded.imported_at,
2014
- snapshot_json = excluded.snapshot_json`).run(input.monitorId, input.source, input.sourceId, input.sourceLabel ?? null, importedAt, JSON.stringify(input.snapshot));
2015
- return this.getProvenance(input.source, input.sourceId);
2057
+ snapshot_json = excluded.snapshot_json`).run(workspaceId, input.monitorId, input.source, input.sourceId, input.sourceLabel ?? null, importedAt, JSON.stringify(input.snapshot));
2058
+ return this.getProvenance(input.source, input.sourceId, { workspaceId });
2016
2059
  }
2017
2060
  saveImportBatch(input) {
2018
2061
  const createdAt = new Date().toISOString();
@@ -2195,6 +2238,49 @@ class UptimeStore {
2195
2238
  this.db.run("PRAGMA foreign_keys = ON");
2196
2239
  }
2197
2240
  }
2241
+ ensureMonitorProvenanceWorkspaceScoped() {
2242
+ const row = this.db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'monitor_provenance'").get();
2243
+ const columns = this.db.query("PRAGMA table_info(monitor_provenance)").all();
2244
+ const hasWorkspaceId = columns.some((column) => column.name === "workspace_id");
2245
+ const hasWorkspacePrimaryKey = Boolean(row?.sql?.includes("PRIMARY KEY (workspace_id, source, source_id)"));
2246
+ if (hasWorkspaceId && hasWorkspacePrimaryKey)
2247
+ return;
2248
+ this.db.run("PRAGMA foreign_keys = OFF");
2249
+ this.db.run("PRAGMA legacy_alter_table = ON");
2250
+ try {
2251
+ const migrate = this.db.transaction(() => {
2252
+ this.db.run("ALTER TABLE monitor_provenance RENAME TO monitor_provenance_old_workspace");
2253
+ this.db.run(`
2254
+ CREATE TABLE monitor_provenance (
2255
+ workspace_id TEXT NOT NULL DEFAULT 'local',
2256
+ monitor_id TEXT NOT NULL REFERENCES monitors(id) ON DELETE CASCADE,
2257
+ source TEXT NOT NULL,
2258
+ source_id TEXT NOT NULL,
2259
+ source_label TEXT,
2260
+ imported_at TEXT NOT NULL,
2261
+ snapshot_json TEXT NOT NULL,
2262
+ PRIMARY KEY (workspace_id, source, source_id)
2263
+ )
2264
+ `);
2265
+ const workspaceSelect = hasWorkspaceId ? "COALESCE(old.workspace_id, monitors.workspace_id, 'local')" : "COALESCE(monitors.workspace_id, 'local')";
2266
+ this.db.run(`
2267
+ INSERT OR REPLACE INTO monitor_provenance (
2268
+ workspace_id, monitor_id, source, source_id, source_label, imported_at, snapshot_json
2269
+ )
2270
+ SELECT
2271
+ ${workspaceSelect}, old.monitor_id, old.source, old.source_id, old.source_label,
2272
+ old.imported_at, old.snapshot_json
2273
+ FROM monitor_provenance_old_workspace old
2274
+ LEFT JOIN monitors ON monitors.id = old.monitor_id
2275
+ `);
2276
+ this.db.run("DROP TABLE monitor_provenance_old_workspace");
2277
+ });
2278
+ migrate();
2279
+ } finally {
2280
+ this.db.run("PRAGMA legacy_alter_table = OFF");
2281
+ this.db.run("PRAGMA foreign_keys = ON");
2282
+ }
2283
+ }
2198
2284
  vacuumInto(backupPath) {
2199
2285
  const quoted = backupPath.replace(/'/g, "''");
2200
2286
  this.db.run(`VACUUM INTO '${quoted}'`);
@@ -2224,10 +2310,11 @@ function verifyBackupFile(backupPath) {
2224
2310
  const missingTables = REQUIRED_TABLES.filter((table) => !tableExists(db, table));
2225
2311
  const schemaVersion = missingTables.includes("schema_migrations") ? null : db.query("SELECT value FROM schema_migrations WHERE key = 'schema_version'").get()?.value ?? null;
2226
2312
  const currentOk = missingTables.length === 0 && schemaVersion === CURRENT_SCHEMA_VERSION;
2313
+ const restorableV4 = schemaVersion === "4" && missingTables.length === 0;
2227
2314
  const restorableV1 = schemaVersion === "1" && missingTables.every((table) => PROBE_TABLES.has(table) || REPORT_AUDIT_TABLES.has(table));
2228
2315
  const restorableV2 = schemaVersion === "2" && missingTables.every((table) => REPORT_AUDIT_TABLES.has(table));
2229
2316
  return {
2230
- ok: integrity === "ok" && (currentOk || restorableV1 || restorableV2),
2317
+ ok: integrity === "ok" && (currentOk || restorableV4 || restorableV1 || restorableV2),
2231
2318
  backupPath,
2232
2319
  integrity,
2233
2320
  schemaVersion,
@@ -2612,6 +2699,7 @@ function checkResultFromRow(row) {
2612
2699
  }
2613
2700
  function provenanceFromRow(row) {
2614
2701
  return {
2702
+ workspaceId: row.workspace_id,
2615
2703
  monitorId: row.monitor_id,
2616
2704
  source: row.source,
2617
2705
  sourceId: row.source_id,
@@ -2699,6 +2787,7 @@ function reportRunFromRow(row) {
2699
2787
  function auditEventFromRow(row) {
2700
2788
  return {
2701
2789
  id: row.id,
2790
+ workspaceId: row.workspace_id,
2702
2791
  action: row.action,
2703
2792
  resourceType: row.resource_type,
2704
2793
  resourceId: row.resource_id,
@@ -3061,12 +3150,18 @@ var MAX_PROBE_RESULT_FUTURE_MS = 5 * 60000;
3061
3150
  class UptimeService {
3062
3151
  store;
3063
3152
  checkRunner;
3153
+ hostedResolveHost;
3154
+ hostedHttpRequest;
3155
+ hostedMaxRedirects;
3064
3156
  leaseOwner = `svc_${randomUUID3().replace(/-/g, "").slice(0, 18)}`;
3065
3157
  inFlightChecks = new Set;
3066
3158
  inFlightReportSchedules = new Set;
3067
3159
  constructor(options = {}) {
3068
3160
  this.store = options.store ?? new UptimeStore({ mode: "local", ...options });
3069
3161
  this.checkRunner = options.checkRunner ?? runMonitorCheck;
3162
+ this.hostedResolveHost = options.hostedResolveHost;
3163
+ this.hostedHttpRequest = options.hostedHttpRequest;
3164
+ this.hostedMaxRedirects = options.hostedMaxRedirects;
3070
3165
  }
3071
3166
  close() {
3072
3167
  this.store.close();
@@ -3201,10 +3296,10 @@ class UptimeService {
3201
3296
  return this.reportStore().listReportRuns(options);
3202
3297
  }
3203
3298
  listAuditEvents(options = {}) {
3204
- return this.reportStore().listAuditEvents(options);
3299
+ return this.auditStore().listAuditEvents(options);
3205
3300
  }
3206
3301
  recordAuditEvent(input) {
3207
- return this.reportStore().recordAuditEvent(input);
3302
+ return this.auditStore().recordAuditEvent(input);
3208
3303
  }
3209
3304
  async runReportSchedule(idOrName, options = {}) {
3210
3305
  const store = this.reportStore();
@@ -3273,39 +3368,7 @@ class UptimeService {
3273
3368
  const monitor = this.store.getMonitor(idOrName);
3274
3369
  if (!monitor)
3275
3370
  throw new Error(`Monitor not found: ${idOrName}`);
3276
- if (!monitor.enabled)
3277
- throw new Error(`Monitor is disabled: ${monitor.name}`);
3278
- if (this.inFlightChecks.has(monitor.id))
3279
- throw new Error(`Monitor check already in progress: ${monitor.name}`);
3280
- const leaseTtlMs = Math.max(60000, (monitor.retryCount + 1) * monitor.timeoutMs + 1e4);
3281
- if (!this.store.acquireCheckLease(monitor.id, this.leaseOwner, leaseTtlMs)) {
3282
- throw new MonitorCheckBusyError(`Monitor check already in progress: ${monitor.name}`);
3283
- }
3284
- this.inFlightChecks.add(monitor.id);
3285
- try {
3286
- let attemptCount = 0;
3287
- let last = null;
3288
- const maxAttempts = Math.max(1, monitor.retryCount + 1);
3289
- while (attemptCount < maxAttempts) {
3290
- attemptCount += 1;
3291
- last = await this.checkRunner(monitor);
3292
- if (last.status === "up")
3293
- break;
3294
- }
3295
- return this.store.recordCheckResult({
3296
- monitorId: monitor.id,
3297
- status: last.status,
3298
- latencyMs: last.latencyMs,
3299
- statusCode: last.statusCode ?? null,
3300
- error: last.error ?? null,
3301
- evidence: last.evidence ?? null,
3302
- attemptCount,
3303
- expectedMonitorRevision: monitor.revision
3304
- });
3305
- } finally {
3306
- this.inFlightChecks.delete(monitor.id);
3307
- this.store.releaseCheckLease(monitor.id, this.leaseOwner);
3308
- }
3371
+ return this.recordMonitorCheck(monitor, { hostedTargetPolicy: false });
3309
3372
  }
3310
3373
  async checkAll() {
3311
3374
  if (this.store.mode === "hosted")
@@ -3317,6 +3380,49 @@ class UptimeService {
3317
3380
  }
3318
3381
  return results;
3319
3382
  }
3383
+ async checkHostedPublicMonitor(idOrName, options = {}) {
3384
+ this.assertHostedPublicChecksEnabled();
3385
+ const workspaceId = this.requireHostedWorkerWorkspaceId(options.workspaceId);
3386
+ const monitor = this.store.getMonitor(idOrName, { workspaceId });
3387
+ if (!monitor)
3388
+ throw new Error(`Monitor not found: ${idOrName}`);
3389
+ this.assertHostedPublicMonitor(monitor);
3390
+ const result = await this.recordMonitorCheck(monitor, { hostedTargetPolicy: true });
3391
+ this.auditStore().recordAuditEvent({
3392
+ workspaceId,
3393
+ action: "hosted_public_check.run",
3394
+ resourceType: "monitor",
3395
+ resourceId: monitor.id,
3396
+ message: `Ran hosted public check for ${monitor.name}`,
3397
+ metadata: {
3398
+ checkResultId: result.id,
3399
+ status: result.status,
3400
+ monitorKind: monitor.kind,
3401
+ operatorPath: "hosted_public_check"
3402
+ },
3403
+ actor: "hosted-public-check-worker"
3404
+ });
3405
+ return result;
3406
+ }
3407
+ async runDueHostedPublicChecks(now = new Date, options = {}) {
3408
+ this.assertHostedPublicChecksEnabled();
3409
+ const workspaceId = this.requireHostedWorkerWorkspaceId(options.workspaceId);
3410
+ const due = this.store.listMonitors({ workspaceId }).filter((monitor) => this.isHostedPublicMonitor(monitor) && this.isDue(monitor, now));
3411
+ const results = [];
3412
+ for (const monitor of due) {
3413
+ const current = this.store.getMonitor(monitor.id, { workspaceId });
3414
+ if (!current || !this.isHostedPublicMonitor(current) || !this.isDue(current, now))
3415
+ continue;
3416
+ try {
3417
+ results.push(await this.checkHostedPublicMonitor(current.id, { workspaceId }));
3418
+ } catch (error) {
3419
+ if (error instanceof MonitorCheckBusyError || error instanceof StaleCheckResultError)
3420
+ continue;
3421
+ throw error;
3422
+ }
3423
+ }
3424
+ return results;
3425
+ }
3320
3426
  startScheduler(options = {}) {
3321
3427
  if (this.store.mode === "hosted")
3322
3428
  throw new Error("hosted scheduler requires check_jobs and probes");
@@ -3362,6 +3468,67 @@ class UptimeService {
3362
3468
  const last = new Date(monitor.lastCheckedAt).getTime();
3363
3469
  return now.getTime() - last >= monitor.intervalSeconds * 1000;
3364
3470
  }
3471
+ async recordMonitorCheck(monitor, options) {
3472
+ if (!monitor.enabled)
3473
+ throw new Error(`Monitor is disabled: ${monitor.name}`);
3474
+ if (this.inFlightChecks.has(monitor.id))
3475
+ throw new Error(`Monitor check already in progress: ${monitor.name}`);
3476
+ const leaseTtlMs = Math.max(60000, (monitor.retryCount + 1) * monitor.timeoutMs + 1e4);
3477
+ if (!this.store.acquireCheckLease(monitor.id, this.leaseOwner, leaseTtlMs)) {
3478
+ throw new MonitorCheckBusyError(`Monitor check already in progress: ${monitor.name}`);
3479
+ }
3480
+ this.inFlightChecks.add(monitor.id);
3481
+ try {
3482
+ let attemptCount = 0;
3483
+ let last = null;
3484
+ const maxAttempts = Math.max(1, monitor.retryCount + 1);
3485
+ while (attemptCount < maxAttempts) {
3486
+ attemptCount += 1;
3487
+ last = options.hostedTargetPolicy ? await this.runHostedPublicCheckAttempt(monitor) : await this.checkRunner(monitor);
3488
+ if (last.status === "up")
3489
+ break;
3490
+ }
3491
+ return this.store.recordCheckResult({
3492
+ monitorId: monitor.id,
3493
+ status: last.status,
3494
+ latencyMs: last.latencyMs,
3495
+ statusCode: last.statusCode ?? null,
3496
+ error: last.error ?? null,
3497
+ evidence: last.evidence ?? null,
3498
+ attemptCount,
3499
+ expectedMonitorRevision: monitor.revision
3500
+ });
3501
+ } finally {
3502
+ this.inFlightChecks.delete(monitor.id);
3503
+ this.store.releaseCheckLease(monitor.id, this.leaseOwner);
3504
+ }
3505
+ }
3506
+ runHostedPublicCheckAttempt(monitor) {
3507
+ return runMonitorCheck(monitor, {
3508
+ hostedTargetPolicy: true,
3509
+ resolveHost: this.hostedResolveHost,
3510
+ hostedHttpRequest: this.hostedHttpRequest,
3511
+ maxRedirects: this.hostedMaxRedirects
3512
+ });
3513
+ }
3514
+ assertHostedPublicChecksEnabled() {
3515
+ if (this.store.mode !== "hosted")
3516
+ throw new Error("hosted public checks require hosted mode");
3517
+ }
3518
+ requireHostedWorkerWorkspaceId(workspaceId) {
3519
+ const value = workspaceId?.trim() || process.env.HASNA_UPTIME_WORKSPACE_ID?.trim();
3520
+ if (!value)
3521
+ throw new Error("hosted public checks require a workspace id");
3522
+ return value;
3523
+ }
3524
+ assertHostedPublicMonitor(monitor) {
3525
+ if (!this.isHostedPublicMonitor(monitor)) {
3526
+ throw new Error("hosted public checks support only HTTP and TCP monitors");
3527
+ }
3528
+ }
3529
+ isHostedPublicMonitor(monitor) {
3530
+ return monitor.kind === "http" || monitor.kind === "tcp";
3531
+ }
3365
3532
  probeStore() {
3366
3533
  if (this.store.mode === "hosted") {
3367
3534
  throw new Error("hosted probe APIs require cloud check_jobs, workspace stores, and audit logging");
@@ -3411,6 +3578,13 @@ class UptimeService {
3411
3578
  }
3412
3579
  return store;
3413
3580
  }
3581
+ auditStore() {
3582
+ const store = this.store;
3583
+ if (typeof store.recordAuditEvent !== "function" || typeof store.listAuditEvents !== "function") {
3584
+ throw new Error("audit logging requires an audit-capable store");
3585
+ }
3586
+ return store;
3587
+ }
3414
3588
  audit(action, resourceType, resourceId, message, metadata) {
3415
3589
  this.reportStore().recordAuditEvent({
3416
3590
  action,
@@ -4118,7 +4292,10 @@ async function handleApiRoute(service, request, url, apiPath, options, hosted, a
4118
4292
  return json(service.listMonitors({ includeDisabled: url.searchParams.get("includeDisabled") === "true", workspaceId: actor?.workspaceId }));
4119
4293
  }
4120
4294
  if (request.method === "POST" && apiPath === "/api/monitors") {
4121
- return json(service.createMonitor(await jsonBody(request), { workspaceId: actor?.workspaceId }), 201);
4295
+ const monitor = service.createMonitor(await jsonBody(request), { workspaceId: actor?.workspaceId });
4296
+ if (hosted && actor)
4297
+ recordHostedMonitorAudit(service, actor, "monitor.create", monitor, { method: request.method, apiPath });
4298
+ return json(monitor, 201);
4122
4299
  }
4123
4300
  if (request.method === "GET" && apiPath === "/api/incidents") {
4124
4301
  const status = url.searchParams.get("status");
@@ -4195,10 +4372,25 @@ async function handleApiRoute(service, request, url, apiPath, options, hosted, a
4195
4372
  return monitor ? json(monitor) : json({ error: "not found" }, 404);
4196
4373
  }
4197
4374
  if (request.method === "PATCH" && !monitorMatch[2]) {
4198
- return json(service.updateMonitor(id, await jsonBody(request), { workspaceId: actor?.workspaceId }));
4375
+ const before = hosted ? service.getMonitor(id, { workspaceId: actor?.workspaceId }) : null;
4376
+ const monitor = service.updateMonitor(id, await jsonBody(request), { workspaceId: actor?.workspaceId });
4377
+ if (hosted && actor) {
4378
+ recordHostedMonitorAudit(service, actor, "monitor.update", monitor, {
4379
+ method: request.method,
4380
+ apiPath,
4381
+ previousRevision: before?.revision ?? null,
4382
+ nextRevision: monitor.revision
4383
+ });
4384
+ }
4385
+ return json(monitor);
4199
4386
  }
4200
4387
  if (request.method === "DELETE" && !monitorMatch[2]) {
4201
- return json({ deleted: service.deleteMonitor(id, { workspaceId: actor?.workspaceId }) });
4388
+ const before = hosted ? service.getMonitor(id, { workspaceId: actor?.workspaceId }) : null;
4389
+ const deleted = service.deleteMonitor(id, { workspaceId: actor?.workspaceId });
4390
+ if (hosted && actor && deleted && before) {
4391
+ recordHostedMonitorAudit(service, actor, "monitor.delete", before, { method: request.method, apiPath });
4392
+ }
4393
+ return json({ deleted });
4202
4394
  }
4203
4395
  if (request.method === "POST" && monitorMatch[2] === "check") {
4204
4396
  if (hosted)
@@ -4242,7 +4434,29 @@ function requireHostedActor(request, url, options, scope) {
4242
4434
  if (requestedWorkspace && requestedWorkspace !== workspaceId) {
4243
4435
  throw new ApiError("workspace access denied", 403);
4244
4436
  }
4245
- return { scopes, workspaceId };
4437
+ return {
4438
+ scopes,
4439
+ workspaceId,
4440
+ actor: token.actor ?? `hosted-token:${workspaceId}:${[...scopes].sort().join(",")}`
4441
+ };
4442
+ }
4443
+ function recordHostedMonitorAudit(service, actor, action, monitor, metadata) {
4444
+ service.recordAuditEvent({
4445
+ workspaceId: actor.workspaceId,
4446
+ action,
4447
+ actor: actor.actor,
4448
+ resourceType: "monitor",
4449
+ resourceId: monitor.id,
4450
+ metadata: {
4451
+ ...metadata,
4452
+ monitorName: monitor.name,
4453
+ monitorKind: monitor.kind,
4454
+ monitorEnabled: monitor.enabled,
4455
+ monitorRevision: monitor.revision,
4456
+ workspaceId: monitor.workspaceId,
4457
+ scopes: [...actor.scopes].sort()
4458
+ }
4459
+ });
4246
4460
  }
4247
4461
  function isLoopbackHost(hostname) {
4248
4462
  const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
@@ -4320,7 +4534,8 @@ function normalizeHostedTokenEntry(entry, defaultWorkspaceId, source) {
4320
4534
  }
4321
4535
  const scopes = normalizeHostedScopes(entry.scopes, `${source}.scopes`);
4322
4536
  const workspaceId = typeof entry.workspaceId === "string" && entry.workspaceId.trim() ? entry.workspaceId.trim() : defaultWorkspaceId;
4323
- return { token: entry.token.trim(), scopes, workspaceId };
4537
+ const actor = typeof entry.actor === "string" && entry.actor.trim() ? entry.actor.trim() : typeof entry.subject === "string" && entry.subject.trim() ? entry.subject.trim() : typeof entry.id === "string" && entry.id.trim() ? entry.id.trim() : undefined;
4538
+ return { token: entry.token.trim(), scopes, workspaceId, actor };
4324
4539
  }
4325
4540
  function normalizeHostedScopes(value, source) {
4326
4541
  if (!Array.isArray(value) || value.length === 0) {
@@ -4420,7 +4635,7 @@ function buildAwsDeploymentPlan(options = {}) {
4420
4635
  const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
4421
4636
  const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
4422
4637
  const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
4423
- const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.22");
4638
+ const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.24");
4424
4639
  const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
4425
4640
  const protectedAccessUrl = protectedAccessMode === "cloudfront_default_domain" ? "https://<cloudfront-domain>" : `https://${hostname}`;
4426
4641
  const cluster = `${prefix}-${stage}`;
@@ -4665,7 +4880,10 @@ function buildPrivateProbeCloudConfig(options = {}) {
4665
4880
  }
4666
4881
  };
4667
4882
  }
4668
- function renderPrivateProbeEnv(config) {
4883
+ function renderPrivateProbeEnv(config, options = {}) {
4884
+ if (!options.allowBlocked && (!config.canStart || config.blockers.length > 0)) {
4885
+ throw new Error("private probe env output is blocked until hosted probe routes and cloud jobs are implemented");
4886
+ }
4669
4887
  const required = ["HASNA_UPTIME_PRIVATE_PROBE_ID"];
4670
4888
  const missing = required.filter((key) => !config.env[key]);
4671
4889
  if (missing.length > 0) {
@@ -4712,6 +4930,7 @@ export {
4712
4930
  runTcpCheck,
4713
4931
  runMonitorCheck,
4714
4932
  runHttpCheck,
4933
+ runHostedTcpCheck,
4715
4934
  runHostedHttpCheck,
4716
4935
  runBrowserPageCheck,
4717
4936
  rollbackImport,