From 73f51662af5edcf0c649c4fa18f1cb7b930eb875 Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Thu, 30 Jul 2026 19:21:28 +0700 Subject: [PATCH] =?UTF-8?q?fix(stash):=20avoid=20nip44=20getter=20entirely?= =?UTF-8?q?=20=E2=80=94=20check=20login=20type=20+=20window.nostr=20direct?= =?UTF-8?q?ly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The NUser extension signer's .nip44 getter calls .nostr which throws "Browser extension not available" when window.nostr is absent. A try/catch around ?.nip44 doesn't work because esbuild optimises it away. Fix: check login type + window.nostr directly (no getter, no throw) to determine nip44 availability, and wrap all actual signer accesses at call-time in their own try/catch. Co-authored-by: shakespeare.diy --- src/hooks/useStash.ts | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/src/hooks/useStash.ts b/src/hooks/useStash.ts index 4659ca5..532a2d0 100644 --- a/src/hooks/useStash.ts +++ b/src/hooks/useStash.ts @@ -1,6 +1,7 @@ import { useCallback, useMemo } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useNostr } from '@nostrify/react'; +import { useNostrLogin } from '@nostrify/react/login'; import { useCurrentUser } from './useCurrentUser'; import { useNostrPublish } from './useNostrPublish'; @@ -47,14 +48,26 @@ const STASH_D_TAG = 'skatespots-stash'; export function useStash() { const { nostr } = useNostr(); const { user } = useCurrentUser(); + const { logins } = useNostrLogin(); const queryClient = useQueryClient(); const { mutateAsync: createEvent } = useNostrPublish(); - // Safely check if nip44 is available without throwing (some signers throw - // "Browser extension not available" when the getter is accessed with no login) - const hasNip44 = (() => { - try { return !!user?.signer?.nip44; } catch { return false; } - })(); + // Determine if nip44 is safely available WITHOUT touching any throwing getter. + // Extension signers throw "Browser extension not available" from their .nostr + // getter when window.nostr isn't present yet (page load race, or no extension). + // We check the login type instead: nsec and bunker have nip44 built-in always; + // extension logins only have it when window.nostr is present; amber never has it. + const hasNip44 = useMemo(() => { + if (!user?.pubkey) return false; + const login = logins.find((l) => l.pubkey === user.pubkey); + if (!login) return false; + // nsec and bunker: nip44 is always available (pure JS, no external dep) + if (login.type === 'nsec' || login.type === 'bunker') return true; + // extension: safe to check window.nostr directly (no getter, no throw) + if (login.type === 'extension') return !!(globalThis as { nostr?: { nip44?: unknown } }).nostr?.nip44; + // amber / unknown: no nip44 support + return false; + }, [user?.pubkey, logins]); // Fetch and decrypt stash const { data: stash, isLoading, error } = useQuery({ @@ -81,8 +94,12 @@ export function useStash() { const event = events[0]; try { - // Decrypt the content (encrypted to self) - const decrypted = await user.signer!.nip44!.decrypt(user.pubkey, event.content); + // Decrypt the content (encrypted to self) — access nip44 inside try/catch + // to guard against extension signers throwing when window.nostr is gone + let nip44Dec: typeof user.signer.nip44; + try { nip44Dec = user.signer?.nip44; } catch { return { items: [], updatedAt: 0 }; } + if (!nip44Dec) return { items: [], updatedAt: 0 }; + const decrypted = await nip44Dec.decrypt(user.pubkey, event.content); const stashData = JSON.parse(decrypted) as Stash; return stashData; } catch (err) { @@ -100,8 +117,13 @@ export function useStash() { throw new Error('Encryption not available'); } + // Access nip44 inside a try/catch at call time (signer is guaranteed by hasNip44 above) + let nip44Enc: typeof user.signer.nip44; + try { nip44Enc = user.signer?.nip44; } catch { throw new Error('Encryption not available'); } + if (!nip44Enc) throw new Error('Encryption not available'); + // Encrypt to self - const encrypted = await user.signer!.nip44!.encrypt(user.pubkey, JSON.stringify(newStash)); + const encrypted = await nip44Enc.encrypt(user.pubkey, JSON.stringify(newStash)); // Publish as replaceable event await createEvent({