Proxy de aprovisionamiento de LNbits
Un pequeño servicio en Node que se sitúa delante de LNbits y ofrece a los clientes Nostr una superficie segura y reducida para aprovisionar monederos, reclamar Lightning Addresses y gestionar concesiones de Nostr Wallet Connect. Esta página es una guía de autoalojamiento: ejecuta tu propia instancia contra tu propio LNbits.
Por qué un proxy
LNbits expone una API administrativa completa. Entregarla a una extensión de navegador significaría confiar autoridad sobre toda la instancia a cada cliente. El proxy publica solo el puñado de rutas que un monedero necesita realmente, autentica a los propietarios con su clave Nostr en lugar de una contraseña, y mantiene la credencial de superusuario de LNbits en el servidor, que es donde corresponde.
Las instancias propias de LNbits necesitan este adaptador para que la interfaz de gestión de monederos de la extensión funcione con ellas. Nada en la extensión es específico de un operador.
Arquitectura
Las peticiones llegan a tu proxy inverso por TLS y se reenvían al servicio en loopback. El servicio habla con LNbits por HTTP y lee directamente dos bases de datos SQLite de LNbits para consultas que la API HTTP no expone.
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, ...)El servicio escucha únicamente en 127.0.0.1. Nunca es accesible directamente desde internet, así que un proxy inverso delante es obligatorio, no opcional.
El acceso directo a la base de datos es como el proxy asocia una clave pública de Nostr con un monedero de LNbits y como escribe los enlaces de pago de Lightning Address. Ambos archivos deben ser legibles y escribibles por el usuario con el que se ejecuta el servicio.
Requisitos previos
- Node.js 24 o superior. El servicio usa el módulo SQLite integrado, que sigue siendo experimental en versiones anteriores.
- Una instancia de LNbits funcionando con la extensión
lnurlpinstalada, y la extensiónnwcprovidersi quieres soporte para Nostr Wallet Connect. - Un backend Lightning detrás de LNbits. El despliegue de referencia usa phoenixd, pero al proxy le da igual cuál ejecutes.
- La clave API de superusuario de LNbits, que el servicio usa para crear cuentas y monederos. Esta clave nunca se reenvía a los clientes.
Instalación y configuración
Clona el repositorio, instala la única dependencia de ejecución y arranca el servicio con su configuración en el entorno.
Variables de entorno
Cinco variables controlan el servicio. Solo una es obligatoria.
| Variable | Default | Description |
|---|---|---|
LNBITS_URL | http://127.0.0.1:5000 | URL base de tu instancia de LNbits. |
LNBITS_ADMIN_KEY | obligatoria | Clave API de superusuario de LNbits. El servicio se niega a aprovisionar sin ella. |
LNBITS_DB_PATH | .../data/database.sqlite3 | Ruta a la base de datos SQLite principal de LNbits. |
LNURLP_DB_PATH | .../data/ext_lnurlp.sqlite3 | Ruta a la base de datos SQLite de la extensión lnurlp. |
PORT | 3003 | Puerto de loopback en el que escucha el servicio. |
Cambia el dominio antes de desplegar
El dominio público es una constante en el código fuente, no una variable de entorno. La verificación NIP-98 rechaza cualquier evento firmado cuya etiqueta u no coincida exactamente, así que una copia sin modificar rechazará todas las peticiones de aprovisionamiento en tu propio dominio. Edita la constante en server.js con tu nombre de host antes de arrancar el servicio.
const DOMAIN = 'zaps.example.com';Lo que expone tu instancia
A continuación está todo lo que responde el servicio. Cualquier ruta que no aparezca aquí devuelve 404, incluido el resto de la API de LNbits.
Aprovisionamiento de monederos
GET/api/provision/challenge
Emite un desafío aleatorio de un solo uso. Sin autenticación. Los desafíos caducan a los 60 segundos y se consumen en el primer uso correcto.
{
"challenge": "7f3a…"
}POST/api/provision
Crea un monedero para una clave pública de Nostr, o devuelve el existente. La respuesta incluye las claves del monedero y se marca como no-store.
Envía el evento NIP-98 firmado en el campo event, con un nombre de monedero opcional.
{
"event": {
"kind": 27235,
"…": "signed NIP-98 event"
},
"name": "My Wallet"
}{
"walletId": "…",
"adminkey": "…",
"inkey": "…",
"lightningAddress": null
}Lightning Address
POST/api/claim-username
Reclama un nombre de usuario de Lightning Address para la clave autenticada. Crea el enlace de pago LNURL y registra el nombre en la cuenta.
GET/api/lightning-address?pubkey={hex}
Consulta la Lightning Address reclamada por una clave pública. Sin autenticación. El parámetro pubkey es obligatorio y debe tener 64 caracteres hexadecimales en minúscula.
{
"lightningAddress": "[email protected]"
}POST/api/release-username
Libera un nombre reclamado, dejándolo disponible para cualquier otra persona y eliminando el enlace de pago.
Nostr Wallet Connect
Gestión de concesiones NWC limitada a cada monedero. Todas las rutas exigen la clave API de administración de ese monedero en la cabecera X-Api-Key; las claves de solo factura se rechazan. Estas rutas no aceptan ninguna cadena de consulta.
GET/api/nwc/connections
Lista las conexiones activas con su uso de presupuesto, además de la clave pública y el relay públicos del proveedor. Devuelve una lista vacía si la persona no ha activado la extensión.
{
"connections": [],
"provider": {
"pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"relay": "wss://relay.example.com"
}
}PUT/api/nwc/connections/{clientPubkey}
Registra una clave pública generada por el cliente como conexión. Repetir la llamada con la misma clave es idempotente y devuelve 200 en lugar de editar la concesión existente.
{
"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 concesión de ese monedero para una clave pública de cliente. Los pagos ya enviados no se cancelan.
Nostr Wallet Connect
El cliente genera su propio secreto y construye la cadena de emparejamiento localmente. El proxy nunca recibe, almacena, registra ni devuelve un secreto de emparejamiento, y nunca lo pone en una URL.
Las concesiones nuevas se fijan a los permisos pay, lookup e info, con un presupuesto diario y una caducidad. LNbits sigue siendo la autoridad sobre propiedad, restricciones de cuenta y cumplimiento del presupuesto.
Rutas de LNbits redirigidas
Se reenvían literalmente dos grupos de rutas de LNbits. Todo lo demás queda bloqueado.
Rutas LNURL públicas, reenviadas con CORS permisivo porque los monederos las llaman desde otro origen. La cabecera Host se reescribe con tu dominio público para que LNbits construya URLs de callback correctas.
GET /.well-known/lnurlp/{username}
GET /lnurlp/api/v1/lnurl/cb/{id}Rutas de monedero autenticadas, que requieren una cabecera X-Api-Key y que usa la vista de monedero de la extensión.
GET|POST /api/v1/wallet
GET|POST /api/v1/paymentsAutenticación NIP-98
El aprovisionamiento y todas las modificaciones de Lightning Address usan desafío-respuesta con un evento Nostr firmado. No hay contraseñas ni sesiones.
- Solicita un desafío.
- Construye un evento de tipo 27235 que lleve el desafío y la petición exacta que autoriza.
- Fírmalo con la clave Nostr de la persona y envíalo en el campo event del cuerpo POST.
- El servicio verifica la firma Schnorr antes de consumir el desafío, de modo que un intento fallido no lo gasta.
Etiquetas obligatorias
Las tres etiquetas son obligatorias y se comprueban de forma exacta. La etiqueta u debe ser la URL absoluta completa del endpoint invocado en tu propio dominio, y la etiqueta method debe coincidir con el método 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());Tiempos
Se aplican dos ventanas independientes: el desafío caduca 60 segundos después de emitirse, y el created_at del evento debe estar dentro de los 60 segundos de la hora del servidor. Un cliente con el reloj muy desviado fallará incluso con un desafío recién emitido.
Proxy inverso
Termina TLS y reenvía al puerto de loopback. El bloque siguiente es la configuración de referencia.
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;
}Establece la IP real del cliente si hay una CDN delante
La limitación de tasa se basa en la entrada más a la derecha de X-Forwarded-For, que añade tu proxy inverso. Si una CDN como Cloudflare sirve tu dominio, esa entrada es la dirección del nodo de la CDN y no la de quien visita, y todas las personas detrás del mismo nodo comparten un único contador: unas pocas peticiones por minuto para todo el mundo a la vez. Restaura la dirección real antes de que la petición llegue al servicio.
# 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;Omite este bloque solo si no hay nada entre internet y tu proxy inverso. Mantén los rangos de direcciones al día con la lista publicada por tu CDN.
Gestión del proceso
Sirve cualquier supervisor. El despliegue de referencia usa pm2, con la configuración inyectada en el entorno del proceso y la lista de procesos guardada para que sobreviva a un reinicio.
Inyecta la clave de superusuario mediante tu supervisor o un archivo de entorno que nunca se suba al repositorio. No debe vivir en el código.
Monitorización
Merece la pena ejecutar dos vigilantes independientes: una comprobación de salud del conjunto y un watchdog para el backend Lightning.
Watchdog del backend Lightning
phoenixd puede quedar en un estado en el que el proceso está vivo y escuchando pero su API HTTP no responde nunca, porque un bucle de reconexión al LSP bloquea el bucle de eventos. Los monederos informan de pagos fallidos mientras systemctl da el servicio por activo. Un temporizador que sondea la API y reinicia tras dos fallos consecutivos se recupera de esto sin que nadie se levante de madrugada.
# /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 doble comprobación importa: un único tiempo de espera agotado suele ser un parpadeo, y reiniciar el backend en cada parpadeo es peor que el fallo.
Comprobación de salud del conjunto
Una comprobación periódica que ejercita LNbits, el proxy, las rutas LNURL públicas y la generación de facturas de extremo a extremo, y envía correo cuando cambia el estado. Ejecútala desde cron en el mismo host.
*/5 * * * * /usr/bin/node /srv/monitor/monitor.mjs >> /srv/monitor/cron.log 2>&1Comprobación de salud del conjunto
Si tu comprobación genera facturas reales, dale un monedero y una dirección propios, usa una caducidad corta y limpia las facturas. Sondear la dirección de una persona real cada pocos minutos llena su historial de pagos con facturas que nadie pagará nunca.
Límites y validación
Límites de tasa
Por dirección de cliente y por minuto. Las peticiones que superan el límite reciben 429.
| Route | Requests / minute |
|---|---|
/api/nwc/connections | 60 |
/api/provision/challenge | 10 |
/api/provision | 5 |
/api/claim-username | 3 |
/api/release-username | 3 |
Nombres de usuario de Lightning Address
Los nombres deben tener entre 3 y 30 caracteres, alfanuméricos en minúscula con puntos, guiones y guiones bajos, empezando y terminando por un carácter alfanumérico.
/^[a-z0-9][a-z0-9._-]{1,28}[a-z0-9]$/Estos nombres están bloqueados:
admin support help info noreply
postmaster webmaster abuse root systemAjustes de conexión NWC
- Nombre de la conexión: de 1 a 50 caracteres, sin caracteres de control.
- Límite diario: de 1 a 9.999.999 sats, aplicado en una ventana móvil de 24 horas.
- Caducidad: de 1 a 365 días.
- Máximo de 50 conexiones activas por monedero.
Cuerpos de las peticiones
64 KB para aprovisionamiento y peticiones de Lightning Address, 4 KB para la creación de conexiones NWC.
Código fuente
El servicio es un único archivo Node con una sola dependencia de ejecución. Léelo antes de ejecutarlo. github.com/nostr-wot/LNbits-proxy