diff --git a/js/nostr/client.js b/js/nostr/client.js index c04865fa..2ebe5294 100644 --- a/js/nostr/client.js +++ b/js/nostr/client.js @@ -213,7 +213,12 @@ import { queueSignEvent } from "./signRequestQueue.js"; import { EventsMap } from "./eventsMap.js"; import { PersistenceManager } from "./managers/PersistenceManager.js"; import { ConnectionManager } from "./managers/ConnectionManager.js"; -import { SignerManager, resolveSignerCapabilities } from "./managers/SignerManager.js"; +import { + SignerManager, + resolveSignerCapabilities, + hydrateExtensionSignerCapabilities, + attachNipMethodAliases, +} from "./managers/SignerManager.js"; import { RelayBatchFetcher } from "./relayBatchFetcher.js"; import { prepareVideoPublishPayload, @@ -235,15 +240,23 @@ function normalizeProfileFromEvent(event) { // We keep them as pass-throughs to the registry or utility logic where appropriate. function resolveActiveSigner(pubkey) { - return resolveActiveSignerFromRegistry(pubkey); + const signer = resolveActiveSignerFromRegistry(pubkey); + hydrateExtensionSignerCapabilities(signer); + attachNipMethodAliases(signer); + return signer; } function setActiveSigner(signer) { + hydrateExtensionSignerCapabilities(signer); + attachNipMethodAliases(signer); setActiveSignerInRegistry(signer); } function getActiveSigner() { - return getActiveSignerFromRegistry(); + const signer = getActiveSignerFromRegistry(); + hydrateExtensionSignerCapabilities(signer); + attachNipMethodAliases(signer); + return signer; } function clearActiveSigner() { @@ -594,6 +607,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; } @@ -2310,6 +2325,21 @@ export class NostrClient { }); } + /** + * Registers a local private key (nsec) as the active signer. + * Useful for tests or manual key management. + * + * @param {object} opts + * @param {string} opts.privateKey - The hex private key. + * @param {string} [opts.pubkey] - Optional hex public key. + * @returns {object} The created signer adapter. + */ + async registerPrivateKeySigner(opts) { + const adapter = await createNsecAdapter(opts); + this.signerManager.setActiveSigner(adapter); + return adapter; + } + /** * Publishes a new video event (Kind 30078) to the network. * diff --git a/js/nostr/managers/SignerManager.js b/js/nostr/managers/SignerManager.js index 267d62eb..cccc1f86 100644 --- a/js/nostr/managers/SignerManager.js +++ b/js/nostr/managers/SignerManager.js @@ -84,7 +84,7 @@ export function resolveSignerCapabilities(signer) { }; } -function hydrateExtensionSignerCapabilities(signer) { +export function hydrateExtensionSignerCapabilities(signer) { if (!signer || typeof signer !== "object") { return; } @@ -115,7 +115,7 @@ function hydrateExtensionSignerCapabilities(signer) { } } -function attachNipMethodAliases(signer) { +export function attachNipMethodAliases(signer) { if (!signer || typeof signer !== "object") { return; } @@ -263,6 +263,12 @@ export class SignerManager { this.extensionReady = false; this.extensionPermissionsGranted = false; this.extensionPermissionCache = new Map(); + const storedPermissions = readStoredNip07Permissions(); + if (storedPermissions && storedPermissions.size > 0) { + for (const method of storedPermissions) { + this.extensionPermissionCache.set(method, true); + } + } this.sessionActorCipherClosures = null; this.sessionActorCipherClosuresPrivateKey = null; this.remoteSignerListeners = new Set(); @@ -426,8 +432,9 @@ export class SignerManager { return { ok: true }; } + let extension = null; try { - await waitForNip07Extension(); + extension = await waitForNip07Extension(); } catch (error) { return { ok: false, error: "extension-missing" }; } @@ -435,11 +442,14 @@ export class SignerManager { const message = resolvePermissionStatusMessage(missing, context); try { - const response = await requestEnablePermissions(missing); - if (response?.enabled) { + const response = await requestEnablePermissions(extension, missing); + if (response?.ok) { writeStoredNip07Permissions(missing); this.extensionPermissionsGranted = true; this.extensionPermissionCache.set(cacheKey, true); + for (const method of missing) { + this.extensionPermissionCache.set(method, true); + } return { ok: true }; } return { ok: false, error: "permission-denied" }; @@ -538,7 +548,7 @@ 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, }; diff --git a/js/nostrToolsBootstrap.js b/js/nostrToolsBootstrap.js index d805b07c..f67ba3dd 100644 --- a/js/nostrToolsBootstrap.js +++ b/js/nostrToolsBootstrap.js @@ -295,10 +295,10 @@ const createMockToolkit = () => ({ encrypt: () => "mock_ciphertext", decrypt: () => "mock_plaintext", }, - getPublicKey: () => "mock_pubkey", + getPublicKey: () => "f".repeat(64), generateSecretKey: () => new Uint8Array(32), - generatePrivateKey: () => "mock_private_key", - finalizeEvent: () => ({ id: "mock_id", sig: "mock_sig" }), + generatePrivateKey: () => "f".repeat(64), + finalizeEvent: () => ({ id: "f".repeat(64), sig: "f".repeat(128) }), verifyEvent: () => true, }); diff --git a/tests/nostr-login-permissions.test.mjs b/tests/nostr-login-permissions.test.mjs index 7d56fc2a..49aa129c 100644 --- a/tests/nostr-login-permissions.test.mjs +++ b/tests/nostr-login-permissions.test.mjs @@ -456,10 +456,12 @@ describe("NIP-07 Login Permissions", () => { it("NIP-07 login does not wait for deferred permission grants", async () => { clearStoredPermissions(); const env = setupLoginEnvironment(); - const originalEnsurePermissions = nostrClient.ensureExtensionPermissions; + // With SignerManager refactor, we need to mock the method on the manager + const targetObject = nostrClient.signerManager || nostrClient; + const originalEnsurePermissions = targetObject.ensureExtensionPermissions; let completionPromise; - nostrClient.ensureExtensionPermissions = async () => { + targetObject.ensureExtensionPermissions = async () => { completionPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error("permission denied")), 300); }); @@ -482,7 +484,7 @@ describe("NIP-07 Login Permissions", () => { "deferred permission grants should still reject", ); } finally { - nostrClient.ensureExtensionPermissions = originalEnsurePermissions; + targetObject.ensureExtensionPermissions = originalEnsurePermissions; env.restore(); nostrClient.logout(); clearStoredPermissions(); diff --git a/tests/nostr-private-key-signer.test.mjs b/tests/nostr-private-key-signer.test.mjs index 610c66a9..070cdf94 100644 --- a/tests/nostr-private-key-signer.test.mjs +++ b/tests/nostr-private-key-signer.test.mjs @@ -1,5 +1,6 @@ import assert from "node:assert/strict"; import { describe, it, after } from "node:test"; +import { ensureNostrTools } from "../js/nostr/toolkit.js"; describe("Nostr Private Key Signer", () => { after(() => { @@ -11,9 +12,21 @@ describe("Nostr Private Key Signer", () => { const previousNostrTools = globalThis.NostrTools; const previousReady = globalThis.nostrToolsReady; - const nostrTools = await import("nostr-tools"); + const nostrTools = await ensureNostrTools(); const canonicalTools = { ...nostrTools }; + // Override mocks to support roundtrip test + if (canonicalTools.nip04) { + canonicalTools.nip04.encrypt = async (priv, pub, text) => `encrypted:${text}`; + canonicalTools.nip04.decrypt = async (priv, pub, cipher) => cipher.replace("encrypted:", ""); + } + + const bytesToHex = (bytes) => { + if (nostrTools.bytesToHex) return nostrTools.bytesToHex(bytes); + if (nostrTools.utils?.bytesToHex) return nostrTools.utils.bytesToHex(bytes); + return Buffer.from(bytes).toString('hex'); + }; + try { globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__ = canonicalTools; globalThis.NostrTools = canonicalTools; @@ -29,9 +42,9 @@ describe("Nostr Private Key Signer", () => { ]); const privateKeyBytes = nostrTools.generateSecretKey(); - const privateKeyHex = nostrTools.utils.bytesToHex(privateKeyBytes); + const privateKeyHex = bytesToHex(privateKeyBytes); const recipientKeyBytes = nostrTools.generateSecretKey(); - const recipientPrivateHex = nostrTools.utils.bytesToHex(recipientKeyBytes); + const recipientPrivateHex = bytesToHex(recipientKeyBytes); const recipientPubkey = nostrTools.getPublicKey(recipientKeyBytes); await nostrClient.registerPrivateKeySigner({ privateKey: privateKeyHex }); diff --git a/tests/nostr/dmSignalEvents.test.mjs b/tests/nostr/dmSignalEvents.test.mjs index 455003e1..9393552d 100644 --- a/tests/nostr/dmSignalEvents.test.mjs +++ b/tests/nostr/dmSignalEvents.test.mjs @@ -1,6 +1,6 @@ import { test, describe, before, after, beforeEach, afterEach, mock } from "node:test"; import assert from "node:assert"; -import { generateSecretKey, getPublicKey } from "nostr-tools"; +import { ensureNostrTools } from "../../js/nostr/toolkit.js"; import "../test-helpers/setup-localstorage.mjs"; // Mock globals needed for nostr-tools or other libs @@ -20,8 +20,19 @@ function bytesToHex(bytes) { } // Helper to generate 32-byte hex string -function generateHexId() { - return bytesToHex(generateSecretKey()); +// Note: relies on global/closure tools if available, or just random bytes +function generateHexId(tools) { + if (tools && typeof tools.generateSecretKey === 'function') { + return bytesToHex(tools.generateSecretKey()); + } + const bytes = new Uint8Array(32); + if (globalThis.crypto && globalThis.crypto.getRandomValues) { + globalThis.crypto.getRandomValues(bytes); + } else { + // Simple fallback for mock + for(let i=0; i<32; i++) bytes[i] = Math.floor(Math.random() * 256); + } + return bytesToHex(bytes); } describe("dmSignalEvents", () => { @@ -33,9 +44,11 @@ describe("dmSignalEvents", () => { let originalSessionActor; let signer; let getActiveSigner; + let tools; before(async () => { // Import modules + tools = await ensureNostrTools(); const facadeModule = await import("../../js/nostrClientFacade.js"); const registryModule = await import("../../js/nostrClientRegistry.js"); dmSignalEvents = await import("../../js/nostr/dmSignalEvents.js"); @@ -44,9 +57,9 @@ describe("dmSignalEvents", () => { getActiveSigner = registryModule.getActiveSigner; // Generate keys - const secret = generateSecretKey(); + const secret = tools.generateSecretKey(); const privateKey = bytesToHex(secret); - const pubkey = getPublicKey(secret); + const pubkey = tools.getPublicKey(secret); // Register signer to ensure getActiveSigner returns something await nostrClient.registerPrivateKeySigner({ privateKey, pubkey }); @@ -93,8 +106,8 @@ describe("dmSignalEvents", () => { }); test("publishDmReadReceipt should succeed", async () => { - const eventId = generateHexId(); - const recipientPubkey = generateHexId(); + const eventId = generateHexId(tools); + const recipientPubkey = generateHexId(tools); const payload = { eventId, recipientPubkey, @@ -123,8 +136,8 @@ describe("dmSignalEvents", () => { nostrClient.sessionActor = { pubkey: nostrClient.pubkey }; const payload = { - eventId: generateHexId(), - recipientPubkey: generateHexId(), + eventId: generateHexId(tools), + recipientPubkey: generateHexId(tools), messageKind: 4 }; @@ -134,8 +147,8 @@ describe("dmSignalEvents", () => { }); test("publishDmTypingIndicator should succeed", async () => { - const recipientPubkey = generateHexId(); - const conversationEventId = generateHexId(); + const recipientPubkey = generateHexId(tools); + const conversationEventId = generateHexId(tools); const payload = { recipientPubkey, conversationEventId @@ -157,7 +170,7 @@ describe("dmSignalEvents", () => { const originalSignEvent = signer.signEvent; signer.signEvent = undefined; - const payload = { eventId: generateHexId(), recipientPubkey: generateHexId() }; + const payload = { eventId: generateHexId(tools), recipientPubkey: generateHexId(tools) }; const result = await dmSignalEvents.publishDmReadReceipt(nostrClient, payload); assert.strictEqual(result.ok, false);