fix(concord): seal rumors without a sig field

The envelope serialized the rumor with Event.toJson, which writes "sig": "".
A rumor is unsigned (NIP-59), and applesauce-based clients such as Accordion
read an object with a string sig as a signed event, fail its signature and
drop it, so Accordion never showed a single Concord message, Join or edition
we sent. Serialize through Rumor, as NIP-59 seals already do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Vitor Pamplona
2026-09-29 12:47:53 -04:00
co-authored by Claude Opus 5.5
parent 9c2cccd68d
commit 99513a92b6
2 changed files with 25 additions and 2 deletions
@@ -32,6 +32,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.nip44Encryption.Nip44
import com.vitorpamplona.quartz.nip44Encryption.Nip44v2
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor
import com.vitorpamplona.quartz.utils.TimeUtils
/**
@@ -78,11 +79,14 @@ object ConcordStreamEnvelope {
authorSigner: NostrSigner,
encrypted: Boolean,
): Event {
// A rumor is unsigned (NIP-59): serialize it without `sig`. applesauce clients read
// `"sig": ""` as a signed event with a bad signature and drop it.
val rumorJson = Rumor.toJson(Rumor.create(rumor))
val content =
if (encrypted) {
encryptChecked(rumor.toJson(), stream.conversationKey)
encryptChecked(rumorJson, stream.conversationKey)
} else {
rumor.toJson()
rumorJson
}
val kind = if (encrypted) KIND_SEAL_ENCRYPTED else KIND_SEAL_PLAINTEXT
return authorSigner.sign(rumor.createdAt, kind, EMPTY_TAGS, content)
@@ -28,8 +28,12 @@ import com.vitorpamplona.quartz.nip01Core.crypto.verify
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
@@ -69,6 +73,21 @@ class ConcordStreamEnvelopeTest {
assertEquals(9, opened.rumor.kind)
}
/**
* A rumor is unsigned (NIP-59): its JSON carries no `sig`. applesauce-based clients
* (Accordion) treat an object with `"sig": ""` as a signed event whose signature fails,
* and silently dropped every Concord message and Guestbook Join we sent.
*/
@Test
fun sealedRumorJsonCarriesNoSig() =
runTest {
val seal = ConcordStreamEnvelope.seal(chatRumor("no sig"), stream, authorSigner, encrypted = false)
val rumorJson = Json.parseToJsonElement(seal.content).jsonObject
assertFalse("sig" in rumorJson, "rumor JSON must not carry a sig: ${seal.content}")
assertEquals(chatRumor("no sig").id, rumorJson["id"]?.jsonPrimitive?.content)
}
@Test
fun encryptedSealRoundTrips() =
runTest {