Add shared NIP-07 permission gate

This commit is contained in:
thePR0M3TH3AN committed 2026-01-28 13:57:40 -05:00
1 parent 16f4b6b9fa
commit cf9082b83f
10 files changed
+185 -67

No files matched your search

+57 -22
View File
@@ -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 };
+15 -4
View File
@@ -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) {
+7 -1
View File
@@ -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,
+30 -8
View File
@@ -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) {
+15 -4
View File
@@ -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 {
+15 -4
View File
@@ -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 {
+12 -2
View File
@@ -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.", {
+1
View File
@@ -7,6 +7,7 @@
export {
nostrClient,
hasDefaultExtensionPermissions,
requestDefaultExtensionPermissions,
} from "./nostr/defaultClient.js";
+4
View File
@@ -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;
}
+29 -22
View File
@@ -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;
}
}
}