| 1 | // The app's own glue. rastrillo's webauthn.mjs only exports register() |
| 2 | // and authenticate() and makes no network call of its own, so this file |
| 3 | // is what imports it, calls it, POSTs to /passkey/* and follows where |
| 4 | // the server says to go. |
| 5 | // |
| 6 | // It is a static, same-origin module, not an inline block: rastrillo's |
| 7 | // default CSP forbids inline script, and Home's scripts are a closed list |
| 8 | // of static files (spec ruling superseding 21/24: no script by default, |
| 9 | // enhance only, prefer none). Its path is stable so that list can name |
| 10 | // it — a rule that cannot name its subject is not a rule. |
| 11 | import { register, authenticate, available } from "/static/webauthn.mjs"; |
| 12 | |
| 13 | async function post(path, body) { |
| 14 | const res = await fetch(path, { |
| 15 | method: "POST", |
| 16 | headers: { "Content-Type": "application/json" }, |
| 17 | body: JSON.stringify(body || {}), |
| 18 | }); |
| 19 | if (!res.ok) throw new Error(path + ": " + res.status); |
| 20 | return res.json(); |
| 21 | } |
| 22 | |
| 23 | function say(el, message) { |
| 24 | const status = document.getElementById("passkey-status"); |
| 25 | if (status) status.textContent = message; |
| 26 | if (el) el.disabled = false; |
| 27 | } |
| 28 | |
| 29 | // Every button here starts hidden in the markup and is revealed only |
| 30 | // once script has run and the platform actually has WebAuthn. A person |
| 31 | // with script off, or on a device with no authenticator, never sees a |
| 32 | // control that cannot work — they see the recovery form, which needs |
| 33 | // neither. |
| 34 | function reveal() { |
| 35 | if (!available()) return; |
| 36 | for (const el of document.querySelectorAll("[data-passkey]")) el.hidden = false; |
| 37 | } |
| 38 | |
| 39 | async function enrol(button) { |
| 40 | button.disabled = true; |
| 41 | try { |
| 42 | const begin = await post("/passkey/register/begin"); |
| 43 | const made = await register({ |
| 44 | challenge: begin.challenge, |
| 45 | rpId: document.body.dataset.rpid, |
| 46 | rpName: "Oficina", |
| 47 | userId: document.body.dataset.userid, |
| 48 | userName: document.body.dataset.username, |
| 49 | }); |
| 50 | await post("/passkey/register/finish", { |
| 51 | clientDataJSON: made.clientDataJSON, |
| 52 | attestationObject: made.attestationObject, |
| 53 | }); |
| 54 | window.location.assign("/passkey/codes"); |
| 55 | } catch (err) { |
| 56 | say(button, "That did not work. You can try again."); |
| 57 | } |
| 58 | } |
| 59 | |
| 60 | async function confirm(button) { |
| 61 | button.disabled = true; |
| 62 | try { |
| 63 | const begin = await post("/passkey/signin/begin"); |
| 64 | const got = await authenticate({ |
| 65 | challenge: begin.challenge, |
| 66 | rpId: document.body.dataset.rpid, |
| 67 | legacyRpId: document.body.dataset.legacyrpid || undefined, |
| 68 | }); |
| 69 | const done = await post("/passkey/signin/finish", { |
| 70 | id: got.credentialId, |
| 71 | clientDataJSON: got.clientDataJSON, |
| 72 | authenticatorData: got.authenticatorData, |
| 73 | signature: got.signature, |
| 74 | }); |
| 75 | window.location.assign(done.to || "/"); |
| 76 | } catch (err) { |
| 77 | say(button, "That did not work. Use a recovery code below."); |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | reveal(); |
| 82 | document.getElementById("passkey-enrol")?.addEventListener("click", (e) => enrol(e.currentTarget)); |
| 83 | document.getElementById("passkey-confirm")?.addEventListener("click", (e) => confirm(e.currentTarget)); |
| 84 | |