全部文档/给你的应用接入 Beagle 登录
身份与服务

给你的应用接入 Beagle 登录

一个 npm 包、三个调用、服务端二十来行。装什么、import 什么、验什么。

2026 年 9 月

最短版本

一个依赖、三个客户端调用、一次服务端校验。动手前只看一篇的话,就看这篇 —— 背后的理由在用 Beagle 登录

  • signIn({ nonce }) —— 搞清楚来访者是谁
  • readLaunch() —— 他是从 Beagle 里打开你的应用的
  • addFriend({ address, name }) —— 把两个人介绍给彼此

三个不能混用的身份

同一个人身上常常有三把钥匙。它们不是彼此的别名,混用是最常见的接入错误。

字段是什么谁用它签名
useridBeagle 身份(Carrier X25519,base58)。好友请求、登录弹窗、Apps 启动都用它。Beagle
钱包 POST 里的 address链上钱包 —— Solana 公钥或 0x… 。钱包路径下的会话标识。钱包
carrierAddressCarrier 可达地址,52 字符。用于发消息、加好友、发奖励。没有人 —— 它是未签名的附加字段,永远不构成证明
不要把链上公钥当成 userid

也不要把 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 readLaunch
launch 路径多出的三条规矩

launch 没有你签发的 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。它们只是展示用的信息,好让你渲染出一个人而不是一串密钥。当资料看待 —— 永远不要当成身份,更不要当成授权。

下一篇Beagle 到底是什么