From deffbf76701f6526e8b450e7598163115531fa83 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 15 Oct 2025 22:36:57 -0400 Subject: [PATCH] Ensure NIP-07 permissions are granted before signing and encrypting --- js/adminListStore.js | 13 ++- js/nostr.js | 210 +++++++++++++++++++++++-------------- js/relayManager.js | 20 +++- js/services/authService.js | 10 ++ js/subscriptions.js | 29 ++++- js/userBlocks.js | 30 +++++- 6 files changed, 232 insertions(+), 80 deletions(-) diff --git a/js/adminListStore.js b/js/adminListStore.js index 2da0679a..627b0bae 100644 --- a/js/adminListStore.js +++ b/js/adminListStore.js @@ -7,7 +7,10 @@ import { ADMIN_EDITORS_NPUBS, isDevMode, } from "./config.js"; -import { nostrClient } from "./nostr.js"; +import { + nostrClient, + requestDefaultExtensionPermissions, +} from "./nostr.js"; import { devLogger, userLogger } from "./utils/logger.js"; import { buildAdminListEvent, @@ -502,6 +505,14 @@ function publishToRelay(url, signedEvent, listKey) { async function persistNostrState(actorNpub, updates = {}) { ensureNostrReady(); + const permissionResult = await requestDefaultExtensionPermissions(); + if (!permissionResult.ok) { + throw createError( + "extension-permission-denied", + "The NIP-07 extension must allow signing before updating admin lists.", + permissionResult.error, + ); + } const extension = window?.nostr; if (!extension || typeof extension.signEvent !== "function") { throw createError( diff --git a/js/nostr.js b/js/nostr.js index e6907127..1c8251d8 100644 --- a/js/nostr.js +++ b/js/nostr.js @@ -94,7 +94,7 @@ const WATCH_HISTORY_REPUBLISH_MAX_DELAY_MS = 5 * 60 * 1000; const WATCH_HISTORY_REPUBLISH_MAX_ATTEMPTS = 8; const WATCH_HISTORY_REPUBLISH_JITTER = 0.25; -const DEFAULT_NIP07_PERMISSION_METHODS = Object.freeze([ +export const DEFAULT_NIP07_PERMISSION_METHODS = Object.freeze([ "get_public_key", "sign_event", "nip04.encrypt", @@ -5589,13 +5589,28 @@ export class NostrClient { window?.nostr && typeof window.nostr.signEvent === "function" ) { - try { - signedEvent = await window.nostr.signEvent(event); - } catch (error) { - userLogger.warn("[nostr] Failed to sign view event with extension:", error); - return { ok: false, error: "signing-failed", details: error }; + const permissionResult = await this.ensureExtensionPermissions( + DEFAULT_NIP07_PERMISSION_METHODS, + ); + if (permissionResult.ok) { + try { + signedEvent = await window.nostr.signEvent(event); + } catch (error) { + userLogger.warn( + "[nostr] Failed to sign view event with extension:", + error, + ); + return { ok: false, error: "signing-failed", details: error }; + } + } else { + userLogger.warn( + "[nostr] Extension permissions missing; signing view event with session key.", + permissionResult.error, + ); } - } else { + } + + if (!signedEvent) { try { if (!this.sessionActor || this.sessionActor.pubkey !== actorPubkey) { await this.ensureSessionActor(true); @@ -5814,72 +5829,17 @@ export class NostrClient { ); } - if (typeof extension.enable === "function") { - devLogger.log("Requesting permissions from NIP-07 extension..."); - const requestedPermissionMethods = Array.from( - DEFAULT_NIP07_PERMISSION_METHODS, - ); - - const permissionVariants = [null]; - const objectPermissions = requestedPermissionMethods - .map((method) => - typeof method === "string" && method.trim() - ? { method: method.trim() } - : null, - ) - .filter(Boolean); - if (objectPermissions.length) { - permissionVariants.push({ permissions: objectPermissions }); + const permissionResult = await this.ensureExtensionPermissions( + DEFAULT_NIP07_PERMISSION_METHODS, + ); + if (!permissionResult.ok) { + const denialMessage = + 'The NIP-07 extension reported "permission denied". Please approve the prompt and try again.'; + const denialError = new Error(denialMessage); + if (permissionResult.error) { + denialError.cause = permissionResult.error; } - const stringPermissions = Array.from( - new Set(objectPermissions.map((entry) => entry.method)), - ); - if (stringPermissions.length) { - permissionVariants.push({ permissions: stringPermissions }); - } - let enableError = null; - for (const options of permissionVariants) { - try { - await runNip07WithRetry( - () => - options - ? extension.enable(options) - : extension.enable(), - { - label: "extension.enable", - ...(options - ? { - timeoutMs: Math.min( - NIP07_LOGIN_TIMEOUT_MS, - getEnableVariantTimeoutMs(), - ), - } - : {}), - retryMultiplier: 1, - }, - ); - enableError = null; - break; - } catch (error) { - enableError = error; - if (options && isDevMode) { - userLogger.warn( - "[nostr] extension.enable request with explicit permissions failed:", - error, - ); - } - } - } - - if (enableError) { - throw new Error( - enableError && enableError.message - ? enableError.message - : "The NIP-07 extension denied the permission request.", - ); - } - - this.markExtensionPermissions(requestedPermissionMethods); + throw denialError; } if (allowAccountSelection && typeof extension.selectAccounts === "function") { @@ -5944,6 +5904,16 @@ export class NostrClient { } this.pubkey = pubkey; devLogger.log("Logged in with extension. Pubkey:", this.pubkey); + + const postLoginPermissions = await this.ensureExtensionPermissions( + DEFAULT_NIP07_PERMISSION_METHODS, + ); + if (!postLoginPermissions.ok && postLoginPermissions.error) { + userLogger.warn( + "[nostr] Extension permissions were not fully granted after login:", + postLoginPermissions.error, + ); + } return this.pubkey; } catch (err) { userLogger.error("Login error:", err); @@ -5992,6 +5962,21 @@ export class NostrClient { return { ok: false, error: "nostr-extension-missing" }; } + const permissionResult = await this.ensureExtensionPermissions( + DEFAULT_NIP07_PERMISSION_METHODS, + ); + if (!permissionResult.ok) { + userLogger.warn( + "[nostr] Cannot send direct message without extension permissions.", + permissionResult.error, + ); + return { + ok: false, + error: "extension-permission-denied", + details: permissionResult.error, + }; + } + const nip04 = extension.nip04; if (!nip04 || typeof nip04.encrypt !== "function") { return { ok: false, error: "nip04-unavailable" }; @@ -6110,8 +6095,27 @@ export class NostrClient { normalizedEventPubkey && normalizedEventPubkey === normalizedLogged ) { - signedEvent = await extensionSigner(event); - } else { + const permissionResult = await this.ensureExtensionPermissions( + DEFAULT_NIP07_PERMISSION_METHODS, + ); + if (permissionResult.ok) { + try { + signedEvent = await extensionSigner(event); + } catch (error) { + userLogger.warn( + "[nostr] Failed to sign event with extension:", + error, + ); + } + } else { + userLogger.warn( + "[nostr] Extension permissions missing; falling back to session signer.", + permissionResult.error, + ); + } + } + + if (!signedEvent) { try { const currentSessionPubkey = typeof this.sessionActor?.pubkey === "string" @@ -6649,8 +6653,31 @@ export class NostrClient { devLogger.log("Creating edited event with root ID:", oldRootId); devLogger.log("Event content:", event.content); + const extensionSigner = window?.nostr?.signEvent; + if (typeof extensionSigner !== "function") { + const error = new Error("A NIP-07 extension with signEvent support is required to edit videos."); + error.code = "nostr-extension-missing"; + throw error; + } + + const permissionResult = await this.ensureExtensionPermissions( + DEFAULT_NIP07_PERMISSION_METHODS, + ); + if (!permissionResult.ok) { + userLogger.warn( + "[nostr] Extension permissions denied while editing a video.", + permissionResult.error, + ); + const error = new Error( + "The NIP-07 extension must grant decrypt and sign permissions before editing a video.", + ); + error.code = "extension-permission-denied"; + error.cause = permissionResult.error; + throw error; + } + try { - const signedEvent = await window.nostr.signEvent(event); + const signedEvent = await extensionSigner(event); devLogger.log("Signed edited event:", signedEvent); const publishResults = await publishEventToRelays( @@ -6791,7 +6818,32 @@ export class NostrClient { content: JSON.stringify(contentObject), }; - const signedEvent = await window.nostr.signEvent(event); + const extensionSigner = window?.nostr?.signEvent; + if (typeof extensionSigner !== "function") { + const error = new Error( + "A NIP-07 extension with signEvent support is required to revert videos.", + ); + error.code = "nostr-extension-missing"; + throw error; + } + + const permissionResult = await this.ensureExtensionPermissions( + DEFAULT_NIP07_PERMISSION_METHODS, + ); + if (!permissionResult.ok) { + userLogger.warn( + "[nostr] Extension permissions denied while reverting a video.", + permissionResult.error, + ); + const error = new Error( + "The NIP-07 extension must grant decrypt and sign permissions before reverting a video.", + ); + error.code = "extension-permission-denied"; + error.cause = permissionResult.error; + throw error; + } + + const signedEvent = await extensionSigner(event); const publishResults = await publishEventToRelays( this.pool, this.relays, @@ -8901,6 +8953,12 @@ function isNip04EncryptedWatchHistoryEvent(pointerEvent, ciphertext) { export const nostrClient = new NostrClient(); +export function requestDefaultExtensionPermissions() { + return nostrClient.ensureExtensionPermissions( + DEFAULT_NIP07_PERMISSION_METHODS, + ); +} + export const recordVideoView = (...args) => nostrClient.recordVideoView(...args); diff --git a/js/relayManager.js b/js/relayManager.js index f17fe764..f8d9db56 100644 --- a/js/relayManager.js +++ b/js/relayManager.js @@ -1,5 +1,9 @@ import { isDevMode } from "./config.js"; -import { DEFAULT_RELAY_URLS, nostrClient } from "./nostr.js"; +import { + DEFAULT_RELAY_URLS, + nostrClient, + requestDefaultExtensionPermissions, +} from "./nostr.js"; import { buildRelayListEvent } from "./nostrEventSchemas.js"; import { devLogger, userLogger } from "./utils/logger.js"; import { @@ -596,6 +600,20 @@ class RelayPreferencesManager { throw error; } + const permissionResult = await requestDefaultExtensionPermissions(); + if (!permissionResult.ok) { + userLogger.warn( + "[RelayPreferencesManager] Extension permissions denied while publishing relay preferences.", + permissionResult.error, + ); + const error = new Error( + "The NIP-07 extension must allow signing before publishing relay preferences.", + ); + error.code = "extension-permission-denied"; + error.cause = permissionResult.error; + throw error; + } + if (!window?.nostr?.signEvent) { const error = new Error( "A NIP-07 extension is required to publish relay preferences." diff --git a/js/services/authService.js b/js/services/authService.js index 0667a455..c8fec37c 100644 --- a/js/services/authService.js +++ b/js/services/authService.js @@ -7,6 +7,7 @@ import { getCurrentUserNpub, } from "../state/appState.js"; import { userLogger } from "../utils/logger.js"; +import { requestDefaultExtensionPermissions } from "../nostr.js"; import { getSavedProfiles, getActiveProfilePubkey, @@ -245,6 +246,15 @@ export default class AuthService { } 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 pubkey = typeof result === "string" ? result diff --git a/js/subscriptions.js b/js/subscriptions.js index f4ce091f..de360ca9 100644 --- a/js/subscriptions.js +++ b/js/subscriptions.js @@ -1,7 +1,8 @@ // js/subscriptions.js import { nostrClient, - convertEventToVideo as sharedConvertEventToVideo + convertEventToVideo as sharedConvertEventToVideo, + requestDefaultExtensionPermissions, } from "./nostr.js"; import { buildSubscriptionListEvent, @@ -76,6 +77,18 @@ class SubscriptionsManager { const newest = events[0]; this.subsEventId = newest.id; + const permissionResult = await requestDefaultExtensionPermissions(); + if (!permissionResult.ok) { + userLogger.warn( + "[SubscriptionsManager] Extension permissions denied while loading subscriptions; treating list as empty.", + permissionResult.error, + ); + this.subscribedPubkeys.clear(); + this.subsEventId = null; + this.loaded = true; + return; + } + let decryptedStr = ""; try { decryptedStr = await window.nostr.nip04.decrypt( @@ -155,6 +168,20 @@ class SubscriptionsManager { throw new Error("No pubkey => cannot publish subscription list."); } + const permissionResult = await requestDefaultExtensionPermissions(); + if (!permissionResult.ok) { + userLogger.warn( + "[SubscriptionsManager] Extension permissions denied while updating subscriptions.", + permissionResult.error, + ); + const error = new Error( + "The NIP-07 extension must allow encryption and signing before updating subscriptions.", + ); + error.code = "extension-permission-denied"; + error.cause = permissionResult.error; + throw error; + } + const plainObj = { subPubkeys: Array.from(this.subscribedPubkeys) }; const plainStr = JSON.stringify(plainObj); diff --git a/js/userBlocks.js b/js/userBlocks.js index 57863c4d..7562b824 100644 --- a/js/userBlocks.js +++ b/js/userBlocks.js @@ -1,5 +1,8 @@ // js/userBlocks.js -import { nostrClient } from "./nostr.js"; +import { + nostrClient, + requestDefaultExtensionPermissions, +} from "./nostr.js"; import { buildBlockListEvent, BLOCK_LIST_IDENTIFIER } from "./nostrEventSchemas.js"; import { userLogger } from "./utils/logger.js"; import { @@ -74,6 +77,17 @@ class UserBlockListManager { return; } + const permissionResult = await requestDefaultExtensionPermissions(); + if (!permissionResult.ok) { + userLogger.warn( + "[UserBlockList] Unable to load block list without extension permissions.", + permissionResult.error, + ); + this.reset(); + this.loaded = true; + return; + } + if (!window?.nostr?.nip04?.decrypt) { userLogger.warn( "[UserBlockList] nip04.decrypt is unavailable; treating block list as empty." @@ -374,6 +388,20 @@ class UserBlockListManager { } async publishBlockList(userPubkey) { + const permissionResult = await requestDefaultExtensionPermissions(); + if (!permissionResult.ok) { + userLogger.warn( + "[UserBlockList] Extension permissions denied while updating the block list.", + permissionResult.error, + ); + const err = new Error( + "The NIP-07 extension must allow encryption and signing before updating the block list.", + ); + err.code = "extension-permission-denied"; + err.cause = permissionResult.error; + throw err; + } + if (!window?.nostr?.nip04?.encrypt) { const err = new Error( "NIP-04 encryption is required to update the block list."