Add session fallback signing to nostr publish flow

This commit is contained in:
thePR0M3TH3AN committed 2025-10-08 22:27:11 -04:00
1 parent ebbddf77bc
commit 98dd51509d
2 files changed
+174 -2

No files matched your search

+80 -2
View File
@@ -5293,7 +5293,85 @@ export class NostrClient {
rejectionLogLevel = "error",
} = {}
) {
const signedEvent = await window.nostr.signEvent(event);
const extension = window?.nostr;
const extensionSigner =
extension && typeof extension.signEvent === "function"
? extension.signEvent.bind(extension)
: null;
const normalizedEventPubkey =
event && typeof event.pubkey === "string"
? event.pubkey.toLowerCase()
: "";
const normalizedLogged =
typeof this.pubkey === "string" ? this.pubkey.toLowerCase() : "";
const usingSessionActor =
normalizedEventPubkey &&
normalizedLogged &&
normalizedEventPubkey !== normalizedLogged;
let eventToSign = event;
let signedEvent = null;
let signerPubkey = null;
if (
extensionSigner &&
normalizedEventPubkey &&
normalizedEventPubkey === normalizedLogged
) {
signedEvent = await extensionSigner(event);
} else {
try {
const currentSessionPubkey =
typeof this.sessionActor?.pubkey === "string"
? this.sessionActor.pubkey.toLowerCase()
: "";
if (
usingSessionActor &&
normalizedEventPubkey &&
normalizedEventPubkey !== currentSessionPubkey
) {
await this.ensureSessionActor(true);
} else {
await this.ensureSessionActor();
}
const sessionActor = this.sessionActor;
if (
!sessionActor ||
typeof sessionActor.pubkey !== "string" ||
!sessionActor.pubkey ||
typeof sessionActor.privateKey !== "string" ||
!sessionActor.privateKey
) {
throw new Error("session-actor-unavailable");
}
const normalizedSessionPubkey = sessionActor.pubkey.toLowerCase();
if (
!normalizedEventPubkey ||
normalizedEventPubkey !== normalizedSessionPubkey ||
event.pubkey !== sessionActor.pubkey
) {
eventToSign = { ...event, pubkey: sessionActor.pubkey };
}
signedEvent = signEventWithPrivateKey(
eventToSign,
sessionActor.privateKey
);
signerPubkey = sessionActor.pubkey;
} catch (error) {
console.warn("[nostr] Failed to sign event with session key:", error);
throw error;
}
}
if (!signerPubkey && signedEvent && typeof signedEvent.pubkey === "string") {
signerPubkey = signedEvent.pubkey;
}
if (isDevMode) {
console.log(`Signed ${devLogLabel} event:`, signedEvent);
}
@@ -5343,7 +5421,7 @@ export class NostrClient {
});
}
return { signedEvent, summary: publishSummary };
return { signedEvent, summary: publishSummary, signerPubkey };
}
async publishVideo(videoPayload, pubkey) {
+94
View File
@@ -722,6 +722,99 @@ async function testRecordVideoViewEmitsJsonPayload() {
);
}
async function testSignAndPublishFallbackUsesSessionActor() {
const originalExtension = window.nostr;
const originalPool = nostrClient.pool;
const originalRelays = nostrClient.relays;
const originalPubkey = nostrClient.pubkey;
const originalSessionActor = nostrClient.sessionActor;
const originalEnsureSessionActor = nostrClient.ensureSessionActor;
const ensureCalls = [];
const publishCalls = [];
try {
delete window.nostr;
nostrClient.pool = {
publish(urls, event) {
publishCalls.push({ urls, event });
return {
on(eventName, handler) {
if (eventName === "ok") {
handler();
}
return true;
},
};
},
};
nostrClient.relays = ["wss://relay.fallback"];
nostrClient.pubkey = "logged-user";
nostrClient.sessionActor = {
pubkey: "session-actor",
privateKey: "session-private",
};
nostrClient.ensureSessionActor = async (forceRenew = false) => {
ensureCalls.push(forceRenew === true);
return nostrClient.sessionActor.pubkey;
};
const event = {
kind: 1,
pubkey: "session-actor",
created_at: 123,
tags: [],
content: "hello fallback",
};
const result = await nostrClient.signAndPublishEvent(event, {
context: "fallback",
logName: "fallback",
});
assert.ok(result, "expected a publish result when falling back to the session actor");
assert.equal(
result.signedEvent.pubkey,
"session-actor",
"fallback signing should use the session actor pubkey",
);
assert.equal(
result.signerPubkey,
"session-actor",
"signer pubkey should reflect the session actor when falling back",
);
assert.ok(
ensureCalls.length > 0,
"fallback signing should attempt to ensure the session actor",
);
assert.ok(
Array.isArray(result.summary?.accepted) && result.summary.accepted.length > 0,
"fallback publish should report accepted relays",
);
assert.equal(
publishCalls.length,
result.summary.accepted.length,
"each accepted relay should correspond to a publish attempt",
);
assert.equal(
result.summary.accepted[0]?.url,
"wss://relay.fallback",
"publish summary should surface the accepting relay",
);
} finally {
if (typeof originalExtension === "undefined") {
delete window.nostr;
} else {
window.nostr = originalExtension;
}
nostrClient.pool = originalPool;
nostrClient.relays = originalRelays;
nostrClient.pubkey = originalPubkey;
nostrClient.sessionActor = originalSessionActor;
nostrClient.ensureSessionActor = originalEnsureSessionActor;
}
}
// Cached totals older than VIEW_COUNT_CACHE_TTL_MS should be discarded and rehydrated from relays.
async function testHydrationRefreshesAfterCacheTtl() {
harness.reset();
@@ -947,6 +1040,7 @@ await testLocalIngestNotifiesImmediately();
await testUnsubscribeStopsCallbacks();
await testRelayCountAggregationUsesBestEstimate();
await testRecordVideoViewEmitsJsonPayload();
await testSignAndPublishFallbackUsesSessionActor();
await testHydrateHistoryPrefersRootEvent();
await testModalPostedTimestampStaysInSync();