Proxy de aprovisionamento do LNbits
Um pequeno serviço em Node que fica à frente do LNbits e dá aos clientes Nostr uma superfície segura e reduzida para aprovisionar carteiras, reclamar Lightning Addresses e gerir concessões de Nostr Wallet Connect. Esta página é um guia de auto-hospedagem: execute a sua própria instância no seu próprio LNbits.
Porquê um proxy
O LNbits expõe uma API administrativa completa. Entregá-la a uma extensão de navegador significaria confiar autoridade sobre toda a instância a cada cliente. O proxy publica apenas o punhado de rotas de que uma carteira realmente precisa, autentica os proprietários com a sua chave Nostr em vez de uma palavra-passe, e mantém a credencial de superutilizador do LNbits no servidor, onde deve estar.
Instâncias próprias do LNbits precisam deste adaptador para que a interface de gestão de carteiras da extensão funcione com elas. Nada na extensão é específico de um operador.
Arquitetura
Os pedidos chegam ao seu proxy reverso por TLS e são encaminhados para o serviço em loopback. O serviço fala com o LNbits por HTTP e lê diretamente duas bases de dados SQLite do LNbits para consultas que a API HTTP não expõe.
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, ...)O serviço escuta apenas em 127.0.0.1. Nunca é acessível diretamente a partir da internet, por isso um proxy reverso à frente é obrigatório, não opcional.
O acesso direto à base de dados é como o proxy associa uma chave pública Nostr a uma carteira do LNbits e como escreve as ligações de pagamento de Lightning Address. Ambos os ficheiros têm de ser legíveis e graváveis pelo utilizador com que o serviço corre.
Pré-requisitos
- Node.js 24 ou superior. O serviço usa o módulo SQLite integrado, que ainda é experimental em versões anteriores.
- Uma instância do LNbits a funcionar com a extensão
lnurlpinstalada, e a extensãonwcproviderse quiser suporte para Nostr Wallet Connect. - Um backend Lightning por trás do LNbits. A implementação de referência usa phoenixd, mas ao proxy é indiferente qual utilize.
- A chave API de superutilizador do LNbits, que o serviço usa para criar contas e carteiras. Esta chave nunca é encaminhada para os clientes.
Instalação e configuração
Clone o repositório, instale a única dependência de execução e arranque o serviço com a configuração no ambiente.
Variáveis de ambiente
Cinco variáveis controlam o serviço. Apenas uma é obrigatória.
| Variable | Default | Description |
|---|---|---|
LNBITS_URL | http://127.0.0.1:5000 | URL base da sua instância do LNbits. |
LNBITS_ADMIN_KEY | obrigatória | Chave API de superutilizador do LNbits. O serviço recusa aprovisionar sem ela. |
LNBITS_DB_PATH | .../data/database.sqlite3 | Caminho para a base de dados SQLite principal do LNbits. |
LNURLP_DB_PATH | .../data/ext_lnurlp.sqlite3 | Caminho para a base de dados SQLite da extensão lnurlp. |
PORT | 3003 | Porta de loopback em que o serviço escuta. |
Mude o domínio antes de implementar
O domínio público é uma constante no código-fonte, não uma variável de ambiente. A verificação NIP-98 rejeita qualquer evento assinado cuja etiqueta u não corresponda exatamente, por isso uma cópia não modificada recusará todos os pedidos de aprovisionamento no seu próprio domínio. Edite a constante em server.js com o seu nome de anfitrião antes de arrancar o serviço.
const DOMAIN = 'zaps.example.com';O que a sua instância expõe
Abaixo está tudo o que o serviço responde. Qualquer rota que não conste desta lista devolve 404, incluindo o resto da API do LNbits.
Aprovisionamento de carteiras
GET/api/provision/challenge
Emite um desafio aleatório de uso único. Sem autenticação. Os desafios expiram ao fim de 60 segundos e são consumidos na primeira utilização bem-sucedida.
{
"challenge": "7f3a…"
}POST/api/provision
Cria uma carteira para uma chave pública Nostr, ou devolve a existente. A resposta inclui as chaves da carteira e é marcada como no-store.
Envie o evento NIP-98 assinado no campo event, com um nome de carteira opcional.
{
"event": {
"kind": 27235,
"…": "signed NIP-98 event"
},
"name": "My Wallet"
}{
"walletId": "…",
"adminkey": "…",
"inkey": "…",
"lightningAddress": null
}Lightning Address
POST/api/claim-username
Reclama um nome de utilizador de Lightning Address para a chave autenticada. Cria a ligação de pagamento LNURL e regista o nome na conta.
GET/api/lightning-address?pubkey={hex}
Consulta a Lightning Address reclamada por uma chave pública. Sem autenticação. O parâmetro pubkey é obrigatório e tem de ter 64 caracteres hexadecimais minúsculos.
{
"lightningAddress": "[email protected]"
}POST/api/release-username
Liberta um nome reclamado, deixando-o disponível para outra pessoa e removendo a ligação de pagamento.
Nostr Wallet Connect
Gestão de concessões NWC limitada a cada carteira. Todas as rotas exigem a chave API de administração dessa carteira no cabeçalho X-Api-Key; chaves apenas de fatura são rejeitadas. Estas rotas não aceitam qualquer cadeia de consulta.
GET/api/nwc/connections
Lista as ligações ativas com o respetivo uso de orçamento, além da chave pública e do relay públicos do fornecedor. Devolve uma lista vazia se a pessoa não tiver ativado a extensão.
{
"connections": [],
"provider": {
"pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"relay": "wss://relay.example.com"
}
}PUT/api/nwc/connections/{clientPubkey}
Regista uma chave pública gerada pelo cliente como ligação. Repetir a chamada com a mesma chave é idempotente e devolve 200 em vez de editar a concessão 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}
Revoga a concessão dessa carteira para uma chave pública de cliente. Pagamentos já enviados não são cancelados.
Nostr Wallet Connect
O cliente gera o seu próprio segredo e constrói a cadeia de emparelhamento localmente. O proxy nunca recebe, armazena, regista nem devolve um segredo de emparelhamento, e nunca o coloca num URL.
As concessões novas ficam fixadas nas permissões pay, lookup e info, com um orçamento diário e uma validade. O LNbits continua a ser a autoridade sobre propriedade, restrições de conta e cumprimento do orçamento.
Rotas do LNbits encaminhadas
São encaminhados literalmente dois grupos de rotas do LNbits. Tudo o resto é bloqueado.
Rotas LNURL públicas, encaminhadas com CORS permissivo porque as carteiras chamam-nas de outra origem. O cabeçalho Host é reescrito com o seu domínio público para que o LNbits construa URLs de callback corretos.
GET /.well-known/lnurlp/{username}
GET /lnurlp/api/v1/lnurl/cb/{id}Rotas de carteira autenticadas, que exigem um cabeçalho X-Api-Key e são usadas pela vista de carteira da extensão.
GET|POST /api/v1/wallet
GET|POST /api/v1/paymentsAutenticação NIP-98
O aprovisionamento e todas as alterações de Lightning Address usam desafio-resposta com um evento Nostr assinado. Não há palavras-passe nem sessões.
- Peça um desafio.
- Construa um evento do tipo 27235 que transporte o desafio e o pedido exato que autoriza.
- Assine-o com a chave Nostr da pessoa e envie-o no campo event do corpo POST.
- O serviço verifica a assinatura Schnorr antes de consumir o desafio, pelo que uma tentativa falhada não o gasta.
Etiquetas obrigatórias
As três etiquetas são obrigatórias e verificadas de forma exata. A etiqueta u tem de ser o URL absoluto completo do endpoint invocado no seu próprio domínio, e a etiqueta method tem de corresponder ao 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());Tempos
Aplicam-se duas janelas independentes: o desafio expira 60 segundos depois de emitido, e o created_at do evento tem de estar dentro de 60 segundos da hora do servidor. Um cliente com o relógio muito desviado falhará mesmo com um desafio acabado de emitir.
Proxy reverso
Termine o TLS e encaminhe para a porta de loopback. O bloco seguinte é a configuração de referência.
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;
}Defina o IP real do cliente se houver uma CDN à frente
A limitação de taxa baseia-se na entrada mais à direita de X-Forwarded-For, que o seu proxy reverso acrescenta. Se uma CDN como a Cloudflare servir o seu domínio, essa entrada é o endereço do nó da CDN e não o de quem visita, e todas as pessoas por trás do mesmo nó partilham um único contador: umas poucas requisições por minuto para toda a gente ao mesmo tempo. Reponha o endereço real antes de o pedido chegar ao serviço.
# 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;Ignore este bloco apenas se não houver nada entre a internet e o seu proxy reverso. Mantenha os intervalos de endereços atualizados com a lista publicada pela sua CDN.
Gestão do processo
Qualquer supervisor serve. A implementação de referência usa pm2, com a configuração injetada no ambiente do processo e a lista de processos guardada para sobreviver a um reinício.
Injete a chave de superutilizador através do seu supervisor ou de um ficheiro de ambiente que nunca vá para o repositório. Não pode viver no código.
Monitorização
Vale a pena correr dois vigilantes independentes: uma verificação de saúde do conjunto e um watchdog para o backend Lightning.
Watchdog do backend Lightning
O phoenixd pode ficar num estado em que o processo está vivo e à escuta mas a sua API HTTP nunca responde, porque um ciclo de reconexão ao LSP bloqueia o ciclo de eventos. As carteiras reportam pagamentos falhados enquanto o systemctl dá o serviço como ativo. Um temporizador que sonda a API e reinicia após duas falhas consecutivas recupera disto sem ninguém ter de acordar.
# /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.targetA dupla verificação importa: um único tempo esgotado costuma ser um soluço, e reiniciar o backend a cada soluço é pior do que a falha.
Verificação de saúde do conjunto
Uma verificação periódica que exercita o LNbits, o proxy, as rotas LNURL públicas e a geração de faturas de ponta a ponta, e envia email quando o estado muda. Corra-a a partir do cron no mesmo anfitrião.
*/5 * * * * /usr/bin/node /srv/monitor/monitor.mjs >> /srv/monitor/cron.log 2>&1Verificação de saúde do conjunto
Se a sua verificação gerar faturas reais, dê-lhe uma carteira e um endereço próprios, use uma validade curta e limpe as faturas. Sondar o endereço de uma pessoa real a cada poucos minutos enche o seu histórico de pagamentos com faturas que ninguém vai pagar.
Limites e validação
Limites de taxa
Por endereço de cliente e por minuto. Os pedidos acima do limite recebem 429.
| Route | Requests / minute |
|---|---|
/api/nwc/connections | 60 |
/api/provision/challenge | 10 |
/api/provision | 5 |
/api/claim-username | 3 |
/api/release-username | 3 |
Nomes de utilizador de Lightning Address
Os nomes têm de ter entre 3 e 30 caracteres, alfanuméricos minúsculos com pontos, hífenes e sublinhados, começando e terminando por um carácter alfanumérico.
/^[a-z0-9][a-z0-9._-]{1,28}[a-z0-9]$/Estes nomes estão bloqueados:
admin support help info noreply
postmaster webmaster abuse root systemDefinições de ligação NWC
- Nome da ligação: de 1 a 50 caracteres, sem caracteres de controlo.
- Limite diário: de 1 a 9 999 999 sats, aplicado numa janela móvel de 24 horas.
- Validade: de 1 a 365 dias.
- Máximo de 50 ligações ativas por carteira.
Corpos dos pedidos
64 KB para aprovisionamento e pedidos de Lightning Address, 4 KB para a criação de ligações NWC.
Código-fonte
O serviço é um único ficheiro Node com uma só dependência de execução. Leia-o antes de o executar. github.com/nostr-wot/LNbits-proxy