{
  "version": 3,
  "sources": ["passkey.js"],
  "sourcesContent": ["/*\n * static/js/passkey.js \u2014 WebAuthn / passkey browser-side logic.\n *\n * Exposes window.Passkey with three entry points:\n *\n *   startConditionalSignIn(authRequestUrl, authResponseUrl)\n *     Feature-detects conditional mediation, fires navigator.credentials.get()\n *     with mediation:\"conditional\" so passkeys appear inline in the email\n *     field autofill dropdown.  Silently no-ops if the browser does not\n *     support conditional UI, if a conditional ceremony is already pending,\n *     or if an explicit ceremony (see signInWithPasskey) is in flight \u2014 the\n *     two ceremonies share a session challenge and must never overlap.\n *\n *   abortConditionalSignIn()\n *     Cancels a pending conditional sign-in (e.g. when the user starts typing\n *     an email address to use the magic-link flow instead, or submits the\n *     form).\n *\n *   registerPasskey(regRequestUrl, regResponseUrl)\n *     Runs the full passkey registration ceremony and POSTs the credential\n *     to the server.  Dispatches custom events on document:\n *       passkey:registered  \u2014 {passkey: {uuid, name, device_type}}\n *       passkey:cancelled   \u2014 user dismissed the browser prompt\n *       passkey:unsupported \u2014 browser does not support WebAuthn\n *       passkey:error       \u2014 {message}\n *\n * CSRF: all POST requests include the X-CSRFToken header, read from the\n * standard Django csrftoken cookie.\n *\n * Signal API: if the server returns 404 for an auth response, and the browser\n * supports signalUnknownCredential, this script notifies the passkey provider\n * so it can remove the stale credential from autofill.\n */\n\n(function () {\n  'use strict';\n\n  /** @type {AbortController|null} Non-null while a conditional ceremony is pending/active. */\n  let _conditionalController = null;\n\n  /** @type {boolean} True from an explicit ceremony's options-fetch through settle. */\n  let _explicitInFlight = false;\n\n  // ---------------------------------------------------------------------------\n  // Public API\n  // ---------------------------------------------------------------------------\n\n  /**\n   * Start WebAuthn conditional UI (passkey autofill) on the sign-in page.\n   *\n   * Fetches authentication options, then calls navigator.credentials.get()\n   * with mediation:\"conditional\".  The browser surfaces matching passkeys\n   * inline inside the email input's autofill dropdown without a modal.\n   *\n   * On success the user is redirected to the destination the server returned\n   * (the sign-in page's `next`, SNOW-825), falling back to /account/.\n   *\n   * No-ops if the browser does not support conditional UI, if a conditional\n   * ceremony is already pending, or if an explicit ceremony is in flight \u2014\n   * conditional and explicit ceremonies share a session challenge and must\n   * never overlap.\n   *\n   * @param {string} authRequestUrl  URL to GET authentication options from.\n   * @param {string} authResponseUrl URL to POST the credential to.\n   */\n  async function startConditionalSignIn(authRequestUrl, authResponseUrl) {\n    if (!_supportsConditionalUI()) return;\n    if (_conditionalController || _explicitInFlight) return;\n\n    // Created eagerly, before any await, so abortConditionalSignIn() can\n    // cancel this ceremony even mid-setup (e.g. while awaiting availability\n    // or the options fetch).\n    const controller = new AbortController();\n    _conditionalController = controller;\n\n    const available = await PublicKeyCredential.isConditionalMediationAvailable().catch(\n      () => false\n    );\n    if (!available || controller.signal.aborted) {\n      _finishConditional(controller);\n      return;\n    }\n\n    let options;\n    try {\n      const resp = await fetch(authRequestUrl, { signal: controller.signal });\n      if (!resp.ok) {\n        _finishConditional(controller);\n        return;\n      }\n      options = await resp.json();\n    } catch {\n      _finishConditional(controller);\n      return;\n    }\n\n    // The abort may have landed while the fetch was in flight, or this\n    // ceremony may have been superseded by a newer one \u2014 either way the\n    // session challenge may have moved on, so bail without proceeding.\n    if (controller.signal.aborted || _conditionalController !== controller) {\n      _finishConditional(controller);\n      return;\n    }\n\n    let parsed;\n    try {\n      parsed = PublicKeyCredential.parseRequestOptionsFromJSON(options);\n    } catch {\n      _finishConditional(controller);\n      return;\n    }\n\n    let credential;\n    try {\n      credential = await navigator.credentials.get({\n        publicKey: parsed,\n        mediation: 'conditional',\n        signal: controller.signal,\n      });\n    } catch (err) {\n      if (err.name === 'AbortError' || err.name === 'NotAllowedError') {\n        _finishConditional(controller);\n        return;\n      }\n      console.error('[passkey] conditional sign-in error:', err);\n      _finishConditional(controller);\n      return;\n    }\n\n    _finishConditional(controller);\n    await _sendAuthResponse(authResponseUrl, credential);\n  }\n\n  /**\n   * Abort a pending conditional sign-in ceremony.\n   *\n   * Call this when the user switches to the magic-link flow (e.g. starts\n   * typing in the email field), submits the form, or starts an explicit\n   * passkey ceremony, so the conditional prompt is dismissed.\n   */\n  function abortConditionalSignIn() {\n    if (_conditionalController) {\n      _conditionalController.abort();\n      _conditionalController = null;\n    }\n  }\n\n  /**\n   * Clear the module-level conditional controller if it still refers to\n   * the ceremony that is finishing \u2014 never clobbers a newer controller\n   * that may have been installed since (e.g. by a fresh\n   * startConditionalSignIn() call).\n   *\n   * @param {AbortController} controller\n   */\n  function _finishConditional(controller) {\n    if (_conditionalController === controller) {\n      _conditionalController = null;\n    }\n  }\n\n  /**\n   * Explicitly sign in with a passkey by showing the browser's passkey picker.\n   *\n   * Unlike startConditionalSignIn, this triggers an immediate modal prompt so\n   * the user can pick a passkey to use.  Intended for an explicit \"Sign in\n   * with a passkey\" button rather than autofill.\n   *\n   * Aborts any pending conditional sign-in first (the two flows share a session\n   * challenge \u2014 only one can be active at a time), and no-ops if another\n   * explicit ceremony is already in flight.\n   *\n   * @param {string} authRequestUrl  URL to GET authentication options from.\n   * @param {string} authResponseUrl URL to POST the credential to.\n   */\n  async function signInWithPasskey(authRequestUrl, authResponseUrl) {\n    if (!window.PublicKeyCredential) return;\n    if (_explicitInFlight) return;\n\n    abortConditionalSignIn();\n\n    _explicitInFlight = true;\n    try {\n      let options;\n      try {\n        const resp = await fetch(authRequestUrl);\n        if (!resp.ok) return;\n        options = await resp.json();\n      } catch {\n        return;\n      }\n\n      let parsed;\n      try {\n        parsed = PublicKeyCredential.parseRequestOptionsFromJSON(options);\n      } catch {\n        return;\n      }\n\n      let credential;\n      try {\n        credential = await navigator.credentials.get({\n          publicKey: parsed,\n          mediation: 'required',\n        });\n      } catch (err) {\n        if (err.name === 'NotAllowedError' || err.name === 'AbortError') return;\n        console.error('[passkey] sign-in error:', err);\n        return;\n      }\n\n      if (credential) {\n        await _sendAuthResponse(authResponseUrl, credential);\n      }\n    } finally {\n      _explicitInFlight = false;\n    }\n  }\n\n  /**\n   * Register a new passkey for the currently signed-in subscriber.\n   *\n   * Fetches creation options from the server, calls navigator.credentials.create(),\n   * and POSTs the result back.  Dispatches document-level custom events to\n   * allow the page to react (see module-level JSDoc for event names).\n   *\n   * @param {string} regRequestUrl  URL to GET registration options from.\n   * @param {string} regResponseUrl URL to POST the new credential to.\n   */\n  async function registerPasskey(regRequestUrl, regResponseUrl) {\n    if (!window.PublicKeyCredential) {\n      _dispatch('passkey:unsupported');\n      return;\n    }\n\n    let options;\n    try {\n      const resp = await fetch(regRequestUrl);\n      if (!resp.ok) {\n        _dispatch('passkey:error', { message: 'Failed to fetch registration options.' });\n        return;\n      }\n      options = await resp.json();\n    } catch (err) {\n      _dispatch('passkey:error', { message: err.message });\n      return;\n    }\n\n    let parsed;\n    try {\n      parsed = PublicKeyCredential.parseCreationOptionsFromJSON(options);\n    } catch (err) {\n      _dispatch('passkey:error', { message: 'Could not parse registration options.' });\n      return;\n    }\n\n    let credential;\n    try {\n      credential = await navigator.credentials.create({ publicKey: parsed });\n    } catch (err) {\n      if (err.name === 'NotAllowedError') {\n        _dispatch('passkey:cancelled');\n        return;\n      }\n      _dispatch('passkey:error', { message: err.message });\n      return;\n    }\n\n    let result;\n    try {\n      const resp = await fetch(regResponseUrl, {\n        method: 'POST',\n        headers: { ..._csrfHeaders(), 'Content-Type': 'application/json' },\n        credentials: 'same-origin',\n        body: JSON.stringify(credential.toJSON()),\n      });\n      result = await resp.json();\n      if (!resp.ok) {\n        _dispatch('passkey:error', { message: result.error || 'Registration failed.' });\n        return;\n      }\n    } catch (err) {\n      _dispatch('passkey:error', { message: err.message });\n      return;\n    }\n\n    _dispatch('passkey:registered', result);\n  }\n\n  // ---------------------------------------------------------------------------\n  // Internal helpers\n  // ---------------------------------------------------------------------------\n\n  /**\n   * Read the sign-in page's post-sign-in destination, if it has one.\n   *\n   * The sign-in form carries `data-next` when the page was reached with a\n   * `?next=` the server accepted (SNOW-825). It is read from the DOM rather\n   * than passed through the ceremony's arguments so both entry points \u2014 the\n   * conditional autofill ceremony and the explicit button \u2014 get it without\n   * either caller having to know about it.\n   *\n   * @returns {string} The destination, or '' when there is none.\n   */\n  function _nextDestination() {\n    const form = document.querySelector('form[data-next]');\n    return (form && form.getAttribute('data-next')) || '';\n  }\n\n  /**\n   * POST an authentication credential to the server and redirect on success.\n   *\n   * The body is the credential plus, when the page has one, the `next`\n   * destination. The server re-validates `next` and echoes back either a\n   * same-site URL or null, so the navigation below can never be pointed off\n   * this origin by the value this function sent.\n   *\n   * When the server returns 404 (unknown credential), calls\n   * PublicKeyCredential.signalUnknownCredential() if available so the passkey\n   * provider can remove the stale entry.\n   *\n   * @param {string}             authResponseUrl\n   * @param {PublicKeyCredential} credential\n   */\n  async function _sendAuthResponse(authResponseUrl, credential) {\n    let resp, data;\n    const payload = credential.toJSON();\n    const next = _nextDestination();\n    if (next) payload.next = next;\n    try {\n      resp = await fetch(authResponseUrl, {\n        method: 'POST',\n        headers: { ..._csrfHeaders(), 'Content-Type': 'application/json' },\n        credentials: 'same-origin',\n        body: JSON.stringify(payload),\n      });\n      data = await resp.json();\n    } catch (err) {\n      console.error('[passkey] auth response error:', err);\n      _dispatch('passkey:auth-error', { message: err.message });\n      return;\n    }\n\n    if (resp.ok && data.ok) {\n      window.location.href = data.next || '/account/';\n      return;\n    }\n\n    if (resp.status === 404 && data.credentialId) {\n      if (typeof PublicKeyCredential.signalUnknownCredential === 'function') {\n        try {\n          await PublicKeyCredential.signalUnknownCredential({\n            rpId: window.location.hostname,\n            credentialId: data.credentialId,\n          });\n        } catch {\n          /* progressive enhancement \u2014 ignore failures */\n        }\n      }\n      _dispatch('passkey:auth-unknown-credential', { credentialId: data.credentialId });\n      return;\n    }\n\n    _dispatch('passkey:auth-error', { message: data.error || 'Authentication failed.' });\n  }\n\n  /**\n   * Return true if the browser supports conditional mediation.\n   *\n   * @returns {boolean}\n   */\n  function _supportsConditionalUI() {\n    return (\n      typeof window.PublicKeyCredential !== 'undefined' &&\n      typeof PublicKeyCredential.isConditionalMediationAvailable === 'function'\n    );\n  }\n\n  /**\n   * Return an object with the X-CSRFToken header if a CSRF token is present.\n   *\n   * @returns {Record<string, string>}\n   */\n  function _csrfHeaders() {\n    const token = _getCsrfToken();\n    return token ? { 'X-CSRFToken': token } : {};\n  }\n\n  /**\n   * Read the Django CSRF token from the csrftoken cookie.\n   *\n   * @returns {string}\n   */\n  function _getCsrfToken() {\n    const match = document.cookie.match(/(?:^|;\\s*)csrftoken=([^;]+)/);\n    return match ? match[1] : '';\n  }\n\n  /**\n   * Dispatch a CustomEvent on document.\n   *\n   * @param {string} name\n   * @param {object} [detail]\n   */\n  function _dispatch(name, detail) {\n    document.dispatchEvent(new CustomEvent(name, { detail: detail || {}, bubbles: true }));\n  }\n\n  // ---------------------------------------------------------------------------\n  // Exports\n  // ---------------------------------------------------------------------------\n\n  window.Passkey = {\n    startConditionalSignIn,\n    abortConditionalSignIn,\n    signInWithPasskey,\n    registerPasskey,\n  };\n})();\n"],
  "mappings": "CAkCC,UAAY,CACX,aAGA,IAAIA,EAAyB,KAGzBC,EAAoB,GAwBxB,eAAeC,EAAuBC,EAAgBC,EAAiB,CAErE,GADI,CAACC,EAAuB,GACxBL,GAA0BC,EAAmB,OAKjD,MAAMK,EAAa,IAAI,gBAMvB,GALAN,EAAyBM,EAKrB,CAHc,MAAM,oBAAoB,gCAAgC,EAAE,MAC5E,IAAM,EACR,GACkBA,EAAW,OAAO,QAAS,CAC3CC,EAAmBD,CAAU,EAC7B,MACF,CAEA,IAAIE,EACJ,GAAI,CACF,MAAMC,EAAO,MAAM,MAAMN,EAAgB,CAAE,OAAQG,EAAW,MAAO,CAAC,EACtE,GAAI,CAACG,EAAK,GAAI,CACZF,EAAmBD,CAAU,EAC7B,MACF,CACAE,EAAU,MAAMC,EAAK,KAAK,CAC5B,MAAQ,CACNF,EAAmBD,CAAU,EAC7B,MACF,CAKA,GAAIA,EAAW,OAAO,SAAWN,IAA2BM,EAAY,CACtEC,EAAmBD,CAAU,EAC7B,MACF,CAEA,IAAII,EACJ,GAAI,CACFA,EAAS,oBAAoB,4BAA4BF,CAAO,CAClE,MAAQ,CACND,EAAmBD,CAAU,EAC7B,MACF,CAEA,IAAIK,EACJ,GAAI,CACFA,EAAa,MAAM,UAAU,YAAY,IAAI,CAC3C,UAAWD,EACX,UAAW,cACX,OAAQJ,EAAW,MACrB,CAAC,CACH,OAASM,EAAK,CACZ,GAAIA,EAAI,OAAS,cAAgBA,EAAI,OAAS,kBAAmB,CAC/DL,EAAmBD,CAAU,EAC7B,MACF,CACA,QAAQ,MAAM,uCAAwCM,CAAG,EACzDL,EAAmBD,CAAU,EAC7B,MACF,CAEAC,EAAmBD,CAAU,EAC7B,MAAMO,EAAkBT,EAAiBO,CAAU,CACrD,CASA,SAASG,GAAyB,CAC5Bd,IACFA,EAAuB,MAAM,EAC7BA,EAAyB,KAE7B,CAUA,SAASO,EAAmBD,EAAY,CAClCN,IAA2BM,IAC7BN,EAAyB,KAE7B,CAgBA,eAAee,EAAkBZ,EAAgBC,EAAiB,CAChE,GAAK,OAAO,qBACR,CAAAH,EAEJ,CAAAa,EAAuB,EAEvBb,EAAoB,GACpB,GAAI,CACF,IAAIO,EACJ,GAAI,CACF,MAAMC,EAAO,MAAM,MAAMN,CAAc,EACvC,GAAI,CAACM,EAAK,GAAI,OACdD,EAAU,MAAMC,EAAK,KAAK,CAC5B,MAAQ,CACN,MACF,CAEA,IAAIC,EACJ,GAAI,CACFA,EAAS,oBAAoB,4BAA4BF,CAAO,CAClE,MAAQ,CACN,MACF,CAEA,IAAIG,EACJ,GAAI,CACFA,EAAa,MAAM,UAAU,YAAY,IAAI,CAC3C,UAAWD,EACX,UAAW,UACb,CAAC,CACH,OAASE,EAAK,CACZ,GAAIA,EAAI,OAAS,mBAAqBA,EAAI,OAAS,aAAc,OACjE,QAAQ,MAAM,2BAA4BA,CAAG,EAC7C,MACF,CAEID,GACF,MAAME,EAAkBT,EAAiBO,CAAU,CAEvD,QAAE,CACAV,EAAoB,EACtB,EACF,CAYA,eAAee,EAAgBC,EAAeC,EAAgB,CAC5D,GAAI,CAAC,OAAO,oBAAqB,CAC/BC,EAAU,qBAAqB,EAC/B,MACF,CAEA,IAAIX,EACJ,GAAI,CACF,MAAMC,EAAO,MAAM,MAAMQ,CAAa,EACtC,GAAI,CAACR,EAAK,GAAI,CACZU,EAAU,gBAAiB,CAAE,QAAS,uCAAwC,CAAC,EAC/E,MACF,CACAX,EAAU,MAAMC,EAAK,KAAK,CAC5B,OAASG,EAAK,CACZO,EAAU,gBAAiB,CAAE,QAASP,EAAI,OAAQ,CAAC,EACnD,MACF,CAEA,IAAIF,EACJ,GAAI,CACFA,EAAS,oBAAoB,6BAA6BF,CAAO,CACnE,MAAc,CACZW,EAAU,gBAAiB,CAAE,QAAS,uCAAwC,CAAC,EAC/E,MACF,CAEA,IAAIR,EACJ,GAAI,CACFA,EAAa,MAAM,UAAU,YAAY,OAAO,CAAE,UAAWD,CAAO,CAAC,CACvE,OAASE,EAAK,CACZ,GAAIA,EAAI,OAAS,kBAAmB,CAClCO,EAAU,mBAAmB,EAC7B,MACF,CACAA,EAAU,gBAAiB,CAAE,QAASP,EAAI,OAAQ,CAAC,EACnD,MACF,CAEA,IAAIQ,EACJ,GAAI,CACF,MAAMX,EAAO,MAAM,MAAMS,EAAgB,CACvC,OAAQ,OACR,QAAS,CAAE,GAAGG,EAAa,EAAG,eAAgB,kBAAmB,EACjE,YAAa,cACb,KAAM,KAAK,UAAUV,EAAW,OAAO,CAAC,CAC1C,CAAC,EAED,GADAS,EAAS,MAAMX,EAAK,KAAK,EACrB,CAACA,EAAK,GAAI,CACZU,EAAU,gBAAiB,CAAE,QAASC,EAAO,OAAS,sBAAuB,CAAC,EAC9E,MACF,CACF,OAASR,EAAK,CACZO,EAAU,gBAAiB,CAAE,QAASP,EAAI,OAAQ,CAAC,EACnD,MACF,CAEAO,EAAU,qBAAsBC,CAAM,CACxC,CAiBA,SAASE,GAAmB,CAC1B,MAAMC,EAAO,SAAS,cAAc,iBAAiB,EACrD,OAAQA,GAAQA,EAAK,aAAa,WAAW,GAAM,EACrD,CAiBA,eAAeV,EAAkBT,EAAiBO,EAAY,CAC5D,IAAIF,EAAMe,EACV,MAAMC,EAAUd,EAAW,OAAO,EAC5Be,EAAOJ,EAAiB,EAC1BI,IAAMD,EAAQ,KAAOC,GACzB,GAAI,CACFjB,EAAO,MAAM,MAAML,EAAiB,CAClC,OAAQ,OACR,QAAS,CAAE,GAAGiB,EAAa,EAAG,eAAgB,kBAAmB,EACjE,YAAa,cACb,KAAM,KAAK,UAAUI,CAAO,CAC9B,CAAC,EACDD,EAAO,MAAMf,EAAK,KAAK,CACzB,OAASG,EAAK,CACZ,QAAQ,MAAM,iCAAkCA,CAAG,EACnDO,EAAU,qBAAsB,CAAE,QAASP,EAAI,OAAQ,CAAC,EACxD,MACF,CAEA,GAAIH,EAAK,IAAMe,EAAK,GAAI,CACtB,OAAO,SAAS,KAAOA,EAAK,MAAQ,YACpC,MACF,CAEA,GAAIf,EAAK,SAAW,KAAOe,EAAK,aAAc,CAC5C,GAAI,OAAO,oBAAoB,yBAA4B,WACzD,GAAI,CACF,MAAM,oBAAoB,wBAAwB,CAChD,KAAM,OAAO,SAAS,SACtB,aAAcA,EAAK,YACrB,CAAC,CACH,MAAQ,CAER,CAEFL,EAAU,kCAAmC,CAAE,aAAcK,EAAK,YAAa,CAAC,EAChF,MACF,CAEAL,EAAU,qBAAsB,CAAE,QAASK,EAAK,OAAS,wBAAyB,CAAC,CACrF,CAOA,SAASnB,GAAyB,CAChC,OACE,OAAO,OAAO,oBAAwB,KACtC,OAAO,oBAAoB,iCAAoC,UAEnE,CAOA,SAASgB,GAAe,CACtB,MAAMM,EAAQC,EAAc,EAC5B,OAAOD,EAAQ,CAAE,cAAeA,CAAM,EAAI,CAAC,CAC7C,CAOA,SAASC,GAAgB,CACvB,MAAMC,EAAQ,SAAS,OAAO,MAAM,6BAA6B,EACjE,OAAOA,EAAQA,EAAM,CAAC,EAAI,EAC5B,CAQA,SAASV,EAAUW,EAAMC,EAAQ,CAC/B,SAAS,cAAc,IAAI,YAAYD,EAAM,CAAE,OAAQC,GAAU,CAAC,EAAG,QAAS,EAAK,CAAC,CAAC,CACvF,CAMA,OAAO,QAAU,CACf,uBAAA7B,EACA,uBAAAY,EACA,kBAAAC,EACA,gBAAAC,CACF,CACF,GAAG",
  "names": ["_conditionalController", "_explicitInFlight", "startConditionalSignIn", "authRequestUrl", "authResponseUrl", "_supportsConditionalUI", "controller", "_finishConditional", "options", "resp", "parsed", "credential", "err", "_sendAuthResponse", "abortConditionalSignIn", "signInWithPasskey", "registerPasskey", "regRequestUrl", "regResponseUrl", "_dispatch", "result", "_csrfHeaders", "_nextDestination", "form", "data", "payload", "next", "token", "_getCsrfToken", "match", "name", "detail"]
}
