Refactor Nostr signer integration

This commit is contained in:
thePR0M3TH3AN committed 2025-10-21 15:13:48 -04:00
1 parent b1dbcde0f7
commit 578d2ddde4
6 files changed
+466 -229

No files matched your search

+25
View File
@@ -42,6 +42,31 @@ When managing relay metadata, use `buildRelayListEvent` so the resulting
replaceable event follows NIP-65 (`kind:10002`) with `"r"` tags describing the
read/write split.
### Active signer registry
Authentication providers should register their capabilities with the
`nostrClient` after login so every publish helper can reuse them:
```js
import { setActiveSigner } from "./nostr.js";
setActiveSigner({
type: "extension", // optional label, used to request NIP-07 permissions
pubkey, // hex or npub of the active account
signEvent: (event) => extension.signEvent(event),
nip04Encrypt: (targetHex, plaintext) => extension.nip04.encrypt(targetHex, plaintext),
nip04Decrypt: (actorHex, ciphertext) => extension.nip04.decrypt(actorHex, ciphertext),
nip44Encrypt: (targetHex, plaintext) => extension.nip44.encrypt(targetHex, plaintext),
nip44Decrypt: (actorHex, ciphertext) => extension.nip44.decrypt(actorHex, ciphertext),
});
```
`setActiveSigner` accepts any object that implements the subset of capabilities
you support. `nostrClient` will prefer the registered signer for signing and
encryption before falling back to session actors. Call `clearActiveSigner()` on
logout if your integration manages session state manually; the built-in logout
handler already does this for the default extension flow.
### Accessing raw events
`NostrClient` now keeps a lightweight cache of parsed videos alongside a
+307 -163
View File
@@ -57,6 +57,8 @@ export const DEFAULT_RELAY_URLS = Object.freeze([
const RELAY_URLS = Array.from(DEFAULT_RELAY_URLS);
let activeSigner = null;
const EVENTS_CACHE_STORAGE_KEY = "bitvid:eventsCache:v1";
const LEGACY_EVENTS_STORAGE_KEY = "bitvidEvents";
const EVENTS_CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
@@ -2432,6 +2434,119 @@ function normalizeActorKey(actor) {
return trimmed.toLowerCase();
}
export function getActiveSigner() {
return activeSigner;
}
export function clearActiveSigner() {
activeSigner = null;
return activeSigner;
}
export function setActiveSigner(candidate = null) {
if (!candidate || typeof candidate !== "object") {
activeSigner = null;
return activeSigner;
}
const pubkeyInput =
typeof candidate.pubkey === "string"
? candidate.pubkey
: typeof candidate.pubKey === "string"
? candidate.pubKey
: "";
const normalizedPubkey = normalizeActorKey(pubkeyInput);
let signEvent = null;
if (typeof candidate.signEvent === "function") {
signEvent = candidate.signEvent;
}
let nip04Encrypt = null;
if (typeof candidate.nip04Encrypt === "function") {
nip04Encrypt = candidate.nip04Encrypt;
} else if (candidate.nip04 && typeof candidate.nip04.encrypt === "function") {
nip04Encrypt = candidate.nip04.encrypt.bind(candidate.nip04);
}
let nip04Decrypt = null;
if (typeof candidate.nip04Decrypt === "function") {
nip04Decrypt = candidate.nip04Decrypt;
} else if (candidate.nip04 && typeof candidate.nip04.decrypt === "function") {
nip04Decrypt = candidate.nip04.decrypt.bind(candidate.nip04);
}
let nip44Encrypt = null;
if (typeof candidate.nip44Encrypt === "function") {
nip44Encrypt = candidate.nip44Encrypt;
} else if (candidate.nip44 && typeof candidate.nip44.encrypt === "function") {
nip44Encrypt = candidate.nip44.encrypt.bind(candidate.nip44);
} else if (
candidate.nip44?.v2 &&
typeof candidate.nip44.v2.encrypt === "function"
) {
nip44Encrypt = candidate.nip44.v2.encrypt.bind(candidate.nip44.v2);
}
let nip44Decrypt = null;
if (typeof candidate.nip44Decrypt === "function") {
nip44Decrypt = candidate.nip44Decrypt;
} else if (candidate.nip44 && typeof candidate.nip44.decrypt === "function") {
nip44Decrypt = candidate.nip44.decrypt.bind(candidate.nip44);
} else if (
candidate.nip44?.v2 &&
typeof candidate.nip44.v2.decrypt === "function"
) {
nip44Decrypt = candidate.nip44.v2.decrypt.bind(candidate.nip44.v2);
}
const signerType =
typeof candidate.type === "string" && candidate.type.trim()
? candidate.type.trim()
: null;
activeSigner = {
type: signerType,
pubkey: normalizedPubkey,
signEvent,
nip04Encrypt,
nip04Decrypt,
nip44Encrypt,
nip44Decrypt,
};
return activeSigner;
}
function resolveActiveSigner(targetActor) {
const signer = getActiveSigner();
if (!signer) {
return null;
}
const normalizedTarget = normalizeActorKey(targetActor);
const normalizedSigner = normalizeActorKey(signer.pubkey);
if (normalizedTarget && normalizedSigner && normalizedTarget !== normalizedSigner) {
return null;
}
if (
typeof signer.signEvent !== "function" &&
typeof signer.nip04Encrypt !== "function" &&
typeof signer.nip04Decrypt !== "function" &&
typeof signer.nip44Encrypt !== "function" &&
typeof signer.nip44Decrypt !== "function"
) {
return null;
}
return signer;
}
function shouldRequestExtensionPermissions(signer) {
return signer?.type === "extension";
}
function canonicalizeWatchHistoryItems(rawItems, maxItems = WATCH_HISTORY_MAX_ITEMS) {
const seen = new Map();
if (Array.isArray(rawItems)) {
@@ -3642,13 +3757,13 @@ export class NostrClient {
typeof this.pubkey === "string" && this.pubkey
? this.pubkey.toLowerCase()
: "";
const extension = window?.nostr;
const canSignWithExtension =
const activeSigner = resolveActiveSigner(this.pubkey);
const canSignWithActiveSigner =
!!normalizedLogged &&
extension &&
typeof extension.signEvent === "function";
activeSigner &&
typeof activeSigner.signEvent === "function";
if (!forceRenew && canSignWithExtension) {
if (!forceRenew && canSignWithActiveSigner) {
return normalizedLogged;
}
@@ -4283,21 +4398,29 @@ export class NostrClient {
}
const actorPubkey = resolvedActor || actorKey;
const extension = window?.nostr;
const extensionActorKey = normalizeActorKey(this.pubkey);
const canUseExtensionSign =
extension &&
typeof extension.signEvent === "function" &&
actorKey === extensionActorKey;
const normalizedLogged = normalizeActorKey(this.pubkey);
const signer = resolveActiveSigner(actorKey);
const canUseActiveSignerSign =
normalizedLogged &&
normalizedLogged === actorKey &&
signer &&
typeof signer.signEvent === "function";
const useActiveSignerEncrypt =
canUseActiveSignerSign &&
signer &&
typeof signer.nip04Encrypt === "function";
const activeSigner = canUseActiveSignerSign ? signer : null;
const encryptionSigner = useActiveSignerEncrypt ? signer : null;
const useExtensionEncrypt =
canUseExtensionSign &&
extension &&
extension.nip04 &&
typeof extension.nip04.encrypt === "function";
if (
(canUseActiveSignerSign || useActiveSignerEncrypt) &&
shouldRequestExtensionPermissions(signer)
) {
await this.ensureExtensionPermissions(DEFAULT_NIP07_PERMISSION_METHODS);
}
let privateKey = "";
if (!canUseExtensionSign) {
if (!canUseActiveSignerSign) {
if (!this.sessionActor || this.sessionActor.pubkey !== actorKey) {
const ensured = await this.ensureSessionActor();
if (normalizeActorKey(ensured) !== actorKey) {
@@ -4313,10 +4436,6 @@ export class NostrClient {
}
}
if (useExtensionEncrypt) {
await this.ensureExtensionPermissions(DEFAULT_NIP07_PERMISSION_METHODS);
}
const canonicalItems = canonicalizeWatchHistoryItems(
Array.isArray(rawItems) ? rawItems : [],
WATCH_HISTORY_MAX_ITEMS,
@@ -4387,8 +4506,8 @@ export class NostrClient {
};
const encryptChunk = async (plaintext) => {
if (useExtensionEncrypt) {
return extension.nip04.encrypt(actorPubkey, plaintext);
if (encryptionSigner) {
return encryptionSigner.nip04Encrypt(actorPubkey, plaintext);
}
const tools = await ensureNip04Tools();
if (!tools?.nip04 || typeof tools.nip04.encrypt !== "function") {
@@ -4398,8 +4517,8 @@ export class NostrClient {
};
const signEvent = async (event) => {
if (canUseExtensionSign) {
return extension.signEvent(event);
if (activeSigner) {
return activeSigner.signEvent(event);
}
return signEventWithPrivateKey(event, privateKey);
};
@@ -4843,13 +4962,24 @@ export class NostrClient {
const actorKeyIsHex = /^[0-9a-f]{64}$/.test(actorKey);
const normalizedLogged = normalizeActorKey(this.pubkey);
const signer = resolveActiveSigner(actorKey);
const canUseActiveSignerDecrypt =
normalizedLogged &&
normalizedLogged === actorKey &&
signer &&
typeof signer.nip04Decrypt === "function";
const decryptSigner = canUseActiveSignerDecrypt ? signer : null;
if (decryptSigner && shouldRequestExtensionPermissions(decryptSigner)) {
await this.ensureExtensionPermissions(DEFAULT_NIP07_PERMISSION_METHODS);
}
devLogger.info("[nostr] Fetching watch history from relays.", {
actor: resolvedActor,
forceRefresh: options.forceRefresh === true,
});
const extension = window?.nostr;
const existingEntry = this.watchHistoryCache.get(actorKey);
const now = Date.now();
const ttl = this.getWatchHistoryCacheTtlMs();
@@ -5122,26 +5252,24 @@ export class NostrClient {
expectedPlaintextFormat:
"JSON string with { version, items, snapshot, chunkIndex, totalChunks }",
});
const extensionDecrypt =
extension &&
typeof extension?.nip04?.decrypt === "function" &&
normalizeActorKey(this.pubkey) === actorKey;
if (extensionDecrypt) {
await this.ensureExtensionPermissions(DEFAULT_NIP07_PERMISSION_METHODS);
if (decryptSigner) {
devLogger.info(
"[nostr] Using logged in user's extension key to decrypt watch history chunk.",
"[nostr] Using active signer to decrypt watch history chunk.",
{
actorKey,
chunkIdentifier: context.chunkIdentifier ?? null,
eventId: context.eventId ?? null,
actorKey,
chunkIdentifier: context.chunkIdentifier ?? null,
eventId: context.eventId ?? null,
},
);
const plaintext = await decryptSigner.nip04Decrypt(actorKey, ciphertext);
devLogger.info(
"[nostr] Successfully decrypted watch history chunk via active signer.",
{
actorKey,
chunkIdentifier: context.chunkIdentifier ?? null,
eventId: context.eventId ?? null,
},
);
const plaintext = await extension.nip04.decrypt(actorKey, ciphertext);
devLogger.info("[nostr] Successfully decrypted watch history chunk via extension key.", {
actorKey,
chunkIdentifier: context.chunkIdentifier ?? null,
eventId: context.eventId ?? null,
});
return plaintext;
}
if (!this.sessionActor || this.sessionActor.pubkey !== actorKey) {
@@ -5841,28 +5969,33 @@ export class NostrClient {
let signedEvent = null;
if (
const signer = resolveActiveSigner(actorPubkey);
const canUseActiveSigner =
normalizedActor &&
normalizedActor === normalizedLogged &&
window?.nostr &&
typeof window.nostr.signEvent === "function"
) {
const permissionResult = await this.ensureExtensionPermissions(
DEFAULT_NIP07_PERMISSION_METHODS,
);
signer &&
typeof signer.signEvent === "function";
if (canUseActiveSigner) {
let permissionResult = { ok: true };
if (shouldRequestExtensionPermissions(signer)) {
permissionResult = await this.ensureExtensionPermissions(
DEFAULT_NIP07_PERMISSION_METHODS,
);
}
if (permissionResult.ok) {
try {
signedEvent = await window.nostr.signEvent(event);
signedEvent = await signer.signEvent(event);
} catch (error) {
userLogger.warn(
"[nostr] Failed to sign view event with extension:",
"[nostr] Failed to sign view event with active signer:",
error,
);
return { ok: false, error: "signing-failed", details: error };
}
} else {
userLogger.warn(
"[nostr] Extension permissions missing; signing view event with session key.",
"[nostr] Active signer permissions missing; signing view event with session key.",
permissionResult.error,
);
}
@@ -6163,6 +6296,17 @@ export class NostrClient {
this.pubkey = pubkey;
devLogger.log("Logged in with extension. Pubkey:", this.pubkey);
setActiveSigner({
type: "extension",
pubkey,
signEvent:
typeof extension.signEvent === "function"
? extension.signEvent.bind(extension)
: null,
nip04: extension.nip04,
nip44: extension.nip44,
});
const postLoginPermissions = await this.ensureExtensionPermissions(
DEFAULT_NIP07_PERMISSION_METHODS,
);
@@ -6181,6 +6325,7 @@ export class NostrClient {
logout() {
this.pubkey = null;
clearActiveSigner();
for (const timer of this.watchHistoryRepublishTimers.values()) {
if (timer && typeof timer.timer === "number") {
clearTimeout(timer.timer);
@@ -6222,33 +6367,41 @@ export class NostrClient {
return { ok: false, error: "nostr-uninitialized" };
}
const extension = window?.nostr;
if (!extension) {
return { ok: false, error: "nostr-extension-missing" };
const activeSignerCandidate =
typeof actorPubkeyOverride === "string" && actorPubkeyOverride.trim()
? resolveActiveSigner(actorPubkeyOverride)
: resolveActiveSigner(this.pubkey);
const baseActiveSigner = activeSignerCandidate || getActiveSigner();
const signer =
typeof actorPubkeyOverride === "string" && actorPubkeyOverride.trim()
? activeSignerCandidate
: baseActiveSigner
? resolveActiveSigner(baseActiveSigner.pubkey || this.pubkey)
: null;
if (!signer || typeof signer.signEvent !== "function") {
return { ok: false, error: "sign-event-unavailable" };
}
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") {
if (typeof signer.nip04Encrypt !== "function") {
return { ok: false, error: "nip04-unavailable" };
}
if (typeof extension.signEvent !== "function") {
return { ok: false, error: "sign-event-unavailable" };
if (shouldRequestExtensionPermissions(signer)) {
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,
};
}
}
let actorHex =
@@ -6260,15 +6413,8 @@ export class NostrClient {
actorHex = this.pubkey.trim();
}
if (!actorHex && typeof extension.getPublicKey === "function") {
try {
actorHex = await extension.getPublicKey();
} catch (error) {
devLogger.warn(
"[nostr] Failed to fetch actor pubkey from extension:",
error
);
}
if (!actorHex && typeof signer?.pubkey === "string") {
actorHex = signer.pubkey;
}
if (!actorHex) {
@@ -6282,7 +6428,7 @@ export class NostrClient {
let ciphertext = "";
try {
ciphertext = await nip04.encrypt(targetHex, trimmedMessage);
ciphertext = await signer.nip04Encrypt(targetHex, trimmedMessage);
} catch (error) {
return { ok: false, error: "encryption-failed", details: error };
}
@@ -6297,7 +6443,7 @@ export class NostrClient {
let signedEvent;
try {
signedEvent = await extension.signEvent(event);
signedEvent = await signer.signEvent(event);
} catch (error) {
return { ok: false, error: "signature-failed", details: error };
}
@@ -6334,12 +6480,6 @@ export class NostrClient {
relaysOverride = null,
} = {}
) {
const extension = window?.nostr;
const extensionSigner =
extension && typeof extension.signEvent === "function"
? extension.signEvent.bind(extension)
: null;
const normalizedEventPubkey =
event && typeof event.pubkey === "string"
? event.pubkey.toLowerCase()
@@ -6351,30 +6491,35 @@ export class NostrClient {
normalizedLogged &&
normalizedEventPubkey !== normalizedLogged;
const signer = resolveActiveSigner(normalizedEventPubkey || this.pubkey);
const canUseActiveSigner =
!usingSessionActor &&
signer &&
typeof signer.signEvent === "function";
let eventToSign = event;
let signedEvent = null;
let signerPubkey = null;
if (
extensionSigner &&
normalizedEventPubkey &&
normalizedEventPubkey === normalizedLogged
) {
const permissionResult = await this.ensureExtensionPermissions(
DEFAULT_NIP07_PERMISSION_METHODS,
);
if (canUseActiveSigner) {
let permissionResult = { ok: true };
if (shouldRequestExtensionPermissions(signer)) {
permissionResult = await this.ensureExtensionPermissions(
DEFAULT_NIP07_PERMISSION_METHODS,
);
}
if (permissionResult.ok) {
try {
signedEvent = await extensionSigner(event);
signedEvent = await signer.signEvent(event);
} catch (error) {
userLogger.warn(
"[nostr] Failed to sign event with extension:",
"[nostr] Failed to sign event with active signer:",
error,
);
}
} else {
userLogger.warn(
"[nostr] Extension permissions missing; falling back to session signer.",
"[nostr] Active signer permissions missing; falling back to session signer.",
permissionResult.error,
);
}
@@ -6918,31 +7063,35 @@ 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.");
const signer = resolveActiveSigner(userPubkeyLower);
if (!signer || typeof signer.signEvent !== "function") {
const error = new Error(
"An active signer 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,
if (shouldRequestExtensionPermissions(signer)) {
const permissionResult = await this.ensureExtensionPermissions(
DEFAULT_NIP07_PERMISSION_METHODS,
);
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;
if (!permissionResult.ok) {
userLogger.warn(
"[nostr] Signer permissions denied while editing a video.",
permissionResult.error,
);
const error = new Error(
"The active signer 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 extensionSigner(event);
const signedEvent = await signer.signEvent(event);
devLogger.log("Signed edited event:", signedEvent);
const publishResults = await publishEventToRelays(
@@ -7083,36 +7232,34 @@ export class NostrClient {
content: JSON.stringify(contentObject),
};
const extension = window?.nostr;
const extensionSigner =
extension && typeof extension.signEvent === "function"
? extension.signEvent.bind(extension)
: null;
if (typeof extensionSigner !== "function") {
const signer = resolveActiveSigner(pubkey);
if (!signer || typeof signer.signEvent !== "function") {
const error = new Error(
"A NIP-07 extension with signEvent support is required to revert videos.",
"An active signer 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,
if (shouldRequestExtensionPermissions(signer)) {
const permissionResult = await this.ensureExtensionPermissions(
DEFAULT_NIP07_PERMISSION_METHODS,
);
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;
if (!permissionResult.ok) {
userLogger.warn(
"[nostr] Signer permissions denied while reverting a video.",
permissionResult.error,
);
const error = new Error(
"The active signer 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 signedEvent = await signer.signEvent(event);
const publishResults = await publishEventToRelays(
this.pool,
this.relays,
@@ -7381,34 +7528,31 @@ export class NostrClient {
const deleteSummaries = [];
if (identifierRecords.length) {
const extension = window?.nostr;
const extensionSigner =
extension && typeof extension.signEvent === "function"
? extension.signEvent.bind(extension)
: null;
if (typeof extensionSigner !== "function") {
const signer = resolveActiveSigner(pubkey);
if (!signer || typeof signer.signEvent !== "function") {
const error = new Error(
"A NIP-07 extension with signEvent support is required to delete videos.",
"An active signer with signEvent support is required to delete 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 deleting videos.",
permissionResult.error,
if (shouldRequestExtensionPermissions(signer)) {
const permissionResult = await this.ensureExtensionPermissions(
DEFAULT_NIP07_PERMISSION_METHODS,
);
const error = new Error(
"The NIP-07 extension must grant decrypt and sign permissions before deleting a video.",
);
error.code = "extension-permission-denied";
error.cause = permissionResult.error;
throw error;
if (!permissionResult.ok) {
userLogger.warn(
"[nostr] Signer permissions denied while deleting videos.",
permissionResult.error,
);
const error = new Error(
"The active signer must grant decrypt and sign permissions before deleting a video.",
);
error.code = "extension-permission-denied";
error.cause = permissionResult.error;
throw error;
}
}
const chunkSize = 100;
@@ -7426,7 +7570,7 @@ export class NostrClient {
: "Delete published video events",
};
const signedDelete = await extensionSigner(deleteEvent);
const signedDelete = await signer.signEvent(deleteEvent);
const publishResults = await publishEventToRelays(
this.pool,
this.relays,
+21 -17
View File
@@ -1,6 +1,7 @@
import { isDevMode } from "./config.js";
import {
DEFAULT_RELAY_URLS,
getActiveSigner,
nostrClient,
requestDefaultExtensionPermissions,
} from "./nostr.js";
@@ -606,28 +607,31 @@ class RelayPreferencesManager {
throw error;
}
const permissionResult = await requestDefaultExtensionPermissions();
if (!permissionResult.ok) {
userLogger.warn(
"[RelayPreferencesManager] Extension permissions denied while publishing relay preferences.",
permissionResult.error,
);
const signer = getActiveSigner();
if (!signer || typeof signer.signEvent !== "function") {
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."
"An active signer with signEvent support is required to publish relay preferences."
);
error.code = "nostr-extension-missing";
throw error;
}
if (signer.type === "extension") {
const permissionResult = await requestDefaultExtensionPermissions();
if (!permissionResult.ok) {
userLogger.warn(
"[RelayPreferencesManager] Signer permissions denied while publishing relay preferences.",
permissionResult.error,
);
const error = new Error(
"The active signer must allow signing before publishing relay preferences.",
);
error.code = "extension-permission-denied";
error.cause = permissionResult.error;
throw error;
}
}
if (!nostrClient?.pool) {
const error = new Error(
"Nostr is not connected yet. Please try again once relays are ready."
@@ -649,7 +653,7 @@ class RelayPreferencesManager {
relays: entries,
});
const signedEvent = await window.nostr.signEvent(event);
const signedEvent = await signer.signEvent(event);
const targets = this.getPublishTargets(options?.relayUrls);
if (!targets.length) {
+68 -32
View File
@@ -1,5 +1,6 @@
// js/subscriptions.js
import {
getActiveSigner,
nostrClient,
convertEventToVideo as sharedConvertEventToVideo,
requestDefaultExtensionPermissions,
@@ -286,14 +287,6 @@ class SubscriptionsManager {
return { ok: false, error };
}
const nostrApi =
typeof window !== "undefined" && window && window.nostr ? window.nostr : null;
if (!nostrApi) {
const error = new Error("Nostr extension is unavailable for decrypting subscriptions.");
error.code = "nostr-extension-missing";
return { ok: false, error };
}
const decryptors = new Map();
const registerDecryptor = (scheme, handler) => {
if (!scheme || typeof handler !== "function" || decryptors.has(scheme)) {
@@ -302,28 +295,48 @@ class SubscriptionsManager {
decryptors.set(scheme, handler);
};
if (nostrApi.nip04 && typeof nostrApi.nip04.decrypt === "function") {
registerDecryptor("nip04", (payload) => nostrApi.nip04.decrypt(userPubkey, payload));
const signer = getActiveSigner();
if (signer?.nip04Decrypt) {
registerDecryptor("nip04", (payload) => signer.nip04Decrypt(userPubkey, payload));
}
const nip44 = nostrApi.nip44 && typeof nostrApi.nip44 === "object" ? nostrApi.nip44 : null;
if (nip44) {
if (typeof nip44.decrypt === "function") {
registerDecryptor("nip44", (payload) => nip44.decrypt(userPubkey, payload));
if (signer?.nip44Decrypt) {
registerDecryptor("nip44", (payload) => signer.nip44Decrypt(userPubkey, payload));
}
const nostrApi =
typeof window !== "undefined" && window && window.nostr ? window.nostr : null;
if (nostrApi) {
if (nostrApi.nip04 && typeof nostrApi.nip04.decrypt === "function") {
registerDecryptor("nip04", (payload) =>
nostrApi.nip04.decrypt(userPubkey, payload)
);
}
const nip44v2 = nip44.v2 && typeof nip44.v2 === "object" ? nip44.v2 : null;
if (nip44v2 && typeof nip44v2.decrypt === "function") {
registerDecryptor("nip44_v2", (payload) => nip44v2.decrypt(userPubkey, payload));
if (!decryptors.has("nip44")) {
registerDecryptor("nip44", (payload) => nip44v2.decrypt(userPubkey, payload));
const nip44 =
nostrApi.nip44 && typeof nostrApi.nip44 === "object" ? nostrApi.nip44 : null;
if (nip44) {
if (typeof nip44.decrypt === "function") {
registerDecryptor("nip44", (payload) => nip44.decrypt(userPubkey, payload));
}
const nip44v2 = nip44.v2 && typeof nip44.v2 === "object" ? nip44.v2 : null;
if (nip44v2 && typeof nip44v2.decrypt === "function") {
registerDecryptor("nip44_v2", (payload) =>
nip44v2.decrypt(userPubkey, payload)
);
if (!decryptors.has("nip44")) {
registerDecryptor("nip44", (payload) => nip44v2.decrypt(userPubkey, payload));
}
}
}
}
if (!decryptors.size) {
const error = new Error("No compatible decryption helpers are available.");
error.code = "subscriptions-no-decryptors";
const error = new Error(
"No active signer or extension decryptors are available for subscriptions."
);
error.code = "nostr-extension-missing";
return { ok: false, error };
}
@@ -413,17 +426,40 @@ 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 signer = getActiveSigner();
if (!signer) {
const error = new Error(
"The NIP-07 extension must allow encryption and signing before updating subscriptions.",
"An active signer is required to update subscriptions."
);
error.code = "extension-permission-denied";
error.cause = permissionResult.error;
error.code = "signer-missing";
throw error;
}
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;
}
}
if (typeof signer.nip04Encrypt !== "function") {
const error = new Error("NIP-04 encryption is required to update subscriptions.");
error.code = "nip04-missing";
throw error;
}
if (typeof signer.signEvent !== "function") {
const error = new Error("Active signer missing signEvent support.");
error.code = "sign-event-missing";
throw error;
}
@@ -440,7 +476,7 @@ class SubscriptionsManager {
*/
let cipherText = "";
try {
cipherText = await window.nostr.nip04.encrypt(userPubkey, plainStr);
cipherText = await signer.nip04Encrypt(userPubkey, plainStr);
} catch (err) {
userLogger.error("Encryption failed:", err);
throw err;
@@ -454,7 +490,7 @@ class SubscriptionsManager {
let signedEvent;
try {
signedEvent = await window.nostr.signEvent(evt);
signedEvent = await signer.signEvent(evt);
} catch (signErr) {
userLogger.error("Failed to sign subscription list:", signErr);
throw signErr;
+38 -16
View File
@@ -1,5 +1,6 @@
// js/userBlocks.js
import {
getActiveSigner,
nostrClient,
requestDefaultExtensionPermissions,
} from "./nostr.js";
@@ -548,9 +549,19 @@ class UserBlockListManager {
return;
}
const activeSigner = getActiveSigner();
const decryptWithActiveSigner =
activeSigner && typeof activeSigner.nip04Decrypt === "function";
let decrypted = "";
try {
decrypted = await window.nostr.nip04.decrypt(normalized, newest.content);
if (decryptWithActiveSigner) {
decrypted = await activeSigner.nip04Decrypt(normalized, newest.content);
} else if (window?.nostr?.nip04?.decrypt) {
decrypted = await window.nostr.nip04.decrypt(normalized, newest.content);
} else {
throw new Error("nip04-unavailable");
}
} catch (err) {
userLogger.error("[UserBlockList] Failed to decrypt block list:", err);
applyBlockedPubkeys([], {
@@ -905,21 +916,32 @@ class UserBlockListManager {
onStatus?.({ status: "publishing" });
const permissionResult = await requestDefaultExtensionPermissions();
if (!permissionResult.ok) {
userLogger.warn(
"[UserBlockList] Extension permissions denied while updating the block list.",
permissionResult.error,
);
const signer = getActiveSigner();
if (!signer) {
const err = new Error(
"The NIP-07 extension must allow encryption and signing before updating the block list.",
"An active signer is required to update the block list."
);
err.code = "extension-permission-denied";
err.cause = permissionResult.error;
err.code = "signer-missing";
throw err;
}
if (!window?.nostr?.nip04?.encrypt) {
if (signer.type === "extension") {
const permissionResult = await requestDefaultExtensionPermissions();
if (!permissionResult.ok) {
userLogger.warn(
"[UserBlockList] Signer permissions denied while updating the block list.",
permissionResult.error,
);
const err = new Error(
"The active signer must allow encryption and signing before updating the block list.",
);
err.code = "extension-permission-denied";
err.cause = permissionResult.error;
throw err;
}
}
if (typeof signer.nip04Encrypt !== "function") {
const err = new Error(
"NIP-04 encryption is required to update the block list."
);
@@ -927,9 +949,9 @@ class UserBlockListManager {
throw err;
}
if (typeof window.nostr.signEvent !== "function") {
const err = new Error("Nostr extension missing signEvent support.");
err.code = "nip04-missing";
if (typeof signer.signEvent !== "function") {
const err = new Error("Active signer missing signEvent support.");
err.code = "sign-event-missing";
throw err;
}
@@ -947,7 +969,7 @@ class UserBlockListManager {
let cipherText = "";
try {
cipherText = await window.nostr.nip04.encrypt(normalized, plaintext);
cipherText = await signer.nip04Encrypt(normalized, plaintext);
} catch (error) {
const err = new Error("Failed to encrypt block list.");
err.code = "nip04-missing";
@@ -960,7 +982,7 @@ class UserBlockListManager {
content: cipherText,
});
const signedEvent = await window.nostr.signEvent(event);
const signedEvent = await signer.signEvent(event);
const publishResults = await publishEventToRelays(
nostrClient.pool,
+7 -1
View File
@@ -52,7 +52,7 @@ const {
} = await import(
"../js/adminListStore.js"
);
const { nostrClient } = await import("../js/nostr.js");
const { nostrClient, setActiveSigner } = await import("../js/nostr.js");
const {
ADMIN_SUPER_NPUB,
ADMIN_LIST_NAMESPACE,
@@ -290,6 +290,12 @@ globalThis.window.nostr.signEvent = async (event) => ({
id: "signed-event",
});
setActiveSigner({
pubkey: superHex,
signEvent: globalThis.window.nostr.signEvent,
type: "test",
});
resetListRegistry();
registerListEvent({