{"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   * Overrides how long to wait for the challenge module to LOAD. Per-instance and per-loader\n   * config, since the right value depends on the population an instance serves; a non-positive\n   * or non-numeric value falls back to {@link DEFAULT_PROTECT_CHECK_LOAD_TIMEOUT_MS}.\n   *\n   * Bounds the handoff only — never the challenge. See the note on the constant.\n   */\n  loadTimeoutMs?: number;\n}\n\n/**\n * Default bound on LOADING the challenge module.\n *\n * Its only job is a network that accepts a connection and then never answers, because every\n * other load failure — a CSP block, DNS, a 404, a body that isn't a valid module — rejects the\n * dynamic import on its own and needs no timer to notice. That makes a generous value the safe\n * one: nothing legitimate is waiting on this timer, while a value tight enough to fire on a\n * genuinely slow connection would fail a load that was going to succeed.\n */\nexport const DEFAULT_PROTECT_CHECK_LOAD_TIMEOUT_MS = 60_000;\n\n/**\n * Ceiling on the configured load bound. `setTimeout` stores its delay in a signed 32-bit int, so a\n * larger value overflows and fires immediately — which would make every load fail instantly, the\n * exact opposite of what an operator asking for a long timeout wanted. Clamping rather than\n * rejecting keeps a fat-fingered config from breaking sign-in.\n */\nconst MAX_PROTECT_CHECK_LOAD_TIMEOUT_MS = 600_000;\n\nfunction resolveLoadTimeoutMs(configured: number | undefined): number {\n  if (typeof configured !== 'number' || !Number.isFinite(configured) || configured <= 0) {\n    return DEFAULT_PROTECT_CHECK_LOAD_TIMEOUT_MS;\n  }\n  return Math.min(configured, MAX_PROTECT_CHECK_LOAD_TIMEOUT_MS);\n}\n\n/**\n * Races the dynamic import against `timeoutMs`, always clearing the timer so a fast load does not\n * leave one pending.\n *\n * The bound stops at the import on purpose. Once `mod.default` is called the challenge owns its\n * own deadline and the host imposes none: the host cannot know an honest duration for a challenge\n * whose type is chosen server-side, per decision, and whose work it deliberately knows nothing\n * about — waiting on a person, or moving a server-chosen number of bytes over an unknown link. A\n * host-side wall over execution aborts legitimate challenges and reports them as timeouts, and\n * since a re-run restarts the work, retrying cannot win on any connection slow enough to trip it.\n *\n * The abort signal is raced too. A stalled import cannot itself be cancelled, but without this the\n * caller's abort would not settle anything: an unmounted component would keep this promise, its\n * closures and its timer alive for the whole load bound, and then report a load failure for what\n * was really a cancellation.\n */\nfunction importWithTimeout(\n  url: string,\n  timeoutMs: number,\n  signal: AbortSignal | undefined,\n): Promise<Record<string, unknown>> {\n  let timeoutId: ReturnType<typeof setTimeout> | undefined;\n  let onAbort: (() => void) | undefined;\n\n  const expiry = new Promise<never>((_, reject) => {\n    timeoutId = setTimeout(() => reject(new Error('Protect check script load timed out')), timeoutMs);\n  });\n  const aborted = new Promise<never>((_, reject) => {\n    if (!signal) {\n      return;\n    }\n    onAbort = () => reject(new Error('Protect check aborted during load'));\n    signal.addEventListener('abort', onAbort, { once: true });\n  });\n\n  return Promise.race([import(/* webpackIgnore: true */ url), expiry, aborted]).finally(() => {\n    clearTimeout(timeoutId);\n    if (signal && onAbort) {\n      signal.removeEventListener('abort', onAbort);\n    }\n  }) as Promise<Record<string, unknown>>;\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, loadTimeoutMs } = 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 importWithTimeout(validated.toString(), resolveLoadTimeoutMs(loadTimeoutMs), signal);\n  } catch {\n    // An abort that landed mid-load is a cancellation, not a load failure. Checked first so the\n    // caller gets the same contract it does everywhere else: if you aborted, you never see\n    // anything but `protect_check_aborted`.\n    if (signal?.aborted) {\n      throw new ClerkRuntimeError('Protect check aborted by caller', { code: 'protect_check_aborted' });\n    }\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":";;;;;;;;;;;;;AA+CA,MAAa,wCAAwC;;;;;;;AAQrD,MAAM,oCAAoC;AAE1C,SAAS,qBAAqB,YAAwC;CACpE,IAAI,OAAO,eAAe,YAAY,CAAC,OAAO,SAAS,UAAU,KAAK,cAAc,GAClF,OAAO;CAET,OAAO,KAAK,IAAI,YAAY,iCAAiC;AAC/D;;;;;;;;;;;;;;;;;AAkBA,SAAS,kBACP,KACA,WACA,QACkC;CAClC,IAAI;CACJ,IAAI;CAEJ,MAAM,SAAS,IAAI,SAAgB,GAAG,WAAW;EAC/C,YAAY,iBAAiB,uBAAO,IAAI,MAAM,qCAAqC,CAAC,GAAG,SAAS;CAClG,CAAC;CACD,MAAM,UAAU,IAAI,SAAgB,GAAG,WAAW;EAChD,IAAI,CAAC,QACH;EAEF,gBAAgB,uBAAO,IAAI,MAAM,mCAAmC,CAAC;EACrE,OAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;CAC1D,CAAC;CAED,OAAO,QAAQ,KAAK;EAAC;;GAAiC;;EAAM;EAAQ;CAAO,CAAC,CAAC,CAAC,cAAc;EAC1F,aAAa,SAAS;EACtB,IAAI,UAAU,SACZ,OAAO,oBAAoB,SAAS,OAAO;CAE/C,CAAC;AACH;;;;;;;;;;;;;;;AAyBA,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,kBAAkB,kBAAkB;CACpD,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,kBAAkB,UAAU,SAAS,GAAG,qBAAqB,aAAa,GAAG,MAAM;CACjG,QAAQ;EAIN,IAAI,QAAQ,SACV,MAAM,IAAI,kBAAkB,mCAAmC,EAAE,MAAM,wBAAwB,CAAC;EAIlG,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"}