← Files OpsTruthARCHIVED FILE
src/deployment.js
5.16 KB · Oct 2, 2026 · 00:31 UTC
import { bounded } from "./utils.js";
import { PLUGIN_VERSION } from "./version.js";
const MAX_PATHS = 8;
const MAX_REDIRECTS = 3;
const REQUEST_TIMEOUT_MS = 8000;
function blockedHostname(hostname) {
const lower = hostname.toLowerCase().replace(/\.$/, "");
if (!lower || lower === "localhost" || lower.endsWith(".localhost") || lower.endsWith(".local") || lower.endsWith(".internal")) return true;
if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(lower) || lower.includes(":")) return true;
return lower === "metadata.google.internal" || lower === "instance-data.ec2.internal";
}
export function safeDeploymentUrl(value) {
let url;
try {
url = new URL(String(value || "").trim());
} catch {
throw new Error("deployment_url_invalid");
}
if (url.protocol !== "https:") throw new Error("deployment_url_must_use_https");
if (url.username || url.password) throw new Error("deployment_url_must_not_include_credentials");
if (url.port && url.port !== "443") throw new Error("deployment_url_port_not_allowed");
if (blockedHostname(url.hostname)) throw new Error("deployment_url_hostname_not_allowed");
url.hash = "";
url.search = "";
return url;
}
function safeHealthPath(value) {
const path = String(value || "").trim();
if (!path.startsWith("/") || path.startsWith("//") || path.includes("?") || path.includes("#") || path.length > 200) {
throw new Error("health_path_invalid");
}
return path;
}
async function fetchWithTimeout(url, method) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
try {
return await fetch(new Request(url, {
method,
redirect: "manual",
headers: {
accept: "application/json, text/plain;q=0.9, text/html;q=0.5",
"user-agent": `opstruth-chatgpt-plugin/${PLUGIN_VERSION}`,
},
signal: controller.signal,
}));
} finally {
clearTimeout(timer);
}
}
async function probeUrl(initialUrl) {
let current = safeDeploymentUrl(initialUrl);
const redirects = [];
const started = Date.now();
let method = "HEAD";
for (let count = 0; count <= MAX_REDIRECTS; count += 1) {
let response = await fetchWithTimeout(current.href, method);
const isRedirect = response.status >= 300 && response.status < 400 && response.headers.get("location");
if (method === "HEAD" && !response.ok && !isRedirect) {
try { await response.body?.cancel(); } catch { /* body was already closed */ }
method = "GET";
response = await fetchWithTimeout(current.href, method);
}
if (response.status >= 300 && response.status < 400 && response.headers.get("location")) {
if (count === MAX_REDIRECTS) throw new Error("deployment_probe_redirect_limit");
const next = safeDeploymentUrl(new URL(response.headers.get("location"), current).href);
try { await response.body?.cancel(); } catch { /* body was already closed */ }
redirects.push(next.href);
current = next;
continue;
}
try { await response.body?.cancel(); } catch { /* body was already closed */ }
return {
requestedUrl: initialUrl,
finalUrl: current.href,
method,
status: response.status,
ok: response.status >= 200 && response.status < 300,
contentType: response.headers.get("content-type"),
redirects,
elapsedMs: Date.now() - started,
};
}
throw new Error("deployment_probe_failed");
}
export async function probeDeployment(args = {}) {
const base = safeDeploymentUrl(args.deployment_url);
const requestedPaths = Array.isArray(args.health_paths) && args.health_paths.length
? bounded(args.health_paths, MAX_PATHS).map(safeHealthPath)
: [base.pathname && base.pathname !== "/" ? base.pathname : "/health"];
const paths = [...new Set(requestedPaths)];
const targets = paths.map((path) => {
const target = new URL(base.origin);
target.pathname = path;
return target.href;
});
const probes = await Promise.all(targets.map(async (target) => {
try {
return await probeUrl(target);
} catch (error) {
return { requestedUrl: target, finalUrl: null, method: "HEAD", status: null, ok: false, contentType: null, redirects: [], error: error.message };
}
}));
const passing = probes.filter((probe) => probe.ok);
return {
contractVersion: "1.0.0",
skill: { name: "deployment-health-probe", version: PLUGIN_VERSION },
status: passing.length === probes.length ? "healthy" : passing.length ? "partial" : "unhealthy",
target: { origin: base.origin, paths },
probes,
verified: passing.map((probe) => `HTTPS ${probe.method} ${probe.finalUrl} returned ${probe.status}`),
warnings: probes.filter((probe) => !probe.ok).map((probe) => `${probe.requestedUrl} did not return a successful HTTPS response.`),
failures: [],
skipped: ["Response bodies", "Provider credentials", "Internal service state"],
notVerified: ["Application correctness", "Database health", "Rollback viability", "Private provider configuration"],
evidence: probes.map((probe) => ({ summary: "Bounded deployment health response", evidence: [probe] })),
changedState: { changed: false, summary: "HTTPS health probing only; no response bodies were retained." },
};
}
SHA-256: a4534aae924f3c5de7fe0057b0e6e846ddd27463ea479e7c85afb26b777ad316