58 lines
1.7 KiB
TypeScript
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);
|
|
}
|