mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
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:
8 files changed
+298
-83
No files matched your search
+12
-1
@@ -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
@@ -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
@@ -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."
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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."
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user