From ea45488ee433d06d3376d7eb34dd730bd6129815 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 21 Oct 2025 13:56:38 -0400 Subject: [PATCH] Handle custom auth providers in saved profiles --- js/app.js | 32 ++++- js/services/authService.js | 71 ++++++++--- js/state/cache.js | 25 +++- js/ui/profileModalController.js | 10 +- tests/state/cache-saved-profiles.test.mjs | 141 ++++++++++++++++++++++ 5 files changed, 256 insertions(+), 23 deletions(-) create mode 100644 tests/state/cache-saved-profiles.test.mjs diff --git a/js/app.js b/js/app.js index ebca329f..b86a9a69 100644 --- a/js/app.js +++ b/js/app.js @@ -1961,11 +1961,39 @@ class Application { setLoadingState(true); try { - const { pubkey } = await this.authService.requestLogin({ + const loginResult = await this.authService.requestLogin({ allowAccountSelection: true, autoApply: false, }); + const { pubkey, authType: loginAuthType, providerId } = + typeof loginResult === "object" && loginResult + ? loginResult + : { pubkey: loginResult }; + + const detailAuthType = + typeof loginResult?.detail?.authType === "string" + ? loginResult.detail.authType + : null; + const detailProviderId = + typeof loginResult?.detail?.providerId === "string" + ? loginResult.detail.providerId + : null; + + const resolvedAuthType = (() => { + const candidates = [detailAuthType, loginAuthType, detailProviderId, providerId]; + for (const candidate of candidates) { + if (typeof candidate !== "string") { + continue; + } + const trimmed = candidate.trim(); + if (trimmed) { + return trimmed; + } + } + return "nip07"; + })(); + const normalizedPubkey = this.normalizeHexPubkey(pubkey); if (!normalizedPubkey) { throw new Error( @@ -1999,7 +2027,7 @@ class Application { npub, name, picture, - authType: "nip07", + authType: resolvedAuthType, }); persistSavedProfiles({ persistActive: false }); diff --git a/js/services/authService.js b/js/services/authService.js index 95665338..12a2a09a 100644 --- a/js/services/authService.js +++ b/js/services/authService.js @@ -151,6 +151,39 @@ export default class AuthService { : "nip07"; } + normalizeAuthType(authTypeCandidate, providerId, providerResult) { + const candidates = []; + + if (typeof authTypeCandidate === "string") { + candidates.push(authTypeCandidate); + } + + if (providerResult && typeof providerResult === "object") { + const resultAuthType = providerResult.authType; + if (typeof resultAuthType === "string") { + candidates.push(resultAuthType); + } + + const resultProviderId = providerResult.providerId; + if (typeof resultProviderId === "string") { + candidates.push(resultProviderId); + } + } + + if (typeof providerId === "string") { + candidates.push(providerId); + } + + for (const candidate of candidates) { + const trimmed = candidate.trim(); + if (trimmed) { + return trimmed; + } + } + + return "nip07"; + } + log(...args) { try { this.logger(...args); @@ -327,10 +360,11 @@ export default class AuthService { providerResult && typeof providerResult === "object" ? providerResult.authType : null; - const authType = - typeof authTypeCandidate === "string" && authTypeCandidate.trim() - ? authTypeCandidate.trim() - : providerId; + const authType = this.normalizeAuthType( + authTypeCandidate, + providerId, + providerResult, + ); const signer = providerResult && typeof providerResult === "object" ? providerResult.signer || null @@ -401,20 +435,24 @@ export default class AuthService { 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 authTypeCandidate = + typeof normalizedOptions.authType === "string" + ? normalizedOptions.authType + : null; + const authType = this.normalizeAuthType( + authTypeCandidate, + providerId, + providerResult, + ); + const signer = Object.prototype.hasOwnProperty.call(normalizedOptions, "signer") + ? normalizedOptions.signer ?? null + : null; const previousPubkey = this.normalizeHexPubkey(getPubkey()) || getPubkey(); const normalized = this.normalizeHexPubkey(pubkey); @@ -477,10 +515,11 @@ export default class AuthService { setCurrentUserNpub(npub); let savedProfilesMutated = false; - const entryAuthType = - typeof authType === "string" && authType.trim() - ? authType.trim() - : "nip07"; + const entryAuthType = this.normalizeAuthType( + authType, + providerId, + providerResult, + ); mutateSavedProfiles((profiles) => { const draft = Array.isArray(profiles) ? profiles.slice() : []; diff --git a/js/state/cache.js b/js/state/cache.js index 2b33256d..6df5467a 100644 --- a/js/state/cache.js +++ b/js/state/cache.js @@ -360,7 +360,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() + : null, })), activePubkey: activePubkeyToPersist, }; @@ -408,7 +411,25 @@ export function loadSavedProfilesFromStorage() { } seenPubkeys.add(normalizedPubkey); - const storedAuthType = candidate.authType === "nsec" ? "nsec" : "nip07"; + const rawAuthType = + typeof candidate.authType === "string" ? candidate.authType : null; + let storedAuthType = null; + if (rawAuthType) { + const trimmedAuthType = rawAuthType.trim(); + if (trimmedAuthType) { + storedAuthType = trimmedAuthType; + if (trimmedAuthType !== rawAuthType) { + needsRewrite = true; + } + } + } + + if (!storedAuthType) { + storedAuthType = "nip07"; + if (rawAuthType !== "nip07") { + needsRewrite = true; + } + } 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 119d7c53..feb47257 100644 --- a/js/ui/profileModalController.js +++ b/js/ui/profileModalController.js @@ -1518,8 +1518,12 @@ export class ProfileModalController { if (meta.npub) { button.dataset.npub = meta.npub; } - if (entry.authType) { - button.dataset.authType = entry.authType; + const normalizedAuthType = + typeof entry.authType === "string" && entry.authType.trim() + ? entry.authType.trim() + : null; + if (normalizedAuthType) { + button.dataset.authType = normalizedAuthType; } const normalizedPubkey = this.normalizeHexPubkey(entry.pubkey); @@ -1555,7 +1559,7 @@ export class ProfileModalController { label.className = "text-3xs font-semibold uppercase tracking-extra-wide text-status-info"; label.textContent = - entry.authType === "nsec" ? "Direct key" : "Saved profile"; + normalizedAuthType === "nsec" ? "Direct key" : "Saved profile"; const action = document.createElement("span"); action.className = "text-xs font-medium text-muted"; diff --git a/tests/state/cache-saved-profiles.test.mjs b/tests/state/cache-saved-profiles.test.mjs new file mode 100644 index 00000000..acf07fd9 --- /dev/null +++ b/tests/state/cache-saved-profiles.test.mjs @@ -0,0 +1,141 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { + setSavedProfiles, + persistSavedProfiles, + loadSavedProfilesFromStorage, + mutateSavedProfiles, + setActiveProfilePubkey, +} from "../../js/state/cache.js"; + +const STORAGE_KEY = "bitvid:savedProfiles:v1"; +const SAMPLE_PUBKEY = + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + +function resetSavedProfilesState() { + if (typeof localStorage?.clear === "function") { + localStorage.clear(); + } + + mutateSavedProfiles(() => [], { persist: false, persistActive: false }); + setActiveProfilePubkey(null, { persist: false }); + + loadSavedProfilesFromStorage(); + + mutateSavedProfiles(() => [], { persist: false, persistActive: false }); + setActiveProfilePubkey(null, { persist: false }); + + if (typeof localStorage?.clear === "function") { + localStorage.clear(); + } +} + +test("persistSavedProfiles preserves custom authType strings", () => { + resetSavedProfilesState(); + + setSavedProfiles( + [ + { + pubkey: SAMPLE_PUBKEY, + npub: null, + name: "", + picture: "", + authType: "custom-wallet", + }, + ], + { persist: false, persistActive: false }, + ); + + persistSavedProfiles({ persistActive: false }); + + const storedRaw = localStorage.getItem(STORAGE_KEY); + assert.ok(storedRaw, "expected saved profiles to be written to storage"); + + const stored = JSON.parse(storedRaw); + assert.equal(stored.entries.length, 1); + assert.equal(stored.entries[0].authType, "custom-wallet"); +}); + +test("loadSavedProfilesFromStorage retains custom provider authType", () => { + resetSavedProfilesState(); + + localStorage.setItem( + STORAGE_KEY, + JSON.stringify({ + version: 1, + entries: [ + { + pubkey: SAMPLE_PUBKEY, + npub: null, + name: "", + picture: "", + authType: "nostr-wallet-connect", + }, + ], + activePubkey: null, + }), + ); + + const { profiles } = loadSavedProfilesFromStorage(); + assert.equal(profiles.length, 1); + assert.equal(profiles[0].authType, "nostr-wallet-connect"); + + const stored = JSON.parse(localStorage.getItem(STORAGE_KEY)); + assert.equal(stored.entries[0].authType, "nostr-wallet-connect"); +}); + +test("loadSavedProfilesFromStorage migrates missing authType to nip07", () => { + resetSavedProfilesState(); + + localStorage.setItem( + STORAGE_KEY, + JSON.stringify({ + version: 1, + entries: [ + { + pubkey: SAMPLE_PUBKEY, + npub: null, + name: "", + picture: "", + }, + ], + activePubkey: null, + }), + ); + + const { profiles } = loadSavedProfilesFromStorage(); + assert.equal(profiles.length, 1); + assert.equal(profiles[0].authType, "nip07"); + + const stored = JSON.parse(localStorage.getItem(STORAGE_KEY)); + assert.equal(stored.entries[0].authType, "nip07"); +}); + +test("loadSavedProfilesFromStorage trims stored authType values", () => { + resetSavedProfilesState(); + + localStorage.setItem( + STORAGE_KEY, + JSON.stringify({ + version: 1, + entries: [ + { + pubkey: SAMPLE_PUBKEY, + npub: null, + name: "", + picture: "", + authType: " extension-provider ", + }, + ], + activePubkey: null, + }), + ); + + const { profiles } = loadSavedProfilesFromStorage(); + assert.equal(profiles.length, 1); + assert.equal(profiles[0].authType, "extension-provider"); + + const stored = JSON.parse(localStorage.getItem(STORAGE_KEY)); + assert.equal(stored.entries[0].authType, "extension-provider"); +});