@hasna/uptime 0.1.11 → 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/cli/index.js CHANGED
@@ -2577,13 +2577,240 @@ import { randomUUID as randomUUID4 } from "crypto";
2577
2577
  import { existsSync as existsSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
2578
2578
 
2579
2579
  // src/checks.ts
2580
+ import dns from "dns/promises";
2581
+ import http from "http";
2582
+ import https from "https";
2583
+ import net2 from "net";
2584
+
2585
+ // src/target-policy.ts
2580
2586
  import net from "net";
2587
+ var SECRET_PARAM_PATTERN = /(token|secret|password|passwd|api[_-]?key|access[_-]?token|auth|credential|session)/i;
2588
+ var DENIED_IPV4_CIDRS = [
2589
+ ["0.0.0.0", 8],
2590
+ ["10.0.0.0", 8],
2591
+ ["100.64.0.0", 10],
2592
+ ["127.0.0.0", 8],
2593
+ ["169.254.0.0", 16],
2594
+ ["172.16.0.0", 12],
2595
+ ["192.0.0.0", 24],
2596
+ ["192.0.2.0", 24],
2597
+ ["192.88.99.0", 24],
2598
+ ["192.168.0.0", 16],
2599
+ ["198.18.0.0", 15],
2600
+ ["198.51.100.0", 24],
2601
+ ["203.0.113.0", 24],
2602
+ ["224.0.0.0", 4],
2603
+ ["240.0.0.0", 4]
2604
+ ];
2605
+ var DENIED_IPV6_CIDRS = [
2606
+ ["::", 128],
2607
+ ["::1", 128],
2608
+ ["64:ff9b::", 96],
2609
+ ["64:ff9b:1::", 48],
2610
+ ["100::", 64],
2611
+ ["100:0:0:1::", 64],
2612
+ ["2001::", 23],
2613
+ ["2001:db8::", 32],
2614
+ ["2002::", 16],
2615
+ ["2620:4f:8000::", 48],
2616
+ ["3fff::", 20],
2617
+ ["5f00::", 16],
2618
+ ["fc00::", 7],
2619
+ ["fe80::", 10],
2620
+ ["ff00::", 8]
2621
+ ];
2622
+ function assertHostedTargetAllowed(target) {
2623
+ if (target.kind === "http" || target.kind === "browser_page") {
2624
+ if (!target.url)
2625
+ throw new Error("HTTP monitors require url");
2626
+ assertHostedHttpUrlAllowed(target.url);
2627
+ return;
2628
+ }
2629
+ if (target.kind === "tcp") {
2630
+ if (!target.host)
2631
+ throw new Error("TCP monitors require host");
2632
+ assertHostedHostAllowed(target.host, "TCP host");
2633
+ if (!Number.isInteger(target.port) || target.port <= 0 || target.port > 65535) {
2634
+ throw new Error("TCP monitors require a port from 1 to 65535");
2635
+ }
2636
+ return;
2637
+ }
2638
+ throw new Error("Monitor kind must be http, tcp, or browser_page");
2639
+ }
2640
+ function assertHostedHttpUrlAllowed(value) {
2641
+ const parsed = new URL(value);
2642
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2643
+ throw new Error("HTTP monitor url must use http or https");
2644
+ }
2645
+ if (parsed.username || parsed.password) {
2646
+ throw new Error("hosted target URLs must not contain userinfo");
2647
+ }
2648
+ for (const key of parsed.searchParams.keys()) {
2649
+ if (SECRET_PARAM_PATTERN.test(key)) {
2650
+ throw new Error(`hosted target URL query parameter is not allowed: ${key}`);
2651
+ }
2652
+ }
2653
+ if (parsed.hash && SECRET_PARAM_PATTERN.test(parsed.hash)) {
2654
+ throw new Error("hosted target URL fragment contains secret-like data");
2655
+ }
2656
+ assertHostedHostAllowed(parsed.hostname, "HTTP host");
2657
+ }
2658
+ function assertHostedHostAllowed(hostname, label = "host") {
2659
+ const host = normalizeHostedHost(hostname);
2660
+ if (!host)
2661
+ throw new Error(`${label} is required`);
2662
+ if (host === "localhost" || host.endsWith(".localhost")) {
2663
+ throw new Error(`${label} is not allowed in hosted mode: localhost`);
2664
+ }
2665
+ if (host.endsWith(".local") || host.endsWith(".internal")) {
2666
+ throw new Error(`${label} is not allowed in hosted mode: private DNS name`);
2667
+ }
2668
+ const ipVersion = net.isIP(host);
2669
+ if (ipVersion === 4 && isDeniedIpv4(host)) {
2670
+ throw new Error(`${label} is not allowed in hosted mode: private or reserved IPv4`);
2671
+ }
2672
+ if (ipVersion === 6 && isDeniedIpv6(host)) {
2673
+ throw new Error(`${label} is not allowed in hosted mode: private or reserved IPv6`);
2674
+ }
2675
+ }
2676
+ function assertHostedResolvedAddressesAllowed(hostname, addresses, label = "resolved address") {
2677
+ if (addresses.length === 0) {
2678
+ throw new Error(`${label} is not allowed in hosted mode: DNS returned no addresses for ${normalizeHostedHost(hostname) || "host"}`);
2679
+ }
2680
+ for (const entry of addresses) {
2681
+ assertHostedAddressAllowed(entry.address, label);
2682
+ }
2683
+ }
2684
+ function assertHostedAddressAllowed(address, label = "resolved address") {
2685
+ const host = normalizeHostedHost(address);
2686
+ const ipVersion = net.isIP(host);
2687
+ if (ipVersion === 4 && isDeniedIpv4(host)) {
2688
+ throw new Error(`${label} is not allowed in hosted mode: private or reserved IPv4`);
2689
+ }
2690
+ if (ipVersion === 6 && isDeniedIpv6(host)) {
2691
+ throw new Error(`${label} is not allowed in hosted mode: private or reserved IPv6`);
2692
+ }
2693
+ if (ipVersion === 0) {
2694
+ throw new Error(`${label} is not allowed in hosted mode: DNS returned a non-IP address`);
2695
+ }
2696
+ }
2697
+ function normalizeHostedHost(hostname) {
2698
+ return hostname.trim().toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
2699
+ }
2700
+ function isDeniedIpv4(ip) {
2701
+ const parts = parseIpv4Words(ip);
2702
+ if (!parts)
2703
+ return true;
2704
+ return DENIED_IPV4_CIDRS.some(([base, prefix]) => ipv4MatchesCidr(parts, parseIpv4Words(base), prefix));
2705
+ }
2706
+ function isDeniedIpv6(ip) {
2707
+ const normalized = ip.toLowerCase();
2708
+ const words = parseIpv6Words(normalized);
2709
+ if (!words)
2710
+ return true;
2711
+ const mappedIpv4 = ipv4FromMappedIpv6Words(words);
2712
+ if (mappedIpv4)
2713
+ return isDeniedIpv4(mappedIpv4);
2714
+ return isIpv4CompatibleIpv6(words) || DENIED_IPV6_CIDRS.some(([base, prefix]) => ipv6MatchesCidr(words, parseIpv6Words(base), prefix));
2715
+ }
2716
+ function isIpv4CompatibleIpv6(words) {
2717
+ if (!words)
2718
+ return false;
2719
+ if (!words.slice(0, 6).every((word) => word === 0))
2720
+ return false;
2721
+ if (words[6] === 0 && (words[7] === 0 || words[7] === 1))
2722
+ return false;
2723
+ return true;
2724
+ }
2725
+ function ipv4FromMappedIpv6Words(words) {
2726
+ if (words[0] !== 0 || words[1] !== 0 || words[2] !== 0 || words[3] !== 0 || words[4] !== 0 || words[5] !== 65535) {
2727
+ return null;
2728
+ }
2729
+ return ipv4FromWords(words[6], words[7]);
2730
+ }
2731
+ function ipv4FromWords(high, low) {
2732
+ return [
2733
+ high >> 8,
2734
+ high & 255,
2735
+ low >> 8,
2736
+ low & 255
2737
+ ].join(".");
2738
+ }
2739
+ function ipv4MatchesCidr(parts, base, prefix) {
2740
+ const mask = prefix === 0 ? 0 : 4294967295 << 32 - prefix >>> 0;
2741
+ return (ipv4ToNumber(parts) & mask) >>> 0 === (ipv4ToNumber(base) & mask) >>> 0;
2742
+ }
2743
+ function ipv4ToNumber(parts) {
2744
+ return (parts[0] << 24 >>> 0 | parts[1] << 16 | parts[2] << 8 | parts[3]) >>> 0;
2745
+ }
2746
+ function ipv6MatchesCidr(words, base, prefix) {
2747
+ const fullWords = Math.floor(prefix / 16);
2748
+ for (let index = 0;index < fullWords; index += 1) {
2749
+ if (words[index] !== base[index])
2750
+ return false;
2751
+ }
2752
+ const remainingBits = prefix % 16;
2753
+ if (remainingBits === 0)
2754
+ return true;
2755
+ const mask = 65535 << 16 - remainingBits & 65535;
2756
+ return (words[fullWords] & mask) === (base[fullWords] & mask);
2757
+ }
2758
+ function parseIpv6Words(value) {
2759
+ let ip = value.toLowerCase();
2760
+ const zoneIndex = ip.indexOf("%");
2761
+ if (zoneIndex >= 0)
2762
+ ip = ip.slice(0, zoneIndex);
2763
+ if (ip.includes(".")) {
2764
+ const lastColon = ip.lastIndexOf(":");
2765
+ if (lastColon < 0)
2766
+ return null;
2767
+ const ipv4 = parseIpv4Words(ip.slice(lastColon + 1));
2768
+ if (!ipv4)
2769
+ return null;
2770
+ ip = `${ip.slice(0, lastColon)}:${(ipv4[0] << 8 | ipv4[1]).toString(16)}:${(ipv4[2] << 8 | ipv4[3]).toString(16)}`;
2771
+ }
2772
+ const compressed = ip.split("::");
2773
+ if (compressed.length > 2)
2774
+ return null;
2775
+ const left = parseIpv6Side(compressed[0]);
2776
+ const right = compressed.length === 2 ? parseIpv6Side(compressed[1]) : [];
2777
+ if (!left || !right)
2778
+ return null;
2779
+ if (compressed.length === 1)
2780
+ return left.length === 8 ? left : null;
2781
+ const missing = 8 - left.length - right.length;
2782
+ if (missing < 1)
2783
+ return null;
2784
+ return [...left, ...Array(missing).fill(0), ...right];
2785
+ }
2786
+ function parseIpv6Side(value) {
2787
+ if (!value)
2788
+ return [];
2789
+ const words = value.split(":");
2790
+ if (words.some((word) => !/^[0-9a-f]{1,4}$/.test(word)))
2791
+ return null;
2792
+ return words.map((word) => Number.parseInt(word, 16));
2793
+ }
2794
+ function parseIpv4Words(value) {
2795
+ const words = value.split(".").map((part) => Number(part));
2796
+ if (words.length !== 4 || words.some((word) => !Number.isInteger(word) || word < 0 || word > 255)) {
2797
+ return null;
2798
+ }
2799
+ return words;
2800
+ }
2801
+
2802
+ // src/checks.ts
2581
2803
  async function runMonitorCheck(monitor, options = {}) {
2582
2804
  if (!monitor.enabled) {
2583
2805
  return { status: "down", latencyMs: null, error: "monitor is disabled" };
2584
2806
  }
2585
- if (monitor.kind === "http")
2586
- return runHttpCheck(monitor, options.fetch ?? fetch);
2807
+ if (monitor.kind === "http") {
2808
+ return options.hostedTargetPolicy ? runHostedHttpCheck(monitor, {
2809
+ resolveHost: options.resolveHost,
2810
+ request: options.hostedHttpRequest,
2811
+ maxRedirects: options.maxRedirects
2812
+ }) : runHttpCheck(monitor, options.fetch ?? fetch);
2813
+ }
2587
2814
  if (monitor.kind === "browser_page")
2588
2815
  return runBrowserPageCheck(monitor, { fetch: options.fetch, runner: options.browserPage });
2589
2816
  if (monitor.kind === "tcp")
@@ -2621,12 +2848,87 @@ async function runHttpCheck(monitor, fetchImpl = fetch) {
2621
2848
  clearTimeout(timeout);
2622
2849
  }
2623
2850
  }
2851
+ async function runHostedHttpCheck(monitor, options = {}) {
2852
+ if (!monitor.url)
2853
+ return { status: "down", latencyMs: null, error: "missing url" };
2854
+ const resolver = options.resolveHost ?? resolveHostedHost;
2855
+ const request = options.request ?? requestHostedHttpPinned;
2856
+ const maxRedirects = options.maxRedirects ?? 5;
2857
+ const controller = new AbortController;
2858
+ const timeout = setTimeout(() => controller.abort(), monitor.timeoutMs);
2859
+ const started = performance.now();
2860
+ const decisions = [];
2861
+ let currentUrl;
2862
+ let redirectCount = 0;
2863
+ try {
2864
+ currentUrl = new URL(monitor.url);
2865
+ } catch (error) {
2866
+ clearTimeout(timeout);
2867
+ return {
2868
+ status: "down",
2869
+ latencyMs: 0,
2870
+ statusCode: null,
2871
+ error: error instanceof Error ? error.message : String(error),
2872
+ evidence: hostedHttpEvidence(null, redirectCount, decisions)
2873
+ };
2874
+ }
2875
+ try {
2876
+ while (true) {
2877
+ throwIfAborted(controller.signal);
2878
+ const stage = redirectCount === 0 ? "request" : "redirect";
2879
+ const address = await resolveAndRecordHostedHttpDecision(currentUrl, stage, resolver, decisions);
2880
+ const response = await request({
2881
+ url: currentUrl,
2882
+ method: monitor.method || "GET",
2883
+ timeoutMs: monitor.timeoutMs,
2884
+ address,
2885
+ signal: controller.signal
2886
+ });
2887
+ const location = redirectLocation(response.headers);
2888
+ if (isRedirectStatus(response.status) && location) {
2889
+ if (redirectCount >= maxRedirects) {
2890
+ const latencyMs2 = elapsed(started);
2891
+ return {
2892
+ status: "down",
2893
+ latencyMs: latencyMs2,
2894
+ statusCode: response.status,
2895
+ error: `too many redirects after ${maxRedirects}`,
2896
+ evidence: hostedHttpEvidence(currentUrl, redirectCount, decisions)
2897
+ };
2898
+ }
2899
+ currentUrl = new URL(location, currentUrl);
2900
+ redirectCount += 1;
2901
+ continue;
2902
+ }
2903
+ const latencyMs = elapsed(started);
2904
+ const ok = monitor.expectedStatus == null ? response.status >= 200 && response.status < 400 : response.status === monitor.expectedStatus;
2905
+ return {
2906
+ status: ok ? "up" : "down",
2907
+ latencyMs,
2908
+ statusCode: response.status,
2909
+ error: ok ? null : `unexpected status ${response.status}`,
2910
+ evidence: hostedHttpEvidence(currentUrl, redirectCount, decisions)
2911
+ };
2912
+ }
2913
+ } catch (error) {
2914
+ const latencyMs = elapsed(started);
2915
+ return {
2916
+ status: "down",
2917
+ latencyMs,
2918
+ statusCode: null,
2919
+ error: error instanceof Error ? error.message : String(error),
2920
+ evidence: hostedHttpEvidence(currentUrl, redirectCount, decisions)
2921
+ };
2922
+ } finally {
2923
+ clearTimeout(timeout);
2924
+ }
2925
+ }
2624
2926
  async function runTcpCheck(monitor) {
2625
2927
  if (!monitor.host || !monitor.port)
2626
2928
  return { status: "down", latencyMs: null, error: "missing host or port" };
2627
2929
  const started = performance.now();
2628
2930
  return new Promise((resolve) => {
2629
- const socket = net.createConnection({ host: monitor.host, port: monitor.port, timeout: monitor.timeoutMs });
2931
+ const socket = net2.createConnection({ host: monitor.host, port: monitor.port, timeout: monitor.timeoutMs });
2630
2932
  let settled = false;
2631
2933
  const finish = (result) => {
2632
2934
  if (settled)
@@ -2714,6 +3016,40 @@ function normalizeBrowserEvidence(sourceUrl, raw) {
2714
3016
  retentionClass: "short"
2715
3017
  };
2716
3018
  }
3019
+ function normalizeHttpTargetPolicyEvidence(raw) {
3020
+ if (!isHttpTargetPolicyEvidence(raw))
3021
+ throw new Error("HTTP target-policy evidence is invalid");
3022
+ return {
3023
+ kind: "http_target_policy",
3024
+ mode: "hosted",
3025
+ finalUrl: raw.finalUrl ? redactUrl(raw.finalUrl) : null,
3026
+ redirectCount: Math.max(0, Math.min(20, Math.trunc(raw.redirectCount))),
3027
+ decisions: raw.decisions.slice(0, 20).map((decision) => ({
3028
+ stage: decision.stage,
3029
+ decision: decision.decision,
3030
+ url: redactUrl(decision.url),
3031
+ host: redactText(normalizeHostedHost(decision.host)),
3032
+ targetClass: "public_http",
3033
+ probeClass: "public",
3034
+ protocol: decision.protocol,
3035
+ resolvedAddresses: decision.resolvedAddresses.slice(0, 20).map((address) => ({
3036
+ address: normalizeHostedHost(address.address),
3037
+ family: address.family
3038
+ })),
3039
+ ruleId: redactText(decision.ruleId),
3040
+ reason: decision.reason ? redactText(decision.reason) : null
3041
+ })),
3042
+ redacted: true,
3043
+ redactionStatus: "redacted",
3044
+ retentionClass: "short"
3045
+ };
3046
+ }
3047
+ function isHttpTargetPolicyEvidence(value) {
3048
+ if (!value || typeof value !== "object" || value.kind !== "http_target_policy")
3049
+ return false;
3050
+ const evidence = value;
3051
+ 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)));
3052
+ }
2717
3053
  function validateBrowserPageUrl(value) {
2718
3054
  const parsed = new URL(value);
2719
3055
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
@@ -2770,6 +3106,130 @@ function redactText(value) {
2770
3106
  function isSecretKey(value) {
2771
3107
  return /(token|secret|password|passwd|api[_-]?key|access[_-]?token|auth|credential|session)/i.test(value);
2772
3108
  }
3109
+ async function resolveAndRecordHostedHttpDecision(url, stage, resolver, decisions) {
3110
+ let addresses = [];
3111
+ try {
3112
+ assertHostedHttpUrlAllowed(url.toString());
3113
+ addresses = normalizeResolvedAddresses(await resolver(normalizeHostedHost(url.hostname)));
3114
+ assertHostedResolvedAddressesAllowed(url.hostname, addresses, "HTTP resolved address");
3115
+ decisions.push({
3116
+ stage,
3117
+ decision: "allowed",
3118
+ url: sanitizePolicyUrl(url),
3119
+ host: normalizeHostedHost(url.hostname),
3120
+ targetClass: "public_http",
3121
+ probeClass: "public",
3122
+ protocol: url.protocol,
3123
+ resolvedAddresses: addresses,
3124
+ ruleId: "hosted-http-runtime-target-policy",
3125
+ reason: null
3126
+ });
3127
+ return addresses[0];
3128
+ } catch (error) {
3129
+ decisions.push({
3130
+ stage,
3131
+ decision: "blocked",
3132
+ url: sanitizePolicyUrl(url),
3133
+ host: normalizeHostedHost(url.hostname),
3134
+ targetClass: "public_http",
3135
+ probeClass: "public",
3136
+ protocol: url.protocol === "http:" || url.protocol === "https:" ? url.protocol : "http:",
3137
+ resolvedAddresses: addresses,
3138
+ ruleId: "hosted-http-runtime-target-policy",
3139
+ reason: error instanceof Error ? error.message : String(error)
3140
+ });
3141
+ throw error;
3142
+ }
3143
+ }
3144
+ async function resolveHostedHost(hostname) {
3145
+ const host = normalizeHostedHost(hostname);
3146
+ const ipVersion = net2.isIP(host);
3147
+ if (ipVersion === 4 || ipVersion === 6)
3148
+ return [{ address: host, family: ipVersion }];
3149
+ return dns.lookup(host, { all: true, verbatim: true });
3150
+ }
3151
+ function normalizeResolvedAddresses(addresses) {
3152
+ return addresses.map((entry) => {
3153
+ const address = normalizeHostedHost(entry.address);
3154
+ const detected = net2.isIP(address);
3155
+ const family = entry.family === 4 || entry.family === 6 ? entry.family : detected;
3156
+ if (family !== 4 && family !== 6) {
3157
+ throw new Error("HTTP resolved address is not allowed in hosted mode: DNS returned a non-IP address");
3158
+ }
3159
+ return { address, family };
3160
+ });
3161
+ }
3162
+ function hostedHttpEvidence(finalUrl, redirectCount, decisions) {
3163
+ return {
3164
+ kind: "http_target_policy",
3165
+ mode: "hosted",
3166
+ finalUrl: finalUrl ? sanitizePolicyUrl(finalUrl) : null,
3167
+ redirectCount,
3168
+ decisions,
3169
+ redacted: true,
3170
+ redactionStatus: "redacted",
3171
+ retentionClass: "short"
3172
+ };
3173
+ }
3174
+ function sanitizePolicyUrl(url) {
3175
+ const copy = new URL(url.toString());
3176
+ copy.username = "";
3177
+ copy.password = "";
3178
+ copy.hash = "";
3179
+ for (const key of copy.searchParams.keys()) {
3180
+ if (isSecretKey(key))
3181
+ copy.searchParams.set(key, "[redacted]");
3182
+ }
3183
+ return copy.toString();
3184
+ }
3185
+ function redirectLocation(headers) {
3186
+ if (!headers)
3187
+ return null;
3188
+ if (headers instanceof Headers)
3189
+ return headers.get("location");
3190
+ const raw = headers.location ?? headers.Location;
3191
+ if (Array.isArray(raw))
3192
+ return raw[0] ?? null;
3193
+ return raw ?? null;
3194
+ }
3195
+ function isRedirectStatus(status) {
3196
+ return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
3197
+ }
3198
+ async function requestHostedHttpPinned(context) {
3199
+ const lookup = (_hostname, _options, callback) => callback(null, context.address.address, context.address.family);
3200
+ return context.url.protocol === "https:" ? requestWithClient(context, https, new https.Agent({ lookup })) : requestWithClient(context, http, new http.Agent({ lookup }));
3201
+ }
3202
+ function requestWithClient(context, client, agent) {
3203
+ return new Promise((resolve, reject) => {
3204
+ const req = client.request(context.url, {
3205
+ method: context.method,
3206
+ agent,
3207
+ signal: context.signal,
3208
+ timeout: context.timeoutMs
3209
+ }, (response) => {
3210
+ response.resume();
3211
+ response.once("end", () => {
3212
+ agent.destroy();
3213
+ resolve({ status: response.statusCode ?? 0, headers: response.headers });
3214
+ });
3215
+ });
3216
+ req.once("timeout", () => {
3217
+ req.destroy(new Error("http timeout"));
3218
+ });
3219
+ req.once("error", (error) => {
3220
+ agent.destroy();
3221
+ reject(error);
3222
+ });
3223
+ req.end();
3224
+ });
3225
+ }
3226
+ function throwIfAborted(signal) {
3227
+ if (signal.aborted)
3228
+ throw new Error("http timeout");
3229
+ }
3230
+ function elapsed(started) {
3231
+ return Math.round((performance.now() - started) * 100) / 100;
3232
+ }
2773
3233
 
2774
3234
  // src/service.ts
2775
3235
  import { createPublicKey, randomUUID as randomUUID3 } from "crypto";
@@ -2786,140 +3246,6 @@ var MIN_RETRY_COUNT = 0;
2786
3246
  var MAX_RETRY_COUNT = 10;
2787
3247
  var MAX_RESULT_LIMIT = 1000;
2788
3248
 
2789
- // src/target-policy.ts
2790
- import net2 from "net";
2791
- var SECRET_PARAM_PATTERN = /(token|secret|password|passwd|api[_-]?key|access[_-]?token|auth|credential|session)/i;
2792
- function assertHostedTargetAllowed(target) {
2793
- if (target.kind === "http" || target.kind === "browser_page") {
2794
- if (!target.url)
2795
- throw new Error("HTTP monitors require url");
2796
- assertHostedHttpUrlAllowed(target.url);
2797
- return;
2798
- }
2799
- if (target.kind === "tcp") {
2800
- if (!target.host)
2801
- throw new Error("TCP monitors require host");
2802
- assertHostedHostAllowed(target.host, "TCP host");
2803
- if (!Number.isInteger(target.port) || target.port <= 0 || target.port > 65535) {
2804
- throw new Error("TCP monitors require a port from 1 to 65535");
2805
- }
2806
- return;
2807
- }
2808
- throw new Error("Monitor kind must be http, tcp, or browser_page");
2809
- }
2810
- function assertHostedHttpUrlAllowed(value) {
2811
- const parsed = new URL(value);
2812
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2813
- throw new Error("HTTP monitor url must use http or https");
2814
- }
2815
- if (parsed.username || parsed.password) {
2816
- throw new Error("hosted target URLs must not contain userinfo");
2817
- }
2818
- for (const key of parsed.searchParams.keys()) {
2819
- if (SECRET_PARAM_PATTERN.test(key)) {
2820
- throw new Error(`hosted target URL query parameter is not allowed: ${key}`);
2821
- }
2822
- }
2823
- if (parsed.hash && SECRET_PARAM_PATTERN.test(parsed.hash)) {
2824
- throw new Error("hosted target URL fragment contains secret-like data");
2825
- }
2826
- assertHostedHostAllowed(parsed.hostname, "HTTP host");
2827
- }
2828
- function assertHostedHostAllowed(hostname, label = "host") {
2829
- const host = normalizeHost(hostname);
2830
- if (!host)
2831
- throw new Error(`${label} is required`);
2832
- if (host === "localhost" || host.endsWith(".localhost")) {
2833
- throw new Error(`${label} is not allowed in hosted mode: localhost`);
2834
- }
2835
- if (host.endsWith(".local") || host.endsWith(".internal")) {
2836
- throw new Error(`${label} is not allowed in hosted mode: private DNS name`);
2837
- }
2838
- const ipVersion = net2.isIP(host);
2839
- if (ipVersion === 4 && isDeniedIpv4(host)) {
2840
- throw new Error(`${label} is not allowed in hosted mode: private or reserved IPv4`);
2841
- }
2842
- if (ipVersion === 6 && isDeniedIpv6(host)) {
2843
- throw new Error(`${label} is not allowed in hosted mode: private or reserved IPv6`);
2844
- }
2845
- }
2846
- function normalizeHost(hostname) {
2847
- return hostname.trim().toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
2848
- }
2849
- function isDeniedIpv4(ip) {
2850
- const parts = ip.split(".").map((part) => Number(part));
2851
- if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
2852
- return true;
2853
- }
2854
- const [a, b] = parts;
2855
- 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;
2856
- }
2857
- function isDeniedIpv6(ip) {
2858
- const normalized = ip.toLowerCase();
2859
- const mappedIpv4 = ipv4FromMappedIpv6(normalized);
2860
- if (mappedIpv4)
2861
- return isDeniedIpv4(mappedIpv4);
2862
- const words = parseIpv6Words(normalized);
2863
- return normalized === "::" || normalized === "::1" || words !== null && (words[0] & 65472) === 65152 || normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("ff");
2864
- }
2865
- function ipv4FromMappedIpv6(ip) {
2866
- const words = parseIpv6Words(ip);
2867
- if (!words)
2868
- return null;
2869
- if (words[0] !== 0 || words[1] !== 0 || words[2] !== 0 || words[3] !== 0 || words[4] !== 0 || words[5] !== 65535) {
2870
- return null;
2871
- }
2872
- return [
2873
- words[6] >> 8,
2874
- words[6] & 255,
2875
- words[7] >> 8,
2876
- words[7] & 255
2877
- ].join(".");
2878
- }
2879
- function parseIpv6Words(value) {
2880
- let ip = value.toLowerCase();
2881
- const zoneIndex = ip.indexOf("%");
2882
- if (zoneIndex >= 0)
2883
- ip = ip.slice(0, zoneIndex);
2884
- if (ip.includes(".")) {
2885
- const lastColon = ip.lastIndexOf(":");
2886
- if (lastColon < 0)
2887
- return null;
2888
- const ipv4 = parseIpv4Words(ip.slice(lastColon + 1));
2889
- if (!ipv4)
2890
- return null;
2891
- ip = `${ip.slice(0, lastColon)}:${(ipv4[0] << 8 | ipv4[1]).toString(16)}:${(ipv4[2] << 8 | ipv4[3]).toString(16)}`;
2892
- }
2893
- const compressed = ip.split("::");
2894
- if (compressed.length > 2)
2895
- return null;
2896
- const left = parseIpv6Side(compressed[0]);
2897
- const right = compressed.length === 2 ? parseIpv6Side(compressed[1]) : [];
2898
- if (!left || !right)
2899
- return null;
2900
- if (compressed.length === 1)
2901
- return left.length === 8 ? left : null;
2902
- const missing = 8 - left.length - right.length;
2903
- if (missing < 1)
2904
- return null;
2905
- return [...left, ...Array(missing).fill(0), ...right];
2906
- }
2907
- function parseIpv6Side(value) {
2908
- if (!value)
2909
- return [];
2910
- const words = value.split(":");
2911
- if (words.some((word) => !/^[0-9a-f]{1,4}$/.test(word)))
2912
- return null;
2913
- return words.map((word) => Number.parseInt(word, 16));
2914
- }
2915
- function parseIpv4Words(value) {
2916
- const words = value.split(".").map((part) => Number(part));
2917
- if (words.length !== 4 || words.some((word) => !Number.isInteger(word) || word < 0 || word > 255)) {
2918
- return null;
2919
- }
2920
- return words;
2921
- }
2922
-
2923
3249
  // src/imports.ts
2924
3250
  function previewImport(store, request, options = {}) {
2925
3251
  const source = normalizeSource(request.source);
@@ -5715,7 +6041,7 @@ class UptimeService {
5715
6041
  throw new Error("Probe job fencing token is invalid");
5716
6042
  if (!job.leaseExpiresAt || job.leaseExpiresAt <= new Date().toISOString())
5717
6043
  throw new Error("Probe job lease expired");
5718
- const evidence = input.evidence ? normalizeBrowserEvidence(monitor.url ?? monitor.host ?? "https://example.invalid", input.evidence) : null;
6044
+ const evidence = input.evidence ? normalizeSubmittedEvidence(monitor.url ?? monitor.host ?? "https://example.invalid", input.evidence) : null;
5719
6045
  const result = this.store.recordCheckResult({
5720
6046
  monitorId: monitor.id,
5721
6047
  checkedAt: input.checkedAt,
@@ -5755,6 +6081,13 @@ class MonitorCheckBusyError extends Error {
5755
6081
  function enabledReportChannels(schedule) {
5756
6082
  return ["email", "sms", "logs"].filter((channel) => Boolean(schedule.channels[channel]));
5757
6083
  }
6084
+ function normalizeSubmittedEvidence(sourceUrl, evidence) {
6085
+ if (evidence.kind === "browser_page")
6086
+ return normalizeBrowserEvidence(sourceUrl, evidence);
6087
+ if (evidence.kind === "http_target_policy")
6088
+ return normalizeHttpTargetPolicyEvidence(evidence);
6089
+ throw new Error("Unsupported probe evidence kind");
6090
+ }
5758
6091
  function validateProbeSubmission(input) {
5759
6092
  if (!input.jobId.trim())
5760
6093
  throw new Error("Probe submission jobId is required");
@@ -6599,7 +6932,7 @@ function buildAwsDeploymentPlan(options = {}) {
6599
6932
  const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
6600
6933
  const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
6601
6934
  const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
6602
- const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.11");
6935
+ const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.12");
6603
6936
  const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
6604
6937
  const protectedAccessUrl = protectedAccessMode === "cloudfront_default_domain" ? "https://<cloudfront-domain>" : `https://${hostname}`;
6605
6938
  const cluster = `${prefix}-${stage}`;
@@ -6741,7 +7074,7 @@ function buildAwsDeploymentPlan(options = {}) {
6741
7074
  "The infrastructure owner repository was not found in this workspace.",
6742
7075
  "The EFS SQLite bridge is single-writer only: web target desired count is 1 and scheduler/public-probe/reporter targets remain 0 until Postgres and cloud leases exist.",
6743
7076
  "Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
6744
- "Public probe execution still needs DNS, redirect, and rebinding SSRF enforcement plus cloud check-job leases.",
7077
+ "Public probe execution still needs cloud check-job leases wired to runHostedHttpCheck and live policy-decision log evidence.",
6745
7078
  "Private probe enrollment, claim, submit, heartbeat, revocation, and rotation are not cloud-backed yet."
6746
7079
  ],
6747
7080
  requiredEvidence: [