Nostr WoT

Documentação

Tudo que você precisa para integrar Web of Trust em seu aplicativo.

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.

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, ...)

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 lnurlp instalada, e a extensão nwcprovider se 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.

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

Variáveis de ambiente

Cinco variáveis controlam o serviço. Apenas uma é obrigatória.

VariableDefaultDescription
LNBITS_URLhttp://127.0.0.1:5000URL base da sua instância do LNbits.
LNBITS_ADMIN_KEYobrigatóriaChave API de superutilizador do LNbits. O serviço recusa aprovisionar sem ela.
LNBITS_DB_PATH.../data/database.sqlite3Caminho para a base de dados SQLite principal do LNbits.
LNURLP_DB_PATH.../data/ext_lnurlp.sqlite3Caminho para a base de dados SQLite da extensão lnurlp.
PORT3003Porta 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.

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

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

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

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

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

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}

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.

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

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

Autenticaçã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.

  1. Peça um desafio.
  2. Construa um evento do tipo 27235 que transporte o desafio e o pedido exato que autoriza.
  3. Assine-o com a chave Nostr da pessoa e envie-o no campo event do corpo POST.
  4. 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.

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());

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.

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;
}

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.

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;

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.

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

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.

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

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

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

Verificaçã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.

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

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.

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

Estes nomes estão bloqueados:

text
admin  support  help  info  noreply
postmaster  webmaster  abuse  root  system

Definiçõ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