From 53b8a2609c2585b47186d7057af980050338ef00 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Feb 2026 19:41:53 +0000 Subject: [PATCH] fix: recognize NIP-07 signer type for extension permission pre-granting The NIP-07 adapter uses type "nip07" but all permission pre-granting checks only matched type "extension", causing NIP-07 browser extension users to fail decryption for block/mute lists, subscriptions, DMs, and watch history. Hashtag preferences appeared to work because their read path had a separate fix, masking the systemic issue. Changes: - shouldRequestExtensionPermissions() now matches both "extension" and "nip07" signer types (central fix affecting DM signals, comments, publish helpers, and watch history fetch) - buildDmDecryptContext() pre-grants extension permissions for NIP-07 - userBlocks.js: remove capabilities-gated decryptor check that failed with lazy NIP-07 module injection (same fix already in subscriptions.js); always resolve signer regardless of allowPermissionPrompt; increase fast-path relay timeout from 6s to 10s - watchHistoryService: recognize NIP-07 type and always resolve signer - All publish paths (blocks, subscriptions, hashtags, relays, uploads, sharing, moderation) now pre-grant permissions for NIP-07 signers https://claude.ai/code/session_01PipsdaTQmw7wBTJ1LmJjkk --- js/app.js | 2 +- js/nostr/client.js | 4 ++-- js/relayManager.js | 2 +- js/services/hashtagPreferencesService.js | 2 +- js/services/moderationService.js | 2 +- js/subscriptions.js | 2 +- js/ui/components/UploadModal.js | 2 +- js/ui/shareNostrController.js | 2 +- js/userBlocks.js | 28 ++++++++++++++---------- js/watchHistoryService.js | 10 ++++++--- 10 files changed, 33 insertions(+), 23 deletions(-) diff --git a/js/app.js b/js/app.js index 513dc84d..d6447640 100644 --- a/js/app.js +++ b/js/app.js @@ -2817,7 +2817,7 @@ class Application { throw error; } - if (signer.type === "extension" && nostrClient.ensureExtensionPermissions) { + if ((signer.type === "extension" || signer.type === "nip07") && nostrClient.ensureExtensionPermissions) { const permissionResult = await nostrClient.ensureExtensionPermissions( DEFAULT_NIP07_CORE_METHODS, ); diff --git a/js/nostr/client.js b/js/nostr/client.js index f84ebe5c..1f7b1af0 100644 --- a/js/nostr/client.js +++ b/js/nostr/client.js @@ -272,7 +272,7 @@ function shouldRequestExtensionPermissions(signer) { if (!signer || typeof signer !== "object") { return false; } - return signer.type === "extension"; + return signer.type === "extension" || signer.type === "nip07"; } import { @@ -1223,7 +1223,7 @@ export class NostrClient { let extensionPermissionResult = null; if ( - activeSigner?.type === "extension" && + (activeSigner?.type === "extension" || activeSigner?.type === "nip07") && typeof this.ensureExtensionPermissions === "function" ) { extensionPermissionResult = await this.ensureExtensionPermissions( diff --git a/js/relayManager.js b/js/relayManager.js index a7d20bfe..972f49fb 100644 --- a/js/relayManager.js +++ b/js/relayManager.js @@ -665,7 +665,7 @@ class RelayPreferencesManager { throw error; } - if (signer.type === "extension") { + if (signer.type === "extension" || signer.type === "nip07") { const permissionResult = await requestDefaultExtensionPermissions(); if (!permissionResult.ok) { userLogger.warn( diff --git a/js/services/hashtagPreferencesService.js b/js/services/hashtagPreferencesService.js index 557207c6..3bc58cdf 100644 --- a/js/services/hashtagPreferencesService.js +++ b/js/services/hashtagPreferencesService.js @@ -1282,7 +1282,7 @@ class HashtagPreferencesService { throw error; } - if (signer.type === "extension") { + if (signer.type === "extension" || signer.type === "nip07") { const permissionResult = await requestDefaultExtensionPermissions( DEFAULT_NIP07_ENCRYPTION_METHODS, ); diff --git a/js/services/moderationService.js b/js/services/moderationService.js index a90b8844..58cef727 100644 --- a/js/services/moderationService.js +++ b/js/services/moderationService.js @@ -2427,7 +2427,7 @@ export class ModerationService { } if ( - signer.type === "extension" && + (signer.type === "extension" || signer.type === "nip07") && typeof this.nostrClient?.ensureExtensionPermissions === "function" ) { const permissionResult = await this.nostrClient.ensureExtensionPermissions([ diff --git a/js/subscriptions.js b/js/subscriptions.js index fd43fe6d..d7eae458 100644 --- a/js/subscriptions.js +++ b/js/subscriptions.js @@ -1356,7 +1356,7 @@ class SubscriptionsManager { throw error; } - if (signer.type === "extension") { + if (signer.type === "extension" || signer.type === "nip07") { const permissionResult = await requestDefaultExtensionPermissions( DEFAULT_NIP07_ENCRYPTION_METHODS, ); diff --git a/js/ui/components/UploadModal.js b/js/ui/components/UploadModal.js index 905d78bb..6274d96f 100644 --- a/js/ui/components/UploadModal.js +++ b/js/ui/components/UploadModal.js @@ -939,7 +939,7 @@ export class UploadModal { } try { - if (signer?.type === "extension") { + if (signer?.type === "extension" || signer?.type === "nip07") { const permissionResult = await requestDefaultExtensionPermissions(); if (!permissionResult?.ok) { alert("Extension permissions are required to unlock storage."); diff --git a/js/ui/shareNostrController.js b/js/ui/shareNostrController.js index 93750464..669c3423 100644 --- a/js/ui/shareNostrController.js +++ b/js/ui/shareNostrController.js @@ -149,7 +149,7 @@ export default class ShareNostrController { throw new Error("share-missing-relays"); } - if (signer.type === "extension" && this.services.nostrClient.ensureExtensionPermissions) { + if ((signer.type === "extension" || signer.type === "nip07") && this.services.nostrClient.ensureExtensionPermissions) { const permissionResult = await this.services.nostrClient.ensureExtensionPermissions( this.services.permissionMethods, ); diff --git a/js/userBlocks.js b/js/userBlocks.js index 83723837..de4b8027 100644 --- a/js/userBlocks.js +++ b/js/userBlocks.js @@ -993,20 +993,26 @@ class UserBlockListManager { return (!requiresNip44 || hasNip44) && (!requiresNip04 || hasNip04); }; + // FIX: Always attempt to resolve the signer if one isn't available, + // regardless of allowPermissionPrompt. The previous guard meant that + // background refreshes (allowPermissionPrompt=false) could never decrypt + // because they skipped signer resolution entirely. ensureActiveSignerForPubkey + // does not itself prompt the user — it only waits for an already-injected + // extension or returns the existing signer. if ( - allowPermissionPrompt && - (!signer || - (!signerHasRequiredDecryptors(signer) && - typeof nostrClient?.ensureActiveSignerForPubkey === "function")) + (!signer || !signerHasRequiredDecryptors(signer)) && + typeof nostrClient?.ensureActiveSignerForPubkey === "function" ) { signer = await nostrClient.ensureActiveSignerForPubkey(normalized); } - const caps = signer?.capabilities || {}; - const signerHasNip04 = - caps.nip04 !== false && typeof signer?.nip04Decrypt === "function"; - const signerHasNip44 = - caps.nip44 !== false && typeof signer?.nip44Decrypt === "function"; + // FIX: Check method existence only, not capabilities. NIP-07 adapters + // always expose decrypt methods that check extension state at call time. + // The capabilities getter dynamically queries window.nostr which may not + // have injected nip04/nip44 modules yet (lazy injection), causing false + // negatives that block decryption even when methods will work at call time. + const signerHasNip04 = typeof signer?.nip04Decrypt === "function"; + const signerHasNip44 = typeof signer?.nip44Decrypt === "function"; const signerAvailable = Boolean(signer); const signerHasDecryptors = signerHasNip04 || signerHasNip44; const signerStatus = signerAvailable @@ -1553,7 +1559,7 @@ class UserBlockListManager { pubkey: normalized, relayUrls: relays, since: fetchSince, - timeoutMs: 6000, + timeoutMs: 10000, }); const legacyFetchPromise = Promise.all([ @@ -2106,7 +2112,7 @@ class UserBlockListManager { throw err; } - if (signer.type === "extension") { + if (signer.type === "extension" || signer.type === "nip07") { const permissionResult = await requestDefaultExtensionPermissions( DEFAULT_NIP07_ENCRYPTION_METHODS, ); diff --git a/js/watchHistoryService.js b/js/watchHistoryService.js index cfecf734..589be951 100644 --- a/js/watchHistoryService.js +++ b/js/watchHistoryService.js @@ -87,10 +87,12 @@ async function ensureWatchHistoryExtensionPermissions(actorKey, options = {}) { const allowPermissionPrompt = options?.allowPermissionPrompt !== false; + // FIX: Always attempt to resolve the signer regardless of allowPermissionPrompt. + // ensureActiveSignerForPubkey does not prompt the user — it only resolves an + // already-injected extension or returns the existing signer. let signer = getActiveSigner(); if ( - allowPermissionPrompt && - (!signer && typeof nostrClient?.ensureActiveSignerForPubkey === "function") + !signer && typeof nostrClient?.ensureActiveSignerForPubkey === "function" ) { signer = await nostrClient.ensureActiveSignerForPubkey(normalizedActor); } @@ -98,7 +100,9 @@ async function ensureWatchHistoryExtensionPermissions(actorKey, options = {}) { const canSign = typeof signer?.canSign === "function" ? signer.canSign() : typeof signer?.signEvent === "function"; - if (!canSign || signer?.type !== "extension") { + // FIX: NIP-07 adapters use type "nip07", not "extension". Both types + // represent browser extension signers that need permission pre-granting. + if (!canSign || (signer?.type !== "extension" && signer?.type !== "nip07")) { return { ok: true }; }