Serialize nostr signEvent requests

This commit is contained in:
thePR0M3TH3AN committed 2026-01-18 10:58:19 -05:00
1 parent 745eb6ac48
commit 95bb462508
7 files changed
+155 -9

No files matched your search

+5 -4
View File
@@ -173,6 +173,7 @@ import {
import { createNip07Adapter } from "./adapters/nip07Adapter.js";
import { createNsecAdapter } from "./adapters/nsecAdapter.js";
import { createNip46Adapter } from "./adapters/nip46Adapter.js";
import { queueSignEvent } from "./signRequestQueue.js";
function normalizeProfileFromEvent(event) {
if (!event || !event.content) return null;
@@ -4484,7 +4485,7 @@ export class NostrClient {
let signedEvent;
try {
signedEvent = await signer.signEvent(event);
signedEvent = await queueSignEvent(signer, event);
} catch (error) {
return { ok: false, error: "signature-failed", details: error };
}
@@ -5065,7 +5066,7 @@ export class NostrClient {
}
try {
const signedEvent = await signer.signEvent(event);
const signedEvent = await queueSignEvent(signer, event);
devLogger.log("Signed edited event:", signedEvent);
const publishResults = await publishEventToRelays(
@@ -5238,7 +5239,7 @@ export class NostrClient {
}
}
const signedEvent = await signer.signEvent(event);
const signedEvent = await queueSignEvent(signer, event);
const publishResults = await publishEventToRelays(
this.pool,
this.relays,
@@ -5551,7 +5552,7 @@ export class NostrClient {
: "Delete published video events",
};
const signedDelete = await signer.signEvent(deleteEvent);
const signedDelete = await queueSignEvent(signer, deleteEvent);
const publishResults = await publishEventToRelays(
this.pool,
this.relays,
+4 -1
View File
@@ -11,6 +11,7 @@ import { devLogger, userLogger } from "../utils/logger.js";
import { LRUCache } from "../utils/lruCache.js";
import { CACHE_POLICIES } from "./cachePolicies.js";
import { isSessionActor } from "./sessionActor.js";
import { queueSignEvent } from "./signRequestQueue.js";
const COMMENT_EVENT_SCHEMA = getNostrEventSchema(NOTE_TYPES.VIDEO_COMMENT);
const CACHE_POLICY = CACHE_POLICIES[NOTE_TYPES.VIDEO_COMMENT];
@@ -1221,7 +1222,9 @@ export async function publishComment(
}
try {
signedEvent = await signer.signEvent(event);
signedEvent = await queueSignEvent(signer, event, {
timeoutMs: options?.timeoutMs,
});
} catch (error) {
userLogger.warn(
"[nostr] Failed to sign comment event with active signer:",
+4 -1
View File
@@ -19,6 +19,7 @@ import {
import { DEFAULT_NIP07_PERMISSION_METHODS } from "./nip07Permissions.js";
import { devLogger, userLogger } from "../utils/logger.js";
import { logRebroadcastCountFailure } from "./countDiagnostics.js";
import { queueSignEvent } from "./signRequestQueue.js";
const REBROADCAST_GUARD_PREFIX = "bitvid:rebroadcast:v1";
const rebroadcastAttemptMemory = new Map();
@@ -359,7 +360,9 @@ export async function signAndPublishEvent({
}
if (permissionResult.ok) {
try {
signedEvent = await signer.signEvent(event);
signedEvent = await queueSignEvent(signer, event, {
timeoutMs: options.timeoutMs,
});
} catch (error) {
userLogger.warn(
"[nostr] Failed to sign event with active signer:",
+4 -1
View File
@@ -6,6 +6,7 @@ import { devLogger, userLogger } from "../utils/logger.js";
import { LRUCache } from "../utils/lruCache.js";
import { CACHE_POLICIES } from "./cachePolicies.js";
import { NOTE_TYPES } from "../nostrEventSchemas.js";
import { queueSignEvent } from "./signRequestQueue.js";
const CACHE_POLICY = CACHE_POLICIES[NOTE_TYPES.VIDEO_REACTION];
const reactionCache = new LRUCache({ maxSize: 100 });
@@ -441,7 +442,9 @@ export async function publishVideoReaction(
}
if (permissionResult.ok) {
try {
signedEvent = await signer.signEvent(event);
signedEvent = await queueSignEvent(signer, event, {
timeoutMs: options?.timeoutMs,
});
} catch (error) {
userLogger.warn(
"[nostr] Failed to sign reaction event with active signer:",
+130
View File
@@ -0,0 +1,130 @@
import { devLogger } from "../utils/logger.js";
const DEFAULT_SIGN_TIMEOUT_MS = 20_000;
const signQueues = new WeakMap();
const SIGN_ERROR_CODES = new Set([
"permission-denied",
"timeout",
"not-capable",
"signer-disconnected",
]);
function resolveTimeoutMs(timeoutMs) {
const candidate = Number(timeoutMs);
if (Number.isFinite(candidate) && candidate > 0) {
return Math.floor(candidate);
}
return DEFAULT_SIGN_TIMEOUT_MS;
}
function toError(value, message) {
if (value instanceof Error) {
return value;
}
const error = new Error(message);
error.details = value;
return error;
}
function normalizeSignError(error) {
const normalized = toError(error, "signEvent failed");
const existingCode = typeof normalized.code === "string" ? normalized.code : "";
if (SIGN_ERROR_CODES.has(existingCode)) {
return normalized;
}
const message = typeof normalized.message === "string"
? normalized.message.toLowerCase()
: "";
if (
existingCode === "extension-permission-denied" ||
message.includes("permission") ||
message.includes("denied") ||
message.includes("rejected")
) {
normalized.code = "permission-denied";
return normalized;
}
if (
existingCode === "nostr-extension-missing" ||
message.includes("disconnected") ||
message.includes("not connected") ||
message.includes("connection closed") ||
message.includes("extension missing")
) {
normalized.code = "signer-disconnected";
return normalized;
}
if (existingCode === "sign-event-unavailable") {
normalized.code = "not-capable";
return normalized;
}
return normalized;
}
function createSignError(code, originalError) {
const error = toError(
originalError,
originalError?.message || `signEvent failed with ${code}`,
);
error.code = code;
if (originalError && originalError !== error) {
error.details = originalError;
}
return error;
}
function runWithTimeout(operation, timeoutMs) {
const resolvedTimeout = resolveTimeoutMs(timeoutMs);
let timeoutId;
let timedOut = false;
const signPromise = Promise.resolve().then(operation);
const timeoutPromise = new Promise((_, reject) => {
timeoutId = setTimeout(() => {
timedOut = true;
reject(createSignError("timeout", new Error(
`signEvent timed out after ${resolvedTimeout}ms`,
)));
}, resolvedTimeout);
});
return Promise.race([signPromise, timeoutPromise]).finally(() => {
if (timeoutId) {
clearTimeout(timeoutId);
}
if (timedOut) {
void signPromise.catch((error) => {
devLogger.warn("[nostr] signEvent resolved after timeout:", error);
});
}
});
}
export async function queueSignEvent(signer, event, options = {}) {
if (!signer || typeof signer !== "object") {
throw createSignError("not-capable", new Error("signer-unavailable"));
}
if (typeof signer.signEvent !== "function") {
throw createSignError("not-capable", new Error("sign-event-unavailable"));
}
const queue = signQueues.get(signer) || Promise.resolve();
const runSign = async () => runWithTimeout(
() => signer.signEvent(event),
options.timeoutMs,
);
const queued = queue.then(runSign, runSign);
signQueues.set(signer, queued.catch(() => {}));
try {
return await queued;
} catch (error) {
throw normalizeSignError(error);
}
}
+4 -1
View File
@@ -13,6 +13,7 @@ import { RELAY_URLS } from "./toolkit.js";
import { normalizePointerInput } from "./watchHistory.js";
import { devLogger, userLogger } from "../utils/logger.js";
import { logViewCountFailure } from "./countDiagnostics.js";
import { queueSignEvent } from "./signRequestQueue.js";
const VIEW_EVENT_GUARD_PREFIX = "bitvid:viewed";
@@ -793,7 +794,9 @@ export async function publishViewEvent(
}
if (permissionResult.ok) {
try {
signedEvent = await signer.signEvent(event);
signedEvent = await queueSignEvent(signer, event, {
timeoutMs: options?.timeoutMs,
});
} catch (error) {
userLogger.warn(
"[nostr] Failed to sign view event with active signer:",
+4 -1
View File
@@ -23,6 +23,7 @@ import {
import { DEFAULT_NIP07_PERMISSION_METHODS } from "./nip07Permissions.js";
import { devLogger, userLogger } from "../utils/logger.js";
import { profileCache } from "../state/profileCache.js";
import { queueSignEvent } from "./signRequestQueue.js";
/**
* Domain utilities for watch-history interactions. This module owns pointer
@@ -1452,7 +1453,9 @@ class WatchHistoryManager {
const signEvent = async (event) => {
if (activeSigner) {
return activeSigner.signEvent(event);
return queueSignEvent(activeSigner, event, {
timeoutMs: options.timeoutMs,
});
}
return this.deps.signEventWithPrivateKey(event, privateKey);
};