UNPKG

@clerk/shared

Version:

Internal package utils used by the Clerk SDKs

1 lines 8.75 kB
{"version":3,"file":"protectCheck.mjs","names":[],"sources":["../../../src/internal/clerk-js/protectCheck.ts"],"sourcesContent":["import { ClerkRuntimeError } from '../../error';\nimport type { ProtectCheckResource } from '../../types';\n\nexport interface ExecuteProtectCheckOptions {\n /**\n * Host-provided visibility handshake, forwarded to the script verbatim as\n * `setWidgetVisible` in the init payload. The script calls it right before revealing UI in\n * the container (and with `false` once its widget is done); the returned promise resolves\n * only after the host has applied the change to the DOM (e.g. removed its own loading\n * spinner), so the script can sequence its reveal without a frame of overlap. A script that\n * knows its widget is imminent may call it immediately to avoid a spinner flash. Scripts\n * must treat the field as optional — older hosts don't provide it.\n */\n setWidgetVisible?: (visible: boolean) => Promise<void>;\n /**\n * Signals that the caller no longer needs the proof token (component unmounted, user\n * navigated away, etc.). When the signal aborts:\n * - If the script has not yet been imported, `executeProtectCheck` rejects with\n * `protect_check_aborted` without loading the script.\n * - The signal is forwarded to the script as `{ signal }` in the second argument so\n * cooperating SDKs can cancel any in-flight UI / network work.\n * - Even if the script ignores the signal and resolves with a token, the helper\n * re-checks `signal.aborted` after the await and rejects with `protect_check_aborted`\n * so the caller never observes a \"successful\" abort.\n *\n * Scripts that don't honor the signal will continue to run; this is best-effort by design.\n */\n signal?: AbortSignal;\n}\n\ninterface ScriptInitOptions {\n token: string;\n uiHints?: Record<string, string>;\n signal?: AbortSignal;\n setWidgetVisible?: (visible: boolean) => Promise<void>;\n}\n\ntype ScriptDefault = (container: HTMLDivElement, init: ScriptInitOptions) => Promise<string>;\n\n/**\n * Validates the `sdk_url` returned by the server before passing it to dynamic `import()`.\n *\n * Rejects:\n * - Anything that fails URL parsing (relative paths, garbage strings)\n * - Non-`https:` schemes — including `http:`, `data:`, `blob:`, `javascript:`. The server\n * always returns an HTTPS URL, but the dynamic-import primitive accepts `data:`/`blob:`\n * modules which would let a tampered response inject arbitrary code into the host page.\n * - URLs containing credentials (`user:pass@host`) — phishing surface, no legitimate use.\n *\n * Throws `ClerkRuntimeError` with code `protect_check_invalid_sdk_url`. We deliberately do\n * NOT silently strip an invalid `protect_check` from the resource: the gate must remain\n * present so the user can't bypass it by manipulating the response. Fail-closed.\n */\nfunction assertValidSdkUrl(sdkUrl: string): URL {\n let parsed: URL;\n try {\n parsed = new URL(sdkUrl);\n } catch {\n throw new ClerkRuntimeError('Protect check sdk_url is not a valid URL', {\n code: 'protect_check_invalid_sdk_url',\n });\n }\n if (parsed.protocol !== 'https:') {\n throw new ClerkRuntimeError('Protect check sdk_url must use HTTPS', {\n code: 'protect_check_invalid_sdk_url',\n });\n }\n if (parsed.username || parsed.password) {\n throw new ClerkRuntimeError('Protect check sdk_url must not contain credentials', {\n code: 'protect_check_invalid_sdk_url',\n });\n }\n return parsed;\n}\n\n/**\n * Loads the Protect challenge SDK from `protectCheck.sdkUrl`, hands it the container element\n * and the spec-defined init payload (`token`, `uiHints`, `signal`), and returns the proof\n * token the SDK produces.\n *\n * The SDK script must:\n * - Be a valid ES module served over HTTPS\n * - Have a default export of the shape `(container, { token, uiHints, signal }) => Promise<string>`\n * - Honor the `signal` to abort any pending work (best-effort)\n *\n * Only the minimal fields (`token`, optional `ui_hints`) are surfaced to the script — the\n * full sign-up/sign-in resource is intentionally NOT passed, to minimize the trust surface\n * granted to third-party Protect scripts.\n *\n * Failure modes are surfaced as `ClerkRuntimeError` with one of:\n * - `protect_check_invalid_sdk_url` — URL fails the safety checks above\n * - `protect_check_aborted` — caller aborted before or during execution\n * - `protect_check_script_load_failed` — network error, CSP block, or invalid module\n * - `protect_check_invalid_script` — module loaded but no callable default export\n * - `protect_check_execution_failed` — the script's default export threw\n */\nexport async function executeProtectCheck(\n protectCheck: Pick<ProtectCheckResource, 'sdkUrl' | 'token' | 'uiHints'>,\n container: HTMLDivElement,\n options: ExecuteProtectCheckOptions = {},\n): Promise<string> {\n const { signal, setWidgetVisible } = options;\n const { sdkUrl, token, uiHints } = protectCheck;\n\n const validated = assertValidSdkUrl(sdkUrl);\n\n if (signal?.aborted) {\n throw new ClerkRuntimeError('Protect check aborted by caller', { code: 'protect_check_aborted' });\n }\n\n let mod: Record<string, unknown>;\n try {\n mod = await import(/* webpackIgnore: true */ validated.toString());\n } catch {\n // Surface a generic message and deliberately omit the original error: Chromium/Firefox embed\n // the sdk_url in the dynamic-import failure text, which a tampered response could plant in the UI.\n throw new ClerkRuntimeError(\n 'Protect check script failed to load. This is commonly caused by a Content Security ' +\n 'Policy that blocks the script origin (add it to your script-src directive), a ' +\n 'network error, or an invalid module.',\n { code: 'protect_check_script_load_failed' },\n );\n }\n\n if (signal?.aborted) {\n throw new ClerkRuntimeError('Protect check aborted by caller', { code: 'protect_check_aborted' });\n }\n\n if (typeof mod.default !== 'function') {\n throw new ClerkRuntimeError('Protect check script does not export a default function', {\n code: 'protect_check_invalid_script',\n });\n }\n\n let proofToken: string;\n try {\n proofToken = await (mod.default as ScriptDefault)(container, { token, uiHints, signal, setWidgetVisible });\n } catch (err) {\n // Distinguish abort-induced rejections from genuine script errors: only relabel as\n // `protect_check_aborted` when the error looks like an abort (`AbortError`), otherwise\n // surface the script's actual failure so production diagnostics aren't masked.\n const looksLikeAbort = err instanceof Error && err.name === 'AbortError';\n if (signal?.aborted && looksLikeAbort) {\n throw new ClerkRuntimeError('Protect check aborted by caller', { code: 'protect_check_aborted' });\n }\n const original = err instanceof Error ? err.message : String(err);\n throw new ClerkRuntimeError(`Protect check script execution failed: ${original}`, {\n code: 'protect_check_execution_failed',\n });\n }\n\n // The script may have ignored the signal and resolved with a token after the abort fired.\n // Re-check here so callers get a consistent contract: if you aborted, you never see a token.\n if (signal?.aborted) {\n throw new ClerkRuntimeError('Protect check aborted by caller', { code: 'protect_check_aborted' });\n }\n\n return proofToken;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAqDA,SAAS,kBAAkB,QAAqB;CAC9C,IAAI;CACJ,IAAI;EACF,SAAS,IAAI,IAAI,MAAM;CACzB,QAAQ;EACN,MAAM,IAAI,kBAAkB,4CAA4C,EACtE,MAAM,gCACR,CAAC;CACH;CACA,IAAI,OAAO,aAAa,UACtB,MAAM,IAAI,kBAAkB,wCAAwC,EAClE,MAAM,gCACR,CAAC;CAEH,IAAI,OAAO,YAAY,OAAO,UAC5B,MAAM,IAAI,kBAAkB,sDAAsD,EAChF,MAAM,gCACR,CAAC;CAEH,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;AAuBA,eAAsB,oBACpB,cACA,WACA,UAAsC,CAAC,GACtB;CACjB,MAAM,EAAE,QAAQ,qBAAqB;CACrC,MAAM,EAAE,QAAQ,OAAO,YAAY;CAEnC,MAAM,YAAY,kBAAkB,MAAM;CAE1C,IAAI,QAAQ,SACV,MAAM,IAAI,kBAAkB,mCAAmC,EAAE,MAAM,wBAAwB,CAAC;CAGlG,IAAI;CACJ,IAAI;EACF,MAAM,MAAM;;GAAiC,UAAU,SAAS;;CAClE,QAAQ;EAGN,MAAM,IAAI,kBACR,yMAGA,EAAE,MAAM,mCAAmC,CAC7C;CACF;CAEA,IAAI,QAAQ,SACV,MAAM,IAAI,kBAAkB,mCAAmC,EAAE,MAAM,wBAAwB,CAAC;CAGlG,IAAI,OAAO,IAAI,YAAY,YACzB,MAAM,IAAI,kBAAkB,2DAA2D,EACrF,MAAM,+BACR,CAAC;CAGH,IAAI;CACJ,IAAI;EACF,aAAa,MAAO,IAAI,QAA0B,WAAW;GAAE;GAAO;GAAS;GAAQ;EAAiB,CAAC;CAC3G,SAAS,KAAK;EAIZ,MAAM,iBAAiB,eAAe,SAAS,IAAI,SAAS;EAC5D,IAAI,QAAQ,WAAW,gBACrB,MAAM,IAAI,kBAAkB,mCAAmC,EAAE,MAAM,wBAAwB,CAAC;EAGlG,MAAM,IAAI,kBAAkB,0CADX,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,KACkB,EAChF,MAAM,iCACR,CAAC;CACH;CAIA,IAAI,QAAQ,SACV,MAAM,IAAI,kBAAkB,mCAAmC,EAAE,MAAM,wBAAwB,CAAC;CAGlG,OAAO;AACT"}