2026-03-04 02:35:13 -05:00

58 lines
1.7 KiB
TypeScript

import { isBlockedHostnameOrIp } from "openclaw/plugin-sdk/tlon";
export type UrbitBaseUrlValidation =
| { ok: true; baseUrl: string; hostname: string }
| { ok: false; error: string };
function hasScheme(value: string): boolean {
return /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(value);
}
export function validateUrbitBaseUrl(raw: string): UrbitBaseUrlValidation {
const trimmed = String(raw ?? "").trim();
if (!trimmed) {
return { ok: false, error: "Required" };
}
const candidate = hasScheme(trimmed) ? trimmed : `https://${trimmed}`;
let parsed: URL;
try {
parsed = new URL(candidate);
} catch {
return { ok: false, error: "Invalid URL" };
}
if (!["http:", "https:"].includes(parsed.protocol)) {
return { ok: false, error: "URL must use http:// or https://" };
}
if (parsed.username || parsed.password) {
return { ok: false, error: "URL must not include credentials" };
}
const hostname = parsed.hostname.trim().toLowerCase().replace(/\.$/, "");
if (!hostname) {
return { ok: false, error: "Invalid hostname" };
}
// Normalize to origin so callers can't smuggle paths/query fragments into the base URL,
// and strip a trailing dot from the hostname (DNS root label).
const isIpv6 = hostname.includes(":");
const host = parsed.port
? `${isIpv6 ? `[${hostname}]` : hostname}:${parsed.port}`
: isIpv6
? `[${hostname}]`
: hostname;
return { ok: true, baseUrl: `${parsed.protocol}//${host}`, hostname };
}
export function isBlockedUrbitHostname(hostname: string): boolean {
const normalized = hostname.trim().toLowerCase().replace(/\.$/, "");
if (!normalized) {
return false;
}
return isBlockedHostnameOrIp(normalized);
}