Merge pull request #2268 from PR0M3TH3AN/claude/fix-moderation-lists-error-OcOyU

Add RelayBatchFetcher to NostrClient initialization
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-09 10:50:45 -05:00
commit 74d15f2946
3 files changed
+163 -15

No files matched your search

+35 -7
View File
@@ -42,6 +42,7 @@ import {
normalizeStoragePointer,
resolveStoragePointerValue,
} from "../utils/storagePointer.js";
import { RelayBatchFetcher } from "./relayBatchFetcher.js";
import {
createWatchHistoryManager,
normalizePointerInput,
@@ -218,9 +219,8 @@ import {
SignerManager,
resolveSignerCapabilities,
hydrateExtensionSignerCapabilities,
attachNipMethodAliases
attachNipMethodAliases,
} from "./managers/SignerManager.js";
import { RelayBatchFetcher } from "./relayBatchFetcher.js";
function normalizeProfileFromEvent(event) {
if (!event || !event.content) return null;
@@ -257,12 +257,8 @@ function setActiveSigner(signer) {
if (signer && typeof signer === "object") {
hydrateExtensionSignerCapabilities(signer);
attachNipMethodAliases(signer);
const capsDescriptor = Object.getOwnPropertyDescriptor(
signer,
"capabilities",
);
const capsDescriptor = Object.getOwnPropertyDescriptor(signer, "capabilities");
const isGetter = capsDescriptor && typeof capsDescriptor.get === "function";
if (!isGetter) {
signer.capabilities = resolveSignerCapabilities(signer);
}
@@ -800,6 +796,8 @@ export class NostrClient {
get sessionActorCipherClosuresPrivateKey() { return this.signerManager.sessionActorCipherClosuresPrivateKey; }
set sessionActorCipherClosuresPrivateKey(val) { this.signerManager.sessionActorCipherClosuresPrivateKey = val; }
get extensionPermissionCache() { return this.signerManager.extensionPermissionCache; }
get pool() { return this.connectionManager.pool; }
set pool(val) { this.connectionManager.pool = val; }
@@ -2979,9 +2977,39 @@ export class NostrClient {
}
async loginWithExtension(options) {
// Check if ensureExtensionPermissions was overridden on this instance (e.g. by tests).
// Only proxy when the own property differs from the prototype method to avoid recursion.
const ownDesc = Object.getOwnPropertyDescriptor(this, "ensureExtensionPermissions");
const isOverridden =
ownDesc &&
typeof ownDesc.value === "function" &&
ownDesc.value !== NostrClient.prototype.ensureExtensionPermissions;
if (isOverridden) {
const sm = this.signerManager;
const smOriginal = sm.ensureExtensionPermissions.bind(sm);
sm.ensureExtensionPermissions = (...args) => ownDesc.value.call(this, ...args);
try {
return await sm.loginWithExtension(options);
} finally {
sm.ensureExtensionPermissions = smOriginal;
}
}
return this.signerManager.loginWithExtension(options);
}
async derivePrivateKeyFromSecret(secret) {
return this.signerManager.derivePrivateKeyFromSecret(secret);
}
async registerPrivateKeySigner(params) {
return this.signerManager.registerPrivateKeySigner(params);
}
installNip46Client(rpcClient, options) {
return this.signerManager.installNip46Client(rpcClient, options);
}
async connectRemoteSigner(params) {
return this.signerManager.connectRemoteSigner(params);
}
+127 -7
View File
@@ -48,6 +48,7 @@ import {
} from "../nip46LoggingUtils.js";
import { devLogger, userLogger } from "../../utils/logger.js";
import { HEX64_REGEX } from "../../utils/hex.js";
import { createPrivateKeyCipherClosures } from "../signerHelpers.js";
import { queueSignEvent } from "../signRequestQueue.js";
import { signEventWithPrivateKey } from "../publishHelpers.js";
@@ -263,13 +264,21 @@ export class SignerManager {
this.extensionReady = false;
this.extensionPermissionsGranted = false;
this.extensionPermissionCache = new Map();
const stored = readStoredNip07Permissions();
if (stored && stored.size > 0) {
this.extensionPermissionsGranted = true;
for (const method of stored) {
this.extensionPermissionCache.set(method, true);
// Hydrate permission cache from localStorage so fresh instances
// recognise previously granted permissions without re-prompting.
try {
const storedPermissions = readStoredNip07Permissions();
if (storedPermissions.size > 0) {
for (const method of storedPermissions) {
this.extensionPermissionCache.set(method, true);
}
this.extensionPermissionsGranted = true;
}
} catch (_) {
// Storage unavailable or corrupt — start with empty cache
}
this.sessionActorCipherClosures = null;
this.sessionActorCipherClosuresPrivateKey = null;
this.remoteSignerListeners = new Set();
@@ -436,8 +445,9 @@ export class SignerManager {
return { ok: true };
}
let extension;
try {
await waitForNip07Extension();
extension = await waitForNip07Extension();
} catch (error) {
return { ok: false, error: "extension-missing" };
}
@@ -552,7 +562,9 @@ export class SignerManager {
const adapter = {
type: "extension",
pubkey: normalized,
signEvent: extension.signEvent.bind(extension),
signEvent: typeof extension.signEvent === "function"
? extension.signEvent.bind(extension)
: undefined,
nip04: extension.nip04,
nip44: extension.nip44,
};
@@ -941,4 +953,112 @@ export class SignerManager {
this.extensionPermissionsGranted = false;
this.emitRemoteSignerChange();
}
installNip46Client(rpcClient, { userPubkey } = {}) {
this.nip46Client = rpcClient;
if (userPubkey) {
this.pubkey = userPubkey;
}
if (rpcClient && typeof rpcClient.getActiveSigner === "function") {
const signer = rpcClient.getActiveSigner();
if (signer) {
this.setActiveSigner(signer);
}
}
this.emitRemoteSignerChange();
}
async derivePrivateKeyFromSecret(secret) {
if (!secret || typeof secret !== "string") {
throw new Error("A private key or nsec string is required.");
}
const trimmed = secret.trim();
const tools = (await ensureNostrTools()) || getCachedNostrTools();
// Handle hex private key
if (HEX64_REGEX.test(trimmed)) {
const privateKeyHex = trimmed.toLowerCase();
if (!tools || typeof tools.getPublicKey !== "function") {
throw new Error("Nostr tools unavailable for key derivation.");
}
const pubkey = tools.getPublicKey(privateKeyHex);
return { privateKey: privateKeyHex, pubkey };
}
// Handle nsec bech32 encoding
if (trimmed.startsWith("nsec1")) {
const nip19 = tools?.nip19;
if (!nip19 || typeof nip19.decode !== "function") {
throw new Error("Nostr tools unavailable for nsec decoding.");
}
const decoded = nip19.decode(trimmed);
if (!decoded || decoded.type !== "nsec" || !decoded.data) {
throw new Error("Invalid nsec key.");
}
const secretBytes = decoded.data;
const privateKeyHex =
typeof secretBytes === "string"
? secretBytes
: Array.from(secretBytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
if (!HEX64_REGEX.test(privateKeyHex)) {
throw new Error("Decoded nsec key is invalid.");
}
const pubkey = tools.getPublicKey(privateKeyHex);
return { privateKey: privateKeyHex, pubkey };
}
throw new Error("Unrecognized key format. Provide a hex private key or nsec string.");
}
async registerPrivateKeySigner({ privateKey, pubkey, persist, passphrase, validator } = {}) {
if (!privateKey || typeof privateKey !== "string" || !HEX64_REGEX.test(privateKey)) {
throw new Error("A valid hex private key is required.");
}
const normalizedPrivateKey = privateKey.toLowerCase();
const normalizedPubkey =
typeof pubkey === "string" && pubkey.trim()
? pubkey.trim().toLowerCase()
: null;
if (!normalizedPubkey || !HEX64_REGEX.test(normalizedPubkey)) {
throw new Error("A valid hex public key is required.");
}
if (typeof validator === "function") {
validator(normalizedPubkey);
}
const cipherClosures = await createPrivateKeyCipherClosures(normalizedPrivateKey);
const adapter = {
type: "nsec",
pubkey: normalizedPubkey,
signEvent: (event) => signEventWithPrivateKey(event, normalizedPrivateKey),
...cipherClosures,
};
this.setActiveSigner(adapter);
this.pubkey = normalizedPubkey;
if (persist && passphrase) {
try {
const encrypted = await encryptSessionPrivateKey(normalizedPrivateKey, passphrase);
persistSessionActorEntry({
pubkey: normalizedPubkey,
encrypted,
source: "nsec",
persisted: true,
});
this.sessionActor = { pubkey: normalizedPubkey, source: "nsec", persisted: true };
} catch (error) {
devLogger.warn("[nostr] Failed to persist private key signer:", error);
}
}
return normalizedPubkey;
}
}
+1 -1
View File
@@ -15,7 +15,7 @@ test("ensureActiveSignerForPubkey prefers concurrent login over late extension i
const client = new NostrClient();
client.pubkey = HEX_PUBKEY;
// Pre-condition: Cache permissions to trigger the waitForNip07Extension logic
client.extensionPermissionCache.add("sign_event");
client.extensionPermissionCache.set("sign_event", true);
// Mock a NIP-46 signer that might be set during the race
const mockNip46Signer = {