From 20a9622f271e41a2647ea2732b550595683811d2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 15:49:07 +0000 Subject: [PATCH] feat: auto-approve zap requests (9734) under "Let's be reasonable" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signing a Lightning zap request moves no money — it only fetches an invoice. The payment itself is the separately-gated value.payInvoice capability, which prompts on every use regardless of policy. So adding 9734 to the reasonable set drops a redundant signature prompt while the meaningful payment prompt stays. Nutzaps (9321) remain excluded: publishing one *is* the payment, since the event carries the spendable ecash proofs. Test pins the contrast. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01WMrHZfwN5tM4ecvdz7xigo --- .../signers/NostrSignerPermissionLedger.kt | 7 ++++++- .../signers/NostrSignerPermissionLedgerTest.kt | 15 +++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) 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