@hasna/uptime 0.1.10 → 0.1.12

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/mcp/index.js CHANGED
@@ -14296,13 +14296,240 @@ function date4(params) {
14296
14296
  // node_modules/zod/v4/classic/external.js
14297
14297
  config(en_default());
14298
14298
  // src/checks.ts
14299
+ import dns from "dns/promises";
14300
+ import http from "http";
14301
+ import https from "https";
14302
+ import net2 from "net";
14303
+
14304
+ // src/target-policy.ts
14299
14305
  import net from "net";
14306
+ var SECRET_PARAM_PATTERN = /(token|secret|password|passwd|api[_-]?key|access[_-]?token|auth|credential|session)/i;
14307
+ var DENIED_IPV4_CIDRS = [
14308
+ ["0.0.0.0", 8],
14309
+ ["10.0.0.0", 8],
14310
+ ["100.64.0.0", 10],
14311
+ ["127.0.0.0", 8],
14312
+ ["169.254.0.0", 16],
14313
+ ["172.16.0.0", 12],
14314
+ ["192.0.0.0", 24],
14315
+ ["192.0.2.0", 24],
14316
+ ["192.88.99.0", 24],
14317
+ ["192.168.0.0", 16],
14318
+ ["198.18.0.0", 15],
14319
+ ["198.51.100.0", 24],
14320
+ ["203.0.113.0", 24],
14321
+ ["224.0.0.0", 4],
14322
+ ["240.0.0.0", 4]
14323
+ ];
14324
+ var DENIED_IPV6_CIDRS = [
14325
+ ["::", 128],
14326
+ ["::1", 128],
14327
+ ["64:ff9b::", 96],
14328
+ ["64:ff9b:1::", 48],
14329
+ ["100::", 64],
14330
+ ["100:0:0:1::", 64],
14331
+ ["2001::", 23],
14332
+ ["2001:db8::", 32],
14333
+ ["2002::", 16],
14334
+ ["2620:4f:8000::", 48],
14335
+ ["3fff::", 20],
14336
+ ["5f00::", 16],
14337
+ ["fc00::", 7],
14338
+ ["fe80::", 10],
14339
+ ["ff00::", 8]
14340
+ ];
14341
+ function assertHostedTargetAllowed(target) {
14342
+ if (target.kind === "http" || target.kind === "browser_page") {
14343
+ if (!target.url)
14344
+ throw new Error("HTTP monitors require url");
14345
+ assertHostedHttpUrlAllowed(target.url);
14346
+ return;
14347
+ }
14348
+ if (target.kind === "tcp") {
14349
+ if (!target.host)
14350
+ throw new Error("TCP monitors require host");
14351
+ assertHostedHostAllowed(target.host, "TCP host");
14352
+ if (!Number.isInteger(target.port) || target.port <= 0 || target.port > 65535) {
14353
+ throw new Error("TCP monitors require a port from 1 to 65535");
14354
+ }
14355
+ return;
14356
+ }
14357
+ throw new Error("Monitor kind must be http, tcp, or browser_page");
14358
+ }
14359
+ function assertHostedHttpUrlAllowed(value) {
14360
+ const parsed = new URL(value);
14361
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
14362
+ throw new Error("HTTP monitor url must use http or https");
14363
+ }
14364
+ if (parsed.username || parsed.password) {
14365
+ throw new Error("hosted target URLs must not contain userinfo");
14366
+ }
14367
+ for (const key of parsed.searchParams.keys()) {
14368
+ if (SECRET_PARAM_PATTERN.test(key)) {
14369
+ throw new Error(`hosted target URL query parameter is not allowed: ${key}`);
14370
+ }
14371
+ }
14372
+ if (parsed.hash && SECRET_PARAM_PATTERN.test(parsed.hash)) {
14373
+ throw new Error("hosted target URL fragment contains secret-like data");
14374
+ }
14375
+ assertHostedHostAllowed(parsed.hostname, "HTTP host");
14376
+ }
14377
+ function assertHostedHostAllowed(hostname3, label = "host") {
14378
+ const host = normalizeHostedHost(hostname3);
14379
+ if (!host)
14380
+ throw new Error(`${label} is required`);
14381
+ if (host === "localhost" || host.endsWith(".localhost")) {
14382
+ throw new Error(`${label} is not allowed in hosted mode: localhost`);
14383
+ }
14384
+ if (host.endsWith(".local") || host.endsWith(".internal")) {
14385
+ throw new Error(`${label} is not allowed in hosted mode: private DNS name`);
14386
+ }
14387
+ const ipVersion = net.isIP(host);
14388
+ if (ipVersion === 4 && isDeniedIpv4(host)) {
14389
+ throw new Error(`${label} is not allowed in hosted mode: private or reserved IPv4`);
14390
+ }
14391
+ if (ipVersion === 6 && isDeniedIpv6(host)) {
14392
+ throw new Error(`${label} is not allowed in hosted mode: private or reserved IPv6`);
14393
+ }
14394
+ }
14395
+ function assertHostedResolvedAddressesAllowed(hostname3, addresses, label = "resolved address") {
14396
+ if (addresses.length === 0) {
14397
+ throw new Error(`${label} is not allowed in hosted mode: DNS returned no addresses for ${normalizeHostedHost(hostname3) || "host"}`);
14398
+ }
14399
+ for (const entry of addresses) {
14400
+ assertHostedAddressAllowed(entry.address, label);
14401
+ }
14402
+ }
14403
+ function assertHostedAddressAllowed(address, label = "resolved address") {
14404
+ const host = normalizeHostedHost(address);
14405
+ const ipVersion = net.isIP(host);
14406
+ if (ipVersion === 4 && isDeniedIpv4(host)) {
14407
+ throw new Error(`${label} is not allowed in hosted mode: private or reserved IPv4`);
14408
+ }
14409
+ if (ipVersion === 6 && isDeniedIpv6(host)) {
14410
+ throw new Error(`${label} is not allowed in hosted mode: private or reserved IPv6`);
14411
+ }
14412
+ if (ipVersion === 0) {
14413
+ throw new Error(`${label} is not allowed in hosted mode: DNS returned a non-IP address`);
14414
+ }
14415
+ }
14416
+ function normalizeHostedHost(hostname3) {
14417
+ return hostname3.trim().toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
14418
+ }
14419
+ function isDeniedIpv4(ip) {
14420
+ const parts = parseIpv4Words(ip);
14421
+ if (!parts)
14422
+ return true;
14423
+ return DENIED_IPV4_CIDRS.some(([base, prefix]) => ipv4MatchesCidr(parts, parseIpv4Words(base), prefix));
14424
+ }
14425
+ function isDeniedIpv6(ip) {
14426
+ const normalized = ip.toLowerCase();
14427
+ const words = parseIpv6Words(normalized);
14428
+ if (!words)
14429
+ return true;
14430
+ const mappedIpv4 = ipv4FromMappedIpv6Words(words);
14431
+ if (mappedIpv4)
14432
+ return isDeniedIpv4(mappedIpv4);
14433
+ return isIpv4CompatibleIpv6(words) || DENIED_IPV6_CIDRS.some(([base, prefix]) => ipv6MatchesCidr(words, parseIpv6Words(base), prefix));
14434
+ }
14435
+ function isIpv4CompatibleIpv6(words) {
14436
+ if (!words)
14437
+ return false;
14438
+ if (!words.slice(0, 6).every((word) => word === 0))
14439
+ return false;
14440
+ if (words[6] === 0 && (words[7] === 0 || words[7] === 1))
14441
+ return false;
14442
+ return true;
14443
+ }
14444
+ function ipv4FromMappedIpv6Words(words) {
14445
+ if (words[0] !== 0 || words[1] !== 0 || words[2] !== 0 || words[3] !== 0 || words[4] !== 0 || words[5] !== 65535) {
14446
+ return null;
14447
+ }
14448
+ return ipv4FromWords(words[6], words[7]);
14449
+ }
14450
+ function ipv4FromWords(high, low) {
14451
+ return [
14452
+ high >> 8,
14453
+ high & 255,
14454
+ low >> 8,
14455
+ low & 255
14456
+ ].join(".");
14457
+ }
14458
+ function ipv4MatchesCidr(parts, base, prefix) {
14459
+ const mask = prefix === 0 ? 0 : 4294967295 << 32 - prefix >>> 0;
14460
+ return (ipv4ToNumber(parts) & mask) >>> 0 === (ipv4ToNumber(base) & mask) >>> 0;
14461
+ }
14462
+ function ipv4ToNumber(parts) {
14463
+ return (parts[0] << 24 >>> 0 | parts[1] << 16 | parts[2] << 8 | parts[3]) >>> 0;
14464
+ }
14465
+ function ipv6MatchesCidr(words, base, prefix) {
14466
+ const fullWords = Math.floor(prefix / 16);
14467
+ for (let index = 0;index < fullWords; index += 1) {
14468
+ if (words[index] !== base[index])
14469
+ return false;
14470
+ }
14471
+ const remainingBits = prefix % 16;
14472
+ if (remainingBits === 0)
14473
+ return true;
14474
+ const mask = 65535 << 16 - remainingBits & 65535;
14475
+ return (words[fullWords] & mask) === (base[fullWords] & mask);
14476
+ }
14477
+ function parseIpv6Words(value) {
14478
+ let ip = value.toLowerCase();
14479
+ const zoneIndex = ip.indexOf("%");
14480
+ if (zoneIndex >= 0)
14481
+ ip = ip.slice(0, zoneIndex);
14482
+ if (ip.includes(".")) {
14483
+ const lastColon = ip.lastIndexOf(":");
14484
+ if (lastColon < 0)
14485
+ return null;
14486
+ const ipv43 = parseIpv4Words(ip.slice(lastColon + 1));
14487
+ if (!ipv43)
14488
+ return null;
14489
+ ip = `${ip.slice(0, lastColon)}:${(ipv43[0] << 8 | ipv43[1]).toString(16)}:${(ipv43[2] << 8 | ipv43[3]).toString(16)}`;
14490
+ }
14491
+ const compressed = ip.split("::");
14492
+ if (compressed.length > 2)
14493
+ return null;
14494
+ const left = parseIpv6Side(compressed[0]);
14495
+ const right = compressed.length === 2 ? parseIpv6Side(compressed[1]) : [];
14496
+ if (!left || !right)
14497
+ return null;
14498
+ if (compressed.length === 1)
14499
+ return left.length === 8 ? left : null;
14500
+ const missing = 8 - left.length - right.length;
14501
+ if (missing < 1)
14502
+ return null;
14503
+ return [...left, ...Array(missing).fill(0), ...right];
14504
+ }
14505
+ function parseIpv6Side(value) {
14506
+ if (!value)
14507
+ return [];
14508
+ const words = value.split(":");
14509
+ if (words.some((word) => !/^[0-9a-f]{1,4}$/.test(word)))
14510
+ return null;
14511
+ return words.map((word) => Number.parseInt(word, 16));
14512
+ }
14513
+ function parseIpv4Words(value) {
14514
+ const words = value.split(".").map((part) => Number(part));
14515
+ if (words.length !== 4 || words.some((word) => !Number.isInteger(word) || word < 0 || word > 255)) {
14516
+ return null;
14517
+ }
14518
+ return words;
14519
+ }
14520
+
14521
+ // src/checks.ts
14300
14522
  async function runMonitorCheck(monitor, options = {}) {
14301
14523
  if (!monitor.enabled) {
14302
14524
  return { status: "down", latencyMs: null, error: "monitor is disabled" };
14303
14525
  }
14304
- if (monitor.kind === "http")
14305
- return runHttpCheck(monitor, options.fetch ?? fetch);
14526
+ if (monitor.kind === "http") {
14527
+ return options.hostedTargetPolicy ? runHostedHttpCheck(monitor, {
14528
+ resolveHost: options.resolveHost,
14529
+ request: options.hostedHttpRequest,
14530
+ maxRedirects: options.maxRedirects
14531
+ }) : runHttpCheck(monitor, options.fetch ?? fetch);
14532
+ }
14306
14533
  if (monitor.kind === "browser_page")
14307
14534
  return runBrowserPageCheck(monitor, { fetch: options.fetch, runner: options.browserPage });
14308
14535
  if (monitor.kind === "tcp")
@@ -14340,12 +14567,87 @@ async function runHttpCheck(monitor, fetchImpl = fetch) {
14340
14567
  clearTimeout(timeout);
14341
14568
  }
14342
14569
  }
14570
+ async function runHostedHttpCheck(monitor, options = {}) {
14571
+ if (!monitor.url)
14572
+ return { status: "down", latencyMs: null, error: "missing url" };
14573
+ const resolver = options.resolveHost ?? resolveHostedHost;
14574
+ const request = options.request ?? requestHostedHttpPinned;
14575
+ const maxRedirects = options.maxRedirects ?? 5;
14576
+ const controller = new AbortController;
14577
+ const timeout = setTimeout(() => controller.abort(), monitor.timeoutMs);
14578
+ const started = performance.now();
14579
+ const decisions = [];
14580
+ let currentUrl;
14581
+ let redirectCount = 0;
14582
+ try {
14583
+ currentUrl = new URL(monitor.url);
14584
+ } catch (error51) {
14585
+ clearTimeout(timeout);
14586
+ return {
14587
+ status: "down",
14588
+ latencyMs: 0,
14589
+ statusCode: null,
14590
+ error: error51 instanceof Error ? error51.message : String(error51),
14591
+ evidence: hostedHttpEvidence(null, redirectCount, decisions)
14592
+ };
14593
+ }
14594
+ try {
14595
+ while (true) {
14596
+ throwIfAborted(controller.signal);
14597
+ const stage = redirectCount === 0 ? "request" : "redirect";
14598
+ const address = await resolveAndRecordHostedHttpDecision(currentUrl, stage, resolver, decisions);
14599
+ const response = await request({
14600
+ url: currentUrl,
14601
+ method: monitor.method || "GET",
14602
+ timeoutMs: monitor.timeoutMs,
14603
+ address,
14604
+ signal: controller.signal
14605
+ });
14606
+ const location = redirectLocation(response.headers);
14607
+ if (isRedirectStatus(response.status) && location) {
14608
+ if (redirectCount >= maxRedirects) {
14609
+ const latencyMs2 = elapsed(started);
14610
+ return {
14611
+ status: "down",
14612
+ latencyMs: latencyMs2,
14613
+ statusCode: response.status,
14614
+ error: `too many redirects after ${maxRedirects}`,
14615
+ evidence: hostedHttpEvidence(currentUrl, redirectCount, decisions)
14616
+ };
14617
+ }
14618
+ currentUrl = new URL(location, currentUrl);
14619
+ redirectCount += 1;
14620
+ continue;
14621
+ }
14622
+ const latencyMs = elapsed(started);
14623
+ const ok = monitor.expectedStatus == null ? response.status >= 200 && response.status < 400 : response.status === monitor.expectedStatus;
14624
+ return {
14625
+ status: ok ? "up" : "down",
14626
+ latencyMs,
14627
+ statusCode: response.status,
14628
+ error: ok ? null : `unexpected status ${response.status}`,
14629
+ evidence: hostedHttpEvidence(currentUrl, redirectCount, decisions)
14630
+ };
14631
+ }
14632
+ } catch (error51) {
14633
+ const latencyMs = elapsed(started);
14634
+ return {
14635
+ status: "down",
14636
+ latencyMs,
14637
+ statusCode: null,
14638
+ error: error51 instanceof Error ? error51.message : String(error51),
14639
+ evidence: hostedHttpEvidence(currentUrl, redirectCount, decisions)
14640
+ };
14641
+ } finally {
14642
+ clearTimeout(timeout);
14643
+ }
14644
+ }
14343
14645
  async function runTcpCheck(monitor) {
14344
14646
  if (!monitor.host || !monitor.port)
14345
14647
  return { status: "down", latencyMs: null, error: "missing host or port" };
14346
14648
  const started = performance.now();
14347
14649
  return new Promise((resolve) => {
14348
- const socket = net.createConnection({ host: monitor.host, port: monitor.port, timeout: monitor.timeoutMs });
14650
+ const socket = net2.createConnection({ host: monitor.host, port: monitor.port, timeout: monitor.timeoutMs });
14349
14651
  let settled = false;
14350
14652
  const finish = (result) => {
14351
14653
  if (settled)
@@ -14433,6 +14735,40 @@ function normalizeBrowserEvidence(sourceUrl, raw) {
14433
14735
  retentionClass: "short"
14434
14736
  };
14435
14737
  }
14738
+ function normalizeHttpTargetPolicyEvidence(raw) {
14739
+ if (!isHttpTargetPolicyEvidence(raw))
14740
+ throw new Error("HTTP target-policy evidence is invalid");
14741
+ return {
14742
+ kind: "http_target_policy",
14743
+ mode: "hosted",
14744
+ finalUrl: raw.finalUrl ? redactUrl(raw.finalUrl) : null,
14745
+ redirectCount: Math.max(0, Math.min(20, Math.trunc(raw.redirectCount))),
14746
+ decisions: raw.decisions.slice(0, 20).map((decision) => ({
14747
+ stage: decision.stage,
14748
+ decision: decision.decision,
14749
+ url: redactUrl(decision.url),
14750
+ host: redactText(normalizeHostedHost(decision.host)),
14751
+ targetClass: "public_http",
14752
+ probeClass: "public",
14753
+ protocol: decision.protocol,
14754
+ resolvedAddresses: decision.resolvedAddresses.slice(0, 20).map((address) => ({
14755
+ address: normalizeHostedHost(address.address),
14756
+ family: address.family
14757
+ })),
14758
+ ruleId: redactText(decision.ruleId),
14759
+ reason: decision.reason ? redactText(decision.reason) : null
14760
+ })),
14761
+ redacted: true,
14762
+ redactionStatus: "redacted",
14763
+ retentionClass: "short"
14764
+ };
14765
+ }
14766
+ function isHttpTargetPolicyEvidence(value) {
14767
+ if (!value || typeof value !== "object" || value.kind !== "http_target_policy")
14768
+ return false;
14769
+ const evidence = value;
14770
+ return evidence.mode === "hosted" && (evidence.finalUrl === null || typeof evidence.finalUrl === "string") && Number.isInteger(evidence.redirectCount) && evidence.redacted === true && evidence.redactionStatus === "redacted" && evidence.retentionClass === "short" && Array.isArray(evidence.decisions) && evidence.decisions.every((decision) => decision && (decision.stage === "request" || decision.stage === "redirect") && (decision.decision === "allowed" || decision.decision === "blocked") && (decision.protocol === "http:" || decision.protocol === "https:") && decision.targetClass === "public_http" && decision.probeClass === "public" && typeof decision.url === "string" && typeof decision.host === "string" && typeof decision.ruleId === "string" && (decision.reason === null || typeof decision.reason === "string") && Array.isArray(decision.resolvedAddresses) && decision.resolvedAddresses.every((address) => address && typeof address.address === "string" && (address.family === 4 || address.family === 6)));
14771
+ }
14436
14772
  function validateBrowserPageUrl(value) {
14437
14773
  const parsed = new URL(value);
14438
14774
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
@@ -14489,6 +14825,130 @@ function redactText(value) {
14489
14825
  function isSecretKey(value) {
14490
14826
  return /(token|secret|password|passwd|api[_-]?key|access[_-]?token|auth|credential|session)/i.test(value);
14491
14827
  }
14828
+ async function resolveAndRecordHostedHttpDecision(url2, stage, resolver, decisions) {
14829
+ let addresses = [];
14830
+ try {
14831
+ assertHostedHttpUrlAllowed(url2.toString());
14832
+ addresses = normalizeResolvedAddresses(await resolver(normalizeHostedHost(url2.hostname)));
14833
+ assertHostedResolvedAddressesAllowed(url2.hostname, addresses, "HTTP resolved address");
14834
+ decisions.push({
14835
+ stage,
14836
+ decision: "allowed",
14837
+ url: sanitizePolicyUrl(url2),
14838
+ host: normalizeHostedHost(url2.hostname),
14839
+ targetClass: "public_http",
14840
+ probeClass: "public",
14841
+ protocol: url2.protocol,
14842
+ resolvedAddresses: addresses,
14843
+ ruleId: "hosted-http-runtime-target-policy",
14844
+ reason: null
14845
+ });
14846
+ return addresses[0];
14847
+ } catch (error51) {
14848
+ decisions.push({
14849
+ stage,
14850
+ decision: "blocked",
14851
+ url: sanitizePolicyUrl(url2),
14852
+ host: normalizeHostedHost(url2.hostname),
14853
+ targetClass: "public_http",
14854
+ probeClass: "public",
14855
+ protocol: url2.protocol === "http:" || url2.protocol === "https:" ? url2.protocol : "http:",
14856
+ resolvedAddresses: addresses,
14857
+ ruleId: "hosted-http-runtime-target-policy",
14858
+ reason: error51 instanceof Error ? error51.message : String(error51)
14859
+ });
14860
+ throw error51;
14861
+ }
14862
+ }
14863
+ async function resolveHostedHost(hostname3) {
14864
+ const host = normalizeHostedHost(hostname3);
14865
+ const ipVersion = net2.isIP(host);
14866
+ if (ipVersion === 4 || ipVersion === 6)
14867
+ return [{ address: host, family: ipVersion }];
14868
+ return dns.lookup(host, { all: true, verbatim: true });
14869
+ }
14870
+ function normalizeResolvedAddresses(addresses) {
14871
+ return addresses.map((entry) => {
14872
+ const address = normalizeHostedHost(entry.address);
14873
+ const detected = net2.isIP(address);
14874
+ const family = entry.family === 4 || entry.family === 6 ? entry.family : detected;
14875
+ if (family !== 4 && family !== 6) {
14876
+ throw new Error("HTTP resolved address is not allowed in hosted mode: DNS returned a non-IP address");
14877
+ }
14878
+ return { address, family };
14879
+ });
14880
+ }
14881
+ function hostedHttpEvidence(finalUrl, redirectCount, decisions) {
14882
+ return {
14883
+ kind: "http_target_policy",
14884
+ mode: "hosted",
14885
+ finalUrl: finalUrl ? sanitizePolicyUrl(finalUrl) : null,
14886
+ redirectCount,
14887
+ decisions,
14888
+ redacted: true,
14889
+ redactionStatus: "redacted",
14890
+ retentionClass: "short"
14891
+ };
14892
+ }
14893
+ function sanitizePolicyUrl(url2) {
14894
+ const copy = new URL(url2.toString());
14895
+ copy.username = "";
14896
+ copy.password = "";
14897
+ copy.hash = "";
14898
+ for (const key of copy.searchParams.keys()) {
14899
+ if (isSecretKey(key))
14900
+ copy.searchParams.set(key, "[redacted]");
14901
+ }
14902
+ return copy.toString();
14903
+ }
14904
+ function redirectLocation(headers) {
14905
+ if (!headers)
14906
+ return null;
14907
+ if (headers instanceof Headers)
14908
+ return headers.get("location");
14909
+ const raw = headers.location ?? headers.Location;
14910
+ if (Array.isArray(raw))
14911
+ return raw[0] ?? null;
14912
+ return raw ?? null;
14913
+ }
14914
+ function isRedirectStatus(status) {
14915
+ return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
14916
+ }
14917
+ async function requestHostedHttpPinned(context) {
14918
+ const lookup = (_hostname, _options, callback) => callback(null, context.address.address, context.address.family);
14919
+ return context.url.protocol === "https:" ? requestWithClient(context, https, new https.Agent({ lookup })) : requestWithClient(context, http, new http.Agent({ lookup }));
14920
+ }
14921
+ function requestWithClient(context, client, agent) {
14922
+ return new Promise((resolve, reject) => {
14923
+ const req = client.request(context.url, {
14924
+ method: context.method,
14925
+ agent,
14926
+ signal: context.signal,
14927
+ timeout: context.timeoutMs
14928
+ }, (response) => {
14929
+ response.resume();
14930
+ response.once("end", () => {
14931
+ agent.destroy();
14932
+ resolve({ status: response.statusCode ?? 0, headers: response.headers });
14933
+ });
14934
+ });
14935
+ req.once("timeout", () => {
14936
+ req.destroy(new Error("http timeout"));
14937
+ });
14938
+ req.once("error", (error51) => {
14939
+ agent.destroy();
14940
+ reject(error51);
14941
+ });
14942
+ req.end();
14943
+ });
14944
+ }
14945
+ function throwIfAborted(signal) {
14946
+ if (signal.aborted)
14947
+ throw new Error("http timeout");
14948
+ }
14949
+ function elapsed(started) {
14950
+ return Math.round((performance.now() - started) * 100) / 100;
14951
+ }
14492
14952
 
14493
14953
  // src/service.ts
14494
14954
  import { createPublicKey, randomUUID as randomUUID3 } from "crypto";
@@ -14505,83 +14965,10 @@ var MIN_RETRY_COUNT = 0;
14505
14965
  var MAX_RETRY_COUNT = 10;
14506
14966
  var MAX_RESULT_LIMIT = 1000;
14507
14967
 
14508
- // src/target-policy.ts
14509
- import net2 from "net";
14510
- var SECRET_PARAM_PATTERN = /(token|secret|password|passwd|api[_-]?key|access[_-]?token|auth|credential|session)/i;
14511
- function assertHostedTargetAllowed(target) {
14512
- if (target.kind === "http" || target.kind === "browser_page") {
14513
- if (!target.url)
14514
- throw new Error("HTTP monitors require url");
14515
- assertHostedHttpUrlAllowed(target.url);
14516
- return;
14517
- }
14518
- if (target.kind === "tcp") {
14519
- if (!target.host)
14520
- throw new Error("TCP monitors require host");
14521
- assertHostedHostAllowed(target.host, "TCP host");
14522
- if (!Number.isInteger(target.port) || target.port <= 0 || target.port > 65535) {
14523
- throw new Error("TCP monitors require a port from 1 to 65535");
14524
- }
14525
- return;
14526
- }
14527
- throw new Error("Monitor kind must be http, tcp, or browser_page");
14528
- }
14529
- function assertHostedHttpUrlAllowed(value) {
14530
- const parsed = new URL(value);
14531
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
14532
- throw new Error("HTTP monitor url must use http or https");
14533
- }
14534
- if (parsed.username || parsed.password) {
14535
- throw new Error("hosted target URLs must not contain userinfo");
14536
- }
14537
- for (const key of parsed.searchParams.keys()) {
14538
- if (SECRET_PARAM_PATTERN.test(key)) {
14539
- throw new Error(`hosted target URL query parameter is not allowed: ${key}`);
14540
- }
14541
- }
14542
- if (parsed.hash && SECRET_PARAM_PATTERN.test(parsed.hash)) {
14543
- throw new Error("hosted target URL fragment contains secret-like data");
14544
- }
14545
- assertHostedHostAllowed(parsed.hostname, "HTTP host");
14546
- }
14547
- function assertHostedHostAllowed(hostname3, label = "host") {
14548
- const host = normalizeHost(hostname3);
14549
- if (!host)
14550
- throw new Error(`${label} is required`);
14551
- if (host === "localhost" || host.endsWith(".localhost")) {
14552
- throw new Error(`${label} is not allowed in hosted mode: localhost`);
14553
- }
14554
- if (host.endsWith(".local") || host.endsWith(".internal")) {
14555
- throw new Error(`${label} is not allowed in hosted mode: private DNS name`);
14556
- }
14557
- const ipVersion = net2.isIP(host);
14558
- if (ipVersion === 4 && isDeniedIpv4(host)) {
14559
- throw new Error(`${label} is not allowed in hosted mode: private or reserved IPv4`);
14560
- }
14561
- if (ipVersion === 6 && isDeniedIpv6(host)) {
14562
- throw new Error(`${label} is not allowed in hosted mode: private or reserved IPv6`);
14563
- }
14564
- }
14565
- function normalizeHost(hostname3) {
14566
- return hostname3.trim().toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
14567
- }
14568
- function isDeniedIpv4(ip) {
14569
- const parts = ip.split(".").map((part) => Number(part));
14570
- if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
14571
- return true;
14572
- }
14573
- const [a, b] = parts;
14574
- 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;
14575
- }
14576
- function isDeniedIpv6(ip) {
14577
- const normalized = ip.toLowerCase();
14578
- 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.");
14579
- }
14580
-
14581
14968
  // src/imports.ts
14582
- function previewImport(store, request) {
14969
+ function previewImport(store, request, options = {}) {
14583
14970
  const source = normalizeSource(request.source);
14584
- const items = dedupePreviewItems(request.records.map((record2) => previewRecord(store, source, record2, request.defaults ?? {})));
14971
+ const items = dedupePreviewItems(request.records.map((record2) => previewRecord(store, source, record2, request.defaults ?? {}, options)));
14585
14972
  return {
14586
14973
  source,
14587
14974
  generatedAt: new Date().toISOString(),
@@ -14655,7 +15042,7 @@ function rollbackImport(store, batchId) {
14655
15042
  items
14656
15043
  };
14657
15044
  }
14658
- function previewRecord(store, source, record2, defaults) {
15045
+ function previewRecord(store, source, record2, defaults, options) {
14659
15046
  const warnings = [];
14660
15047
  let candidate;
14661
15048
  try {
@@ -14675,13 +15062,16 @@ function previewRecord(store, source, record2, defaults) {
14675
15062
  reason: error51 instanceof Error ? error51.message : String(error51)
14676
15063
  };
14677
15064
  }
14678
- const provenance = store.getProvenance(candidate.source, candidate.sourceId);
14679
- const monitor = provenance ? store.getMonitor(provenance.monitorId) : store.getMonitor(candidate.name);
14680
- if (provenance && !monitor) {
15065
+ const monitorOptions = options.workspaceId ? { workspaceId: options.workspaceId } : undefined;
15066
+ const rawProvenance = store.getProvenance(candidate.source, candidate.sourceId);
15067
+ const provenanceMonitor = rawProvenance ? store.getMonitor(rawProvenance.monitorId, monitorOptions) : null;
15068
+ const provenance = provenanceMonitor ? rawProvenance : null;
15069
+ const monitor = provenanceMonitor ?? store.getMonitor(candidate.name, monitorOptions);
15070
+ if (rawProvenance && !provenanceMonitor && !options.workspaceId) {
14681
15071
  return { candidate, action: "create", monitor: null, provenance, warnings: ["source provenance points to a missing monitor"], reason: null };
14682
15072
  }
14683
15073
  if (provenance && monitor) {
14684
- const nameOwner = store.getMonitor(candidate.name);
15074
+ const nameOwner = store.getMonitor(candidate.name, monitorOptions);
14685
15075
  if (nameOwner && nameOwner.id !== monitor.id) {
14686
15076
  return {
14687
15077
  candidate,
@@ -17020,8 +17410,8 @@ class UptimeService {
17020
17410
  const execute = () => this.submitProbeResultInTransaction(input);
17021
17411
  return this.store.runInTransaction ? this.store.runInTransaction(execute) : execute();
17022
17412
  }
17023
- previewImport(request) {
17024
- return previewImport(this.store, request);
17413
+ previewImport(request, options = {}) {
17414
+ return previewImport(this.store, request, options);
17025
17415
  }
17026
17416
  applyImport(request) {
17027
17417
  return applyImport(this.store, request);
@@ -17361,7 +17751,7 @@ class UptimeService {
17361
17751
  throw new Error("Probe job fencing token is invalid");
17362
17752
  if (!job.leaseExpiresAt || job.leaseExpiresAt <= new Date().toISOString())
17363
17753
  throw new Error("Probe job lease expired");
17364
- const evidence = input.evidence ? normalizeBrowserEvidence(monitor.url ?? monitor.host ?? "https://example.invalid", input.evidence) : null;
17754
+ const evidence = input.evidence ? normalizeSubmittedEvidence(monitor.url ?? monitor.host ?? "https://example.invalid", input.evidence) : null;
17365
17755
  const result = this.store.recordCheckResult({
17366
17756
  monitorId: monitor.id,
17367
17757
  checkedAt: input.checkedAt,
@@ -17401,6 +17791,13 @@ class MonitorCheckBusyError extends Error {
17401
17791
  function enabledReportChannels(schedule) {
17402
17792
  return ["email", "sms", "logs"].filter((channel) => Boolean(schedule.channels[channel]));
17403
17793
  }
17794
+ function normalizeSubmittedEvidence(sourceUrl, evidence) {
17795
+ if (evidence.kind === "browser_page")
17796
+ return normalizeBrowserEvidence(sourceUrl, evidence);
17797
+ if (evidence.kind === "http_target_policy")
17798
+ return normalizeHttpTargetPolicyEvidence(evidence);
17799
+ throw new Error("Unsupported probe evidence kind");
17800
+ }
17404
17801
  function validateProbeSubmission(input) {
17405
17802
  if (!input.jobId.trim())
17406
17803
  throw new Error("Probe submission jobId is required");
package/dist/service.d.ts CHANGED
@@ -170,7 +170,9 @@ export declare class UptimeService {
170
170
  result: CheckResult;
171
171
  receipt: ProbeSubmissionReceipt;
172
172
  };
173
- previewImport(request: ImportRequest): ImportPreview;
173
+ previewImport(request: ImportRequest, options?: {
174
+ workspaceId?: string;
175
+ }): ImportPreview;
174
176
  applyImport(request: ImportRequest): ImportApplyResult;
175
177
  rollbackImport(batchId: string): ImportRollbackResult;
176
178
  backup(destinationPath?: string): UptimeBackup;