@clerk/shared
Version:
Internal package utils used by the Clerk SDKs
146 lines (144 loc) • 8.04 kB
JavaScript
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
const require_clerkRuntimeError = require('../../_chunks/clerkRuntimeError-CYZdTZ_x.js');
require('../../_chunks/error-Ce3s-kIk.js');
//#region src/internal/clerk-js/protectCheck.ts
/**
* Default bound on LOADING the challenge module.
*
* Its only job is a network that accepts a connection and then never answers, because every
* other load failure — a CSP block, DNS, a 404, a body that isn't a valid module — rejects the
* dynamic import on its own and needs no timer to notice. That makes a generous value the safe
* one: nothing legitimate is waiting on this timer, while a value tight enough to fire on a
* genuinely slow connection would fail a load that was going to succeed.
*/
const DEFAULT_PROTECT_CHECK_LOAD_TIMEOUT_MS = 6e4;
/**
* Ceiling on the configured load bound. `setTimeout` stores its delay in a signed 32-bit int, so a
* larger value overflows and fires immediately — which would make every load fail instantly, the
* exact opposite of what an operator asking for a long timeout wanted. Clamping rather than
* rejecting keeps a fat-fingered config from breaking sign-in.
*/
const MAX_PROTECT_CHECK_LOAD_TIMEOUT_MS = 6e5;
function resolveLoadTimeoutMs(configured) {
if (typeof configured !== "number" || !Number.isFinite(configured) || configured <= 0) return DEFAULT_PROTECT_CHECK_LOAD_TIMEOUT_MS;
return Math.min(configured, MAX_PROTECT_CHECK_LOAD_TIMEOUT_MS);
}
/**
* Races the dynamic import against `timeoutMs`, always clearing the timer so a fast load does not
* leave one pending.
*
* The bound stops at the import on purpose. Once `mod.default` is called the challenge owns its
* own deadline and the host imposes none: the host cannot know an honest duration for a challenge
* whose type is chosen server-side, per decision, and whose work it deliberately knows nothing
* about — waiting on a person, or moving a server-chosen number of bytes over an unknown link. A
* host-side wall over execution aborts legitimate challenges and reports them as timeouts, and
* since a re-run restarts the work, retrying cannot win on any connection slow enough to trip it.
*
* The abort signal is raced too. A stalled import cannot itself be cancelled, but without this the
* caller's abort would not settle anything: an unmounted component would keep this promise, its
* closures and its timer alive for the whole load bound, and then report a load failure for what
* was really a cancellation.
*/
function importWithTimeout(url, timeoutMs, signal) {
let timeoutId;
let onAbort;
const expiry = new Promise((_, reject) => {
timeoutId = setTimeout(() => reject(/* @__PURE__ */ new Error("Protect check script load timed out")), timeoutMs);
});
const aborted = new Promise((_, reject) => {
if (!signal) return;
onAbort = () => reject(/* @__PURE__ */ new Error("Protect check aborted during load"));
signal.addEventListener("abort", onAbort, { once: true });
});
return Promise.race([
import(
/* webpackIgnore: true */
url
),
expiry,
aborted
]).finally(() => {
clearTimeout(timeoutId);
if (signal && onAbort) signal.removeEventListener("abort", onAbort);
});
}
/**
* Validates the `sdk_url` returned by the server before passing it to dynamic `import()`.
*
* Rejects:
* - Anything that fails URL parsing (relative paths, garbage strings)
* - Non-`https:` schemes — including `http:`, `data:`, `blob:`, `javascript:`. The server
* always returns an HTTPS URL, but the dynamic-import primitive accepts `data:`/`blob:`
* modules which would let a tampered response inject arbitrary code into the host page.
* - URLs containing credentials (`user:pass@host`) — phishing surface, no legitimate use.
*
* Throws `ClerkRuntimeError` with code `protect_check_invalid_sdk_url`. We deliberately do
* NOT silently strip an invalid `protect_check` from the resource: the gate must remain
* present so the user can't bypass it by manipulating the response. Fail-closed.
*/
function assertValidSdkUrl(sdkUrl) {
let parsed;
try {
parsed = new URL(sdkUrl);
} catch {
throw new require_clerkRuntimeError.ClerkRuntimeError("Protect check sdk_url is not a valid URL", { code: "protect_check_invalid_sdk_url" });
}
if (parsed.protocol !== "https:") throw new require_clerkRuntimeError.ClerkRuntimeError("Protect check sdk_url must use HTTPS", { code: "protect_check_invalid_sdk_url" });
if (parsed.username || parsed.password) throw new require_clerkRuntimeError.ClerkRuntimeError("Protect check sdk_url must not contain credentials", { code: "protect_check_invalid_sdk_url" });
return parsed;
}
/**
* Loads the Protect challenge SDK from `protectCheck.sdkUrl`, hands it the container element
* and the spec-defined init payload (`token`, `uiHints`, `signal`), and returns the proof
* token the SDK produces.
*
* The SDK script must:
* - Be a valid ES module served over HTTPS
* - Have a default export of the shape `(container, { token, uiHints, signal }) => Promise<string>`
* - Honor the `signal` to abort any pending work (best-effort)
*
* Only the minimal fields (`token`, optional `ui_hints`) are surfaced to the script — the
* full sign-up/sign-in resource is intentionally NOT passed, to minimize the trust surface
* granted to third-party Protect scripts.
*
* Failure modes are surfaced as `ClerkRuntimeError` with one of:
* - `protect_check_invalid_sdk_url` — URL fails the safety checks above
* - `protect_check_aborted` — caller aborted before or during execution
* - `protect_check_script_load_failed` — network error, CSP block, or invalid module
* - `protect_check_invalid_script` — module loaded but no callable default export
* - `protect_check_execution_failed` — the script's default export threw
*/
async function executeProtectCheck(protectCheck, container, options = {}) {
const { signal, setWidgetVisible, loadTimeoutMs } = options;
const { sdkUrl, token, uiHints } = protectCheck;
const validated = assertValidSdkUrl(sdkUrl);
if (signal?.aborted) throw new require_clerkRuntimeError.ClerkRuntimeError("Protect check aborted by caller", { code: "protect_check_aborted" });
let mod;
try {
mod = await importWithTimeout(validated.toString(), resolveLoadTimeoutMs(loadTimeoutMs), signal);
} catch {
if (signal?.aborted) throw new require_clerkRuntimeError.ClerkRuntimeError("Protect check aborted by caller", { code: "protect_check_aborted" });
throw new require_clerkRuntimeError.ClerkRuntimeError("Protect check script failed to load. This is commonly caused by a Content Security Policy that blocks the script origin (add it to your script-src directive), a network error, or an invalid module.", { code: "protect_check_script_load_failed" });
}
if (signal?.aborted) throw new require_clerkRuntimeError.ClerkRuntimeError("Protect check aborted by caller", { code: "protect_check_aborted" });
if (typeof mod.default !== "function") throw new require_clerkRuntimeError.ClerkRuntimeError("Protect check script does not export a default function", { code: "protect_check_invalid_script" });
let proofToken;
try {
proofToken = await mod.default(container, {
token,
uiHints,
signal,
setWidgetVisible
});
} catch (err) {
const looksLikeAbort = err instanceof Error && err.name === "AbortError";
if (signal?.aborted && looksLikeAbort) throw new require_clerkRuntimeError.ClerkRuntimeError("Protect check aborted by caller", { code: "protect_check_aborted" });
throw new require_clerkRuntimeError.ClerkRuntimeError(`Protect check script execution failed: ${err instanceof Error ? err.message : String(err)}`, { code: "protect_check_execution_failed" });
}
if (signal?.aborted) throw new require_clerkRuntimeError.ClerkRuntimeError("Protect check aborted by caller", { code: "protect_check_aborted" });
return proofToken;
}
//#endregion
exports.DEFAULT_PROTECT_CHECK_LOAD_TIMEOUT_MS = DEFAULT_PROTECT_CHECK_LOAD_TIMEOUT_MS;
exports.executeProtectCheck = executeProtectCheck;
//# sourceMappingURL=protectCheck.js.map