From cf9082b83f6f8500e4d82e6e9604bef7a5fb2aba Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 28 Jan 2026 13:57:40 -0500 Subject: [PATCH] Add shared NIP-07 permission gate --- js/nostr/client.js | 79 +++++++++++++++++++++++++++----------- js/nostr/commentEvents.js | 19 +++++++-- js/nostr/defaultClient.js | 8 +++- js/nostr/dmSignalEvents.js | 38 ++++++++++++++---- js/nostr/reactionEvents.js | 19 +++++++-- js/nostr/viewEvents.js | 19 +++++++-- js/nostr/watchHistory.js | 14 ++++++- js/nostrClientFacade.js | 1 + js/nostrClientRegistry.js | 4 ++ js/subscriptions.js | 51 +++++++++++++----------- 10 files changed, 185 insertions(+), 67 deletions(-) diff --git a/js/nostr/client.js b/js/nostr/client.js index 2348e8dd..c04641cf 100644 --- a/js/nostr/client.js +++ b/js/nostr/client.js @@ -395,6 +395,16 @@ function shouldRequestExtensionPermissions(signer) { return signer.type === "extension"; } +function normalizePermissionMethods(methods) { + if (!Array.isArray(methods)) { + return []; + } + const normalized = methods + .map((method) => normalizePermissionMethod(method)) + .filter(Boolean); + return Array.from(new Set(normalized)); +} + const EVENTS_CACHE_STORAGE_KEY = "bitvid:eventsCache:v1"; // We use the policy TTL, but currently the storage backend is hardcoded to IDB (with localStorage fallback). // Future refactors should make EventsCacheStore dynamic based on CACHE_POLICIES[NOTE_TYPES.VIDEO_POST].storage. @@ -1156,7 +1166,9 @@ export class NostrClient { getActivePubkey: () => this.pubkey, getSessionActor: () => this.sessionActor, ensureSessionActor: () => this.ensureSessionActor(), - ensureExtensionPermissions: (...args) => this.ensureExtensionPermissions(...args), + ensureExtensionPermissions: (...args) => this.ensureExtensionPermissionsGate(...args), + hasRequiredExtensionPermissions: (...args) => + this.hasRequiredExtensionPermissions(...args), resolveActiveSigner, shouldRequestExtensionPermissions, signEventWithPrivateKey, @@ -1266,6 +1278,7 @@ export class NostrClient { this.pendingHandshakeCancel = null; this.remoteSignerListeners = new Set(); this.sessionActorListeners = new Set(); + this.extensionPermissionRequestPromise = null; const storedRemoteSigner = this.getStoredNip46Metadata(); this.remoteSignerStatus = { state: storedRemoteSigner.hasSession ? "stored" : "idle", @@ -2776,16 +2789,8 @@ export class NostrClient { this.extensionPermissionCache = new Set(); } - if (!Array.isArray(methods)) { - return; - } - let didChange = false; - for (const method of methods) { - const normalized = normalizePermissionMethod(method); - if (!normalized) { - continue; - } + for (const normalized of normalizePermissionMethods(methods)) { if (!this.extensionPermissionCache.has(normalized)) { didChange = true; } @@ -2801,20 +2806,50 @@ export class NostrClient { } } - async ensureExtensionPermissions(methods = DEFAULT_NIP07_PERMISSION_METHODS) { - if (!Array.isArray(methods)) { - methods = []; + hasRequiredExtensionPermissions( + methods = DEFAULT_NIP07_PERMISSION_METHODS, + ) { + const normalized = normalizePermissionMethods(methods); + if (!normalized.length) { + return true; + } + if (!this.extensionPermissionCache) { + return false; + } + return normalized.every((method) => this.extensionPermissionCache.has(method)); + } + + async ensureExtensionPermissionsGate( + methods = DEFAULT_NIP07_PERMISSION_METHODS, + ) { + const normalized = normalizePermissionMethods(methods); + if (!normalized.length) { + return { ok: true }; } - const normalized = Array.from( - new Set( - methods - .map((method) => - typeof method === "string" && method.trim() ? method.trim() : "", - ) - .filter(Boolean), - ), - ); + if (this.hasRequiredExtensionPermissions(normalized)) { + return { ok: true, cached: true }; + } + + if (this.extensionPermissionRequestPromise) { + return this.extensionPermissionRequestPromise; + } + + const requestPromise = this.ensureExtensionPermissions(normalized) + .catch((error) => ({ ok: false, error })) + .finally(() => { + if (this.extensionPermissionRequestPromise === requestPromise) { + this.extensionPermissionRequestPromise = null; + } + }); + + this.extensionPermissionRequestPromise = requestPromise; + + return requestPromise; + } + + async ensureExtensionPermissions(methods = DEFAULT_NIP07_PERMISSION_METHODS) { + const normalized = normalizePermissionMethods(methods); if (!normalized.length) { return { ok: true }; diff --git a/js/nostr/commentEvents.js b/js/nostr/commentEvents.js index 8d930ed3..29d11c81 100644 --- a/js/nostr/commentEvents.js +++ b/js/nostr/commentEvents.js @@ -1213,10 +1213,21 @@ export async function publishComment( ? shouldRequestExtensionPermissions(signer) : false; - if (shouldRequestPermissions) { - permissionResult = await client.ensureExtensionPermissions( - DEFAULT_NIP07_PERMISSION_METHODS, - ); + const hasCachedPermissions = + typeof client?.hasRequiredExtensionPermissions === "function" && + client.hasRequiredExtensionPermissions(DEFAULT_NIP07_PERMISSION_METHODS); + + if (shouldRequestPermissions && !hasCachedPermissions) { + const permissionGate = + typeof client?.ensureExtensionPermissionsGate === "function" + ? client.ensureExtensionPermissionsGate.bind(client) + : typeof client?.ensureExtensionPermissions === "function" + ? client.ensureExtensionPermissions.bind(client) + : null; + + if (permissionGate) { + permissionResult = await permissionGate(DEFAULT_NIP07_PERMISSION_METHODS); + } } if (!permissionResult.ok) { diff --git a/js/nostr/defaultClient.js b/js/nostr/defaultClient.js index 13363919..2614692e 100644 --- a/js/nostr/defaultClient.js +++ b/js/nostr/defaultClient.js @@ -15,7 +15,7 @@ export const nostrClient = new NostrClient(); registerNostrClient(nostrClient, { requestPermissions: ( methods = DEFAULT_NIP07_PERMISSION_METHODS, - ) => nostrClient.ensureExtensionPermissions(methods), + ) => nostrClient.ensureExtensionPermissionsGate(methods), }); export function requestDefaultExtensionPermissions( @@ -24,6 +24,12 @@ export function requestDefaultExtensionPermissions( return requestRegisteredPermissions(methods); } +export function hasDefaultExtensionPermissions( + methods = DEFAULT_NIP07_PERMISSION_METHODS, +) { + return nostrClient.hasRequiredExtensionPermissions(methods); +} + export { NostrClient, DEFAULT_NIP07_ENCRYPTION_METHODS, diff --git a/js/nostr/dmSignalEvents.js b/js/nostr/dmSignalEvents.js index b138d497..581bc3e0 100644 --- a/js/nostr/dmSignalEvents.js +++ b/js/nostr/dmSignalEvents.js @@ -101,10 +101,21 @@ export async function publishDmReadReceipt( ? shouldRequestExtensionPermissions(signer) : false; - if (shouldRequestPermissions) { - permissionResult = await client.ensureExtensionPermissions( - DEFAULT_NIP07_PERMISSION_METHODS, - ); + const hasCachedPermissions = + typeof client?.hasRequiredExtensionPermissions === "function" && + client.hasRequiredExtensionPermissions(DEFAULT_NIP07_PERMISSION_METHODS); + + if (shouldRequestPermissions && !hasCachedPermissions) { + const permissionGate = + typeof client?.ensureExtensionPermissionsGate === "function" + ? client.ensureExtensionPermissionsGate.bind(client) + : typeof client?.ensureExtensionPermissions === "function" + ? client.ensureExtensionPermissions.bind(client) + : null; + + if (permissionGate) { + permissionResult = await permissionGate(DEFAULT_NIP07_PERMISSION_METHODS); + } } if (!permissionResult.ok) { @@ -242,10 +253,21 @@ export async function publishDmTypingIndicator( ? shouldRequestExtensionPermissions(signer) : false; - if (shouldRequestPermissions) { - permissionResult = await client.ensureExtensionPermissions( - DEFAULT_NIP07_PERMISSION_METHODS, - ); + const hasCachedPermissions = + typeof client?.hasRequiredExtensionPermissions === "function" && + client.hasRequiredExtensionPermissions(DEFAULT_NIP07_PERMISSION_METHODS); + + if (shouldRequestPermissions && !hasCachedPermissions) { + const permissionGate = + typeof client?.ensureExtensionPermissionsGate === "function" + ? client.ensureExtensionPermissionsGate.bind(client) + : typeof client?.ensureExtensionPermissions === "function" + ? client.ensureExtensionPermissions.bind(client) + : null; + + if (permissionGate) { + permissionResult = await permissionGate(DEFAULT_NIP07_PERMISSION_METHODS); + } } if (!permissionResult.ok) { diff --git a/js/nostr/reactionEvents.js b/js/nostr/reactionEvents.js index d059f2cc..76a695b2 100644 --- a/js/nostr/reactionEvents.js +++ b/js/nostr/reactionEvents.js @@ -435,10 +435,21 @@ export async function publishVideoReaction( if (canUseActiveSigner) { let permissionResult = { ok: true }; - if (shouldRequestExtensionPermissions(signer)) { - permissionResult = await client.ensureExtensionPermissions( - DEFAULT_NIP07_PERMISSION_METHODS - ); + const hasCachedPermissions = + typeof client?.hasRequiredExtensionPermissions === "function" && + client.hasRequiredExtensionPermissions(DEFAULT_NIP07_PERMISSION_METHODS); + + if (shouldRequestExtensionPermissions(signer) && !hasCachedPermissions) { + const permissionGate = + typeof client?.ensureExtensionPermissionsGate === "function" + ? client.ensureExtensionPermissionsGate.bind(client) + : typeof client?.ensureExtensionPermissions === "function" + ? client.ensureExtensionPermissions.bind(client) + : null; + + if (permissionGate) { + permissionResult = await permissionGate(DEFAULT_NIP07_PERMISSION_METHODS); + } } if (permissionResult.ok) { try { diff --git a/js/nostr/viewEvents.js b/js/nostr/viewEvents.js index 284e5d92..425ec826 100644 --- a/js/nostr/viewEvents.js +++ b/js/nostr/viewEvents.js @@ -787,10 +787,21 @@ export async function publishViewEvent( if (canUseActiveSigner) { let permissionResult = { ok: true }; - if (shouldRequestExtensionPermissions(signer)) { - permissionResult = await client.ensureExtensionPermissions( - DEFAULT_NIP07_PERMISSION_METHODS - ); + const hasCachedPermissions = + typeof client?.hasRequiredExtensionPermissions === "function" && + client.hasRequiredExtensionPermissions(DEFAULT_NIP07_PERMISSION_METHODS); + + if (shouldRequestExtensionPermissions(signer) && !hasCachedPermissions) { + const permissionGate = + typeof client?.ensureExtensionPermissionsGate === "function" + ? client.ensureExtensionPermissionsGate.bind(client) + : typeof client?.ensureExtensionPermissions === "function" + ? client.ensureExtensionPermissions.bind(client) + : null; + + if (permissionGate) { + permissionResult = await permissionGate(DEFAULT_NIP07_PERMISSION_METHODS); + } } if (permissionResult.ok) { try { diff --git a/js/nostr/watchHistory.js b/js/nostr/watchHistory.js index 04e9ecfa..2820946e 100644 --- a/js/nostr/watchHistory.js +++ b/js/nostr/watchHistory.js @@ -1530,7 +1530,12 @@ class WatchHistoryManager { if (activeSigner) { if (this.deps.shouldRequestExtensionPermissions?.(activeSigner)) { - await this.deps.ensureExtensionPermissions?.(DEFAULT_NIP07_PERMISSION_METHODS); + const hasCachedPermissions = + typeof this.deps.hasRequiredExtensionPermissions === "function" && + this.deps.hasRequiredExtensionPermissions(DEFAULT_NIP07_PERMISSION_METHODS); + if (!hasCachedPermissions) { + await this.deps.ensureExtensionPermissions?.(DEFAULT_NIP07_PERMISSION_METHODS); + } } const encryptors = []; @@ -1863,7 +1868,12 @@ class WatchHistoryManager { const decryptSigner = canUseActiveSignerDecrypt ? signer : null; if (decryptSigner && this.deps.shouldRequestExtensionPermissions?.(decryptSigner)) { - await this.deps.ensureExtensionPermissions?.(DEFAULT_NIP07_PERMISSION_METHODS); + const hasCachedPermissions = + typeof this.deps.hasRequiredExtensionPermissions === "function" && + this.deps.hasRequiredExtensionPermissions(DEFAULT_NIP07_PERMISSION_METHODS); + if (!hasCachedPermissions) { + await this.deps.ensureExtensionPermissions?.(DEFAULT_NIP07_PERMISSION_METHODS); + } } devLogger.info("[nostr] Fetching watch history from relays.", { diff --git a/js/nostrClientFacade.js b/js/nostrClientFacade.js index 6aabbde3..32e02cd7 100644 --- a/js/nostrClientFacade.js +++ b/js/nostrClientFacade.js @@ -7,6 +7,7 @@ export { nostrClient, + hasDefaultExtensionPermissions, requestDefaultExtensionPermissions, } from "./nostr/defaultClient.js"; diff --git a/js/nostrClientRegistry.js b/js/nostrClientRegistry.js index 9adbd272..9d8c231d 100644 --- a/js/nostrClientRegistry.js +++ b/js/nostrClientRegistry.js @@ -88,6 +88,10 @@ export function registerNostrClient(client, options = {}) { ) { requestPermissionsDelegate = (...args) => registeredNostrClient.ensureExtensionPermissions(...args); + if (typeof registeredNostrClient.ensureExtensionPermissionsGate === "function") { + requestPermissionsDelegate = (...args) => + registeredNostrClient.ensureExtensionPermissionsGate(...args); + } return; } diff --git a/js/subscriptions.js b/js/subscriptions.js index 4c2bfc21..af7fe8c2 100644 --- a/js/subscriptions.js +++ b/js/subscriptions.js @@ -1,6 +1,7 @@ // js/subscriptions.js import { getActiveSigner, + hasDefaultExtensionPermissions, nostrClient, requestDefaultExtensionPermissions, } from "./nostrClientFacade.js"; @@ -750,16 +751,19 @@ class SubscriptionsManager { (!signerHasNip04 && !windowHasNip04 && requiresNip04) ) { try { - const permissionResult = await requestDefaultExtensionPermissions(); - if (!permissionResult?.ok) { - const error = - permissionResult?.error instanceof Error - ? permissionResult.error - : new Error( - "Extension permissions denied while decrypting subscriptions." - ); - error.code = "nostr-permission-denied"; - return { ok: false, error }; + const hasCachedPermissions = hasDefaultExtensionPermissions(); + if (!hasCachedPermissions) { + const permissionResult = await requestDefaultExtensionPermissions(); + if (!permissionResult?.ok) { + const error = + permissionResult?.error instanceof Error + ? permissionResult.error + : new Error( + "Extension permissions denied while decrypting subscriptions." + ); + error.code = "nostr-permission-denied"; + return { ok: false, error }; + } } } catch (error) { const wrapped = error instanceof Error ? error : new Error(String(error)); @@ -979,18 +983,21 @@ class SubscriptionsManager { } if (signer.type === "extension") { - const permissionResult = await requestDefaultExtensionPermissions(); - if (!permissionResult.ok) { - userLogger.warn( - "[SubscriptionsManager] Signer permissions denied while updating subscriptions.", - permissionResult.error, - ); - const error = new Error( - "The active signer must allow encryption and signing before updating subscriptions.", - ); - error.code = "extension-permission-denied"; - error.cause = permissionResult.error; - throw error; + const hasCachedPermissions = hasDefaultExtensionPermissions(); + if (!hasCachedPermissions) { + const permissionResult = await requestDefaultExtensionPermissions(); + if (!permissionResult.ok) { + userLogger.warn( + "[SubscriptionsManager] Signer permissions denied while updating subscriptions.", + permissionResult.error, + ); + const error = new Error( + "The active signer must allow encryption and signing before updating subscriptions.", + ); + error.code = "extension-permission-denied"; + error.cause = permissionResult.error; + throw error; + } } }