@clerk/shared
Version:
Internal package utils used by the Clerk SDKs
288 lines (286 loc) • 11.4 kB
JavaScript
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
const require_keys = require('../../keys.js');
const require_globs = require('../../globs.js');
const require_underscore = require('../../underscore.js');
const require_url = require('../../url.js');
const require_logger = require('../../logger.js');
const require_internal_clerk_js_path = require('./path.js');
const require_internal_clerk_js_querystring = require('./querystring.js');
//#region src/internal/clerk-js/url.ts
const DUMMY_URL_BASE = "http://clerk-dummy";
const BANNED_URI_PROTOCOLS = ["javascript:"];
const BANNED_HREF_PROTOCOLS = [
"javascript:",
"data:",
"vbscript:",
"blob:"
];
const { isDevOrStagingUrl } = require_keys.createDevOrStagingUrlCache();
const accountPortalCache = /* @__PURE__ */ new Map();
function isDevAccountPortalOrigin(hostname = window.location.hostname) {
if (!hostname) return false;
let res = accountPortalCache.get(hostname);
if (res === void 0) {
res = require_url.isLegacyDevAccountPortalOrigin(hostname) || require_url.isCurrentDevAccountPortalOrigin(hostname);
accountPortalCache.set(hostname, res);
}
return res;
}
function getETLDPlusOneFromFrontendApi(frontendApi) {
return frontendApi.replace("clerk.", "");
}
function buildURL(params, options = {}) {
const { base, hashPath, hashSearch, searchParams, hashSearchParams, ...rest } = params;
let fallbackBase = "";
if (typeof window !== "undefined" && !!window.location) fallbackBase = window.location.href;
else fallbackBase = "http://react-native-fake-base-url";
const url = new URL(base || "", fallbackBase);
if (searchParams instanceof URLSearchParams) searchParams.forEach((value, key) => {
if (value !== null && value !== void 0) url.searchParams.set(require_underscore.camelToSnake(key), value);
});
Object.assign(url, rest);
if (hashPath || hashSearch || hashSearchParams) {
const dummyUrlForHash = new URL(DUMMY_URL_BASE + url.hash.substring(1));
dummyUrlForHash.pathname = require_internal_clerk_js_path.joinPaths(dummyUrlForHash.pathname, hashPath || "");
const searchParamsFromHashSearchString = require_internal_clerk_js_querystring.getQueryParams(hashSearch || "");
for (const [key, val] of Object.entries(searchParamsFromHashSearchString)) dummyUrlForHash.searchParams.append(key, val);
if (hashSearchParams) {
const paramsArr = Array.isArray(hashSearchParams) ? hashSearchParams : [hashSearchParams];
for (const _params of paramsArr) {
if (!(_params instanceof URLSearchParams) && typeof _params !== "object") continue;
new URLSearchParams(_params).forEach((value, key) => {
if (value !== null && value !== void 0) dummyUrlForHash.searchParams.set(require_underscore.camelToSnake(key), value);
});
}
}
const newHash = dummyUrlForHash.href.replace(DUMMY_URL_BASE, "");
if (newHash !== "/") url.hash = newHash;
}
const { stringify, skipOrigin } = options;
if (stringify) return skipOrigin ? url.href.replace(url.origin, "") : url.href;
return url;
}
function toURL(url) {
return new URL(url.toString(), window.location.origin);
}
/**
*
* stripOrigin(url: URL | string): string
*
* Strips the origin part of a URL and preserves path, search and hash is applicable
*
* References:
* https://developer.mozilla.org/en-US/docs/Web/API/URL
*
* @param url
* @returns Returns the URL href without the origin
*/
function stripOrigin(url) {
if (typeof window.location === "undefined" && typeof url === "string") return url;
url = toURL(url);
return url.href.replace(url.origin, "");
}
/**
* trimTrailingSlash(path: string): string
*
* Strips the trailing slashes from a string
*
* @returns Returns the string without trailing slashes
*
* @param path
*/
const trimTrailingSlash = (path) => {
return (path || "").replace(/\/+$/, "");
};
/**
* trimLeadingSlash(path: string): string
*
* Strips the leading slashes from a string
*
* @returns Returns the string without leading slashes
*
* @param path
*/
const trimLeadingSlash = (path) => {
return (path || "").replace(/^\/+/, "");
};
const hasExternalAccountSignUpError = (signUp) => {
const { externalAccount } = signUp.verifications;
return !!externalAccount.error;
};
function getSearchParameterFromHash({ hash = window.location.hash, paramName }) {
const h = hash.startsWith("#") ? hash.substring(1) : hash;
return new URL(h, DUMMY_URL_BASE).searchParams.get(paramName);
}
function isValidUrl(val) {
if (!val) return false;
try {
new URL(val);
return true;
} catch {
return false;
}
}
function relativeToAbsoluteUrl(url, origin) {
try {
return new URL(url);
} catch {
return new URL(url, origin);
}
}
const disallowedPatterns = [
/\0/,
/^\/\//,
/[\x00-\x1F]/
];
/**
* Check for potentially problematic URLs that could have been crafted to intentionally bypass the origin check. Note that the URLs passed to this
* function are assumed to be from an "allowed origin", so we are not executing origin-specific checks here.
*/
function isProblematicUrl(url) {
if (hasBannedProtocol(url)) return true;
for (const pattern of disallowedPatterns) if (pattern.test(url.pathname)) return true;
return false;
}
function isDataUri(val) {
if (!val || !isValidUrl(val)) return false;
return new URL(val).protocol === "data:";
}
/**
* Checks if a URL uses javascript: protocol.
* This prevents some XSS attacks through javascript: URLs.
*
* IMPORTANT: This does not check for `data:` or other protocols which
* are dangerous if used for links or setting the window location.
*
* @param val - The URL to check
* @returns True if the URL contains a banned protocol, false otherwise
*/
function hasBannedProtocol(val) {
if (!isValidUrl(val)) return false;
const protocol = new URL(val).protocol;
return BANNED_URI_PROTOCOLS.some((bp) => bp === protocol);
}
/**
* Checks if a URL contains a banned protocol for href attributes in links.
* This prevents some XSS attacks through javascript:, data:, vbscript:, and blob: URLs.
*
* @param val - The URL to check
* @returns True if the URL contains a banned protocol, false otherwise
*/
function hasBannedHrefProtocol(val) {
if (!isValidUrl(val)) return false;
const protocol = new URL(val).protocol;
return BANNED_HREF_PROTOCOLS.some((bp) => bp === protocol);
}
/**
* Sanitizes an href value by checking for dangerous protocols.
* Returns null if the href contains a dangerous protocol, otherwise returns the original href.
* This prevents some XSS attacks through javascript:, data:, vbscript:, and blob: URLs.
*
* @param href - The href value to sanitize
* @returns The sanitized href or null if dangerous
*/
function sanitizeHref(href) {
if (!href || href.trim() === "") return null;
if (href.startsWith("/") || href.startsWith("#") || href.startsWith("?")) return href;
if (!href.includes(":")) return href;
try {
if (hasBannedHrefProtocol(new URL(href))) return null;
return href;
} catch {
return href;
}
}
const hasUrlInFragment = (_url) => {
return new URL(_url, DUMMY_URL_BASE).hash.startsWith("#/");
};
/**
* Creates a new URL by merging a fragment-based URL, if found.
* The result URL has the original and the fragment pathnames appended
* and also includes all search params from both places.
*
* @example
* ```ts
* mergeFragmentIntoUrl('https://accounts.clerk.com/sign-in?user_param=hello#/verify/factor-one?redirect_url=/protected')
* // Returns: 'https://accounts.clerk.com/sign-in/verify/factor-one?user_param=hello&redirect_url=/protected'
* ```
*/
const mergeFragmentIntoUrl = (_url) => {
const url = new URL(_url);
if (!hasUrlInFragment(url)) return url;
const fragmentUrl = new URL(url.hash.replace("#/", "/"), url.href);
const mergedPathname = [url.pathname, fragmentUrl.pathname].map((s) => s.split("/")).flat().filter(Boolean).join("/");
const mergedUrl = new URL(mergedPathname, url.origin);
url.searchParams.forEach((val, key) => {
mergedUrl.searchParams.set(key, val);
});
fragmentUrl.searchParams.forEach((val, key) => {
mergedUrl.searchParams.set(key, val);
});
return mergedUrl;
};
const pathFromFullPath = (fullPath) => {
return fullPath.replace(/CLERK-ROUTER\/(.*?)\//, "");
};
const frontendApiRedirectPathsWithUserInput = ["/oauth/authorize"];
const frontendApiRedirectPathsNoUserInput = [
"/v1/verify",
"/v1/tickets/accept",
"/oauth/authorize-with-immediate-redirect",
"/oauth/end_session"
];
function isRedirectForFAPIInitiatedFlow(frontendApi, redirectUrl) {
const url = new URL(redirectUrl, DUMMY_URL_BASE);
const path = url.pathname;
const isValidFrontendRedirectPath = frontendApiRedirectPathsWithUserInput.includes(path) || frontendApiRedirectPathsNoUserInput.includes(path);
return frontendApi === url.host && isValidFrontendRedirectPath;
}
function requiresUserInput(redirectUrl) {
const url = new URL(redirectUrl, DUMMY_URL_BASE);
return frontendApiRedirectPathsWithUserInput.includes(url.pathname);
}
const isAllowedRedirect = (allowedRedirectOrigins, currentOrigin) => (_url) => {
let url = _url;
if (typeof url === "string") url = relativeToAbsoluteUrl(url, currentOrigin);
if (!allowedRedirectOrigins) return true;
const isSameOrigin = currentOrigin === url.origin;
const isAllowed = !isProblematicUrl(url) && (isSameOrigin || allowedRedirectOrigins.map((origin) => typeof origin === "string" ? require_globs.globs.toRegexp(trimTrailingSlash(origin)) : origin).some((origin) => origin.test(trimTrailingSlash(url.origin))));
if (!isAllowed) require_logger.logger.warnOnce(`Clerk: Redirect URL ${url} is not on one of the allowedRedirectOrigins, falling back to the default redirect URL.`);
return isAllowed;
};
function createAllowedRedirectOrigins(allowedRedirectOrigins, frontendApi, instanceType) {
if (Array.isArray(allowedRedirectOrigins) && !!allowedRedirectOrigins.length) return allowedRedirectOrigins;
const origins = [];
if (typeof window !== "undefined" && !!window.location) origins.push(window.location.origin);
origins.push(`https://${getETLDPlusOneFromFrontendApi(frontendApi)}`);
origins.push(`https://*.${getETLDPlusOneFromFrontendApi(frontendApi)}`);
if (instanceType === "development") origins.push(`https://${frontendApi}`);
return origins;
}
//#endregion
exports.buildURL = buildURL;
exports.createAllowedRedirectOrigins = createAllowedRedirectOrigins;
exports.getETLDPlusOneFromFrontendApi = getETLDPlusOneFromFrontendApi;
exports.getSearchParameterFromHash = getSearchParameterFromHash;
exports.hasBannedHrefProtocol = hasBannedHrefProtocol;
exports.hasBannedProtocol = hasBannedProtocol;
exports.hasExternalAccountSignUpError = hasExternalAccountSignUpError;
exports.hasUrlInFragment = hasUrlInFragment;
exports.isAllowedRedirect = isAllowedRedirect;
exports.isDataUri = isDataUri;
exports.isDevAccountPortalOrigin = isDevAccountPortalOrigin;
exports.isDevOrStagingUrl = isDevOrStagingUrl;
exports.isProblematicUrl = isProblematicUrl;
exports.isRedirectForFAPIInitiatedFlow = isRedirectForFAPIInitiatedFlow;
exports.isValidUrl = isValidUrl;
exports.mergeFragmentIntoUrl = mergeFragmentIntoUrl;
exports.pathFromFullPath = pathFromFullPath;
exports.relativeToAbsoluteUrl = relativeToAbsoluteUrl;
exports.requiresUserInput = requiresUserInput;
exports.sanitizeHref = sanitizeHref;
exports.stripOrigin = stripOrigin;
exports.toURL = toURL;
exports.trimLeadingSlash = trimLeadingSlash;
exports.trimTrailingSlash = trimTrailingSlash;
//# sourceMappingURL=url.js.map