UNPKG

next-auth-pubkey

Version:

A light-weight Lightning and Nostr auth provider for your Next.js app that's entirely self-hosted and plugs seamlessly into the next-auth framework.

176 lines (175 loc) 6.04 kB
export const vanilla = function ({ hardConfig, query, }) { let session; let createIntervalId; let errorUrl; const callbackController = new AbortController(); const createController = new AbortController(); // cleanup when the hook unmounts of polling is successful function cleanup() { clearInterval(createIntervalId); callbackController.abort(); createController.abort(); } // redirect user to error page if something goes wrong function error(e) { console.error(e); if (errorUrl) { window.location.replace(errorUrl); } } // callback the api to authenticate the user function callback(event) { if (!session || !session.k1) throw new Error("missing k1"); const k1 = session.k1; const params = new URLSearchParams({ event: JSON.stringify(event) }); return fetch(hardConfig.apis.callback, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: params, cache: "default", signal: callbackController.signal, }) .then(function (r) { return r.json(); }) .then(function (d) { if (d && d.success) { cleanup(); let url = new URL(query.redirect_uri); url.searchParams.append("state", query.state); url.searchParams.append("code", k1); window.location.replace(url); } }) .catch(function (e) { console.error(e); }); } // create a new k1 nd inject content into dom function create() { const params = new URLSearchParams({ state: query.state }); if (session && session.k1) { params.append("k1", session.k1); } return fetch(hardConfig.apis.create, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: params, cache: "default", signal: createController.signal, }) .then(function (r) { return r.json(); }) .then(function (d) { if (d && d.error) { if (d.url) errorUrl = d.url; throw new Error(d.message || d.error); } session = d; if (!session || !session.k1) return; // show wrapper const wrapper = document.getElementById(hardConfig.ids.wrapper); if (wrapper) { wrapper.style.display = "block"; } // hide loader const loading = document.getElementById(hardConfig.ids.loading); if (loading) { loading.style.display = "none"; } const button = document.getElementById(hardConfig.ids.button); if (button) { button.addEventListener("click", function () { if (session && session.k1) { clearError(); triggerExtension(session.k1); } }); } // trigger extension triggerExtension(session.k1); createIntervalId = setInterval(create, session.createInterval); }) .catch(function (e) { if (!createController.signal.aborted) { error(e); } }); } function setError(message, retry = true) { const error = document.getElementById(hardConfig.ids.error); if (error) { error.textContent = message; } const details = document.getElementById(hardConfig.ids.details); if (details) { details.style.display = "block"; } if (retry) { const button = document.getElementById(hardConfig.ids.button); if (button) { button.style.display = "flex"; } } } function clearError() { const error = document.getElementById(hardConfig.ids.error); if (error) { error.textContent = ""; } const details = document.getElementById(hardConfig.ids.details); if (details) { details.style.display = "none"; } const button = document.getElementById(hardConfig.ids.button); if (button) { button.style.display = "none"; } } function callWithTimeout(targetFunction, timeoutMs) { return new Promise((resolve, reject) => { Promise.race([ targetFunction(), new Promise((resolve, reject) => setTimeout(() => reject(new Error("timed out after " + timeoutMs + " ms waiting for extension")), timeoutMs)), ]) .then(resolve) .catch(reject); }); } // trigger the nostr extension / handle errors on failure function triggerExtension(k1) { if (!window.nostr) { setError("nostr extension not found", false); return; } return callWithTimeout(() => window.nostr.signEvent({ kind: 22242, created_at: Math.floor(Date.now() / 1000), tags: [["challenge", k1]], content: "Authentication", }), 5000) .then((event) => { if (event) { callback(event); } else { setError("extension returned empty event"); } }) .catch((e) => { const message = e instanceof Error ? e.message : "nostr extension failed to sign event"; if (message === "window.nostr call already executing") { return; } setError(message); }); } // setup intervals and create first qr code create(); };