diff --git a/packages/agent-world-sdk/src/peer-protocol.ts b/packages/agent-world-sdk/src/peer-protocol.ts index 2fd5ebb..36f618d 100644 --- a/packages/agent-world-sdk/src/peer-protocol.ts +++ b/packages/agent-world-sdk/src/peer-protocol.ts @@ -125,6 +125,10 @@ export function registerPeerRoutes( ann.publicKey as string ); if (!result.ok) return reply.code(403).send({ error: result.error }); + const headerFrom = req.headers["x-agentworld-from"] as string; + if (headerFrom !== ann.from) { + return reply.code(400).send({ error: "X-AgentWorld-From does not match body from" }); + } } else { const { signature, ...signable } = ann; if ( @@ -171,6 +175,10 @@ export function registerPeerRoutes( msg.publicKey as string ); if (!result.ok) return reply.code(403).send({ error: result.error }); + const headerFrom = req.headers["x-agentworld-from"] as string; + if (headerFrom !== msg.from) { + return reply.code(400).send({ error: "X-AgentWorld-From does not match body from" }); + } } else { const { signature, ...signable } = msg; if ( diff --git a/src/index.ts b/src/index.ts index f0401b1..92e09b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,7 +9,7 @@ import * as path from "path" import { execSync } from "child_process" import { loadOrCreateIdentity, deriveDidKey } from "./identity" import { initDb, listPeers, getPeer, flushDb, getPeerIds, getEndpointAddress, setTofuTtl, findPeersByCapability } from "./peer-db" -import { startPeerServer, stopPeerServer, getInbox, setSelfMeta, handleUdpMessage } from "./peer-server" +import { startPeerServer, stopPeerServer, setSelfMeta, handleUdpMessage, addWorldMembers, removeWorld, clearWorldMembers } from "./peer-server" import { sendP2PMessage, pingPeer, broadcastLeave, SendOptions } from "./peer-client" import { upsertDiscoveredPeer } from "./peer-db" import { buildChannel, wireInboundToGateway, CHANNEL_CONFIG_SCHEMA } from "./channel" @@ -89,14 +89,17 @@ async function refreshWorldMembers(): Promise { }) if (!resp.ok) continue const body = await resp.json() as { members?: Array<{ agentId: string; alias?: string; endpoints?: Endpoint[] }> } + const memberIds: string[] = [] for (const member of body.members ?? []) { if (member.agentId === identity!.agentId) continue + memberIds.push(member.agentId) upsertDiscoveredPeer(member.agentId, "", { alias: member.alias, endpoints: member.endpoints, source: "gossip", }) } + addWorldMembers(worldId, memberIds) } catch { /* world unreachable — skip */ } } } @@ -199,6 +202,7 @@ export default function register(api: any) { _memberRefreshTimer = null } _joinedWorlds.clear() + clearWorldMembers() if (identity) { await broadcastLeave(identity, listPeers(), peerPort, buildSendOpts()) } @@ -275,7 +279,7 @@ export default function register(api: any) { } console.log(`Peer port: ${peerPort}`) console.log(`Known peers: ${listPeers().length}`) - console.log(`Inbox messages: ${getInbox().length}`) + console.log(`Worlds joined: ${_joinedWorlds.size}`) }) p2p @@ -324,18 +328,16 @@ export default function register(api: any) { }) p2p - .command("inbox") - .description("Show received messages") + .command("worlds") + .description("Show joined worlds") .action(() => { - const msgs = getInbox() - if (msgs.length === 0) { - console.log("No messages received yet.") + if (_joinedWorlds.size === 0) { + console.log("Not joined any worlds yet. Use 'openclaw join_world ' to join one.") return } - console.log("=== Inbox ===") - for (const m of msgs.slice(0, 20)) { - const time = new Date(m.receivedAt).toLocaleTimeString() - console.log(` [${time}] from ${m.from}: ${m.content}`) + console.log("=== Joined Worlds ===") + for (const [id, info] of _joinedWorlds) { + console.log(` ${id} — ${info.address}:${info.port}`) } }) }, @@ -358,7 +360,7 @@ export default function register(api: any) { `Transport: ${activeTransport?.id ?? "http-only"}`, ...(_quicTransport?.isActive() ? [`QUIC: \`${_quicTransport.address}\``] : []), `Peers: ${peers.length} known`, - `Inbox: ${getInbox().length} messages`, + `Worlds: ${_joinedWorlds.size} joined`, ].join("\n"), } }, @@ -443,7 +445,6 @@ export default function register(api: any) { return { content: [{ type: "text", text: "P2P service not started." }] } } const peers = listPeers() - const inbox = getInbox() const activeTransport = _transportManager?.active const lines = [ ...((_agentMeta.name) ? [`Agent name: ${_agentMeta.name}`] : []), @@ -453,7 +454,7 @@ export default function register(api: any) { ...(_quicTransport?.isActive() ? [`QUIC endpoint: ${_quicTransport.address}`] : []), `Plugin version: v${_agentMeta.version}`, `Known peers: ${peers.length}`, - `Unread inbox: ${inbox.length} messages`, + `Worlds joined: ${_joinedWorlds.size}`, ] return { content: [{ type: "text", text: lines.join("\n") }] } }, @@ -582,10 +583,13 @@ export default function register(api: any) { return { content: [{ type: "text", text: `Failed to join world: ${result.error}` }], isError: true } } - // Populate peer DB from members list in join response + // Populate peer DB + world membership allowlist from members list + const worldId = (result.data?.worldId ?? params.world_id ?? params.address) as string + const memberIds: string[] = [worldAgentId!] if (result.data?.members && Array.isArray(result.data.members)) { for (const member of result.data.members as Array<{ agentId: string; alias?: string; endpoints?: Endpoint[] }>) { if (member.agentId === identity.agentId) continue + memberIds.push(member.agentId) upsertDiscoveredPeer(member.agentId, "", { alias: member.alias, endpoints: member.endpoints, @@ -593,8 +597,7 @@ export default function register(api: any) { }) } } - - const worldId = (result.data?.worldId ?? params.world_id ?? params.address) as string + addWorldMembers(worldId, memberIds) const members = result.data?.members as unknown[] | undefined const memberCount = members?.length ?? 0 diff --git a/src/peer-server.ts b/src/peer-server.ts index 469469f..08534e5 100644 --- a/src/peer-server.ts +++ b/src/peer-server.ts @@ -4,6 +4,7 @@ * Trust model: * Layer 1 — Ed25519 signature (universal trust anchor) * Layer 2 — TOFU: agentId -> publicKey binding + * Layer 3 — World membership: only co-members can exchange messages * * All source IP filtering has been removed. Trust is established at the * application layer via Ed25519 signatures, not at the network layer. @@ -21,11 +22,37 @@ const MAX_MESSAGE_AGE_MS = 5 * 60 * 1000 // 5 minutes export type MessageHandler = (msg: P2PMessage & { verified: boolean }) => void let server: FastifyInstance | null = null -const _inbox: (P2PMessage & { verified: boolean; receivedAt: number })[] = [] const _handlers: MessageHandler[] = [] let _identity: Identity | null = null +// ── World membership allowlist ─────────────────────────────────────────────── +const _worldMembers = new Map>() + +export function addWorldMembers(worldId: string, memberIds: string[]): void { + let set = _worldMembers.get(worldId) + if (!set) { + set = new Set() + _worldMembers.set(worldId, set) + } + for (const id of memberIds) set.add(id) +} + +export function removeWorld(worldId: string): void { + _worldMembers.delete(worldId) +} + +export function isCoMember(agentId: string): boolean { + for (const members of _worldMembers.values()) { + if (members.has(agentId)) return true + } + return false +} + +export function clearWorldMembers(): void { + _worldMembers.clear() +} + interface SelfMeta { agentId?: string publicKey?: string @@ -40,6 +67,8 @@ export interface PeerServerOptions { testMode?: boolean /** Identity for response signing (optional) */ identity?: Identity + /** When set, incoming messages are dispatched to this handler and world co-member check is skipped (used by World Servers) */ + onMessage?: MessageHandler } export function setSelfMeta(meta: SelfMeta): void { @@ -96,67 +125,6 @@ export async function startPeerServer(port: number = 8099, opts?: PeerServerOpti }) server.get("/peer/ping", async () => ({ ok: true, ts: Date.now() })) - server.get("/peer/inbox", async () => _inbox.slice(0, 100)) - server.get("/peer/peers", async () => ({ peers: getPeersForExchange(20) })) - - server.post("/peer/announce", async (req, reply) => { - const ann = req.body as any - - if (!ann?.publicKey || !ann?.from) { - return reply.code(400).send({ error: "Missing 'from' or 'publicKey'" }) - } - - // Verify X-AgentWorld-* header signature - const rawBody = (req as any).rawBody as string - const authority = (req.headers["host"] as string) ?? "localhost" - const reqPath = req.url.split("?")[0] - const result = verifyHttpRequestHeaders( - req.headers as Record, - req.method, reqPath, authority, rawBody, ann.publicKey - ) - if (!result.ok) return reply.code(403).send({ error: result.error }) - const headerFrom = req.headers["x-agentworld-from"] as string - if (headerFrom !== ann.from) { - return reply.code(400).send({ error: "X-AgentWorld-From does not match body 'from'" }) - } - - const agentId: string = ann.from - - const knownPeer = getPeer(agentId) - if (!knownPeer?.publicKey && agentIdFromPublicKey(ann.publicKey) !== agentId) { - return reply.code(400).send({ error: "agentId does not match publicKey" }) - } - - const endpoints: Endpoint[] = ann.endpoints ?? [] - - upsertDiscoveredPeer(agentId, ann.publicKey, { - alias: ann.alias, - version: ann.version, - discoveredVia: agentId, - source: "gossip", - endpoints, - capabilities: ann.capabilities ?? [], - }) - - for (const p of ann.peers ?? []) { - if (!p.agentId || p.agentId === agentId) continue - upsertDiscoveredPeer(p.agentId, p.publicKey, { - alias: p.alias, - discoveredVia: agentId, - source: "gossip", - lastSeen: p.lastSeen, - endpoints: p.endpoints ?? [], - capabilities: p.capabilities ?? [], - }) - } - - console.log(`[p2p] peer-exchange from=${agentId} shared=${ann.peers?.length ?? 0} peers`) - - const self = _selfMeta.agentId - ? { agentId: _selfMeta.agentId, publicKey: _selfMeta.publicKey, alias: _selfMeta.alias, version: _selfMeta.version, endpoints: _selfMeta.endpoints } - : undefined - return { ok: true, ...(self ? { self } : {}), peers: getPeersForExchange(20) } - }) server.post("/peer/message", async (req, reply) => { const raw = req.body as any @@ -196,6 +164,11 @@ export async function startPeerServer(port: number = 8099, opts?: PeerServerOpti }) } + // World co-member check (skip when onMessage handler is set — world servers handle their own auth) + if (!opts?.onMessage && !isCoMember(agentId)) { + return reply.code(403).send({ error: "Not a world co-member" }) + } + const msg: P2PMessage = { from: agentId, publicKey: raw.publicKey, @@ -211,13 +184,9 @@ export async function startPeerServer(port: number = 8099, opts?: PeerServerOpti return { ok: true } } - const entry = { ...msg, verified: true, receivedAt: Date.now() } - _inbox.unshift(entry) - if (_inbox.length > 500) _inbox.pop() - console.log(`[p2p] <- verified from=${agentId} event=${msg.event}`) - _handlers.forEach((h) => h(entry)) + _handlers.forEach((h) => h({ ...msg, verified: true })) return { ok: true } }) @@ -238,6 +207,12 @@ export async function startPeerServer(port: number = 8099, opts?: PeerServerOpti } const agentId: string = rot.oldAgentId + + // Only accept key rotation from known peers or co-members + if (!getPeer(agentId) && !isCoMember(agentId)) { + return reply.code(403).send({ error: "Unknown agent — key rotation requires existing relationship" }) + } + let oldPublicKeyB64: string, newPublicKeyB64: string try { oldPublicKeyB64 = multibaseToBase64(rot.oldIdentity.publicKeyMultibase) @@ -294,10 +269,6 @@ export async function stopPeerServer(): Promise { _identity = null } -export function getInbox(): typeof _inbox { - return _inbox -} - /** * Process a raw UDP datagram as a P2PMessage. * Returns true if the message was valid and handled, false otherwise. @@ -332,6 +303,11 @@ export function handleUdpMessage(data: Buffer, from: string): boolean { return false } + // World co-member check + if (!isCoMember(raw.from)) { + return false + } + const msg: P2PMessage = { from: raw.from, publicKey: raw.publicKey, @@ -347,12 +323,8 @@ export function handleUdpMessage(data: Buffer, from: string): boolean { return true } - const entry = { ...msg, verified: true, receivedAt: Date.now() } - _inbox.unshift(entry) - if (_inbox.length > 500) _inbox.pop() - console.log(`[p2p] <- verified (UDP) from=${raw.from} event=${msg.event}`) - _handlers.forEach((h) => h(entry)) + _handlers.forEach((h) => h({ ...msg, verified: true })) return true } diff --git a/test/key-rotation.test.mjs b/test/key-rotation.test.mjs index 698e73a..bd9c7e4 100644 --- a/test/key-rotation.test.mjs +++ b/test/key-rotation.test.mjs @@ -11,7 +11,7 @@ const require = createRequire(import.meta.url) const pkgVersion = require("../package.json").version const PROTOCOL_VERSION = pkgVersion.split(".").slice(0, 2).join(".") -const { startPeerServer, stopPeerServer } = await import("../dist/peer-server.js") +const { startPeerServer, stopPeerServer, addWorldMembers } = await import("../dist/peer-server.js") const { initDb } = await import("../dist/peer-db.js") const { agentIdFromPublicKey, signWithDomainSeparator, DOMAIN_SEPARATORS, signHttpRequest, canonicalize } = await import("../dist/identity.js") @@ -106,9 +106,10 @@ describe("key rotation endpoint", () => { fs.rmSync(tmpDir, { recursive: true }) }) - test("accepts valid key rotation", async () => { + test("accepts valid key rotation from co-member", async () => { const oldKey = makeKeypair() const newKey = makeKeypair() + addWorldMembers("test-world", [oldKey.agentId]) const resp = await fetch(`http://[::1]:${port}/peer/key-rotation`, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -119,6 +120,19 @@ describe("key rotation endpoint", () => { assert.equal(json.ok, true) }) + test("rejects key rotation from unknown agent", async () => { + const oldKey = makeKeypair() + const newKey = makeKeypair() + const resp = await fetch(`http://[::1]:${port}/peer/key-rotation`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(makeRotationBody(oldKey, newKey)), + }) + assert.equal(resp.status, 403) + const json = await resp.json() + assert.match(json.error, /Unknown agent/) + }) + test("rejects invalid old key proof", async () => { const oldKey = makeKeypair() const newKey = makeKeypair() @@ -135,6 +149,7 @@ describe("key rotation endpoint", () => { const oldKey = makeKeypair() const newKey = makeKeypair() const otherKey = makeKeypair() + addWorldMembers("test-world", [otherKey.agentId]) const signable = { agentId: otherKey.agentId, oldPublicKey: oldKey.publicKey, @@ -185,7 +200,8 @@ describe("key rotation endpoint", () => { const attackerKey = makeKeypair() const newKey = makeKeypair() - // Establish TOFU for tofuKey by sending a -signed message + // Register as co-member so message goes through, establishing TOFU + addWorldMembers("test-world", [tofuKey.agentId]) const msgPayload = { from: tofuKey.agentId, publicKey: tofuKey.publicKey, diff --git a/test/request-signing.test.mjs b/test/request-signing.test.mjs index 4a432a5..f0783e7 100644 --- a/test/request-signing.test.mjs +++ b/test/request-signing.test.mjs @@ -22,7 +22,7 @@ const PROTOCOL_VERSION = pkgVersion.split(".").slice(0, 2).join(".") const nacl = (await import("tweetnacl")).default -const { startPeerServer, stopPeerServer } = await import("../dist/peer-server.js") +const { startPeerServer, stopPeerServer, addWorldMembers } = await import("../dist/peer-server.js") const { initDb, flushDb } = await import("../dist/peer-db.js") const { agentIdFromPublicKey, @@ -64,6 +64,7 @@ describe("request signing", () => { dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "dap-reqsign-")) initDb(dataDir) await startPeerServer(PORT, { identity: selfKey, testMode: true }) + addWorldMembers("test-world", [senderKey.agentId]) }) after(async () => { @@ -235,30 +236,13 @@ describe("request signing", () => { assert.equal(resp.status, 403) }) - test("announce with headers is accepted", async () => { - const timestamp = Date.now() - const payload = { - from: senderKey.agentId, - publicKey: senderKey.publicKey, - alias: "test-node", - endpoints: [], - capabilities: [], - timestamp, - peers: [], - } - const signature = signMessage(senderKey.privateKey, payload) - const announcement = { ...payload, signature } - const body = JSON.stringify(announcement) - const awHeaders = signHttpRequest(senderKey, "POST", `[::1]:${PORT}`, "/peer/announce", body) - - const resp = await fetch(`http://[::1]:${PORT}/peer/announce`, { - method: "POST", - headers: { "Content-Type": "application/json", ...awHeaders }, - body, - }) - assert.equal(resp.status, 200) - const result = await resp.json() - assert.ok(result.ok || result.peers) + test("removed routes return 404", async () => { + const resp1 = await fetch(`http://[::1]:${PORT}/peer/announce`, { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + assert.equal(resp1.status, 404) + const resp2 = await fetch(`http://[::1]:${PORT}/peer/inbox`) + assert.equal(resp2.status, 404) + const resp3 = await fetch(`http://[::1]:${PORT}/peer/peers`) + assert.equal(resp3.status, 404) }) test("response includes signing headers", async () => { diff --git a/test/response-signing.test.mjs b/test/response-signing.test.mjs index e250792..2ec36dc 100644 --- a/test/response-signing.test.mjs +++ b/test/response-signing.test.mjs @@ -101,12 +101,26 @@ describe("P2a — response signing on /peer/* endpoints", () => { assert.ok(result.ok, `Response signature invalid: ${JSON.stringify(result)}`) }) - test("/peer/peers response has valid AgentWorld signature headers", async () => { - const resp = await fetch(`http://[::1]:${PORT}/peer/peers`) - const body = await resp.text() - assert.equal(resp.status, 200) - const result = verifyResponseSig(resp.headers, 200, body, selfKey.publicKey) - assert.ok(result.ok, `Response signature invalid: ${JSON.stringify(result)}`) + test("/peer/message error response (non-co-member) has valid signature", async () => { + const otherKey = makeKeypair() + const body = JSON.stringify({ + from: otherKey.agentId, + publicKey: otherKey.publicKey, + event: "chat", + content: "test", + timestamp: Date.now(), + }) + const { signHttpRequest } = await import("../dist/identity.js") + const awHeaders = signHttpRequest(otherKey, "POST", `[::1]:${PORT}`, "/peer/message", body) + const resp = await fetch(`http://[::1]:${PORT}/peer/message`, { + method: "POST", + headers: { "Content-Type": "application/json", ...awHeaders }, + body, + }) + const respBody = await resp.text() + assert.equal(resp.status, 403) + const result = verifyResponseSig(resp.headers, 403, respBody, selfKey.publicKey) + assert.ok(result.ok, `Error response signature invalid: ${JSON.stringify(result)}`) }) test("/peer/message error response has valid signature", async () => { diff --git a/test/transport-enforcement.test.mjs b/test/transport-enforcement.test.mjs new file mode 100644 index 0000000..bdae17c --- /dev/null +++ b/test/transport-enforcement.test.mjs @@ -0,0 +1,181 @@ +/** + * Transport-layer enforcement — world-scoped isolation + * + * Verifies that: + * 1. /peer/message rejects non-co-members with 403 + * 2. /peer/message accepts co-members + * 3. UDP messages from non-co-members are silently dropped + * 4. UDP messages from co-members are accepted + * 5. addWorldMembers / removeWorld / isCoMember / clearWorldMembers work correctly + * 6. Removed routes (/peer/inbox, /peer/peers, /peer/announce) return 404 + */ +import { test, describe, before, after } from "node:test" +import assert from "node:assert/strict" +import * as os from "node:os" +import * as fs from "node:fs" +import * as path from "node:path" + +const nacl = (await import("tweetnacl")).default + +const { + startPeerServer, stopPeerServer, + addWorldMembers, removeWorld, isCoMember, clearWorldMembers, + handleUdpMessage, + onMessage, +} = await import("../dist/peer-server.js") +const { initDb, flushDb } = await import("../dist/peer-db.js") +const { agentIdFromPublicKey, signHttpRequest, signWithDomainSeparator, DOMAIN_SEPARATORS, canonicalize } = await import("../dist/identity.js") + +const PORT = 18125 + +function makeIdentity() { + const kp = nacl.sign.keyPair() + const pubB64 = Buffer.from(kp.publicKey).toString("base64") + const privB64 = Buffer.from(kp.secretKey.slice(0, 32)).toString("base64") + const agentId = agentIdFromPublicKey(pubB64) + return { publicKey: pubB64, privateKey: privB64, agentId, secretKey: kp.secretKey } +} + +function sendSignedMsg(port, identity, payload) { + const body = JSON.stringify(canonicalize(payload)) + const awHeaders = signHttpRequest(identity, "POST", `[::1]:${port}`, "/peer/message", body) + return fetch(`http://[::1]:${port}/peer/message`, { + method: "POST", + headers: { "Content-Type": "application/json", ...awHeaders }, + body, + }) +} + +function buildUdpMessage(identity, event, content) { + const msg = { + from: identity.agentId, + publicKey: identity.publicKey, + event, + content, + timestamp: Date.now(), + } + const sig = signWithDomainSeparator(DOMAIN_SEPARATORS.MESSAGE, msg, identity.secretKey) + return Buffer.from(JSON.stringify({ ...msg, signature: sig })) +} + +describe("Transport enforcement — world-scoped isolation", () => { + let selfKey, memberKey, strangerKey, tmpDir + + before(async () => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "dap-enforce-")) + initDb(tmpDir) + selfKey = makeIdentity() + memberKey = makeIdentity() + strangerKey = makeIdentity() + await startPeerServer(PORT, { identity: selfKey, testMode: true }) + addWorldMembers("test-world", [memberKey.agentId]) + }) + + after(async () => { + clearWorldMembers() + await stopPeerServer() + flushDb() + fs.rmSync(tmpDir, { recursive: true, force: true }) + }) + + // ── allowlist unit tests ────────────────────────────────────────────────── + + test("isCoMember returns true for added members", () => { + assert.ok(isCoMember(memberKey.agentId)) + }) + + test("isCoMember returns false for strangers", () => { + assert.equal(isCoMember(strangerKey.agentId), false) + }) + + test("removeWorld clears membership for that world", () => { + const tmpKey = makeIdentity() + addWorldMembers("tmp-world", [tmpKey.agentId]) + assert.ok(isCoMember(tmpKey.agentId)) + removeWorld("tmp-world") + assert.equal(isCoMember(tmpKey.agentId), false) + }) + + test("clearWorldMembers removes all worlds", () => { + addWorldMembers("w1", ["a1"]) + addWorldMembers("w2", ["a2"]) + assert.ok(isCoMember("a1")) + clearWorldMembers() + assert.equal(isCoMember("a1"), false) + assert.equal(isCoMember("a2"), false) + // Re-add for subsequent tests + addWorldMembers("test-world", [memberKey.agentId]) + }) + + // ── HTTP /peer/message enforcement ──────────────────────────────────────── + + test("/peer/message accepts co-member", async () => { + const payload = { + from: memberKey.agentId, + publicKey: memberKey.publicKey, + event: "chat", + content: "hello from member", + timestamp: Date.now(), + signature: "placeholder", + } + const resp = await sendSignedMsg(PORT, memberKey, payload) + assert.equal(resp.status, 200) + }) + + test("/peer/message rejects non-co-member with 403", async () => { + const payload = { + from: strangerKey.agentId, + publicKey: strangerKey.publicKey, + event: "chat", + content: "hello from stranger", + timestamp: Date.now(), + signature: "placeholder", + } + const resp = await sendSignedMsg(PORT, strangerKey, payload) + assert.equal(resp.status, 403) + const body = await resp.json() + assert.match(body.error, /Not a world co-member/) + }) + + // ── UDP enforcement ─────────────────────────────────────────────────────── + + test("UDP accepts co-member message", () => { + const udpMsg = buildUdpMessage(memberKey, "chat", "udp hello") + const result = handleUdpMessage(udpMsg, "127.0.0.1") + assert.ok(result, "UDP message from co-member should be accepted") + }) + + test("UDP rejects non-co-member message", () => { + const udpMsg = buildUdpMessage(strangerKey, "chat", "udp hello") + const result = handleUdpMessage(udpMsg, "127.0.0.1") + assert.equal(result, false, "UDP message from non-co-member should be dropped") + }) + + // ── Removed routes ──────────────────────────────────────────────────────── + + test("/peer/inbox returns 404", async () => { + const resp = await fetch(`http://[::1]:${PORT}/peer/inbox`) + assert.equal(resp.status, 404) + }) + + test("/peer/peers returns 404", async () => { + const resp = await fetch(`http://[::1]:${PORT}/peer/peers`) + assert.equal(resp.status, 404) + }) + + test("/peer/announce returns 404", async () => { + const resp = await fetch(`http://[::1]:${PORT}/peer/announce`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: "{}", + }) + assert.equal(resp.status, 404) + }) + + test("/peer/ping still works (public)", async () => { + const resp = await fetch(`http://[::1]:${PORT}/peer/ping`) + assert.equal(resp.status, 200) + const body = await resp.json() + assert.ok(body.ok) + }) +})