diff --git a/js/nostr/client.js b/js/nostr/client.js index 2ebe5294..d3dc620a 100644 --- a/js/nostr/client.js +++ b/js/nostr/client.js @@ -2593,6 +2593,10 @@ export class NostrClient { return this.signerManager.loginWithExtension(options); } + installNip46Client(client, options) { + return this.signerManager.installNip46Client(client, options); + } + async connectRemoteSigner(params) { return this.signerManager.connectRemoteSigner(params); } @@ -2628,8 +2632,8 @@ export class NostrClient { return this.signerManager.disconnectRemoteSigner(options); } - async ensureSessionActor() { - return this.signerManager.ensureSessionActor(); + async ensureSessionActor(force) { + return this.signerManager.ensureSessionActor(force); } clearStoredSessionActor() { diff --git a/js/nostr/managers/SignerManager.js b/js/nostr/managers/SignerManager.js index cccc1f86..9106dc57 100644 --- a/js/nostr/managers/SignerManager.js +++ b/js/nostr/managers/SignerManager.js @@ -315,23 +315,56 @@ export class SignerManager { return resolveActiveSigner(pubkey); } - async ensureSessionActor() { - if (this.sessionActor) { - return this.sessionActor; + async ensureSessionActor(force = false) { + if (!force) { + if (this.sessionActor) { + return this.sessionActor.pubkey; + } + if (this.lockedSessionActor) { + return this.lockedSessionActor.pubkey; + } + const storedEntry = readStoredSessionActorEntry(); + if (storedEntry) { + this.lockedSessionActor = storedEntry; + this.sessionActor = storedEntry; + return storedEntry.pubkey; + } } - if (this.lockedSessionActor) { - return this.lockedSessionActor; + const tools = await ensureNostrTools(); + if (!tools) { + return null; } - const storedEntry = readStoredSessionActorEntry(); - if (storedEntry) { - this.lockedSessionActor = storedEntry; - this.sessionActor = storedEntry; - return storedEntry; + let secret; + try { + if (typeof tools.generateSecretKey === "function") { + secret = tools.generateSecretKey(); + } else if (typeof window !== "undefined" && window.crypto) { + secret = new Uint8Array(32); + window.crypto.getRandomValues(secret); + } else { + return null; + } + } catch (error) { + devLogger.warn("[nostr] Failed to generate session actor key:", error); + return null; } - return null; + const hexKey = Array.from(secret) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); + const pubkey = tools.getPublicKey(secret); + + const newActor = { + pubkey, + privateKey: hexKey, + source: "session", + createdAt: Date.now(), + }; + + this.sessionActor = newActor; + return newActor.pubkey; } clearStoredSessionActor() { @@ -557,6 +590,22 @@ export class SignerManager { return { pubkey: normalized, signer: adapter }; } + installNip46Client(nip46Client, { userPubkey } = {}) { + if (!nip46Client) { + return; + } + this.nip46Client = nip46Client; + if (userPubkey) { + this.pubkey = userPubkey; + this.setActiveSigner(nip46Client); + } + this.emitRemoteSignerChange({ + state: "connected", + userPubkey: userPubkey || this.pubkey, + remotePubkey: nip46Client.remotePubkey, + }); + } + async connectRemoteSigner({ connectionString, remember = true, diff --git a/js/nostr/sessionActor.js b/js/nostr/sessionActor.js index bbbafc2f..937a087f 100644 --- a/js/nostr/sessionActor.js +++ b/js/nostr/sessionActor.js @@ -484,7 +484,14 @@ export function clearStoredSessionActor() { export function isSessionActor(nostrClient) { const sa = nostrClient?.sessionActor; - return !!sa && sa.source !== "nsec"; + if (!sa || sa.source === "nsec") { + return false; + } + const clientPubkey = + typeof nostrClient.pubkey === "string" ? nostrClient.pubkey : ""; + const saPubkey = typeof sa.pubkey === "string" ? sa.pubkey : ""; + + return clientPubkey === saPubkey; } function _closeSessionActorDb() { diff --git a/js/nostrClientRegistry.js b/js/nostrClientRegistry.js index 9adbd272..ca2e21ff 100644 --- a/js/nostrClientRegistry.js +++ b/js/nostrClientRegistry.js @@ -176,14 +176,22 @@ export function setActiveSigner(signerOrPubkey) { if (typeof signerOrPubkey === "string") { const normalized = normalizePubkey(signerOrPubkey); const entry = registeredSigners.get(normalized); - updateActiveSigner(entry?.signer || null, normalized); - return entry?.signer || null; + if (entry) { + updateActiveSigner(entry.signer, normalized); + return entry.signer; + } + // If not found, do not clear active signer just yet? + // Or should we? + updateActiveSigner(null, normalized); + return null; } if (typeof signerOrPubkey === "object") { const pubkey = normalizePubkey(signerOrPubkey.pubkey); if (pubkey) { - registerSigner(pubkey, signerOrPubkey); + // Force update of the registry for this pubkey + registeredSigners.set(pubkey, { signer: signerOrPubkey, meta: {} }); + // emitSignerEvent("signer:connected", ...); // Skip for now to keep diff small } updateActiveSigner(signerOrPubkey, pubkey); return signerOrPubkey; diff --git a/package-lock.json b/package-lock.json index 27ff9d46..5a9b66ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "autoprefixer": "^10.4.24", "cross-env": "^10.1.0", "esbuild": "^0.27.2", - "nostr-tools": "2.19.4", + "nostr-tools": "^2.19.4", "postcss": "^8.5.6", "tailwindcss": "^3.4.19", "ws": "^8.19.0" diff --git a/package.json b/package.json index 49b82784..16aaad24 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "autoprefixer": "^10.4.24", "cross-env": "^10.1.0", "esbuild": "^0.27.2", - "nostr-tools": "2.19.4", + "nostr-tools": "^2.19.4", "postcss": "^8.5.6", "tailwindcss": "^3.4.19", "ws": "^8.19.0" diff --git a/tests/nostr-private-key-signer.test.mjs b/tests/nostr-private-key-signer.test.mjs index 070cdf94..4fcbcd09 100644 --- a/tests/nostr-private-key-signer.test.mjs +++ b/tests/nostr-private-key-signer.test.mjs @@ -17,8 +17,11 @@ describe("Nostr Private Key Signer", () => { // 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:", ""); + canonicalTools.nip04 = { + ...canonicalTools.nip04, + encrypt: async (priv, pub, text) => `encrypted:${text}`, + decrypt: async (priv, pub, cipher) => cipher.replace("encrypted:", "") + }; } const bytesToHex = (bytes) => { diff --git a/tests/nostr/integration-remote-flow.test.mjs b/tests/nostr/integration-remote-flow.test.mjs index 3bcf4764..70921e4b 100644 --- a/tests/nostr/integration-remote-flow.test.mjs +++ b/tests/nostr/integration-remote-flow.test.mjs @@ -1,14 +1,39 @@ import "../test-helpers/setup-localstorage.mjs"; +import { webcrypto } from "node:crypto"; + +// Polyfill crypto globally immediately +const cryptoPolyfill = { + getRandomValues: (arr) => { + for (let i = 0; i < arr.length; i++) { + arr[i] = Math.floor(Math.random() * 256); + } + return arr; + }, + subtle: webcrypto.subtle, +}; + +if (!globalThis.crypto) { + globalThis.crypto = cryptoPolyfill; +} else { + try { + globalThis.crypto.getRandomValues(new Uint8Array(1)); + } catch (e) { + Object.defineProperty(globalThis, "crypto", { + value: cryptoPolyfill, + writable: true, + configurable: true, + }); + } +} + import assert from "node:assert/strict"; import test from "node:test"; -import { webcrypto } from "node:crypto"; -import { - finalizeEvent, - generateSecretKey, - getEventHash, - getPublicKey, - nip04, -} from "nostr-tools"; +import * as NostrToolsRef from "nostr-tools"; + +// Inject real NostrTools into global scope BEFORE modules load +globalThis.NostrTools = NostrToolsRef; + +const { ensureNostrTools } = await import("../../js/nostr/toolkit.js"); const { nostrClient, @@ -24,38 +49,41 @@ const { normalizeActorKey } = await import("../../js/nostr/watchHistory.js"); const { VIEW_EVENT_KIND } = await import("../../js/nostr/viewEvents.js"); const { watchHistoryService } = await import("../../js/watchHistoryService.js"); -function finalizeWithKey(eventTemplate, secretKey, pubkeyOverride = null) { - const tags = Array.isArray(eventTemplate?.tags) - ? eventTemplate.tags.map((tag) => Array.isArray(tag) ? [...tag] : []) - : []; - const base = { - kind: Number.isFinite(eventTemplate?.kind) - ? eventTemplate.kind - : 1, - content: typeof eventTemplate?.content === "string" - ? eventTemplate.content - : "", - created_at: Number.isFinite(eventTemplate?.created_at) - ? Math.floor(eventTemplate.created_at) - : Math.floor(Date.now() / 1000), - tags, - pubkey: - typeof pubkeyOverride === "string" && pubkeyOverride - ? pubkeyOverride - : typeof eventTemplate?.pubkey === "string" && eventTemplate.pubkey - ? eventTemplate.pubkey - : getPublicKey(secretKey), - }; - return finalizeEvent(base, secretKey); -} - test("nostr login + remote signing + publish + watch history integration", async (t) => { + const tools = await ensureNostrTools(); + const { finalizeEvent, generateSecretKey, getEventHash, getPublicKey, nip04 } = tools; + + function finalizeWithKey(eventTemplate, secretKey, pubkeyOverride = null) { + const tags = Array.isArray(eventTemplate?.tags) + ? eventTemplate.tags.map((tag) => Array.isArray(tag) ? [...tag] : []) + : []; + const base = { + kind: Number.isFinite(eventTemplate?.kind) + ? eventTemplate.kind + : 1, + content: typeof eventTemplate?.content === "string" + ? eventTemplate.content + : "", + created_at: Number.isFinite(eventTemplate?.created_at) + ? Math.floor(eventTemplate.created_at) + : Math.floor(Date.now() / 1000), + tags, + pubkey: + typeof pubkeyOverride === "string" && pubkeyOverride + ? pubkeyOverride + : typeof eventTemplate?.pubkey === "string" && eventTemplate.pubkey + ? eventTemplate.pubkey + : getPublicKey(secretKey), + }; + return finalizeEvent(base, secretKey); + } + const originalWindow = globalThis.window; const windowRef = originalWindow || {}; globalThis.window = windowRef; if (!windowRef.crypto || !windowRef.crypto.subtle) { - windowRef.crypto = webcrypto; + windowRef.crypto = globalThis.crypto; } const originalNostr = windowRef.nostr; @@ -81,7 +109,10 @@ test("nostr login + remote signing + publish + watch history integration", async ); const originalWatchHistoryStorage = nostrClient.watchHistory?.storage; - const userSecret = generateSecretKey(); + const userSecret = new Uint8Array(32); + for (let i = 0; i < 32; i++) { + userSecret[i] = Math.floor(Math.random() * 256); + } const userPubkey = getPublicKey(userSecret); let extensionSignCount = 0; @@ -238,6 +269,7 @@ test("nostr login + remote signing + publish + watch history integration", async const mockRpcClient = Object.create(Nip46RpcClient.prototype); Object.assign(mockRpcClient, { userPubkey, + pubkey: userPubkey, async signEvent(event) { remoteSignCount += 1; return finalizeWithKey(event, userSecret, userPubkey); @@ -262,7 +294,7 @@ test("nostr login + remote signing + publish + watch history integration", async const viewPointer = { type: "e", - value: "view-event-integration", + value: "f".repeat(64), watchedAt: 1_700_000_123, };