oficina / home Public

Clone
git clone https://amadan.net/oficina/home

Plain git — no account needed to clone.

Download

Download this file

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.
11import { register, authenticate, available } from "/static/webauthn.mjs";
12
13async 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
23function 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.
34function reveal() {
35 if (!available()) return;
36 for (const el of document.querySelectorAll("[data-passkey]")) el.hidden = false;
37}
38
39async 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
60async 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
81reveal();
82document.getElementById("passkey-enrol")?.addEventListener("click", (e) => enrol(e.currentTarget));
83document.getElementById("passkey-confirm")?.addEventListener("click", (e) => confirm(e.currentTarget));
84