@decentnetwork/dora
DHCP-style naming for AgentNet (Discover, Offer, Request, Acknowledge). Runs as a normal Carrier peer without admin rights.
npm install -g @decentnetwork/dora@decentnetwork/dora is the DHCP-style name and IP registry for the Decent Network virtual LAN. A dora server is an ordinary Carrier peer: it auto-accepts friend requests and answers register, lookup and list requests that arrive as Carrier text messages. Its roster maps each node's userid to a name and a 10.86.x.x address, stored in a YAML file. It runs as a normal user, with no TUN device and no root. The package ships a dora binary for running a registry, plus the server, client, store, allocator and wire-format helpers as a library.
When to use it
- Run a private registry for your own @decentnetwork/lan nodes, so they get stable virtual IPs and names that do not collide with the public doras.
- Embed the client (
DoraClient) in your own Carrier app to register a name and resolve peers by name, userid or virtual IP, using whatever text transport you already have. - Embed the server (
DoraServer) in a long-running Node process that already owns a @decentnetwork/peerPeer.
If you only want to join the public virtual LAN, you do not need this package directly: agentnet init already points at the public doras. If you only need messaging between peers, use @decentnetwork/peer and skip dora entirely.
Install
npm install @decentnetwork/dora@0.1.16To embed the server, also install the Carrier SDK it takes a Peer from:
npm install @decentnetwork/peer@0.1.172Quickstart
1. Run a registry with the CLI
--range-start and --range-end are required in 0.1.16. A registry is authoritative only for the band it declares, so pick a band that no other dora you federate with uses.
mkdir dora-demo && cd dora-demo
npm init -y
npm install @decentnetwork/dora@0.1.16
npx dora --data-dir ./data --range-start 10.86.2.10 --range-end 10.86.2.254 \
--http-port 18790 --name docs-dora --verbose
# in another terminal: read-only status JSON
curl -s http://127.0.0.1:18790/api/statusCopy the address= value from the first line and share it. Clients friend that address once (agentnet dora enable --address <address>). The server accepts the request automatically.
Verified output (npm @decentnetwork/dora@0.1.16, 2026-11-06). The server took about 35 seconds to join the network and print registry ready:
registry identity: address=V58F5TvJAM7VEA8GyrtSgp87mA3WpbQnDWDRjSYqTKq6JbGTNeuP userid=DmkXCCAiXu36tdAnxjHQqPRgRB1yQct5WAtsFWLhvQKR
registry roster file: …/dora-demo/data/roster.yaml
registry pool start: 10.86.2.10 end: 10.86.2.254
[registry] dora server started — friend requests will auto-accept
registry ready — clients should configure registry.userid in decentlan's config.yaml
[registry] status UI on http://127.0.0.1:18790
$ curl -s http://127.0.0.1:18790/api/status # trimmed
{
"self": {
"userid": "DmkXCCAiXu36tdAnxjHQqPRgRB1yQct5WAtsFWLhvQKR",
"address": "V58F5TvJAM7VEA8GyrtSgp87mA3WpbQnDWDRjSYqTKq6JbGTNeuP",
"name": "docs-dora",
"segment": "10.86.2.10-10.86.2.254",
"capacity": 245,
"used": 0,
…
},
"siblings": [],
"records": [],
"conflicts": [],
…
}
^C
shutting down registryWithout the range flags the CLI exits with code 2 and prints the public federation bands, so you can pick a free one.
2. Use the client from code
DoraClient does not open a network connection itself. You supply sendText and onText adapters (in a real app, backed by Peer friend messages). This sample answers requests in-process with the exported DoraStore and IpAllocator, so it runs offline and shows the exact wire frames:
// quickstart.mjs
// Library quickstart: drive DoraClient against an in-process registry built
// from the exported DoraStore + IpAllocator + encode/decode wire helpers.
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
DoraClient, DoraStore, IpAllocator, encode, decode, randomIpInSubnet, DORA_PREFIX,
} from "@decentnetwork/dora";
const dir = mkdtempSync(join(process.env.DORA_DEMO_DIR ?? tmpdir(), "dora-"));
const store = new DoraStore(join(dir, "roster.yaml"));
const allocator = new IpAllocator(store, { rangeStart: "10.86.2.10", rangeEnd: "10.86.2.254" });
console.log("segment:", allocator.segment(), "capacity:", allocator.capacity());
// A fake transport: the "registry" answers register/lookup/list in-process.
const REGISTRY = "registry-userid";
let deliver = () => {};
async function sendText(to, text) {
const req = decode(text);
let res;
if (req.op === "register") {
const record = { userid: req.userid, name: req.name, address: req.address,
virtualIp: allocator.nextFree(), registeredAt: new Date().toISOString() };
store.put(record);
res = { op: "register-ok", record };
} else if (req.op === "lookup") {
const rec = req.by === "name" ? store.findByName(req.value)
: req.by === "ip" ? store.findByIp(req.value) : store.get(req.value);
res = rec ? { op: "lookup-ok", record: rec } : { op: "lookup-err", reason: "not found" };
} else {
res = { op: "list-ok", records: store.list(), total: store.list().length };
}
setTimeout(() => deliver(to, encode(res)), 10);
}
const client = new DoraClient({
registryUserids: [REGISTRY],
sendText,
onText: (h) => { deliver = h; },
timeoutMs: 2000,
});
const rec = await client.register({ userid: "node-a-userid", name: "node-a" });
console.log("registered:", rec.name, rec.virtualIp);
console.log("lookup by name:", (await client.lookup({ by: "name", value: "node-a" }))?.virtualIp);
console.log("roster size:", (await client.list()).length);
console.log("wire frame:", encode({ op: "lookup", by: "ip", value: rec.virtualIp }));
console.log("prefix:", DORA_PREFIX, "non-dora text decodes to:", decode("hello"));
console.log("fallback IP:", randomIpInSubnet("10.86.0.0/16"));Verified output (npm @decentnetwork/dora@0.1.16, Node 24.3.0, 2026-11-06):
$ node quickstart.mjs
segment: 10.86.2.10-10.86.2.254 capacity: 245
registered: node-a 10.86.2.10
lookup by name: 10.86.2.10
roster size: 1
wire frame: DORA:{"op":"lookup","by":"ip","value":"10.86.2.10"}
prefix: DORA: non-dora text decodes to: null
fallback IP: 10.86.198.2513. Embed the server
This is the same wiring as the dora CLI (dist/cli.js), minus the lock file, sibling replication and status page:
// server.mjs — embed a dora registry in your own Node process.
import { mkdirSync } from "node:fs";
import { resolve } from "node:path";
import { Peer } from "@decentnetwork/peer";
import { DoraServer, DoraStore, IpAllocator } from "@decentnetwork/dora";
const dataDir = resolve(process.argv[2] ?? "./my-dora");
mkdirSync(dataDir, { recursive: true });
const peer = await Peer.create({
keyFile: resolve(dataDir, "keypair.json"),
compatibilityMode: "legacy",
// Same public bootstrap + express nodes the dora CLI uses (dist/cli.js).
bootstrapNodes: [
{ host: "13.58.208.50", port: 33445, pk: "89vny8MrKdDKs7Uta9RdVmspPjnRMdwMmaiEW27pZ7gh" },
{ host: "18.216.102.47", port: 33445, pk: "G5z8MqiNDFTadFUPfMdYsYtkUDbX5mNCMVHMZtsCnFeb" },
{ host: "54.193.141.205", port: 33445, pk: "7TfZWZNV8vnBxxWzJXuvKgX2QyKkLpg2oXx3LQ5tg8LW" },
],
expressNodes: [
{ host: "lens.beagle.chat", port: 443, pk: "ECbs4GxwGzxGerNkmqDJFibEmevu8jAXqAZtikccvD95" },
],
});
await peer.start();
console.log(`dora address=${peer.address()} userid=${peer.userid()}`);
await peer.joinNetwork();
await peer.announceSelf(15000).catch((e) => console.warn("announce:", e.message));
const store = new DoraStore(resolve(dataDir, "roster.yaml"));
const allocator = new IpAllocator(store, { rangeStart: "10.86.2.10", rangeEnd: "10.86.2.254" });
const server = new DoraServer({ peer, store, allocator, verbose: true });
server.start();
console.log("serving segment", allocator.segment());
process.on("SIGINT", async () => { await peer.stop(); process.exit(0); });Verified output (npm @decentnetwork/dora@0.1.16 + @decentnetwork/peer@0.1.172, 2026-11-06):
$ node server.mjs ./embedded-dora
dora address=ZGiTEA8PV35YHAhdUPNXoGMcY7wVba6XJ5B63zeow4PnDr5HPGMw userid=FgaTFrzoAjXwTFRnLzKdm2y422ShsXR7DP5FVKRHof2f
[registry] dora server started — friend requests will auto-accept
serving segment 10.86.2.10-10.86.2.254API reference
All exports come from @decentnetwork/dora (dist/index.d.ts). Several are re-exported under new names: RegistryServer as DoraServer, RegistryClient as DoraClient, RegistryStore as DoraStore, and RegistryRecord as DoraRecord.
DoraServer
new DoraServer(opts: DoraServerOptions)
start(): void
availabilityLog(): AvailabilityLog | null
availabilitySummary(): string[]DoraServerOptions field | Type | Notes |
|---|---|---|
peer | Peer (from @decentnetwork/peer) | The registry's Carrier identity. Start it before start(). |
store | DoraStore | Roster persistence |
allocator | IpAllocator | IP band this server owns |
verbose? | boolean | Log every served operation |
siblings? | Array<{ userid; address?; name? }> | Sibling registries to replicate rosters from. Empty means replication is off. |
availabilityFile? | string | Where to persist sibling reachability |
syncIntervalMs? | number | Sibling pull interval (default 60 s) |
replicaTtlMs? | number | Drop replicas not re-asserted within this window (default 15 min) |
DoraClient
new DoraClient(opts: DoraClientOptions)
ensureSubscribed(): void
register(opts: { userid: string; name: string; address?: string; requestedIp?: string; replace?: boolean }): Promise<DoraRecord>
lookup(opts: { by: "userid" | "name" | "ip"; value: string }): Promise<DoraRecord | null>
list(): Promise<DoraRecord[]>DoraClientOptions field | Type | Notes |
|---|---|---|
registryUserids | string[] | Registries to try in order. The first one to answer wins. |
sendText | (toUserid, text) => Promise<void> | Your transport, for example a Carrier friend message |
onText | (handler: (fromUserid, text) => void) => void | Subscribe the client to incoming text |
timeoutMs? | number | Per-registry wait (default 10 s) |
list() pages through large rosters automatically. When every registry fails, the client throws AllRegistriesUnavailableError. Catch it and fall back to randomIpInSubnet().
DoraStore
new DoraStore(filePath: string) // YAML roster; created on first write
get(userid): DoraRecord | null
findByName(name): DoraRecord | null
findByIp(ip): DoraRecord | null
list(): DoraRecord[]
listOwned(): DoraRecord[] // records without replicatedFrom
put(record: DoraRecord): void // upsert + persist; caller checks collisions
remove(userid): boolean
touch(userid): void // update lastSeenAt
putReplicatedBatch(records, fromUserid, skip?): { merged: number; conflicts: IpConflict[] }
findIpConflicts(): IpConflict[]
pruneStaleReplicas(ttlMs: number, now?: number): numberIpAllocator
new IpAllocator(store: DoraStore, opts?: AllocatorOptions) // { rangeStart?, rangeEnd? }
capacity(): number
segment(): string // "<start>-<end>"
overlaps(segment: string): boolean
ownsIp(ip: string): boolean
acceptRequested(ip: string): boolean
nextFree(): string | null // null when the band is exhaustedWire format and helpers
const DORA_PREFIX = "DORA:";
encode(msg: DoraRequest | DoraResponse): string // "DORA:" + JSON
decode(text: string): DoraRequest | DoraResponse | null // null for non-dora text
randomIpInSubnet(subnetCidr?: string, reservedFirstSlashTwentyFour?: boolean): string
class AllRegistriesUnavailableError extends Error| Request | Response |
|---|---|
RegisterRequest { op: "register", userid, name, address?, requestedIp?, replace? } | RegisterOk { op: "register-ok", record } / RegisterErr { op: "register-err", reason, suggestion? } |
LookupRequest { op: "lookup", by: "userid"|"name"|"ip", value } | LookupOk { op: "lookup-ok", record } / LookupErr { op: "lookup-err", reason } |
ListRequest { op: "list", offset? } | ListOk { op: "list-ok", records, total?, seg? } / ListErr { op: "list-err", reason } |
DoraRecord fields: userid, name, virtualIp, registeredAt, lastSeenAt?, address? (the full Carrier address, which peers need to send a friend request), replicatedFrom? and replicatedAt?.
Configuration
CLI flags
dora --range-start <ip> --range-end <ip> [options]
dora friend-add <userid> [name] [address] [--data-dir <dir>]| Flag | Default | Effect |
|---|---|---|
--data-dir <path> | ~/.dora (or a legacy ~/.decent-registry if only that exists) | Keypair, friend store, roster.yaml, dora.pid lock. The keypair here is the registry's public identity, so keep it stable. |
--range-start <ip>, --range-end <ip> | required | The IP band this registry owns |
--udp-port <port> | ephemeral | A stable Carrier UDP port. Recommended for an always-on registry. |
--verbose | off | Log every operation |
--peers <userid>[=<address>][#<name>],… | none | Sibling registries to replicate from |
--sync-interval-ms <n>, --replica-ttl-ms <n> | 60000 / 900000 | Replication timing |
--http-port <n> | off | Serve a read-only status page (/) and JSON (/api/status) |
--http-host <addr> | 127.0.0.1 | Bind address for the status page. The roster maps identities to addresses, so expose it deliberately. |
--name <s> | none | Label shown on the status page |
dora friend-add writes a userid straight into the data dir's friend store (keypair.json.friends.json) and exits. Use it to onboard a node whose friend request cannot reach the registry, then restart the server.
The CLI refuses to start if another dora process is using the same --data-dir. It checks both the process list and the dora.pid file.
Data files
roster.yaml (the server rewrites it on every change, so edit it only while dora is stopped):
records:
- userid: node-a-userid
name: node-a
virtualIp: 10.86.2.10
registeredAt: '2026-11-06T08:37:15.267Z'Bootstrap and express nodes are fixed in dist/cli.js. There is no CLI flag for them. When embedding, pass your own list to Peer.create().
Platforms
- Node.js >= 20 (
engines). Verified with Node 24.3.0 on macOS (arm64). - No native modules, root or TUN. The CLI uses
psfor duplicate-instance detection on Linux and macOS. - Needs outbound UDP/TCP to the Carrier bootstrap nodes. A fixed
--udp-porthelps clients reach a long-lived registry. - Runtime dependencies:
@decentnetwork/peer(^0.1.101) andjs-yaml.
Version, source and changelog
- Verified version: 0.1.16 (
lateston npm, published 2026-07-21). - npm: https://www.npmjs.com/package/@decentnetwork/dora
- Source: https://github.com/0xli/dora (branch
main, commitd86369891abd30c600d95d3498fb53b2593a0571,package.jsonversion 0.1.16). Design notes are in https://github.com/0xli/dora/tree/main/docs - License: GPL-3.0-or-later.
- Changelog: none is published. The repository has no release tags.