Proxy de provisionnement LNbits
Un petit service Node placé devant LNbits, qui offre aux clients Nostr une surface réduite et sûre pour provisionner des portefeuilles, réclamer des Lightning Addresses et gérer des autorisations Nostr Wallet Connect. Cette page est un guide d'auto-hébergement : faites tourner votre propre instance sur votre propre LNbits.
Pourquoi un proxy
LNbits expose une API d'administration complète. La confier à une extension de navigateur reviendrait à accorder à chaque client une autorité sur l'instance entière. Le proxy ne publie que la poignée de routes dont un portefeuille a réellement besoin, authentifie les propriétaires avec leur clé Nostr plutôt qu'un mot de passe, et garde l'identifiant super-utilisateur de LNbits sur le serveur, là où il doit rester.
Les instances LNbits personnalisées ont besoin de cet adaptateur pour que l'interface de gestion des portefeuilles de l'extension fonctionne avec elles. Rien dans l'extension n'est propre à un opérateur.
Architecture
Les requêtes arrivent sur votre proxy inverse en TLS et sont transmises au service sur la boucle locale. Le service dialogue avec LNbits en HTTP et lit directement deux bases SQLite de LNbits pour les recherches que l'API HTTP n'expose pas.
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, ...)Le service n'écoute que sur 127.0.0.1. Il n'est jamais joignable directement depuis internet, donc un proxy inverse devant lui est obligatoire, pas optionnel.
L'accès direct à la base est ce qui permet au proxy d'associer une clé publique Nostr à un portefeuille LNbits et d'écrire les liens de paiement Lightning Address. Les deux fichiers doivent être lisibles et modifiables par l'utilisateur sous lequel tourne le service.
Prérequis
- Node.js 24 ou plus récent. Le service utilise le module SQLite intégré, encore expérimental sur les versions antérieures.
- Une instance LNbits fonctionnelle avec l'extension
lnurlpinstallée, et l'extensionnwcprovidersi vous voulez la prise en charge de Nostr Wallet Connect. - Un backend Lightning derrière LNbits. Le déploiement de référence utilise phoenixd, mais le proxy se moque de celui que vous employez.
- La clé API super-utilisateur de LNbits, dont le service se sert pour créer comptes et portefeuilles. Cette clé n'est jamais transmise aux clients.
Installation et configuration
Clonez le dépôt, installez l'unique dépendance d'exécution et démarrez le service avec sa configuration dans l'environnement.
Variables d'environnement
Cinq variables pilotent le service. Une seule est obligatoire.
| Variable | Default | Description |
|---|---|---|
LNBITS_URL | http://127.0.0.1:5000 | URL de base de votre instance LNbits. |
LNBITS_ADMIN_KEY | obligatoire | Clé API super-utilisateur LNbits. Le service refuse de provisionner sans elle. |
LNBITS_DB_PATH | .../data/database.sqlite3 | Chemin vers la base SQLite principale de LNbits. |
LNURLP_DB_PATH | .../data/ext_lnurlp.sqlite3 | Chemin vers la base SQLite de l'extension lnurlp. |
PORT | 3003 | Port de boucle locale sur lequel le service écoute. |
Changez le domaine avant de déployer
Le domaine public est une constante dans le code source, pas une variable d'environnement. La vérification NIP-98 rejette tout événement signé dont la balise u ne correspond pas exactement, si bien qu'une copie non modifiée refusera chaque requête de provisionnement sur votre propre domaine. Modifiez la constante dans server.js avec votre nom d'hôte avant de démarrer le service.
const DOMAIN = 'zaps.example.com';Ce que votre instance expose
Tout ce à quoi le service répond figure ci-dessous. Toute route absente de cette liste renvoie 404, y compris le reste de l'API LNbits.
Provisionnement de portefeuille
GET/api/provision/challenge
Émet un défi aléatoire à usage unique. Sans authentification. Les défis expirent au bout de 60 secondes et sont consommés à la première utilisation réussie.
{
"challenge": "7f3a…"
}POST/api/provision
Crée un portefeuille pour une clé publique Nostr, ou renvoie celui qui existe déjà. La réponse contient les clés du portefeuille et porte l'en-tête no-store.
Envoyez l'événement NIP-98 signé dans le champ event, avec un nom de portefeuille facultatif.
{
"event": {
"kind": 27235,
"…": "signed NIP-98 event"
},
"name": "My Wallet"
}{
"walletId": "…",
"adminkey": "…",
"inkey": "…",
"lightningAddress": null
}Lightning Address
POST/api/claim-username
Réclame un nom d'utilisateur Lightning Address pour la clé authentifiée. Crée le lien de paiement LNURL et enregistre le nom sur le compte.
GET/api/lightning-address?pubkey={hex}
Recherche la Lightning Address réclamée par une clé publique. Sans authentification. Le paramètre pubkey est obligatoire et doit faire 64 caractères hexadécimaux minuscules.
{
"lightningAddress": "[email protected]"
}POST/api/release-username
Libère un nom réclamé, le rendant disponible pour quelqu'un d'autre et supprimant le lien de paiement.
Nostr Wallet Connect
Gestion des autorisations NWC limitée à chaque portefeuille. Chaque route exige la clé API d'administration de ce portefeuille dans l'en-tête X-Api-Key ; les clés de facturation seule sont rejetées. Ces routes n'acceptent aucune chaîne de requête.
GET/api/nwc/connections
Liste les connexions actives avec leur consommation de budget, ainsi que la clé publique et le relais publics du fournisseur. Renvoie une liste vide si la personne n'a pas activé l'extension.
{
"connections": [],
"provider": {
"pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"relay": "wss://relay.example.com"
}
}PUT/api/nwc/connections/{clientPubkey}
Enregistre comme connexion une clé publique générée par le client. Rappeler la même clé est idempotent et renvoie 200 au lieu de modifier l'autorisation existante.
{
"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}
Révoque l'autorisation de ce portefeuille pour une clé publique cliente. Les paiements déjà envoyés ne sont pas annulés.
Nostr Wallet Connect
Le client génère son propre secret et construit la chaîne d'appairage localement. Le proxy ne reçoit, ne stocke, ne journalise et ne renvoie jamais un secret d'appairage, et n'en place jamais dans une URL.
Les nouvelles autorisations sont figées sur les permissions pay, lookup et info, avec un budget quotidien et une expiration. LNbits reste la référence pour la propriété, les restrictions de compte et l'application du budget.
Routes LNbits relayées
Deux groupes de routes LNbits sont transmis tels quels. Tout le reste est bloqué.
Routes LNURL publiques, transmises avec un CORS permissif parce que les portefeuilles les appellent depuis une autre origine. L'en-tête Host est réécrit avec votre domaine public pour que LNbits construise des URL de rappel correctes.
GET /.well-known/lnurlp/{username}
GET /lnurlp/api/v1/lnurl/cb/{id}Routes de portefeuille authentifiées, qui exigent un en-tête X-Api-Key et que la vue portefeuille de l'extension utilise.
GET|POST /api/v1/wallet
GET|POST /api/v1/paymentsAuthentification NIP-98
Le provisionnement et chaque modification de Lightning Address utilisent un défi-réponse avec un événement Nostr signé. Ni mots de passe, ni sessions.
- Demandez un défi.
- Construisez un événement de type 27235 portant le défi et la requête exacte qu'il autorise.
- Signez-le avec la clé Nostr de la personne et envoyez-le dans le champ event du corps POST.
- Le service vérifie la signature Schnorr avant de consommer le défi, si bien qu'une tentative ratée ne le gaspille pas.
Balises obligatoires
Les trois balises sont obligatoires et vérifiées à l'identique. La balise u doit être l'URL absolue complète de l'endpoint appelé sur votre propre domaine, et la balise method doit correspondre à la méthode 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());Fenêtres de temps
Deux fenêtres indépendantes s'appliquent : le défi expire 60 secondes après son émission, et le created_at de l'événement doit se situer à moins de 60 secondes de l'heure du serveur. Un client dont l'horloge dérive fortement échouera même avec un défi tout neuf.
Proxy inverse
Terminez le TLS et transmettez vers le port de boucle locale. Le bloc ci-dessous est la configuration de référence.
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;
}Rétablissez l'IP réelle du client si un CDN est devant
La limitation de débit s'appuie sur l'entrée la plus à droite de X-Forwarded-For, que votre proxy inverse ajoute. Si un CDN comme Cloudflare sert votre domaine, cette entrée est l'adresse du nœud du CDN et non celle du visiteur : toutes les personnes derrière le même nœud partagent alors un seul compteur, soit quelques requêtes par minute pour tout le monde à la fois. Rétablissez l'adresse réelle avant que la requête n'atteigne le service.
# 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;Ne sautez ce bloc que si rien ne se trouve entre internet et votre proxy inverse. Gardez les plages d'adresses à jour avec la liste publiée par votre CDN.
Gestion du processus
N'importe quel superviseur convient. Le déploiement de référence utilise pm2, avec la configuration injectée dans l'environnement du processus et la liste des processus enregistrée pour survivre à un redémarrage.
Injectez la clé super-utilisateur via votre superviseur ou un fichier d'environnement qui n'est jamais versionné. Elle ne doit pas vivre dans le dépôt.
Supervision
Deux veilleurs indépendants méritent d'être en place : un contrôle de santé de l'ensemble, et un chien de garde pour le backend Lightning.
Chien de garde du backend Lightning
phoenixd peut atteindre un état où le processus est vivant et à l'écoute mais où son API HTTP ne répond plus, parce qu'une boucle de reconnexion vers le LSP bloque la boucle d'événements. Les portefeuilles signalent des paiements échoués pendant que systemctl donne le service pour actif. Un minuteur qui sonde l'API et redémarre après deux échecs consécutifs corrige cela sans réveiller personne.
# /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.targetLa double vérification compte : un seul dépassement de délai est souvent un hoquet, et redémarrer le backend à chaque hoquet est pire que la panne.
Contrôle de santé de l'ensemble
Un contrôle périodique qui sollicite LNbits, le proxy, les routes LNURL publiques et la génération de facture de bout en bout, et envoie un courriel à chaque changement d'état. Lancez-le depuis cron sur le même hôte.
*/5 * * * * /usr/bin/node /srv/monitor/monitor.mjs >> /srv/monitor/cron.log 2>&1Contrôle de santé de l'ensemble
Si votre contrôle génère de vraies factures, donnez-lui un portefeuille et une adresse dédiés, utilisez une expiration courte et nettoyez les factures. Sonder l'adresse d'une personne réelle toutes les quelques minutes remplit son historique de paiements de factures que personne ne réglera jamais.
Limites et validation
Limites de débit
Par adresse cliente et par minute. Les requêtes au-delà de la limite reçoivent 429.
| Route | Requests / minute |
|---|---|
/api/nwc/connections | 60 |
/api/provision/challenge | 10 |
/api/provision | 5 |
/api/claim-username | 3 |
/api/release-username | 3 |
Noms d'utilisateur Lightning Address
Les noms doivent faire de 3 à 30 caractères, alphanumériques minuscules avec points, tirets et tirets bas, en commençant et en finissant par un caractère alphanumérique.
/^[a-z0-9][a-z0-9._-]{1,28}[a-z0-9]$/Ces noms sont bloqués :
admin support help info noreply
postmaster webmaster abuse root systemRéglages de connexion NWC
- Nom de la connexion : de 1 à 50 caractères, sans caractères de contrôle.
- Limite quotidienne : de 1 à 9 999 999 sats, appliquée sur une fenêtre glissante de 24 heures.
- Expiration : de 1 à 365 jours.
- Maximum de 50 connexions actives par portefeuille.
Corps des requêtes
64 Ko pour le provisionnement et les requêtes Lightning Address, 4 Ko pour la création de connexion NWC.
Code source
Le service tient dans un seul fichier Node avec une unique dépendance d'exécution. Lisez-le avant de le lancer. github.com/nostr-wot/LNbits-proxy