From a980dcd3e97c617d668b82822b4059a41831bd97 Mon Sep 17 00:00:00 2001 From: Padreug Date: Tue, 26 May 2026 08:57:25 +0200 Subject: [PATCH] feat(lnbits): emit NIP-40 expiration on kind-21000 RPC events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a 5-minute expiration tag to every outbound RPC envelope. Belt- and-suspenders with the handler-side max_age check (aiolabs/lnbits e4b5bcd7) — the tag lets compliant relays drop expired events at the relay layer before they reach LNbits, while the handler's own time-bounds check defends against a stripped tag. Closes aiolabs/satmachineadmin#15 (S1 / G4 — no replay window on RPC events) on the ATM emission side. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/lnbits/src/client.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/lnbits/src/client.ts b/packages/lnbits/src/client.ts index b820b95..0add0b8 100644 --- a/packages/lnbits/src/client.ts +++ b/packages/lnbits/src/client.ts @@ -366,12 +366,23 @@ export class LnbitsClient { // Build + sign the kind-21000 event ourselves. The server reads our // pubkey directly off the signature, so there's no separate // authIdentifier in the envelope (unlike LightningPubClient). + // + // NIP-40 expiration: 5 minutes past now. Defence-in-depth at the + // relay layer — compliant relays (per NIP-40) drop expired events + // before they reach the LNbits handler. The handler also enforces + // its own max_age window (aiolabs/lnbits e4b5bcd7), so a replay + // attacker can't bypass this by stripping the tag; the tag just + // lets the relay short-circuit earlier. + const now = Math.floor(Date.now() / 1000) const event = finalizeEvent( { kind: LNBITS_KIND_RPC, content: encrypted, - tags: [['p', this.config.serverPubkey]], - created_at: Math.floor(Date.now() / 1000), + tags: [ + ['p', this.config.serverPubkey], + ['expiration', String(now + 300)], + ], + created_at: now, }, this.identity.privateKey, )