mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
0139b91b5d
* chore: Replace lodash with es-toolkit Migrate all direct lodash imports to es-toolkit/compat for a smaller, faster, lodash-compatible utility library. Transitive lodash usage from other packages remains unchanged. * fix: Restore isPlainObject semantics in CanCan policy The lodash migration aliased `isObject` to `lodash/isPlainObject` and the codemod incorrectly mapped the local name to es-toolkit's `isObject`, which also returns true for arrays and functions. This caused condition objects in policy definitions to be skipped, breaking authorization checks across the codebase. * fix: Restore unicode-aware length counting in validators es-toolkit/compat's size() returns string.length, while lodash's _.size() counts unicode code points. Switch to [...value].length to preserve the previous behavior so multi-byte characters like emoji count as one.
165 lines
3.6 KiB
TypeScript
165 lines
3.6 KiB
TypeScript
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, and whitespace from input
|
|
// to extract a clean hostname
|
|
function normalizeUrl(url: string) {
|
|
const stripped = trim(url.replace(/(https?:)?\/\//, ""));
|
|
// Extract authority (everything before the first slash)
|
|
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;
|
|
return hostWithPort.split(/[:?]/)[0];
|
|
}
|
|
|
|
// 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();
|
|
|
|
// if the url doesn't include the base url, then it must be a custom domain
|
|
const baseUrlStart = host === baseDomain ? 0 : host.indexOf(`.${baseDomain}`);
|
|
|
|
if (baseUrlStart === -1) {
|
|
return { teamSubdomain: "", host, port: undefined, custom: true };
|
|
}
|
|
|
|
// we consider anything in front of the baseUrl to be the subdomain
|
|
const subdomain = host.substring(0, baseUrlStart);
|
|
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",
|
|
];
|