feat(lnbits): emit NIP-40 expiration on kind-21000 RPC events

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) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-26 08:57:25 +02:00
commit a980dcd3e9

View file

@ -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,
)