@decentnetwork/beagle-ui
The Beagle chat UI without the machine it runs on — shared by the desktop install and the browser build.
npm install @decentnetwork/beagle-ui@decentnetwork/beagle-ui is the Beagle chat interface as a single browser bundle. It is the same UI that the Beagle desktop client serves and that app.beagle.chat runs in a tab. You mount it into a page and tell it how to reach files. It then drives a Beagle backend through fetch("/api/…") calls. A second entry point, @decentnetwork/beagle-ui/wallet-proof, holds the wallet-binding helpers (Ethereum and Solana proofs tied to a Beagle userid). That entry has TypeScript types and also runs in Node.
When to use it
- Host the Beagle UI in your own page or shell (an Electron/Tauri wrapper, a browser extension, a branded web page) on top of a backend that speaks Beagle's
/api/*routes: thebeagleCLI, or your own adapter. - Bind a wallet address to a Beagle identity, or verify such a binding on a server, with
wallet-proof.
If you want a working chat client rather than a component, run @decentnetwork/beagle, which serves this bundle for you. To build a different UI on the protocol, use @decentnetwork/peer.
The main bundle is browser-only. Importing it in Node throws window is not defined. It also expects globals instead of importing them:
| Global | Needed for |
|---|---|
window.React, window.ReactDOM (with createRoot) | Everything |
window.PeerWebRTC (@decentnetwork/peer-webrtc) | Audio/video calls. Without it the UI logs a warning and turns calls off. |
window.ChatComponents (@decentnetwork/chat-components) | Interactive form messages |
Install
npm install @decentnetwork/beagle-ui@0.2.40 react@18.3.1 react-dom@18.3.1 vite@5.4.10The package README asks consumers to pin an exact version. UI changes ship manually, and a caret range would pull in a UI nobody chose for your page. React and Vite are only what this page was verified with.
Quickstart
A minimal Vite page that mounts the UI against a locally running beagle process:
<!-- index.html -->
<!doctype html>
<html>
<head><meta charset="utf-8" /><title>Beagle UI host</title></head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>// src/globals.js
// beagle-ui reads React and ReactDOM as globals; set them before it loads.
import React from "react";
import * as ReactDOMClient from "react-dom/client";
window.React = React;
window.ReactDOM = ReactDOMClient;// src/main.js
import "./globals.js";
import { mount, setUiHost, BEAGLE_UI_VERSION } from "@decentnetwork/beagle-ui";
setUiHost({
fileUrl: (name) => "/api/file-download?name=" + encodeURIComponent(name),
fileDownloadUrl: (name) => "/api/file-download?name=" + encodeURIComponent(name) + "&dl=1",
});
mount(document.getElementById("root"));
console.log("beagle-ui", BEAGLE_UI_VERSION, "mounted");// vite.config.js
import { defineConfig } from "vite";
// Forward the UI's /api/* calls to a local `beagle` process (default port 8766).
export default defineConfig({
server: { proxy: { "/api": process.env.BEAGLE_API ?? "http://127.0.0.1:8766" } },
preview: { proxy: { "/api": process.env.BEAGLE_API ?? "http://127.0.0.1:8766" } },
});npx vite build
# with a backend running, e.g. `npx beagle --port 18766` (see /docs/beagle):
BEAGLE_API=http://127.0.0.1:18766 npx vite previewsetUiHost() must run before mount(), because its flags are read during the first render. Importing ./globals.js first makes sure React is on window before the bundle runs.
Verified output (npm @decentnetwork/beagle-ui@0.2.40, React 18.3.1, Vite 5.4.10, Node 24.3.0, 2026-11-06). In the verification run the built page was previewed against beagle 0.1.131 on port 18766 and opened in a browser. It rendered the chat UI (friends list and Beagle Assistant), and the browser console showed:
$ npx vite build
vite v5.4.10 building for production...
transforming...
✓ 26 modules transformed.
rendering chunks...
computing gzip size...
dist/index.html 0.22 kB │ gzip: 0.18 kB
dist/assets/index-CCw0fUVg.js 413.37 kB │ gzip: 127.69 kB
✓ built in 532ms
browser console:
[log] beagle-ui 0.2.40 mounted
[log] [rtc] Carrier bootstrap ICE servers: {0: stun:13.58.208.50:3478, …}
[log] [rtc] ICE warmup done (relay)
[warn] [call] WebRTC or peer-webrtc unavailable — calls disabledThe last line is expected, because this page did not load window.PeerWebRTC.
Wallet proofs in Node
// wallet.mjs
// Pure helpers from @decentnetwork/beagle-ui/wallet-proof also run in Node.
// Bind a Solana wallet to a Beagle userid, then verify the proof.
import { generateKeyPairSync, sign } from "node:crypto";
import {
base58Encode, signSolProof, encodeWalletProof, decodeWalletProof,
verifyWalletProof, isWalletAddress,
} from "@decentnetwork/beagle-ui/wallet-proof";
// A throwaway ed25519 key stands in for a browser wallet (Phantom etc.).
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
const raw = Buffer.from(publicKey.export({ format: "jwk" }).x, "base64url");
const address = base58Encode(raw);
const wallet = { signMessage: async (bytes) => sign(null, bytes, privateKey) };
const userid = "D6p9dm91J49jTkUUrYFVnFRnRqrtEmwEwCR1YuPAZYye"; // a Beagle userid
console.log("valid sol address:", isWalletAddress("sol", address));
const proof = await signSolProof(wallet, { userid, address });
console.log(proof.message);
const blob = encodeWalletProof(proof); // what gets stored in the profile record
console.log("decoded ok:", decodeWalletProof(blob)?.signature === proof.signature);
console.log("verify:", verifyWalletProof({ chain: "sol", address, userid, proof: blob }));
console.log("verify for another userid:",
verifyWalletProof({ chain: "sol", address, userid: "someone-else", proof: blob }));Verified output (npm @decentnetwork/beagle-ui@0.2.40, Node 24.3.0, 2026-11-06):
$ node wallet.mjs
valid sol address: true
Beagle wallet binding
I bind this wallet to my Beagle identity.
userid: D6p9dm91J49jTkUUrYFVnFRnRqrtEmwEwCR1YuPAZYye
chain: sol
address: 2Ag8oCNN2LVr8vy8hnyahBmxgyMRyTncvNK3FWFYU9BN
issued: 2026-11-06T08:50:28Z
decoded ok: true
verify: { ok: true, issued: '2026-11-06T08:50:28Z' }
verify for another userid: { ok: false, error: 'proof names another Beagle identity' }API reference
@decentnetwork/beagle-ui (browser)
This entry ships no .d.ts. The names below are its runtime exports in 0.2.40.
| Export | Signature | Purpose |
|---|---|---|
BEAGLE_UI_VERSION | string | Bundle version ("0.2.40") |
mount | (el: Element) => void | Render the app into el with ReactDOM.createRoot, and remove a #boot placeholder if one exists |
setUiHost | (host: object) => void | Platform hooks. Call before mount. |
dkGet | (path) => Promise<any> | fetch(path) and parse JSON |
dkPost | (path, body) => Promise<any> | POST JSON with a 15-second timeout. Resolves { ok: false, error } on failure. |
dkApi | object | UI request helpers, e.g. send(userid, text), add(address), accept(userid), reject(userid), remove(userid), alias(userid, alias), markRead(userid) (all POST to /api/*) |
dkCopy | (text) => Promise<boolean> | Clipboard copy with a fallback |
dkFileSize | (bytes) => string | "1.5 KB" style formatting |
shortKey | (s, head = 6, tail = 5) => string | Shorten a userid or address |
Icon, Btn, Tag, Mono, Section, StatusDot | React components | Building blocks of the Beagle look |
DkAvatar, DkIdenticon, DkImgAvatar, DkPunkAvatar, DkEnsAvatar | React components | Avatars for peers |
setUiHost() keys (all optional; anything left out keeps the local-server default):
| Key | Purpose |
|---|---|
fileUrl(name), fileDownloadUrl(name) | Turn a stored file into a URL for <img> / <a download>. These requests bypass a patched fetch, so a backend inside the tab must supply them. |
prefetchThreadFiles(msgs) | Resolve a thread's files before it renders |
DkWelcome, DkLockedOut, DkBrowserNotice | Components for first run, "another tab owns the identity", and the browser caveat |
hostNeedsWelcome, hostPrefersRtcFile, hostHasKeyBackup | Feature flags (first-run flow, DataChannel file transfer, keypair export) |
hostEnsWrite, hostWalletBind, hostAssistant, hostBrandHref | Read by the bundle's setUiHost but not described in the README |
hostTabs, hideApps, hostApps, hostProfileSections, hostHeaderActions, isProtocolMessage | Same as above: read by the bundle, not described in the README |
@decentnetwork/beagle-ui/wallet-proof (browser and Node, typed)
type WalletChain = "eth" | "sol";
interface WalletProof { message: string; signature: string }
type VerifyResult = { ok: true; issued: string } | { ok: false; error: string };
const WALLET_PROOF_VERSION: 1;
const WALLET_CHAINS: WalletChain[];
const COIN_TYPE: { eth: "60"; sol: "501" };
const PROOF_KEY: { eth: "proof.eth"; sol: "proof.sol" };
const BIND_MAX_AGE_MS: number;
function isWalletAddress(chain: string, address: unknown): boolean;
function proofTime(date?: Date): string;
function walletProofMessage(f: { userid; chain: WalletChain; address; issued }): string;
function parseWalletProofMessage(msg: unknown): { userid; chain; address; issued } | null;
function encodeWalletProof(p: WalletProof): string;
function decodeWalletProof(text: unknown): WalletProof | null;
function base58Encode(bytes: Uint8Array): string;
function base58Decode(str: string): Uint8Array;
function signEthProof(provider: { request(a: { method: string; params: unknown[] }): Promise<unknown> },
f: { userid: string; address: string; issued?: string }): Promise<WalletProof>; // personal_sign
function signSolProof(wallet: { signMessage(m: Uint8Array, display?: string): Promise<unknown> },
f: { userid: string; address: string; issued?: string }): Promise<WalletProof>;
function eip191Hash(message: string): Uint8Array;
function recoverEthAddress(message: string, signature: string): string;
function verifyWalletProof(a: { chain; address; userid; proof: string | WalletProof | null | undefined;
maxAgeMs?: number; now?: number }): VerifyResult;
function verifyRecordWallet(record, chain: WalletChain):
{ ok: true; address; via: "proof"; issued } | { ok: true; address; via: "owner" } | { ok: false; error };
function applyWalletBind(record, req: { chain; address; proof?; userid; now? }):
{ ok: true; addresses; texts } | { ok: false; status: number; error: string };verifyWalletProof checks the chain, the userid, the address, the issue time (maxAgeMs) and then the signature: secp256k1 recovery for eth, ed25519 for sol.
Configuration
There are no config files or environment variables. Everything is set through setUiHost(). The UI expects a backend that answers Beagle's /api/* routes on the page's origin. The beagle CLI does this, and a Vite proxy as above forwards them. The UI keeps small per-browser preferences in localStorage (for example a dismissed update notice).
Platforms
- Main bundle: modern browsers (ES modules,
fetch,AbortController). Verified as a Vite 5.4.10 build with React 18.3.1, loaded in an embedded desktop browser. Not importable in Node. wallet-proof: browsers and Node (verified with Node 24.3.0). Its@noble/curvesand@noble/hashescode is bundled in.- The package declares no runtime dependencies. React, ReactDOM,
peer-webrtcandchat-componentscome from the host page.
Version, source and changelog
- Verified version: 0.2.40 (
lateston npm, published 2026-11-06). - npm: https://www.npmjs.com/package/@decentnetwork/beagle-ui
- Source repository is not public yet. Browse the published bundle at https://www.npmjs.com/package/@decentnetwork/beagle-ui?activeTab=code
- License: MIT.
- Changelog: none is published in the package.