给你的应用接入 Beagle 登录
一个 npm 包、三个调用、服务端二十来行。装什么、import 什么、验什么。
2026 年 9 月最短版本
一个依赖、三个客户端调用、一次服务端校验。动手前只看一篇的话,就看这篇 —— 背后的理由在用 Beagle 登录。
signIn({ nonce })—— 搞清楚来访者是谁readLaunch()—— 他是从 Beagle 里打开你的应用的addFriend({ address, name })—— 把两个人介绍给彼此
三个不能混用的身份
同一个人身上常常有三把钥匙。它们不是彼此的别名,混用是最常见的接入错误。
| 字段 | 是什么 | 谁用它签名 |
|---|---|---|
userid | Beagle 身份(Carrier X25519,base58)。好友请求、登录弹窗、Apps 启动都用它。 | Beagle |
钱包 POST 里的 address | 链上钱包 —— Solana 公钥或 0x… 。钱包路径下的会话标识。 | 钱包 |
carrierAddress | Carrier 可达地址,52 字符。用于发消息、加好友、发奖励。 | 没有人 —— 它是未签名的附加字段,永远不构成证明 |
也不要把 Carrier 地址当成钱包。Carrier 地址只告诉你去哪里找这个人,它本身不证明任何事。
用户可以从三扇门进来
三条路签发的是同一个会话。先从你自己的域名取一次性 nonce,并且始终让用户签你的 origin,不是 app.beagle.chat。
| 谁在签名 | 入口 | 签名前缀 |
|---|---|---|
| app.beagle.chat 上的浏览器,或本机 CLI(127.0.0.1:8766) | /connect 弹窗 | decent-auth |
| 已经在 Beagle 里、从 Apps 标签打开你的应用 | URL 片段 #beagle= | decent-launch |
| iOS / Android Beagle,或任何持有 Solana、ETH 的钱包 | 本地签名,无弹窗 | beagle-meet-wallet |
GET /api/auth/nonce -> 一次性,约 120 秒
POST /api/auth/decent { userid, signature, ... }
POST /api/auth/beagle-launch { #beagle= 带来的断言 }
POST /api/auth/wallet { address, signature, ... }把 decent-launch 的断言当作 decent-auth 去验会失败;而一个两者都照单全收的服务等于没有安全可言。另外务必对着 location.origin 验签,不要对着裸主机名——曾经有一个接入正是因为这个不匹配,几个月里根本不可能成功过。
怎么写代码
不要自己手写弹窗和密码学。@decentnetwork/beagle-connect 就是客户端库,也是你唯一需要加的依赖 —— 它自己会带上 @decentnetwork/peer,那正是服务端验签要用的。
npm install @decentnetwork/beagle-connect
# package.json — that is the only dependency you add.
# It pulls @decentnetwork/peer itself, which is what your server verifies with.登录。 弹窗打开,用户看到你的 origin 和自己的身份,点同意。signIn 在 resolve 之前会先验一次签名,但那次只是给你的界面用的 —— 签发会话之前,服务端必须再验一次。
import { signIn } from "@decentnetwork/beagle-connect";
// The nonce MUST come from your server and be single-use.
// That is the whole replay defence.
const { nonce } = await fetch("/api/auth/nonce").then((r) => r.json());
const who = await signIn({ nonce });
// { userid, address, name, avatar, sig, nonce, uiOrigin }
await fetch("/api/auth/beagle", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(who),
});从 Apps 标签进来的。 Beagle 把签名断言放在 URL 的 fragment 里,所以它不会进服务器日志、也不会出现在 Referer 头。readLaunch 会验签、拒绝超过两分钟的断言,并把 fragment 从地址栏里抹掉。
import { readLaunch } from "@decentnetwork/beagle-connect";
// null when there is nothing to read. Throws when a fragment is present
// but invalid or stale — surface that, do not treat it as signed out.
const who = await readLaunch();
if (who) {
await fetch("/api/auth/beagle-launch", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(who),
});
}服务端验签。 两条路是同一个原语、不同前缀 —— 而且是 XEdDSA,不是 Ed25519,因为 Carrier 密钥是 X25519,它的公钥那一半就是 userid。
import { verifyDetached, base58ToBytes } from "@decentnetwork/peer";
const hexToBytes = (h) => Uint8Array.from(h.match(/../g).map((b) => parseInt(b, 16)));
function check(prefix, userid, origin, salt, sigHex) {
const msg = new TextEncoder().encode(`${prefix}\n${origin}\n${salt}`);
return verifyDetached(base58ToBytes(userid), msg, hexToBytes(sigHex));
}
check("decent-auth", userid, MY_ORIGIN, nonce, sig); // from signIn
check("decent-launch", userid, MY_ORIGIN, ts, sig); // from readLaunchlaunch 没有你签发的 nonce,所以服务端必须:(1) 用 decent-launch 前缀验签;(2) 时间戳偏离超过 120 秒就拒绝;(3) 把这个窗口内收下过的每个 sig 记住,重复的一律拒绝。少了第三条,这个断言就是一个有效期两分钟的 bearer token —— 而两分钟足够了。
把两个人介绍给彼此。 授权是一次动作一个弹窗,你的站点拿不到任何可以留存复用的凭证。
import { addFriend } from "@decentnetwork/beagle-connect";
// address is the `address` field from THEIR signIn — a userid alone
// is not enough to address a friend request.
await addFriend({ address: theirCarrierAddress, name: theirDisplayName });签了:前缀、你的 origin、nonce 或时间戳。没签:name、avatar、punkId、address。它们只是展示用的信息,好让你渲染出一个人而不是一串密钥。当资料看待 —— 永远不要当成身份,更不要当成授权。