shared/utils/domains.ts
Rama main · réplica de lectura del repositorio original.
import { trim } from "es-toolkit/compat";
import env from "../env";
type Domain = {
teamSubdomain: string;
host: string;
port?: string;
custom: boolean;
};
/**
* Removes the top level domain from the argument and slugifies it
*
* @param domain Domain string to slugify
* @returns String with only non top-level domains
*/
export function slugifyDomain(domain: string) {
return domain.split(".").slice(0, -1).join("-");
}
// strips protocol, userinfo, port, path, query, fragment, and whitespace from input
// to extract a clean hostname
function normalizeUrl(url: string) {
const stripped = trim(url.replace(/(https?:)?\/\//i, ""));
// Extract authority, which ends at the same delimiters as in a browser
const authority = stripped.split(/[/\\?#]/)[0];
// Strip userinfo if present (e.g. "user:pass@host" → "host")
const atIndex = authority.lastIndexOf("@");
const hostWithPort =
atIndex !== -1 ? authority.substring(atIndex + 1) : authority;
const host = hostWithPort.split(":")[0];
// Hostnames are case-insensitive and may carry a trailing root label
return host.toLowerCase().replace(/\.$/, "");
}
// The base domain is where root cookies are set in hosted mode
// It's also appended to a team's hosted subdomain to form their app URL
export function getBaseDomain() {
const normalEnvUrl = normalizeUrl(env.URL);
const tokens = normalEnvUrl.split(".");
// remove reserved subdomains like "app"
// from the env URL to form the base domain
return tokens.length > 1 && RESERVED_SUBDOMAINS.includes(tokens[0])
? tokens.slice(1).join(".")
: normalEnvUrl;
}
// we originally used the parse-domain npm module however this includes
// a large list of possible TLD's which increase the size of the bundle
// unnecessarily for our usecase of trusted input.
export function parseDomain(url: string): Domain {
if (!url) {
throw new TypeError("a non-empty url is required");
}
let port;
try {
const parsedUrl = new URL(url);
port = parsedUrl.port || undefined;
} catch (_err) {
// ignore
}
const host = normalizeUrl(url);
const baseDomain = getBaseDomain();
const suffix = `.${baseDomain}`;
// the host must be the base domain, or end with it on a label boundary,
// otherwise it is a custom domain
if (host !== baseDomain && !host.endsWith(suffix)) {
return { teamSubdomain: "", host, port: undefined, custom: true };
}
// we consider anything in front of the baseUrl to be the subdomain
const subdomain =
host === baseDomain ? "" : host.substring(0, host.length - suffix.length);
const isReservedSubdomain = RESERVED_SUBDOMAINS.includes(subdomain);
return {
teamSubdomain: isReservedSubdomain ? "" : subdomain,
host,
port,
custom: false,
};
}
export function getCookieDomain(domain: string, isCloudHosted: boolean) {
// always use the base URL for cookies when in hosted mode
// and the domain is not custom
if (isCloudHosted) {
const parsed = parseDomain(domain);
if (!parsed.custom) {
return getBaseDomain();
}
}
return domain;
}
export const RESERVED_SUBDOMAINS = [
"about",
"account",
"admin",
"advertising",
"api",
"app",
"assets",
"archive",
"beta",
"billing",
"blog",
"cache",
"cdn",
"code",
"community",
"dashboard",
"developer",
"developers",
"forum",
"help",
"home",
"http",
"https",
"imap",
"localhost",
"mail",
"marketing",
"mcp",
"mobile",
"multiplayer",
"new",
"news",
"newsletter",
"ns1",
"ns2",
"ns3",
"ns4",
"password",
"profile",
"realtime",
"sandbox",
"script",
"scripts",
"setup",
"signin",
"signup",
"site",
"smtp",
"support",
"status",
"static",
"stats",
"test",
"update",
"updates",
"ws",
"wss",
"web",
"websockets",
"www",
"www1",
"www2",
"www3",
"www4",
];