Proxy di provisioning LNbits
Un piccolo servizio Node che sta davanti a LNbits e offre ai client Nostr una superficie sicura e ristretta per creare wallet, rivendicare Lightning Address e gestire le concessioni Nostr Wallet Connect. Questa pagina è una guida all'auto-hosting: gestisci la tua istanza sulla tua installazione di LNbits.
Perché un proxy
LNbits espone un'API amministrativa completa. Consegnarla a un'estensione del browser significherebbe affidare a ogni client autorità sull'intera istanza. Il proxy pubblica solo la manciata di percorsi di cui un wallet ha davvero bisogno, autentica i proprietari con la loro chiave Nostr invece che con una password, e tiene la credenziale di superutente di LNbits sul server, dove deve stare.
Le istanze LNbits personalizzate hanno bisogno di questo adattatore perché l'interfaccia di gestione wallet dell'estensione funzioni con loro. Nell'estensione non c'è nulla di specifico per un singolo operatore.
Architettura
Le richieste arrivano al tuo proxy inverso via TLS e vengono inoltrate al servizio sul loopback. Il servizio parla con LNbits via HTTP e legge direttamente due database SQLite di LNbits per le ricerche che l'API HTTP non espone.
Nostr client
| HTTPS
v
Reverse proxy (TLS, zaps.example.com)
| HTTP, loopback
v
LNbits proxy :3003 --- reads ---> database.sqlite3
| HTTP ext_lnurlp.sqlite3
v
LNbits :5000
|
v
Lightning backend (phoenixd, LND, ...)Il servizio ascolta solo su 127.0.0.1. Non è mai raggiungibile direttamente da internet, quindi un proxy inverso davanti è obbligatorio, non facoltativo.
L'accesso diretto al database è il modo in cui il proxy associa una chiave pubblica Nostr a un wallet LNbits e scrive i link di pagamento della Lightning Address. Entrambi i file devono essere leggibili e scrivibili dall'utente con cui gira il servizio.
Prerequisiti
- Node.js 24 o successivo. Il servizio usa il modulo SQLite integrato, ancora sperimentale nelle versioni precedenti.
- Un'istanza LNbits funzionante con l'estensione
lnurlpinstallata, e l'estensionenwcproviderse vuoi il supporto per Nostr Wallet Connect. - Un backend Lightning dietro LNbits. Il deployment di riferimento usa phoenixd, ma al proxy non importa quale usi.
- La chiave API di superutente di LNbits, che il servizio usa per creare account e wallet. Questa chiave non viene mai inoltrata ai client.
Installazione e configurazione
Clona il repository, installa l'unica dipendenza di runtime e avvia il servizio con la sua configurazione nell'ambiente.
Variabili d'ambiente
Cinque variabili controllano il servizio. Solo una è obbligatoria.
| Variable | Default | Description |
|---|---|---|
LNBITS_URL | http://127.0.0.1:5000 | URL base della tua istanza LNbits. |
LNBITS_ADMIN_KEY | obbligatoria | Chiave API di superutente LNbits. Senza di essa il servizio si rifiuta di creare wallet. |
LNBITS_DB_PATH | .../data/database.sqlite3 | Percorso del database SQLite principale di LNbits. |
LNURLP_DB_PATH | .../data/ext_lnurlp.sqlite3 | Percorso del database SQLite dell'estensione lnurlp. |
PORT | 3003 | Porta di loopback su cui il servizio ascolta. |
Cambia il dominio prima di andare in produzione
Il dominio pubblico è una costante nel codice sorgente, non una variabile d'ambiente. La verifica NIP-98 rifiuta ogni evento firmato il cui tag u non corrisponda esattamente, quindi una copia non modificata rifiuterà ogni richiesta di provisioning sul tuo dominio. Modifica la costante in server.js con il tuo nome host prima di avviare il servizio.
const DOMAIN = 'zaps.example.com';Cosa espone la tua istanza
Qui sotto c'è tutto ciò a cui il servizio risponde. Qualsiasi percorso non presente in questo elenco restituisce 404, incluso il resto dell'API di LNbits.
Creazione del wallet
GET/api/provision/challenge
Emette una sfida casuale monouso. Nessuna autenticazione. Le sfide scadono dopo 60 secondi e vengono consumate al primo uso riuscito.
{
"challenge": "7f3a…"
}POST/api/provision
Crea un wallet per una chiave pubblica Nostr, o restituisce quello esistente. La risposta contiene le chiavi del wallet ed è marcata no-store.
Invia l'evento NIP-98 firmato nel campo event, con un nome del wallet facoltativo.
{
"event": {
"kind": 27235,
"…": "signed NIP-98 event"
},
"name": "My Wallet"
}{
"walletId": "…",
"adminkey": "…",
"inkey": "…",
"lightningAddress": null
}Lightning Address
POST/api/claim-username
Rivendica un nome utente Lightning Address per la chiave autenticata. Crea il link di pagamento LNURL e registra il nome sull'account.
GET/api/lightning-address?pubkey={hex}
Cerca la Lightning Address rivendicata da una chiave pubblica. Nessuna autenticazione. Il parametro pubkey è obbligatorio e deve avere 64 caratteri esadecimali minuscoli.
{
"lightningAddress": "[email protected]"
}POST/api/release-username
Rilascia un nome rivendicato, rendendolo disponibile ad altri e rimuovendo il link di pagamento.
Nostr Wallet Connect
Gestione delle concessioni NWC limitata al singolo wallet. Ogni rotta richiede la chiave API di amministrazione di quel wallet nell'header X-Api-Key; le chiavi di sola fattura vengono rifiutate. Queste rotte non accettano alcuna stringa di query.
GET/api/nwc/connections
Elenca le connessioni attive con il relativo consumo di budget, oltre alla chiave pubblica e al relay pubblici del fornitore. Restituisce un elenco vuoto se la persona non ha attivato l'estensione.
{
"connections": [],
"provider": {
"pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"relay": "wss://relay.example.com"
}
}PUT/api/nwc/connections/{clientPubkey}
Registra come connessione una chiave pubblica generata dal client. Ripetere la chiamata con la stessa chiave è idempotente e restituisce 200 invece di modificare la concessione esistente.
{
"name": "My phone",
"dailyLimit": 10000,
"days": 90
}curl -X PUT "https://zaps.example.com/api/nwc/connections/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" \
-H "X-Api-Key: $WALLET_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"My phone","dailyLimit":10000,"days":90}'DELETE/api/nwc/connections/{clientPubkey}
Revoca la concessione di quel wallet per una chiave pubblica client. I pagamenti già inviati non vengono annullati.
Nostr Wallet Connect
Il client genera il proprio segreto e costruisce la stringa di pairing localmente. Il proxy non riceve, memorizza, registra né restituisce mai un segreto di pairing, e non lo inserisce mai in un URL.
Le nuove concessioni sono fissate ai permessi pay, lookup e info, con un budget giornaliero e una scadenza. LNbits resta l'autorità su proprietà, restrizioni dell'account e applicazione del budget.
Percorsi LNbits inoltrati
Due gruppi di percorsi LNbits vengono inoltrati così come sono. Tutto il resto è bloccato.
Percorsi LNURL pubblici, inoltrati con CORS permissivo perché i wallet li chiamano da un'altra origine. L'header Host viene riscritto con il tuo dominio pubblico affinché LNbits costruisca URL di callback corretti.
GET /.well-known/lnurlp/{username}
GET /lnurlp/api/v1/lnurl/cb/{id}Percorsi wallet autenticati, che richiedono un header X-Api-Key e sono usati dalla vista wallet dell'estensione.
GET|POST /api/v1/wallet
GET|POST /api/v1/paymentsAutenticazione NIP-98
Il provisioning e ogni modifica alla Lightning Address usano sfida-risposta con un evento Nostr firmato. Niente password, niente sessioni.
- Richiedi una sfida.
- Costruisci un evento di tipo 27235 che porti la sfida e la richiesta esatta che autorizza.
- Firmalo con la chiave Nostr della persona e invialo nel campo event del corpo POST.
- Il servizio verifica la firma Schnorr prima di consumare la sfida, quindi un tentativo fallito non la brucia.
Tag obbligatori
Tutti e tre i tag sono obbligatori e verificati in modo esatto. Il tag u deve essere l'URL assoluto completo dell'endpoint chiamato sul tuo dominio, e il tag method deve corrispondere al metodo HTTP.
const { challenge } = await fetch(
"https://zaps.example.com/api/provision/challenge"
).then(r => r.json());
const event = await window.nostr.signEvent({
kind: 27235,
created_at: Math.floor(Date.now() / 1000),
tags: [
["u", "https://zaps.example.com/api/provision"],
["method", "POST"],
["challenge", challenge],
],
content: "",
});
const wallet = await fetch("https://zaps.example.com/api/provision", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ event, name: "My Wallet" }),
}).then(r => r.json());Tempi
Si applicano due finestre indipendenti: la sfida scade 60 secondi dopo l'emissione, e il created_at dell'evento deve rientrare in 60 secondi dall'ora del server. Un client con l'orologio molto sfasato fallirà anche con una sfida appena emessa.
Proxy inverso
Termina il TLS e inoltra alla porta di loopback. Il blocco qui sotto è la configurazione di riferimento.
server {
listen 443 ssl;
server_name zaps.example.com;
ssl_certificate /etc/letsencrypt/live/zaps.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/zaps.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3003;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
}
}
server {
listen 80;
server_name zaps.example.com;
return 301 https://$host$request_uri;
}Imposta l'IP reale del client se davanti c'è una CDN
La limitazione di frequenza si basa sulla voce più a destra di X-Forwarded-For, che il tuo proxy inverso aggiunge. Se una CDN come Cloudflare serve il tuo dominio, quella voce è l'indirizzo del nodo della CDN e non quello di chi visita, e tutte le persone dietro lo stesso nodo condividono un unico contatore: poche richieste al minuto per tutti insieme. Ripristina l'indirizzo reale prima che la richiesta raggiunga il servizio.
# Inside the server block, before location /
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
# ... the rest of your CDN's published ranges
real_ip_header CF-Connecting-IP;
real_ip_recursive on;Salta questo blocco solo se tra internet e il tuo proxy inverso non c'è nulla. Tieni gli intervalli di indirizzi aggiornati con l'elenco pubblicato dalla tua CDN.
Gestione del processo
Va bene qualsiasi supervisore. Il deployment di riferimento usa pm2, con la configurazione iniettata nell'ambiente del processo e l'elenco dei processi salvato per sopravvivere a un riavvio.
Inietta la chiave di superutente tramite il tuo supervisore o un file d'ambiente che non finisce mai nel repository. Non deve vivere nel codice.
Monitoraggio
Vale la pena far girare due sorveglianti indipendenti: un controllo di salute dell'insieme e un watchdog per il backend Lightning.
Watchdog del backend Lightning
phoenixd può finire in uno stato in cui il processo è vivo e in ascolto ma la sua API HTTP non risponde mai, perché un ciclo di riconnessione all'LSP blocca il ciclo di eventi. I wallet segnalano pagamenti falliti mentre systemctl dà il servizio per attivo. Un timer che interroga l'API e riavvia dopo due fallimenti consecutivi risolve la cosa senza svegliare nessuno.
# /etc/systemd/system/check-phoenixd.timer
[Unit]
Description=Run phoenixd health check every 2 minutes
[Timer]
OnBootSec=60
OnUnitActiveSec=120
AccuracySec=10
[Install]
WantedBy=timers.targetIl doppio controllo conta: un singolo timeout di solito è un singhiozzo, e riavviare il backend a ogni singhiozzo è peggio del guasto.
Controllo di salute dell'insieme
Un controllo periodico che mette alla prova LNbits, il proxy, i percorsi LNURL pubblici e la generazione di fatture da capo a fondo, e invia un'email a ogni cambio di stato. Eseguilo da cron sullo stesso host.
*/5 * * * * /usr/bin/node /srv/monitor/monitor.mjs >> /srv/monitor/cron.log 2>&1Controllo di salute dell'insieme
Se il tuo controllo genera fatture reali, dagli un wallet e un indirizzo dedicati, usa una scadenza breve e ripulisci le fatture. Interrogare l'indirizzo di una persona reale ogni pochi minuti riempie la sua cronologia dei pagamenti di fatture che nessuno pagherà mai.
Limiti e validazione
Limiti di frequenza
Per indirizzo del client e per minuto. Le richieste oltre il limite ricevono 429.
| Route | Requests / minute |
|---|---|
/api/nwc/connections | 60 |
/api/provision/challenge | 10 |
/api/provision | 5 |
/api/claim-username | 3 |
/api/release-username | 3 |
Nomi utente Lightning Address
I nomi devono avere da 3 a 30 caratteri, alfanumerici minuscoli con punti, trattini e trattini bassi, iniziando e finendo con un carattere alfanumerico.
/^[a-z0-9][a-z0-9._-]{1,28}[a-z0-9]$/Questi nomi sono bloccati:
admin support help info noreply
postmaster webmaster abuse root systemImpostazioni della connessione NWC
- Nome della connessione: da 1 a 50 caratteri, senza caratteri di controllo.
- Limite giornaliero: da 1 a 9.999.999 sat, applicato su una finestra mobile di 24 ore.
- Scadenza: da 1 a 365 giorni.
- Massimo 50 connessioni attive per wallet.
Corpo delle richieste
64 KB per provisioning e richieste Lightning Address, 4 KB per la creazione di una connessione NWC.
Codice sorgente
Il servizio è un singolo file Node con una sola dipendenza di runtime. Leggilo prima di eseguirlo. github.com/nostr-wot/LNbits-proxy