@lastrum/auth
v0.2.1
Published
Sign-In with Liquid via Lastrum (SIWL) — server-side verify + client helpers + React component.
Maintainers
Readme
@lastrum/auth
Sign-In with Liquid via Lastrum (SIWL) — login federado para apps web, estilo Sign-In with Ethereum (EIP-4361) adaptado para Liquid Network.
- Sem custódia, sem senha, sem OAuth.
- Identidade pseudonima por-origem (mesma carteira gera pubkeys diferentes em apps diferentes — privacy by design).
- Verificação offline no servidor (sem chamadas pra Lastrum).
- Zero dependências exceto
@noble/secp256k1+@noble/hashes.
Install
npm install @lastrum/auth
# ou
bun add @lastrum/authQuickstart
Servidor (Node.js)
import express from "express";
import { InMemoryNonceStore, verifyMessage } from "@lastrum/auth/server";
const app = express();
app.use(express.json());
const nonceStore = new InMemoryNonceStore();
app.post("/auth/nonce", async (req, res) => {
const nonce = await nonceStore.issue({ ttlSeconds: 600 });
res.json({ nonce, expirationMinutes: 10 });
});
app.post("/auth/verify", async (req, res) => {
const { message, pubkey, address, signature } = req.body;
const result = await verifyMessage(message, {
expectedDomain: req.hostname, // ou seu domain fixo
pubkey,
address,
signature,
nonceStore,
});
if (!result.ok) return res.status(401).json({ error: "auth_failed" });
// result.pubkey é a identidade da sessão (estável por usuário pra este app)
const session = createSession(result.pubkey);
res.cookie("session", session, { httpOnly: true, secure: true, sameSite: "lax" });
res.json({ ok: true, pubkey: result.pubkey });
});
app.listen(3000);Cliente (browser)
import { SignInWithLastrumClient } from "@lastrum/auth";
const client = new SignInWithLastrumClient({
nonceEndpoint: "/auth/nonce",
});
document.getElementById("login").addEventListener("click", async () => {
const result = await client.signIn({
domain: location.host,
uri: location.origin + "/login",
statement: "Login para gerenciar seus pedidos.",
});
// POST pro servidor verificar
const res = await fetch("/auth/verify", {
method: "POST",
credentials: "include",
headers: { "content-type": "application/json" },
body: JSON.stringify(result),
});
if (res.ok) window.location.href = "/dashboard";
});React (opcional)
O componente aceita 3 modos de configuração — escolha conforme sua arquitetura:
1. Produção com backend (recomendado)
import { SignInWithLastrum } from "@lastrum/auth/react";
<SignInWithLastrum
nonceEndpoint="/auth/nonce"
verifyEndpoint="/auth/verify"
onLogin={({ pubkey }) => console.log("Logado:", pubkey)}
/>Servidor controla emissão e single-use do nonce (Redis), e emite a sessão (cookie/JWT) após verify. Padrão pra SaaS, exchanges, qualquer dapp com API própria. Use isto em produção.
2. SPA sem backend / Edge functions
import { SignInWithLastrum } from "@lastrum/auth/react";
import { verifyMessage, InMemoryNonceStore } from "@lastrum/auth";
const nonces = new InMemoryNonceStore();
<SignInWithLastrum
nonceFn={async () => nonces.issue({ ttlSeconds: 600 })}
verifyFn={async ({ message, signature, pubkey, address }) => {
const r = await verifyMessage(message, {
expectedDomain: location.host, pubkey, address, signature, nonceStore: nonces,
});
if (!r.ok) throw new Error(r.reason);
return r;
}}
onLogin={(result) => /* persiste em localStorage / Zustand / etc */}
/>Sem servidor — nonceFn produz o nonce, verifyFn valida assinatura
client-side. Anti-replay vive em memória do tab (suficiente pra dapps
puramente client-side onde a sessão também é client-side).
3. Demo mode (sem verify)
<SignInWithLastrum
nonceFn={() => crypto.randomUUID()}
onLogin={(result) => console.log("Signed!", result.signature)}
/>Sem verifyEndpoint/verifyFn → o componente pula verify e dispara
onLogin com o SignInResult cru ({ message, signature, address, pubkey }).
Útil pra páginas de showcase, playgrounds, prototipagem.
⚠️ Quando escolher cada um: se você tem backend e quer sessão real, 1. Se é SPA puro / Edge / Cloudflare Worker, 2. Se é só demo / preview UI, 3.
API
verifyMessage(message, opts) → VerifyResult
Server-side. Verifica:
- Parse OK
domainbate comexpectedDomainpubkeyeaddressbatem com os reportados- Janela temporal:
Issued At ≤ now + 60s skew,now ≤ Expiration Time, janela ≤ 10 min - Nonce single-use (consome via
nonceStore) - Assinatura ECDSA secp256k1 sobre
sha256(message)válida
Retorna { ok: true, pubkey, address, issuedAt, message } ou
{ ok: false, reason: "..." } com reason opaca pra log.
SignInWithLastrumClient.signIn(params) → SignInResult
Cliente. Encadeia: fetch nonce → liquid_requestAccounts → buildMessage →
liquid_signMessage. Retorna shape pronto pra POST no /verify.
NonceStore
Interface mínima. Implementações:
InMemoryNonceStore— pra dev/teste- Em produção, wrap Redis (ver doc
docs/integrations/login-with-redbird.md)
buildMessage(params) / parseMessage(text)
Helpers puros se você quer montar/parsear mensagens fora do client. Formato em ADR-0025.
Versionamento
Pacote segue semver próprio começando em 0.1.0. Independente da extensão Lastrum
(que usa cohort version 3.x). Compatível com Lastrum v3.5+ (Spec 010 — Lastrum Provider).
License
MIT
