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
JavaScript
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();
};