UNPKG

@authduo/authduo

Version:

Free User-sovereign Authentication for the World

108 lines (91 loc) 2.54 kB
import {pubsub, signal} from "@benev/slate" import {AuthFile} from "./types.js" import {Login} from "./utils/login.js" import {openPopup} from "./utils/open-popup.js" import {LoginTokens} from "../auth/tokens/types.js" import {nullcatch} from "../auth/utils/nullcatch.js" import {JsonStorage} from "../tools/json-storage.js" import {setupInApp} from "../manager/fed-api/setup-in-app.js" export class Auth { static defaultUrl = "https://authduo.org/" static version = 1 static #auth: Auth | null = null static get() { if (!this.#auth) this.#auth = new this() return this.#auth } onChange = pubsub<[Login | null]>() #fileStorage = new JsonStorage<AuthFile>("authduo") #login = signal<Login | null>(null) constructor() { this.#fileStorage.onChangeFromOutside(() => void this.load()) this.#login.on(login => this.onChange.publish(login)) this.load() } get authfile(): AuthFile { const authfile = this.#fileStorage.get() return (authfile && "version" in authfile && authfile.version === Auth.version) ? authfile : {version: Auth.version, tokens: null} } async load() { const {tokens} = this.authfile this.#login.value = tokens && await nullcatch( async() => Login.verify(tokens, {allowedAudiences: [window.origin]}) ) return this.#login.value } save(tokens: LoginTokens | null) { const {authfile} = this authfile.tokens = tokens this.#fileStorage.set(authfile) } get login() { const login = this.#login.value const valid = login && (Date.now() < login.expiresAt) if (!valid && login) this.#login.value = null return this.#login.value } set login(login: Login | null) { this.#login.value = login this.save(login && login.tokens) } async popup(url = Auth.defaultUrl) { const appWindow = window const popupWindow = openPopup(url) if (!popupWindow) return null const appOrigin = window.origin const popupOrigin = new URL(url, window.location.href).origin return new Promise<Login | null>((resolve, reject) => { const {dispose} = setupInApp( appWindow, popupWindow, popupOrigin, async loginTokens => { popupWindow.close() try { this.login = await nullcatch( async() => Login.verify(loginTokens, { allowedIssuers: [popupOrigin], allowedAudiences: [appOrigin], }) ) dispose() resolve(this.login) } catch (err) { dispose() reject(err) } }, ) popupWindow.onclose = () => { dispose() resolve(this.login) } }) } }