Completes the inbound half. A member now files one of three things when a kind:1059 arrives as an application payload, and which one depends only on whether their key opens it. The carve-out first. MarmotInboundManager rejects any inner event whose pubkey is not the MLS sender's credential identity -- MIP-03, and the check that stops a member minting events attributed to somebody else. A gift wrap is keyed to a throwaway key by construction and names nobody, so it cannot satisfy a check about its author; kind:1059 is now exempt. The check is not weakened, it is relocated. What replaces it is `seal.pubKey == senderIdentity` on a seal whose signature verifies -- a signature bound to an MLS leaf, rather than a plaintext field compared to one. It is strictly harder to forge: the attack it stops is a member re-wrapping a seal they were legitimately sent and passing it off to a third party as its author's, and that fails here because the MLS frame says who actually sent this one. senderIdentity also stops being optional. It was previously only compared; now every sender-derived field reads from it, because the payload carries no author at all. A leaf with no identity is an error rather than a mismatch. Attribution is resolved in NostrDao and handed to fromGroupEventResult, rather than added to GroupEventResult.ApplicationMessage where it belongs. quartz is a binary dependency here (com.vitorpamplona.quartz:quartz:1.14.0) and the local checkout is a reference copy, not a build input, so the result type cannot gain a field without publishing a fork. NostrDao holds the group, the leaf index is already on the result, and an application message advances no epoch, so the tree has not moved by the time it reads it. Same value, no fork. The three outcomes: The recipient opens the wrap and gets the words. The rumor is stored as its own MarmotInnerEvent keyed on the rumor's id -- the id the sender queued -- so both sides of the conversation hold one message under one identity. The wrap keeps its own row as the wire artifact. A bystander gets a line with no content. That is the feature working: the group is meant to see that a private message was sent and to whom, and nothing else. The sender, on a re-sync, is indistinguishable from a bystander, because the wrap's key was discarded and we cannot open our own message. Left unguarded this files an empty placeholder over the row sendChatMessage wrote -- which is the only copy of those words anywhere. Hence the early return on senderIdentity == us, mirroring the guard NostrDao.persistInboundChatMessage already carries on the NIP-17 path. A failed validation drops the message and logs rather than throwing. The caller is inside storeNostrEvent's transaction, and a forged direct message should cost its own line, not the whole event. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This is a Kotlin Multiplatform project targeting Android, iOS, Desktop (JVM).
-
/composeApp is for code that will be shared across your Compose Multiplatform applications. It contains several subfolders:
- commonMain is for code that’s common for all targets.
- Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name. For example, if you want to use Apple’s CoreCrypto for the iOS part of your Kotlin app, the iosMain folder would be the right place for such calls. Similarly, if you want to edit the Desktop (JVM) specific part, the jvmMain folder is the appropriate location.
-
/iosApp contains iOS applications. Even if you’re sharing your UI with Compose Multiplatform, you need this entry point for your iOS app. This is also where you should add SwiftUI code for your project.
Build and Run Android Application
To build and run the development version of the Android app, use the run configuration from the run widget in your IDE’s toolbar or build it directly from the terminal:
- on macOS/Linux
./gradlew :composeApp:assembleDebug - on Windows
.\gradlew.bat :composeApp:assembleDebug
Build and Run Desktop (JVM) Application
To build and run the development version of the desktop app, use the run configuration from the run widget in your IDE’s toolbar or run it directly from the terminal:
- on macOS/Linux
./gradlew :composeApp:run - on Windows
.\gradlew.bat :composeApp:run
Build and Run iOS Application
To build and run the development version of the iOS app, use the run configuration from the run widget in your IDE’s toolbar or open the /iosApp directory in Xcode and run it from there.
Learn more about Kotlin Multiplatform…