@debugg-ai/debugg-ai-mcp
Version:
Zero-Config, Fully AI-Managed End-to-End Testing for all code gen platforms.
127 lines (126 loc) • 4.13 kB
JavaScript
/**
* URL Parser Utilities
* Helper functions for parsing and validating URLs, specifically for detecting localhost URLs
*/
/**
* Normalize a user-supplied URL string before validation.
* Handles bare hostnames without a scheme (e.g. "localhost:3000" → "http://localhost:3000").
*/
export function normalizeUrl(input) {
if (typeof input !== 'string')
return input;
if (/^https?:\/\//i.test(input))
return input;
// Bare local hostname (no scheme) — prepend http://
if (/^(localhost\.?|127\.0\.0\.1|0\.0\.0\.0|host\.docker\.internal|\[::1\])(:\d+)?(\/.*)?$/i.test(input)) {
return `http://${input}`;
}
return input;
}
/**
* Check if a hostname represents a local/tunnelable address.
*/
function isLocalhostHostname(hostname) {
// Strip IPv6 brackets: new URL('http://[::1]:3000').hostname === '[::1]' in WHATWG spec
const h = hostname.replace(/^\[|\]$/g, '').toLowerCase();
return (h === 'localhost' ||
h === 'localhost.' || // trailing-dot FQDN notation
h === '127.0.0.1' ||
h === '::1' ||
h === '0.0.0.0' ||
h === 'host.docker.internal' ||
h.startsWith('192.168.') ||
h.startsWith('10.') ||
(h.startsWith('172.') &&
parseInt(h.split('.')[1], 10) >= 16 &&
parseInt(h.split('.')[1], 10) <= 31));
}
/**
* Parse a URL and determine if it's a localhost URL
*/
export function parseUrl(urlString) {
try {
const url = new URL(urlString);
const isLocalhost = isLocalhostHostname(url.hostname);
const port = url.port ? parseInt(url.port, 10) : undefined;
return {
originalUrl: urlString,
isLocalhost,
hostname: url.hostname,
port,
protocol: url.protocol,
pathname: url.pathname,
search: url.search,
hash: url.hash
};
}
catch {
throw new Error(`Invalid URL format: ${urlString}`);
}
}
/**
* Extract port from localhost URL
* Returns the port number if it's a localhost URL, otherwise returns undefined
*/
export function extractLocalhostPort(urlString) {
try {
const parsed = parseUrl(urlString);
if (parsed.isLocalhost) {
return parsed.port || (parsed.protocol === 'https:' ? 443 : 80);
}
return undefined;
}
catch {
return undefined;
}
}
/**
* Check if a URL is a localhost URL (shorthand function)
*/
export function isLocalhostUrl(urlString) {
try {
const parsed = parseUrl(urlString);
return parsed.isLocalhost;
}
catch {
return false;
}
}
/**
* Replace ngrok tunnel URLs with the original localhost origin in any string/object.
* Used to sanitize backend responses that contain internal tunnel URLs before
* returning them to callers who only know the original localhost address.
*/
export function replaceTunnelUrls(value, localhostOrigin) {
if (typeof value === 'string') {
return value.replace(/https?:\/\/[^\s/"]+\.ngrok\.debugg\.ai/g, localhostOrigin.replace(/\/$/, ''));
}
if (Array.isArray(value)) {
return value.map(item => replaceTunnelUrls(item, localhostOrigin));
}
if (value !== null && typeof value === 'object') {
const result = {};
for (const [k, v] of Object.entries(value)) {
result[k] = replaceTunnelUrls(v, localhostOrigin);
}
return result;
}
return value;
}
/**
* Generate a tunneled URL for a localhost URL
*/
export function generateTunnelUrl(originalUrl, tunnelId, tunnelDomain = 'ngrok.debugg.ai') {
try {
const parsed = parseUrl(originalUrl);
if (!parsed.isLocalhost) {
return originalUrl; // Return original URL if not localhost
}
// Create the tunneled URL maintaining the path, search, and hash
const tunnelUrl = `https://${tunnelId}.${tunnelDomain}${parsed.pathname}${parsed.search}${parsed.hash}`;
return tunnelUrl;
}
catch {
return originalUrl; // Return original URL if parsing fails
}
}