Merge pull request #588 from PR0M3TH3AN/codex/export-default_nip07_permission_methods

Ensure NIP-07 permissions are granted before signing and encrypting
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-15 22:53:41 -04:00
commit cb6fc5e619
8 files changed
+298 -83

No files matched your search

+12 -1
View File
@@ -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(
+134 -76
View File
@@ -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);
+19 -1
View File
@@ -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."
+10
View File
@@ -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
+28 -1
View File
@@ -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);
+5 -1
View File
@@ -285,7 +285,11 @@ export default class WatchHistoryController {
? latest.map(normalizeEntry).filter(Boolean)
: [];
} catch (error) {
this.showError("Failed to load watch history. Please try again.");
const message =
error?.code === "watch-history-extension-permission-denied"
? error.message
: "Failed to load watch history. Please try again.";
this.showError(message);
if (error && typeof error === "object") {
error.handled = true;
}
+29 -1
View File
@@ -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."
+61 -2
View File
@@ -5,6 +5,7 @@ import {
normalizePointerInput,
pointerKey,
normalizeActorKey,
requestDefaultExtensionPermissions,
} from "./nostr.js";
import {
WATCH_HISTORY_CACHE_TTL_MS,
@@ -98,6 +99,56 @@ function getSessionActorKey() {
return normalizeActorKey(nostrClient?.sessionActor?.pubkey);
}
function shouldUseExtensionForHistory(actorKey) {
const normalizedActor = normalizeActorKey(actorKey);
if (!normalizedActor) {
return false;
}
const loggedActor = normalizeActorKey(nostrClient?.pubkey);
if (!loggedActor || loggedActor !== normalizedActor) {
return false;
}
if (typeof window === "undefined") {
return false;
}
const extension = window.nostr;
if (!extension || !extension.nip04) {
return false;
}
return typeof extension.nip04.decrypt === "function";
}
async function ensureWatchHistoryExtensionPermissions(actorKey) {
if (!shouldUseExtensionForHistory(actorKey)) {
return { ok: true };
}
const permissionResult = await requestDefaultExtensionPermissions();
if (permissionResult.ok) {
return { ok: true };
}
const message =
"Approve your NIP-07 extension to decrypt encrypted watch history.";
const error = new Error(message);
error.code = "watch-history-extension-permission-denied";
error.cause = permissionResult.error;
userLogger.warn(
"[watchHistoryService] Extension denied decrypt permission required for watch history.",
{
actor: normalizeActorKey(actorKey) || null,
error: permissionResult.error,
},
);
return { ok: false, error };
}
function resolveEffectiveActorKey(actorInput) {
const supplied =
typeof actorInput === "string" && actorInput.trim()
@@ -1252,8 +1303,16 @@ function scheduleWatchHistoryRefresh(actorKey, cacheEntry = {}) {
return cacheEntry.promise;
}
const promise = nostrClient
.resolveWatchHistory(actorKey, { forceRefresh: true })
const promise = Promise.resolve()
.then(async () => {
const permissionResult = await ensureWatchHistoryExtensionPermissions(
actorKey,
);
if (!permissionResult.ok) {
throw permissionResult.error;
}
return nostrClient.resolveWatchHistory(actorKey, { forceRefresh: true });
})
.then((resolvedItems) => updateFingerprintCache(actorKey, resolvedItems))
.then(() => {
const latest = state.fingerprintCache.get(actorKey);