From 62d411e3ff77f43b4dcfb4d178d282e699634f98 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 21 Oct 2025 13:41:57 -0400 Subject: [PATCH] Refactor auth service to use provider registry --- js/app.js | 5 + js/index.js | 5 + js/services/authProviders/index.js | 29 ++++++ js/services/authProviders/nip07.js | 46 +++++++++ js/services/authService.js | 160 ++++++++++++++++++++++++----- 5 files changed, 220 insertions(+), 25 deletions(-) create mode 100644 js/services/authProviders/index.js create mode 100644 js/services/authProviders/nip07.js diff --git a/js/app.js b/js/app.js index dafa855c..ebca329f 100644 --- a/js/app.js +++ b/js/app.js @@ -39,6 +39,9 @@ import watchHistoryService from "./watchHistoryService.js"; import r2Service from "./services/r2Service.js"; import PlaybackService from "./services/playbackService.js"; import AuthService from "./services/authService.js"; +import getAuthProvider, { + providers as authProviders, +} from "./services/authProviders/index.js"; import NwcSettingsService from "./services/nwcSettingsService.js"; import nostrService from "./services/nostrService.js"; import DiscussionCountService from "./services/discussionCountService.js"; @@ -355,6 +358,8 @@ class Application { relayManager, logger: devLogger, accessControl, + authProviders, + getAuthProvider, }); this.authEventUnsubscribes.push( this.authService.on("auth:login", (detail) => { diff --git a/js/index.js b/js/index.js index abbba126..0594640e 100644 --- a/js/index.js +++ b/js/index.js @@ -29,6 +29,9 @@ import { isLockdownMode, } from "./config.js"; import AuthService from "./services/authService.js"; +import getAuthProvider, { + providers as authProviders, +} from "./services/authProviders/index.js"; import { accessControl } from "./accessControl.js"; import { nostrClient } from "./nostr.js"; import { userBlocks } from "./userBlocks.js"; @@ -106,6 +109,8 @@ function getLockdownAuthService() { relayManager, logger: devLogger, accessControl, + authProviders, + getAuthProvider, }); try { diff --git a/js/services/authProviders/index.js b/js/services/authProviders/index.js new file mode 100644 index 00000000..490fb792 --- /dev/null +++ b/js/services/authProviders/index.js @@ -0,0 +1,29 @@ +import nip07 from "./nip07.js"; + +const map = { + [(nip07 && typeof nip07.id === "string" && nip07.id.trim()) || "nip07"]: nip07, +}; + +if (!Object.prototype.hasOwnProperty.call(map, "nip07")) { + map.nip07 = nip07; +} + +export const providers = Object.freeze({ ...map }); + +export function getProvider(providerId = "nip07") { + const id = + typeof providerId === "string" && providerId.trim() + ? providerId.trim() + : "nip07"; + + const provider = providers[id]; + if (!provider || typeof provider.login !== "function") { + const error = new Error(`Unknown auth provider: ${id}`); + error.code = "unknown-auth-provider"; + throw error; + } + + return provider; +} + +export default getProvider; diff --git a/js/services/authProviders/nip07.js b/js/services/authProviders/nip07.js new file mode 100644 index 00000000..31aa7529 --- /dev/null +++ b/js/services/authProviders/nip07.js @@ -0,0 +1,46 @@ +const PROVIDER_ID = "nip07"; + +function normalizePubkey(result) { + if (typeof result === "string") { + return result; + } + + if (!result || typeof result !== "object") { + return ""; + } + + if (typeof result.pubkey === "string") { + return result.pubkey; + } + + if (typeof result.publicKey === "string") { + return result.publicKey; + } + + return ""; +} + +export default { + id: PROVIDER_ID, + async login({ nostrClient, options } = {}) { + if (!nostrClient || typeof nostrClient.login !== "function") { + const error = new Error("Nostr login is not available."); + error.code = "provider-unavailable"; + throw error; + } + + const result = await nostrClient.login(options || {}); + const pubkey = normalizePubkey(result); + const signer = + result && typeof result === "object" && result.signer + ? result.signer + : null; + + return { + authType: PROVIDER_ID, + pubkey, + signer, + rawResult: result, + }; + }, +}; diff --git a/js/services/authService.js b/js/services/authService.js index 2a440d8d..95665338 100644 --- a/js/services/authService.js +++ b/js/services/authService.js @@ -21,6 +21,9 @@ import { getProfileCacheEntry as getCachedProfileEntry, setProfileCacheEntry as setCachedProfileEntry, } from "../state/cache.js"; +import getDefaultAuthProvider, { + providers as defaultAuthProviders, +} from "./authProviders/index.js"; class SimpleEventEmitter { constructor(logger = null) { @@ -85,11 +88,17 @@ export default class AuthService { relayManager, logger, accessControl, + authProviders, + getAuthProvider, } = {}) { this.nostrClient = nostrClient || null; this.userBlocks = userBlocks || null; this.relayManager = relayManager || null; this.accessControl = accessControl || null; + this.authProviders = + authProviders && typeof authProviders === "object" + ? authProviders + : defaultAuthProviders; if (typeof logger === "function") { this.logger = logger; @@ -106,6 +115,40 @@ export default class AuthService { userLogger.warn("[AuthService] logger threw", logError); } }); + + if (typeof getAuthProvider === "function") { + this.resolveAuthProvider = (providerId) => { + const normalizedId = this.normalizeProviderId(providerId); + const provider = getAuthProvider(normalizedId); + if (!provider || typeof provider.login !== "function") { + const error = new Error(`Unknown auth provider: ${normalizedId}`); + error.code = "unknown-auth-provider"; + throw error; + } + return provider; + }; + } else { + this.resolveAuthProvider = (providerId) => { + const normalizedId = this.normalizeProviderId(providerId); + const registry = this.authProviders || defaultAuthProviders; + const provider = + registry && typeof registry === "object" + ? registry[normalizedId] + : null; + + if (provider && typeof provider.login === "function") { + return provider; + } + + return getDefaultAuthProvider(normalizedId); + }; + } + } + + normalizeProviderId(providerId) { + return typeof providerId === "string" && providerId.trim() + ? providerId.trim() + : "nip07"; } log(...args) { @@ -252,42 +295,85 @@ export default class AuthService { return null; } - async requestLogin(options = {}) { - if (!this.nostrClient || typeof this.nostrClient.login !== "function") { - throw new Error("Nostr login is not available."); - } + async requestLogin(rawOptions = {}) { + const normalizedOptions = + rawOptions && typeof rawOptions === "object" ? { ...rawOptions } : {}; + const { + providerId: requestedProviderId = "nip07", + persistActive, + autoApply, + ...providerOptions + } = normalizedOptions; - const result = await this.nostrClient.login(options); - const permissionResult = await requestDefaultExtensionPermissions(); - if (!permissionResult.ok) { - const error = new Error( - "The NIP-07 extension denied the permission request required to finish logging in.", - ); - error.code = "extension-permission-denied"; - error.cause = permissionResult.error; - throw error; - } + const providerId = this.normalizeProviderId(requestedProviderId); + const provider = this.resolveAuthProvider(providerId); + const providerResult = await provider.login({ + nostrClient: this.nostrClient, + options: providerOptions, + }); + + const rawPubkey = + providerResult && typeof providerResult === "object" + ? providerResult.pubkey + : providerResult; const pubkey = - typeof result === "string" - ? result - : result && typeof result === "object" - ? result.pubkey || result.publicKey || "" + typeof rawPubkey === "string" + ? rawPubkey + : providerResult && typeof providerResult === "object" + ? providerResult.publicKey || "" : ""; + const authTypeCandidate = + providerResult && typeof providerResult === "object" + ? providerResult.authType + : null; + const authType = + typeof authTypeCandidate === "string" && authTypeCandidate.trim() + ? authTypeCandidate.trim() + : providerId; + const signer = + providerResult && typeof providerResult === "object" + ? providerResult.signer || null + : null; + const detailAuthType = + typeof authType === "string" && authType.trim() ? authType.trim() : "nip07"; + + if (authType === "nip07") { + const permissionResult = await requestDefaultExtensionPermissions(); + if (!permissionResult.ok) { + const error = new Error( + "The NIP-07 extension denied the permission request required to finish logging in.", + ); + error.code = "extension-permission-denied"; + error.cause = permissionResult.error; + throw error; + } + } + const trimmed = typeof pubkey === "string" ? pubkey.trim() : ""; if (!trimmed) { - return { pubkey: null }; + return { pubkey: null, providerId, authType: detailAuthType, signer }; } - if (options?.autoApply === false) { - return { pubkey: trimmed }; + if (autoApply === false) { + return { pubkey: trimmed, providerId, authType: detailAuthType, signer }; } const detail = await this.login(trimmed, { - persistActive: options?.persistActive !== false, + persistActive: persistActive !== false, + providerId, + authType: detailAuthType, + signer, + providerResult, }); - return { pubkey: trimmed, detail }; + return { + pubkey: trimmed, + detail, + providerId, + authType: detailAuthType, + signer, + }; } async handleUploadSubmit(payload, { publish } = {}) { @@ -311,9 +397,24 @@ export default class AuthService { async login(pubkey, options = {}) { const normalizedOptions = - options && typeof options === "object" ? options : { persistActive: true }; + options && typeof options === "object" ? { ...options } : {}; const persistActive = normalizedOptions.persistActive === false ? false : true; + const providerId = this.normalizeProviderId(normalizedOptions.providerId); + const authTypeCandidate = + typeof normalizedOptions.authType === "string" && normalizedOptions.authType.trim() + ? normalizedOptions.authType.trim() + : providerId; + const authType = authTypeCandidate || "nip07"; + const signer = Object.prototype.hasOwnProperty.call(normalizedOptions, "signer") + ? normalizedOptions.signer ?? null + : null; + const providerResult = Object.prototype.hasOwnProperty.call( + normalizedOptions, + "providerResult", + ) + ? normalizedOptions.providerResult ?? null + : null; const previousPubkey = this.normalizeHexPubkey(getPubkey()) || getPubkey(); const normalized = this.normalizeHexPubkey(pubkey); @@ -376,6 +477,11 @@ export default class AuthService { setCurrentUserNpub(npub); let savedProfilesMutated = false; + const entryAuthType = + typeof authType === "string" && authType.trim() + ? authType.trim() + : "nip07"; + mutateSavedProfiles((profiles) => { const draft = Array.isArray(profiles) ? profiles.slice() : []; const existingIndex = draft.findIndex((entry) => { @@ -391,7 +497,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: entryAuthType, }; if (existingIndex >= 0) { @@ -438,6 +544,10 @@ export default class AuthService { identityChanged, savedProfiles: this.cloneSavedProfiles(), activeProfilePubkey: getActiveProfilePubkey(), + providerId, + authType: entryAuthType, + signer, + providerResult, postLogin: { pubkey: normalizedLoginPubkey, blocksLoaded: false,