LNbits-Provisionierungsproxy
Ein schlanker Node-Dienst, der vor LNbits sitzt und Nostr-Clients eine sichere, eng gefasste Oberfläche bietet, um Wallets bereitzustellen, Lightning-Adressen zu beanspruchen und Nostr-Wallet-Connect-Freigaben zu verwalten. Diese Seite ist eine Self-Hosting-Anleitung: Betreibe deine eigene Instanz auf deinem eigenen LNbits.
Warum überhaupt ein Proxy
LNbits stellt eine vollständige Administrations-API bereit. Sie einer Browser-Erweiterung zu überlassen hieße, jedem Client Autorität über die gesamte Instanz zuzutrauen. Der Proxy veröffentlicht nur die wenigen Pfade, die eine Wallet wirklich braucht, authentifiziert Wallet-Inhaber über ihren Nostr-Schlüssel statt über ein Passwort und behält die LNbits-Superuser-Zugangsdaten auf dem Server, wo sie hingehören.
Eigene LNbits-Instanzen brauchen diesen Adapter, damit die Wallet-Verwaltung der Erweiterung mit ihnen funktioniert. Nichts in der Erweiterung ist auf einen einzelnen Betreiber zugeschnitten.
Architektur
Anfragen erreichen deinen Reverse Proxy über TLS und werden an den Dienst auf dem Loopback weitergereicht. Der Dienst spricht per HTTP mit LNbits und liest zwei LNbits-SQLite-Datenbanken direkt, für Abfragen, die die HTTP-API nicht anbietet.
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, ...)Der Dienst lauscht ausschließlich auf 127.0.0.1. Er ist nie direkt aus dem Internet erreichbar, ein Reverse Proxy davor ist also Pflicht, nicht Kür.
Über den direkten Datenbankzugriff ordnet der Proxy einen öffentlichen Nostr-Schlüssel einer LNbits-Wallet zu und schreibt die Zahlungslinks der Lightning-Adresse. Beide Dateien müssen für den Benutzer, unter dem der Dienst läuft, les- und schreibbar sein.
Voraussetzungen
- Node.js 24 oder neuer. Der Dienst nutzt das eingebaute SQLite-Modul, das in älteren Versionen noch experimentell ist.
- Eine laufende LNbits-Instanz mit installierter
lnurlp-Erweiterung, und dernwcprovider-Erweiterung, wenn du Nostr Wallet Connect unterstützen willst. - Ein Lightning-Backend hinter LNbits. Das Referenz-Deployment nutzt phoenixd, dem Proxy ist es aber gleich, welches du betreibst.
- Der LNbits-Superuser-API-Schlüssel, mit dem der Dienst Konten und Wallets anlegt. Dieser Schlüssel wird nie an Clients weitergereicht.
Installation und Konfiguration
Klone das Repository, installiere die einzige Laufzeitabhängigkeit und starte den Dienst mit seiner Konfiguration in der Umgebung.
Umgebungsvariablen
Fünf Variablen steuern den Dienst. Nur eine ist erforderlich.
| Variable | Default | Description |
|---|---|---|
LNBITS_URL | http://127.0.0.1:5000 | Basis-URL deiner LNbits-Instanz. |
LNBITS_ADMIN_KEY | erforderlich | LNbits-Superuser-API-Schlüssel. Ohne ihn verweigert der Dienst die Bereitstellung. |
LNBITS_DB_PATH | .../data/database.sqlite3 | Pfad zur SQLite-Hauptdatenbank von LNbits. |
LNURLP_DB_PATH | .../data/ext_lnurlp.sqlite3 | Pfad zur SQLite-Datenbank der lnurlp-Erweiterung. |
PORT | 3003 | Loopback-Port, auf dem der Dienst lauscht. |
Ändere die Domain vor dem Deployment
Die öffentliche Domain ist eine Konstante im Quelltext, keine Umgebungsvariable. Die NIP-98-Prüfung lehnt jedes signierte Ereignis ab, dessen u-Tag nicht exakt übereinstimmt, eine unveränderte Kopie weist also jede Bereitstellungsanfrage auf deiner eigenen Domain zurück. Trage in server.js deinen eigenen Hostnamen in die Konstante ein, bevor du den Dienst startest.
const DOMAIN = 'zaps.example.com';Was deine Instanz bereitstellt
Unten steht alles, worauf der Dienst antwortet. Jeder Pfad, der nicht in dieser Liste steht, liefert 404, auch der Rest der LNbits-API.
Wallet-Bereitstellung
GET/api/provision/challenge
Gibt eine zufällige Einmal-Challenge aus. Ohne Authentifizierung. Challenges verfallen nach 60 Sekunden und werden bei der ersten erfolgreichen Nutzung verbraucht.
{
"challenge": "7f3a…"
}POST/api/provision
Legt eine Wallet für einen öffentlichen Nostr-Schlüssel an oder gibt die vorhandene zurück. Die Antwort enthält die Wallet-Schlüssel und trägt no-store.
Sende das signierte NIP-98-Ereignis im Feld event, optional mit einem Wallet-Namen.
{
"event": {
"kind": 27235,
"…": "signed NIP-98 event"
},
"name": "My Wallet"
}{
"walletId": "…",
"adminkey": "…",
"inkey": "…",
"lightningAddress": null
}Lightning-Adresse
POST/api/claim-username
Beansprucht einen Lightning-Adress-Benutzernamen für den authentifizierten Schlüssel. Legt den LNURL-Zahlungslink an und vermerkt den Namen im Konto.
GET/api/lightning-address?pubkey={hex}
Schlägt die von einem öffentlichen Schlüssel beanspruchte Lightning-Adresse nach. Ohne Authentifizierung. Der Parameter pubkey ist erforderlich und muss aus 64 hexadezimalen Kleinbuchstaben bestehen.
{
"lightningAddress": "[email protected]"
}POST/api/release-username
Gibt einen beanspruchten Namen wieder frei, macht ihn für andere verfügbar und entfernt den Zahlungslink.
Nostr Wallet Connect
Wallet-bezogene Verwaltung von NWC-Freigaben. Jede Route verlangt den Admin-API-Schlüssel dieser Wallet im Header X-Api-Key; reine Rechnungsschlüssel werden abgewiesen. Diese Routen akzeptieren überhaupt keine Query-Parameter.
GET/api/nwc/connections
Listet aktive Verbindungen samt Budgetverbrauch auf, dazu den öffentlichen Anbieterschlüssel und das Relay. Gibt eine leere Liste zurück, wenn die Person die Erweiterung nicht aktiviert hat.
{
"connections": [],
"provider": {
"pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"relay": "wss://relay.example.com"
}
}PUT/api/nwc/connections/{clientPubkey}
Registriert einen vom Client erzeugten öffentlichen Schlüssel als Verbindung. Ein erneuter Aufruf mit demselben Schlüssel ist idempotent und liefert 200, statt die bestehende Freigabe zu ändern.
{
"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}
Widerruft die Freigabe dieser Wallet für einen öffentlichen Client-Schlüssel. Bereits abgeschickte Zahlungen werden nicht storniert.
Nostr Wallet Connect
Der Client erzeugt sein eigenes Geheimnis und baut die Pairing-Zeichenkette lokal. Der Proxy empfängt, speichert, protokolliert und liefert nie ein Pairing-Geheimnis und schreibt es nie in eine URL.
Neue Freigaben sind fest auf die Rechte pay, lookup und info gesetzt, mit Tagesbudget und Ablaufdatum. Für Eigentum, Kontobeschränkungen und Budgetdurchsetzung bleibt LNbits maßgeblich.
Weitergereichte LNbits-Pfade
Zwei Gruppen von LNbits-Pfaden werden unverändert weitergereicht. Alles andere ist blockiert.
Öffentliche LNURL-Pfade, mit großzügigem CORS weitergereicht, weil Wallets sie von fremden Ursprüngen aufrufen. Der Host-Header wird auf deine öffentliche Domain umgeschrieben, damit LNbits korrekte Callback-URLs bildet.
GET /.well-known/lnurlp/{username}
GET /lnurlp/api/v1/lnurl/cb/{id}Authentifizierte Wallet-Pfade, die einen X-Api-Key-Header verlangen und von der Wallet-Ansicht der Erweiterung genutzt werden.
GET|POST /api/v1/wallet
GET|POST /api/v1/paymentsNIP-98-Authentifizierung
Bereitstellung und jede Änderung an einer Lightning-Adresse laufen über Challenge-Response mit einem signierten Nostr-Ereignis. Keine Passwörter, keine Sitzungen.
- Fordere eine Challenge an.
- Baue ein Ereignis der Art 27235, das die Challenge und genau die Anfrage trägt, die es autorisiert.
- Signiere es mit dem Nostr-Schlüssel der Person und sende es im Feld event des POST-Körpers.
- Der Dienst prüft die Schnorr-Signatur, bevor er die Challenge verbraucht, ein Fehlversuch verbrennt sie also nicht.
Erforderliche Tags
Alle drei Tags sind Pflicht und werden exakt geprüft. Das u-Tag muss die vollständige absolute URL des aufgerufenen Endpunkts auf deiner eigenen Domain sein, und das method-Tag muss zur HTTP-Methode passen.
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());Zeitfenster
Es gelten zwei unabhängige Fenster: Die Challenge verfällt 60 Sekunden nach der Ausgabe, und das created_at des Ereignisses muss innerhalb von 60 Sekunden der Serverzeit liegen. Ein Client mit stark abweichender Uhr scheitert selbst mit einer frischen Challenge.
Reverse Proxy
Beende TLS und leite an den Loopback-Port weiter. Der Block unten ist die Referenzkonfiguration.
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;
}Setze die echte Client-IP, wenn ein CDN davorsteht
Die Ratenbegrenzung schlüsselt nach dem rechtesten X-Forwarded-For-Eintrag, den dein Reverse Proxy anhängt. Liefert ein CDN wie Cloudflare deine Domain aus, ist dieser Eintrag die Adresse des CDN-Knotens statt die der Besucherin, und alle hinter demselben Knoten teilen sich einen einzigen Zähler: ein paar Anfragen pro Minute für alle zusammen. Stelle die echte Adresse wieder her, bevor die Anfrage den Dienst erreicht.
# 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;Lass diesen Block nur weg, wenn zwischen Internet und deinem Reverse Proxy nichts steht. Halte die Adressbereiche mit der veröffentlichten Liste deines CDN aktuell.
Prozessverwaltung
Jeder Supervisor taugt. Das Referenz-Deployment nutzt pm2, mit der Konfiguration in der Prozessumgebung und gespeicherter Prozessliste, damit sie einen Neustart übersteht.
Gib den Superuser-Schlüssel über deinen Supervisor oder eine Umgebungsdatei weiter, die nie eingecheckt wird. Im Repository hat er nichts verloren.
Überwachung
Zwei unabhängige Wächter lohnen sich: eine Gesundheitsprüfung für den Gesamtaufbau und ein Watchdog für das Lightning-Backend.
Watchdog für das Lightning-Backend
phoenixd kann in einen Zustand geraten, in dem der Prozess lebt und lauscht, seine HTTP-API aber nie antwortet, weil eine Reconnect-Schleife zum LSP die Event-Loop blockiert. Wallets melden fehlgeschlagene Zahlungen, während systemctl den Dienst als aktiv führt. Ein Timer, der die API abfragt und nach zwei aufeinanderfolgenden Fehlschlägen neu startet, behebt das, ohne dass jemand aufstehen muss.
# /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.targetDie doppelte Prüfung ist wichtig: Eine einzelne Zeitüberschreitung ist meist ein Aussetzer, und das Backend bei jedem Aussetzer neu zu starten ist schlimmer als der Fehler selbst.
Gesundheitsprüfung des Aufbaus
Eine regelmäßige Prüfung, die LNbits, den Proxy, die öffentlichen LNURL-Pfade und die Rechnungserzeugung von Anfang bis Ende durchspielt und bei Zustandswechseln eine E-Mail schickt. Führe sie per cron auf demselben Host aus.
*/5 * * * * /usr/bin/node /srv/monitor/monitor.mjs >> /srv/monitor/cron.log 2>&1Gesundheitsprüfung des Aufbaus
Erzeugt deine Prüfung echte Rechnungen, gib ihr eine eigene Wallet und Adresse, setze eine kurze Gültigkeit und räume die Rechnungen auf. Die Adresse einer echten Person alle paar Minuten abzufragen füllt deren Zahlungsverlauf mit Rechnungen, die nie jemand begleicht.
Grenzwerte und Validierung
Ratenbegrenzung
Pro Client-Adresse und Minute. Anfragen über dem Limit erhalten 429.
| Route | Requests / minute |
|---|---|
/api/nwc/connections | 60 |
/api/provision/challenge | 10 |
/api/provision | 5 |
/api/claim-username | 3 |
/api/release-username | 3 |
Benutzernamen für Lightning-Adressen
Namen müssen 3 bis 30 Zeichen lang sein, alphanumerisch in Kleinbuchstaben mit Punkten, Bindestrichen und Unterstrichen, und mit einem alphanumerischen Zeichen beginnen und enden.
/^[a-z0-9][a-z0-9._-]{1,28}[a-z0-9]$/Diese Namen sind gesperrt:
admin support help info noreply
postmaster webmaster abuse root systemNWC-Verbindungseinstellungen
- Verbindungsname: 1 bis 50 Zeichen, keine Steuerzeichen.
- Tageslimit: 1 bis 9.999.999 Sats, durchgesetzt über ein gleitendes 24-Stunden-Fenster.
- Ablauf: 1 bis 365 Tage.
- Höchstens 50 aktive Verbindungen pro Wallet.
Anfragekörper
64 KB für Bereitstellung und Lightning-Adress-Anfragen, 4 KB für das Anlegen einer NWC-Verbindung.
Quelltext
Der Dienst ist eine einzige Node-Datei mit einer einzigen Laufzeitabhängigkeit. Lies ihn, bevor du ihn betreibst. github.com/nostr-wot/LNbits-proxy