Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline (Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/* packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
121 lines
3.9 KiB
TypeScript
121 lines
3.9 KiB
TypeScript
import { isIP } from "node:net";
|
|
import { OPENAI_COMPATIBLE_PROVIDER_ID } from "@rakazo/contracts";
|
|
import { isCloudMetadataAddress, isLinkLocalAddress, isPrivateAddress } from "./network-address.js";
|
|
|
|
export { OPENAI_COMPATIBLE_PROVIDER_ID };
|
|
|
|
const METADATA_HOSTS = new Set(["metadata.google.internal", "metadata.goog"]);
|
|
|
|
export function openAiCompatAllowPublicHosts(): boolean {
|
|
return process.env.RAKAZO_OPENAI_COMPAT_ALLOW_PUBLIC === "1";
|
|
}
|
|
|
|
export function normalizeOpenAiCompatibleBaseUrl(raw: string): string {
|
|
const trimmed = raw.trim();
|
|
let url: URL;
|
|
try {
|
|
url = new URL(trimmed);
|
|
} catch {
|
|
throw new Error("Base URL must be an absolute HTTP(S) URL");
|
|
}
|
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
throw new Error("Base URL must use http or https");
|
|
}
|
|
if (url.username || url.password) {
|
|
throw new Error("Base URL must not contain credentials");
|
|
}
|
|
let path = url.pathname.replace(/\/+$/, "") || "";
|
|
const VERSIONED_API_ROOT = /\/v\d+$/;
|
|
if (!VERSIONED_API_ROOT.test(path)) {
|
|
path = path ? `${path}/v1` : "/v1";
|
|
}
|
|
return `${url.origin}${path}`;
|
|
}
|
|
|
|
function normalizeHostname(hostname: string): string {
|
|
return hostname
|
|
.toLowerCase()
|
|
.replace(/^\[|\]$/g, "")
|
|
.replace(/\.$/, "");
|
|
}
|
|
|
|
export function isPrivateOpenAiCompatibleHostname(hostname: string): boolean {
|
|
const normalized = normalizeHostname(hostname);
|
|
if (
|
|
normalized === "localhost" ||
|
|
normalized.endsWith(".localhost") ||
|
|
normalized === "host.docker.internal"
|
|
) {
|
|
return true;
|
|
}
|
|
if (METADATA_HOSTS.has(normalized)) return false;
|
|
const ipKind = isIP(normalized);
|
|
if (ipKind === 4 || ipKind === 6) return isPrivateAddress(normalized);
|
|
return false;
|
|
}
|
|
|
|
function isBlockedHostname(hostname: string): boolean {
|
|
const normalized = normalizeHostname(hostname);
|
|
return (
|
|
METADATA_HOSTS.has(normalized) ||
|
|
(isIP(normalized) !== 0 &&
|
|
(isCloudMetadataAddress(normalized) || isLinkLocalAddress(normalized)))
|
|
);
|
|
}
|
|
|
|
export function assertAllowedOpenAiCompatibleUrl(
|
|
raw: string,
|
|
opts?: { allowPublic?: boolean },
|
|
): URL {
|
|
const normalized = normalizeOpenAiCompatibleBaseUrl(raw);
|
|
return assertAllowedOpenAiCompatibleRequestUrl(normalized, opts);
|
|
}
|
|
|
|
/**
|
|
* When an API key will be sent, refuse public http:// endpoints so the Bearer
|
|
* token is not cleartext on the public internet. Private / loopback http stays
|
|
* allowed (local model servers). https:// is always fine for this check.
|
|
*/
|
|
export function assertHttpsForKeyedOpenAiCompatibleUrl(
|
|
url: URL,
|
|
apiKey: string | undefined | null,
|
|
): void {
|
|
if (!apiKey?.trim()) return;
|
|
if (url.protocol === "https:") return;
|
|
const hostname = normalizeHostname(url.hostname);
|
|
if (isPrivateOpenAiCompatibleHostname(hostname)) return;
|
|
throw new Error(
|
|
"OpenAI-compatible endpoints that send an API key must use HTTPS. Use an https:// URL, or omit the API key on a private HTTP endpoint.",
|
|
);
|
|
}
|
|
|
|
export function assertAllowedOpenAiCompatibleRequestUrl(
|
|
raw: string,
|
|
opts?: { allowPublic?: boolean },
|
|
): URL {
|
|
let url: URL;
|
|
try {
|
|
url = new URL(raw);
|
|
} catch {
|
|
throw new Error("Model server URL must be an absolute HTTP(S) URL");
|
|
}
|
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
throw new Error("Model server URL must use http or https");
|
|
}
|
|
if (url.username || url.password) {
|
|
throw new Error("Model server URL must not contain credentials");
|
|
}
|
|
const hostname = normalizeHostname(url.hostname);
|
|
if (isBlockedHostname(hostname)) {
|
|
throw new Error("Base URL targets a blocked metadata or link-local host");
|
|
}
|
|
const allowPublic = opts?.allowPublic ?? openAiCompatAllowPublicHosts();
|
|
if (isPrivateOpenAiCompatibleHostname(hostname)) return url;
|
|
if (!allowPublic) {
|
|
throw new Error(
|
|
"Public model endpoints are blocked. Set RAKAZO_OPENAI_COMPAT_ALLOW_PUBLIC=1 to allow them. A private reverse proxy on localhost or an RFC1918 address does not need that gate.",
|
|
);
|
|
}
|
|
return url;
|
|
}
|