Nostr WoT

Documentazione

Tutto ciò di cui hai bisogno per integrare Web of Trust nella tua applicazione.

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.

text
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 lnurlp installata, e l'estensione nwcprovider se 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.

terminale
$git clone https://github.com/nostr-wot/LNbits-proxy.git
$cd LNbits-proxy
$npm ci
$npm test

Variabili d'ambiente

Cinque variabili controllano il servizio. Solo una è obbligatoria.

VariableDefaultDescription
LNBITS_URLhttp://127.0.0.1:5000URL base della tua istanza LNbits.
LNBITS_ADMIN_KEYobbligatoriaChiave API di superutente LNbits. Senza di essa il servizio si rifiuta di creare wallet.
LNBITS_DB_PATH.../data/database.sqlite3Percorso del database SQLite principale di LNbits.
LNURLP_DB_PATH.../data/ext_lnurlp.sqlite3Percorso del database SQLite dell'estensione lnurlp.
PORT3003Porta 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.

javascript
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.

json
{
  "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.

json
{
  "event": {
    "kind": 27235,
    "…": "signed NIP-98 event"
  },
  "name": "My Wallet"
}
json
{
  "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.

json
{
  "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.

json
{
  "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.

json
{
  "name": "My phone",
  "dailyLimit": 10000,
  "days": 90
}
bash
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.

text
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.

text
GET|POST  /api/v1/wallet
GET|POST  /api/v1/payments

Autenticazione NIP-98

Il provisioning e ogni modifica alla Lightning Address usano sfida-risposta con un evento Nostr firmato. Niente password, niente sessioni.

  1. Richiedi una sfida.
  2. Costruisci un evento di tipo 27235 che porti la sfida e la richiesta esatta che autorizza.
  3. Firmalo con la chiave Nostr della persona e invialo nel campo event del corpo POST.
  4. 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.

javascript
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.

nginx
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.

nginx
# 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.

terminale
$pm2 start server.js --name lnbits-proxy
$pm2 save
$pm2 startup
$pm2 logs lnbits-proxy

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.

ini
# /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.target

Il 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.

text
*/5 * * * * /usr/bin/node /srv/monitor/monitor.mjs >> /srv/monitor/cron.log 2>&1

Controllo 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.

RouteRequests / minute
/api/nwc/connections60
/api/provision/challenge10
/api/provision5
/api/claim-username3
/api/release-username3

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.

javascript
/^[a-z0-9][a-z0-9._-]{1,28}[a-z0-9]$/

Questi nomi sono bloccati:

text
admin  support  help  info  noreply
postmaster  webmaster  abuse  root  system

Impostazioni 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