@hasna/uptime 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/dist/api.js +474 -138
- package/dist/checks.d.ts +37 -5
- package/dist/checks.d.ts.map +1 -1
- package/dist/checks.js +471 -4
- package/dist/cli/index.js +473 -140
- package/dist/cloud-plan.js +2 -2
- package/dist/imports.js +100 -17
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +480 -140
- package/dist/mcp/index.js +471 -138
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +474 -138
- package/dist/store.js +100 -17
- package/dist/target-policy.d.ts +7 -0
- package/dist/target-policy.d.ts.map +1 -1
- package/dist/types.d.ts +26 -1
- package/dist/types.d.ts.map +1 -1
- package/docs/aws-deployment-runbook.md +15 -9
- package/infra/aws/README.md +22 -2
- package/infra/aws/main.tf +288 -0
- package/infra/aws/outputs.tf +7 -0
- package/infra/aws/terraform.tfvars.example +4 -1
- package/infra/aws/variables.tf +43 -1
- package/package.json +1 -1
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
|
|
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 =
|
|
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,140 +14965,6 @@ 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
|
-
const mappedIpv4 = ipv4FromMappedIpv6(normalized);
|
|
14579
|
-
if (mappedIpv4)
|
|
14580
|
-
return isDeniedIpv4(mappedIpv4);
|
|
14581
|
-
const words = parseIpv6Words(normalized);
|
|
14582
|
-
return normalized === "::" || normalized === "::1" || words !== null && (words[0] & 65472) === 65152 || normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("ff");
|
|
14583
|
-
}
|
|
14584
|
-
function ipv4FromMappedIpv6(ip) {
|
|
14585
|
-
const words = parseIpv6Words(ip);
|
|
14586
|
-
if (!words)
|
|
14587
|
-
return null;
|
|
14588
|
-
if (words[0] !== 0 || words[1] !== 0 || words[2] !== 0 || words[3] !== 0 || words[4] !== 0 || words[5] !== 65535) {
|
|
14589
|
-
return null;
|
|
14590
|
-
}
|
|
14591
|
-
return [
|
|
14592
|
-
words[6] >> 8,
|
|
14593
|
-
words[6] & 255,
|
|
14594
|
-
words[7] >> 8,
|
|
14595
|
-
words[7] & 255
|
|
14596
|
-
].join(".");
|
|
14597
|
-
}
|
|
14598
|
-
function parseIpv6Words(value) {
|
|
14599
|
-
let ip = value.toLowerCase();
|
|
14600
|
-
const zoneIndex = ip.indexOf("%");
|
|
14601
|
-
if (zoneIndex >= 0)
|
|
14602
|
-
ip = ip.slice(0, zoneIndex);
|
|
14603
|
-
if (ip.includes(".")) {
|
|
14604
|
-
const lastColon = ip.lastIndexOf(":");
|
|
14605
|
-
if (lastColon < 0)
|
|
14606
|
-
return null;
|
|
14607
|
-
const ipv43 = parseIpv4Words(ip.slice(lastColon + 1));
|
|
14608
|
-
if (!ipv43)
|
|
14609
|
-
return null;
|
|
14610
|
-
ip = `${ip.slice(0, lastColon)}:${(ipv43[0] << 8 | ipv43[1]).toString(16)}:${(ipv43[2] << 8 | ipv43[3]).toString(16)}`;
|
|
14611
|
-
}
|
|
14612
|
-
const compressed = ip.split("::");
|
|
14613
|
-
if (compressed.length > 2)
|
|
14614
|
-
return null;
|
|
14615
|
-
const left = parseIpv6Side(compressed[0]);
|
|
14616
|
-
const right = compressed.length === 2 ? parseIpv6Side(compressed[1]) : [];
|
|
14617
|
-
if (!left || !right)
|
|
14618
|
-
return null;
|
|
14619
|
-
if (compressed.length === 1)
|
|
14620
|
-
return left.length === 8 ? left : null;
|
|
14621
|
-
const missing = 8 - left.length - right.length;
|
|
14622
|
-
if (missing < 1)
|
|
14623
|
-
return null;
|
|
14624
|
-
return [...left, ...Array(missing).fill(0), ...right];
|
|
14625
|
-
}
|
|
14626
|
-
function parseIpv6Side(value) {
|
|
14627
|
-
if (!value)
|
|
14628
|
-
return [];
|
|
14629
|
-
const words = value.split(":");
|
|
14630
|
-
if (words.some((word) => !/^[0-9a-f]{1,4}$/.test(word)))
|
|
14631
|
-
return null;
|
|
14632
|
-
return words.map((word) => Number.parseInt(word, 16));
|
|
14633
|
-
}
|
|
14634
|
-
function parseIpv4Words(value) {
|
|
14635
|
-
const words = value.split(".").map((part) => Number(part));
|
|
14636
|
-
if (words.length !== 4 || words.some((word) => !Number.isInteger(word) || word < 0 || word > 255)) {
|
|
14637
|
-
return null;
|
|
14638
|
-
}
|
|
14639
|
-
return words;
|
|
14640
|
-
}
|
|
14641
|
-
|
|
14642
14968
|
// src/imports.ts
|
|
14643
14969
|
function previewImport(store, request, options = {}) {
|
|
14644
14970
|
const source = normalizeSource(request.source);
|
|
@@ -17425,7 +17751,7 @@ class UptimeService {
|
|
|
17425
17751
|
throw new Error("Probe job fencing token is invalid");
|
|
17426
17752
|
if (!job.leaseExpiresAt || job.leaseExpiresAt <= new Date().toISOString())
|
|
17427
17753
|
throw new Error("Probe job lease expired");
|
|
17428
|
-
const evidence = input.evidence ?
|
|
17754
|
+
const evidence = input.evidence ? normalizeSubmittedEvidence(monitor.url ?? monitor.host ?? "https://example.invalid", input.evidence) : null;
|
|
17429
17755
|
const result = this.store.recordCheckResult({
|
|
17430
17756
|
monitorId: monitor.id,
|
|
17431
17757
|
checkedAt: input.checkedAt,
|
|
@@ -17465,6 +17791,13 @@ class MonitorCheckBusyError extends Error {
|
|
|
17465
17791
|
function enabledReportChannels(schedule) {
|
|
17466
17792
|
return ["email", "sms", "logs"].filter((channel) => Boolean(schedule.channels[channel]));
|
|
17467
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
|
+
}
|
|
17468
17801
|
function validateProbeSubmission(input) {
|
|
17469
17802
|
if (!input.jobId.trim())
|
|
17470
17803
|
throw new Error("Probe submission jobId is required");
|