From 6b251dbda5ea84e7d9764dddfae1fbc6a49a0ecf Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 2 Jul 2026 10:02:43 -0400 Subject: [PATCH] =?UTF-8?q?[reactions]=20Fix=20"Missing=20signing=20primit?= =?UTF-8?q?ives"=20=E2=80=94=20session=20actor=20must=20have=20a=20usable?= =?UTF-8?q?=20key?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of "Failed to send reaction" (after the dispatch fix made the button reach the publish path): reactions are published under the anonymous SESSION ACTOR, signed with its private key. But ensureSessionActor adopted a stored session-actor entry as that key — and a persisted nsec login is passphrase-encrypted with privateKey "" ("never exposes plaintext"). So a leftover saved-nsec entry hijacked the anonymous telemetry key, and signEventWithPrivateKey(event, "") threw "Missing signing primitives". (Repro: logged in with NIP-07 while a saved nsec account exists on the device.) Every ensureSessionActor caller uses it to SIGN telemetry (reactions / view events / watch-history), so the returned actor must carry a usable plaintext key. Now it only reuses a current/stored actor that has one; otherwise it tracks the locked entry (for other flows) and generates a fresh ephemeral signing key — instead of returning a keyless actor that can sign nothing. Existing session-actor suites green; lint + build green. Follow-up: the fresh ephemeral key isn't persisted, so a like isn't remembered across reloads (different anonymous key) — separate from this crash fix. Co-Authored-By: Claude Opus 4.8 --- js/nostr/managers/SignerManager.js | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/js/nostr/managers/SignerManager.js b/js/nostr/managers/SignerManager.js index 6338d4fc..43a819f9 100644 --- a/js/nostr/managers/SignerManager.js +++ b/js/nostr/managers/SignerManager.js @@ -138,16 +138,30 @@ export class SignerManager { } async ensureSessionActor(force = false) { + // The session actor is an ANONYMOUS telemetry key used to SIGN reactions / + // view events / watch-history when they aren't tied to the login. It must + // therefore carry a usable plaintext private key. A persisted nsec login is + // passphrase-encrypted (privateKey ""), so adopting one as the session actor + // made reactions fail with "Missing signing primitives" (a leftover saved-nsec + // entry hijacked the anonymous key). Only reuse an actor that can actually + // sign; otherwise fall through and generate a fresh ephemeral key. + const hasUsableKey = (actor) => + Boolean( + actor && + typeof actor.privateKey === "string" && + HEX64_REGEX.test(actor.privateKey), + ); + if (!force) { - if (this.sessionActor) { + if (hasUsableKey(this.sessionActor)) { return this.sessionActor.pubkey; } - if (this.lockedSessionActor) { - return this.lockedSessionActor.pubkey; - } const storedEntry = readStoredSessionActorEntry(); if (storedEntry) { + // Track a locked/encrypted entry (other flows read it) but don't sign with it. this.lockedSessionActor = storedEntry; + } + if (hasUsableKey(storedEntry)) { this.sessionActor = storedEntry; return storedEntry.pubkey; }