diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/signers/NostrSignerPermissionLedger.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/signers/NostrSignerPermissionLedger.kt index 7c4f82e742..33bab12ae1 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/signers/NostrSignerPermissionLedger.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/signers/NostrSignerPermissionLedger.kt @@ -27,6 +27,7 @@ import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent +import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.utils.TimeUtils @@ -139,7 +140,10 @@ class NostrSignerPermissionLedger( * when doing so **cannot harm the user**. Everything here is *additive, public, and * non-destructive* — creating a new note-like event that the user could delete afterwards, in * the same risk class as the original kind 1/6/7 set. None of them can silently: - * - spend money (zaps/nutzaps are gated separately and always prompt), + * - spend money — signing a **zap request** (9734) moves nothing; it only fetches a Lightning + * invoice, and the payment itself is the separately-gated `value.payInvoice` capability that + * prompts on *every* use regardless of policy. **Nutzaps (9321) are excluded**: publishing + * one *is* the payment (the event carries the spendable ecash proofs), so it stays ASK. * - overwrite account configuration (profile 0, contacts 3, relay/mute/bookmark lists are * replaceable — a bad write can wipe settings, so they stay ASK), * - delete existing content (kind 5), or @@ -157,6 +161,7 @@ class NostrSignerPermissionLedger( PictureEvent.KIND, // 20 — picture posts (same risk as kind 1) ChannelMessageEvent.KIND, // 42 — public chat messages HighlightEvent.KIND, // 9802 — highlighted snippets shared publicly + LnZapRequestEvent.KIND, // 9734 — Lightning zap request; the payment itself still prompts CommentEvent.KIND, // 1111 — NIP-22 threaded comments (same risk as kind 1) StatusEvent.KIND, // 30315 — ephemeral user status / presence ) diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/napplet/signers/NostrSignerPermissionLedgerTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/napplet/signers/NostrSignerPermissionLedgerTest.kt index 1764a2b650..4c3b10a62c 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/napplet/signers/NostrSignerPermissionLedgerTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/napplet/signers/NostrSignerPermissionLedgerTest.kt @@ -50,10 +50,12 @@ class NostrSignerPermissionLedgerTest { val ledger = NostrSignerPermissionLedger(InMemoryNostrSignerPermissionStore()) ledger.setPolicy(coordinate, AppSignerPolicy.REASONABLE) - // Profile (0), contacts (3), deletion (5), relay list (10002), zap request (9734), - // and gift-wrapped DM (1059) must never auto-sign — they change config, delete, spend, - // or leak. Decryption reveals private content, so it also asks. - for (kind in listOf(0, 3, 5, 10002, 9734, 1059)) { + // Profile (0), contacts (3), deletion (5), relay list (10002), nutzap (9321), and + // gift-wrapped DM (1059) must never auto-sign — they change config, delete, spend ecash, + // or leak. A nutzap in particular *is* the payment (it carries the ecash proofs), unlike a + // zap request (9734) which only fetches an invoice. Decryption reveals private content, so + // it also asks. + for (kind in listOf(0, 3, 5, 10002, 9321, 1059)) { assertEquals( NostrOpDecision.ASK, ledger.decide(coordinate, NostrSignerOp.SignKind(kind)), @@ -61,6 +63,11 @@ class NostrSignerPermissionLedgerTest { ) } assertEquals(NostrOpDecision.ASK, ledger.decide(coordinate, NostrSignerOp.Decrypt)) + + // Contrast: a Lightning zap request (9734) auto-signs (the payment prompts separately), + // but an ecash nutzap (9321) does not — publishing it spends the tokens. + assertEquals(NostrOpDecision.ALLOW, ledger.decide(coordinate, NostrSignerOp.SignKind(9734))) + assertEquals(NostrOpDecision.ASK, ledger.decide(coordinate, NostrSignerOp.SignKind(9321))) } @Test