From 494b00b92671fc52977479a76f20c5d2fc2e3ea8 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 21 Oct 2025 09:46:19 -0400 Subject: [PATCH] Refactor auth login providers --- js/services/authProviders/index.js | 31 ++++++ js/services/authProviders/nip07.js | 36 +++++++ js/services/authService.js | 160 ++++++++++++++++++++++++----- js/state/cache.js | 10 +- js/ui/profileModalController.js | 30 +++++- 5 files changed, 240 insertions(+), 27 deletions(-) create mode 100644 js/services/authProviders/index.js create mode 100644 js/services/authProviders/nip07.js diff --git a/js/services/authProviders/index.js b/js/services/authProviders/index.js new file mode 100644 index 00000000..5636cf40 --- /dev/null +++ b/js/services/authProviders/index.js @@ -0,0 +1,31 @@ +import nip07Provider from "./nip07.js"; + +const providers = [nip07Provider].filter( + (provider) => provider && typeof provider.id === "string", +); + +const providersById = providers.reduce((acc, provider) => { + const key = provider.id; + if (!acc[key]) { + acc[key] = provider; + } + return acc; +}, Object.create(null)); + +export function getAuthProvider(providerId) { + if (typeof providerId !== "string") { + return null; + } + const trimmed = providerId.trim(); + if (!trimmed) { + return null; + } + return providersById[trimmed] || null; +} + +export function listAuthProviders() { + return providers.slice(); +} + +export { providersById }; +export default providers; diff --git a/js/services/authProviders/nip07.js b/js/services/authProviders/nip07.js new file mode 100644 index 00000000..8fd16bbd --- /dev/null +++ b/js/services/authProviders/nip07.js @@ -0,0 +1,36 @@ +const PROVIDER_ID = "nip07"; +const PROVIDER_LABEL = "NIP-07 browser extension"; + +async function login({ nostrClient, options } = {}) { + if (!nostrClient || typeof nostrClient.login !== "function") { + throw new Error("NIP-07 login is not available."); + } + + const normalizedOptions = + options && typeof options === "object" ? options : {}; + const result = await nostrClient.login(normalizedOptions); + + let pubkey = ""; + let signer = null; + if (typeof result === "string") { + pubkey = result; + } else if (result && typeof result === "object") { + if (typeof result.pubkey === "string") { + pubkey = result.pubkey; + } else if (typeof result.publicKey === "string") { + pubkey = result.publicKey; + } + + if (result.signer !== undefined) { + signer = result.signer; + } + } + + return { authType: PROVIDER_ID, pubkey, signer }; +} + +export default Object.freeze({ + id: PROVIDER_ID, + label: PROVIDER_LABEL, + login, +}); diff --git a/js/services/authService.js b/js/services/authService.js index 76b5a2e1..5c37ff29 100644 --- a/js/services/authService.js +++ b/js/services/authService.js @@ -19,6 +19,7 @@ import { getProfileCacheEntry as getCachedProfileEntry, setProfileCacheEntry as setCachedProfileEntry, } from "../state/cache.js"; +import { getAuthProvider } from "./authProviders/index.js"; class SimpleEventEmitter { constructor(logger = null) { @@ -163,12 +164,42 @@ export default class AuthService { return Array.isArray(saved) ? saved.map((entry) => ({ ...entry })) : []; } - emitProfileList(reason = "update") { - this.emit("profile:updated", { + emitProfileList(reason = "update", extraDetail = null) { + const savedProfiles = this.cloneSavedProfiles(); + const activeProfilePubkey = getActiveProfilePubkey(); + const detail = { reason, - savedProfiles: this.cloneSavedProfiles(), - activeProfilePubkey: getActiveProfilePubkey(), - }); + savedProfiles, + activeProfilePubkey, + }; + + if (extraDetail && typeof extraDetail === "object") { + Object.assign(detail, extraDetail); + } + + if (!("activeProfileAuthType" in detail)) { + const normalizedActive = this.normalizeHexPubkey(activeProfilePubkey); + if (normalizedActive) { + const activeEntry = savedProfiles.find((entry) => { + const normalizedEntry = this.normalizeHexPubkey(entry?.pubkey); + return normalizedEntry && normalizedEntry === normalizedActive; + }); + + if ( + activeEntry && + typeof activeEntry.authType === "string" && + activeEntry.authType.trim() + ) { + detail.activeProfileAuthType = activeEntry.authType; + } else { + detail.activeProfileAuthType = null; + } + } else { + detail.activeProfileAuthType = null; + } + } + + this.emit("profile:updated", detail); } safeEncodeNpub(pubkey) { @@ -239,32 +270,72 @@ export default class AuthService { } async requestLogin(options = {}) { - if (!this.nostrClient || typeof this.nostrClient.login !== "function") { - throw new Error("Nostr login is not available."); + const normalizedOptions = + options && typeof options === "object" ? { ...options } : {}; + const { + providerId: rawProviderId, + autoApply, + persistActive: persistActiveOption, + ...providerOptions + } = normalizedOptions; + + const providerId = + typeof rawProviderId === "string" && rawProviderId.trim() + ? rawProviderId.trim() + : "nip07"; + + const provider = getAuthProvider(providerId); + if (!provider || typeof provider.login !== "function") { + throw new Error(`Unknown auth provider: ${providerId}`); } - const result = await this.nostrClient.login(options); - const pubkey = - typeof result === "string" - ? result - : result && typeof result === "object" - ? result.pubkey || result.publicKey || "" - : ""; + const providerResult = await provider.login({ + nostrClient: this.nostrClient, + options: providerOptions, + }); + + let pubkey = ""; + let signer = null; + if (typeof providerResult === "string") { + pubkey = providerResult; + } else if (providerResult && typeof providerResult === "object") { + if (typeof providerResult.pubkey === "string") { + pubkey = providerResult.pubkey; + } else if (typeof providerResult.publicKey === "string") { + pubkey = providerResult.publicKey; + } + + if (providerResult.signer !== undefined) { + signer = providerResult.signer; + } + } const trimmed = typeof pubkey === "string" ? pubkey.trim() : ""; + + const authType = + providerResult && + typeof providerResult === "object" && + typeof providerResult.authType === "string" && + providerResult.authType.trim() + ? providerResult.authType.trim() + : provider.id; + if (!trimmed) { - return { pubkey: null }; + return { pubkey: null, authType, signer, providerId }; } - if (options?.autoApply === false) { - return { pubkey: trimmed }; + if (autoApply === false) { + return { pubkey: trimmed, authType, signer, providerId }; } const detail = await this.login(trimmed, { - persistActive: options?.persistActive !== false, + persistActive: persistActiveOption !== false, + authType, + signer, + providerId, }); - return { pubkey: trimmed, detail }; + return { pubkey: trimmed, detail, authType, signer, providerId }; } async handleUploadSubmit(payload, { publish } = {}) { @@ -292,6 +363,8 @@ export default class AuthService { const persistActive = normalizedOptions.persistActive === false ? false : true; + const existingProfiles = getSavedProfiles(); + const previousPubkey = this.normalizeHexPubkey(getPubkey()) || getPubkey(); const normalized = this.normalizeHexPubkey(pubkey); const trimmed = typeof pubkey === "string" ? pubkey.trim() : ""; @@ -300,6 +373,42 @@ export default class AuthService { throw new Error("A valid pubkey is required for login."); } + const normalizedAuthType = (() => { + if ( + typeof normalizedOptions.authType === "string" && + normalizedOptions.authType.trim() + ) { + return normalizedOptions.authType.trim(); + } + + if (Array.isArray(existingProfiles)) { + const normalizedNext = normalized || this.normalizeHexPubkey(trimmed); + if (normalizedNext) { + const existingEntry = existingProfiles.find((entry) => { + const normalizedEntry = this.normalizeHexPubkey(entry?.pubkey); + return normalizedEntry && normalizedEntry === normalizedNext; + }); + if ( + existingEntry && + typeof existingEntry.authType === "string" && + existingEntry.authType.trim() + ) { + return existingEntry.authType.trim(); + } + } + } + + return "nip07"; + })(); + + const normalizedSigner = + normalizedOptions.signer !== undefined ? normalizedOptions.signer : null; + const providerId = + typeof normalizedOptions.providerId === "string" && + normalizedOptions.providerId.trim() + ? normalizedOptions.providerId.trim() + : null; + const identityChanged = previousPubkey !== nextPubkey; if (normalized) { @@ -333,7 +442,7 @@ export default class AuthService { npub: npub || (cachedProfile.npub ?? null), name: cachedProfile.name || draft[existingIndex]?.name || "", picture: cachedProfile.picture || draft[existingIndex]?.picture || "", - authType: "nip07", + authType: normalizedAuthType, }; if (existingIndex >= 0) { @@ -365,9 +474,11 @@ export default class AuthService { persistSavedProfiles({ persistActive: true }); } - if (savedProfilesMutated) { - this.emitProfileList("login"); - } + this.emitProfileList("login", { + authType: normalizedAuthType, + signer: normalizedSigner, + providerId, + }); const activeLoginPubkey = getPubkey(); const normalizedLoginPubkey = @@ -380,6 +491,9 @@ export default class AuthService { identityChanged, savedProfiles: this.cloneSavedProfiles(), activeProfilePubkey: getActiveProfilePubkey(), + authType: normalizedAuthType, + signer: normalizedSigner, + providerId, postLogin: { pubkey: normalizedLoginPubkey, blocksLoaded: false, diff --git a/js/state/cache.js b/js/state/cache.js index 385083a7..495ca93e 100644 --- a/js/state/cache.js +++ b/js/state/cache.js @@ -267,7 +267,10 @@ export function persistSavedProfiles({ persistActive = true } = {}) { typeof entry.npub === "string" && entry.npub.trim() ? entry.npub.trim() : null, name: typeof entry.name === "string" ? entry.name : "", picture: typeof entry.picture === "string" ? entry.picture : "", - authType: entry.authType === "nsec" ? "nsec" : "nip07", + authType: + typeof entry.authType === "string" && entry.authType.trim() + ? entry.authType.trim() + : "nip07", })), activePubkey: activePubkeyToPersist, }; @@ -306,7 +309,10 @@ export function loadSavedProfilesFromStorage() { } seenPubkeys.add(normalizedPubkey); - const storedAuthType = candidate.authType === "nsec" ? "nsec" : "nip07"; + const storedAuthType = + typeof candidate.authType === "string" && candidate.authType.trim() + ? candidate.authType.trim() + : "nip07"; const npub = typeof candidate.npub === "string" && candidate.npub.trim() ? candidate.npub.trim() diff --git a/js/ui/profileModalController.js b/js/ui/profileModalController.js index 0566d61d..ae917714 100644 --- a/js/ui/profileModalController.js +++ b/js/ui/profileModalController.js @@ -5,6 +5,7 @@ import { BITVID_WEBSITE_URL as CONFIG_BITVID_WEBSITE_URL, MAX_WALLET_DEFAULT_ZAP as CONFIG_MAX_WALLET_DEFAULT_ZAP, } from "../config.js"; +import { getAuthProvider } from "../services/authProviders/index.js"; const noop = () => {}; @@ -15,6 +16,32 @@ const DEFAULT_BITVID_WEBSITE_URL = "https://bitvid.network/"; const NWC_URI_SCHEME = "nostr+walletconnect://"; const DEFAULT_MAX_WALLET_DEFAULT_ZAP = 100000000; +function resolveAuthTypeLabel(authType) { + if (typeof authType !== "string") { + return "Saved profile"; + } + + const trimmed = authType.trim(); + if (!trimmed) { + return "Saved profile"; + } + + const provider = getAuthProvider(trimmed); + if (provider && typeof provider.label === "string" && provider.label) { + return provider.label; + } + + if (trimmed === "nsec") { + return "Direct key"; + } + + if (trimmed === "nip07") { + return "Saved profile"; + } + + return trimmed; +} + const DEFAULT_INTERNAL_NWC_SETTINGS = Object.freeze({ nwcUri: "", defaultZap: null, @@ -1306,8 +1333,7 @@ export class ProfileModalController { const label = document.createElement("span"); label.className = "profile-card__label"; - label.textContent = - entry.authType === "nsec" ? "Direct key" : "Saved profile"; + label.textContent = resolveAuthTypeLabel(entry.authType); const action = document.createElement("span"); action.className = "profile-card__action";