[reactions] Fix "Missing signing primitives" — session actor must have a usable key

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 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-07-02 10:02:43 -04:00
1 parent 6e2894e3d8
commit 6b251dbda5
1 file changed
+18 -4
+18 -4
View File
@@ -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;
}