/** * agentbbs Phase 2 — cross-host federation. * * Phase 1 (`agentbbs-tools.ts`) gives every host an append-only room log at * `/room-.jsonl`, and derives `roomId` deterministically from * the room label. Two independent hosts that register `#sales` therefore * compute the *same* roomId without ever talking to each other. That is the * property this module builds on. * * ## Why a signed union-merge, and not a consensus protocol * * A room log is an append-only set of immutable envelopes. Two hosts that each * append locally hold two subsets of the same logical set, so reconciling them * is a set union — there is no conflicting write to arbitrate, and therefore * nothing for a consensus round to decide. Union is commutative, associative * and idempotent, which makes sync order-independent and safe to retry: pulling * the same peer twice, or pulling A-then-B versus B-then-A, converges on the * same log. That is a CRDT (a grow-only set keyed by `envelopeId`), and it is * strictly cheaper and less failure-prone than the Byzantine agreement the * plugin README gestures at. * * What union does *not* give you is authenticity. If any peer can inject an * envelope, the merge faithfully replicates forgeries. So every envelope is * Ed25519-signed by its originating node, and a receiver verifies the signature * against the *pinned* public key it recorded when the peer was added — not * against a key carried in the envelope, which would let an attacker sign with * their own key and claim any origin. Trust is pinned at peer-add time; the * wire is treated as hostile. * * ## Transport * * Pull-based HTTP. A node serves `GET /agentbbs/v1/rooms/:roomId/envelopes?since=N` * and peers poll it. Pull is deliberate: it needs no inbound connectivity from * the peer's side, survives a node being offline (it just catches up later), * and gives the *receiver* control over how much it ingests. Push would invert * that and make every node an unauthenticated write target. * * Designed for a private overlay (Tailscale/WireGuard) where the network * already authenticates the host. Signatures mean a compromised or hostile peer * still cannot forge another node's envelopes. See `bindHost` on serve() — it * binds loopback by default, and binding a routable interface is an explicit * operator choice. * * ## Bounds * * Every ingest path is bounded: envelope count per sync, byte size per envelope, * total bytes per response, peer count, and hop count. An unbounded merge from * an untrusted peer is a memory-exhaustion primitive, so limits are enforced on * the *receiving* side where they cannot be negotiated away by the sender. * * @module @claude-flow/cli/mcp-tools/agentbbs-federation */ import { type Server } from 'node:http'; /** Max envelopes accepted from one peer in one sync. */ export declare const MAX_ENVELOPES_PER_SYNC = 5000; /** Max serialized bytes for a single envelope. */ export declare const MAX_ENVELOPE_BYTES: number; /** Max total bytes read from one peer response. */ export declare const MAX_SYNC_RESPONSE_BYTES: number; /** Max peers in the registry. */ export declare const MAX_PEERS = 256; /** Max federation hops before an envelope stops propagating. */ export declare const MAX_HOPS = 8; /** Per-request timeout when pulling from a peer. */ export declare const SYNC_TIMEOUT_MS = 15000; export interface NodeIdentity { nodeId: string; publicKey: string; privateKey: string; createdAt: string; } export interface FederationPeer { nodeId: string; url: string; publicKey: string; label?: string; addedAt: string; lastSyncedAt?: string; lastSeq?: Record; } export interface SignedEnvelope { envelopeId: string; roomId: string; seq: number; msgType: string; payload: unknown; timestamp: string; origin?: string; hops?: number; signature?: string; } /** * Deterministic byte string an envelope's signature covers. * * Field order is fixed here rather than relying on `JSON.stringify` key order, * because a receiver must reconstruct byte-identical input from a payload that * survived a JSON round trip. `payload` is canonicalized recursively with * sorted keys for the same reason: `{a:1,b:2}` and `{b:2,a:1}` are the same * value and must not produce different signatures. * * `hops` is deliberately excluded — it is mutated in transit by design, so * including it would invalidate the signature at the first relay. */ export declare function canonicalEnvelopeBytes(env: SignedEnvelope): Uint8Array; /** * Load or create this host's long-lived Ed25519 identity. * * Phase 1 minted an ephemeral key per process, which is fine for local token * signing but useless across hosts: a peer cannot pin a key that changes on * every restart. This persists one, 0600, and derives a stable nodeId from the * public key so identity is verifiable rather than self-asserted. */ export declare function getNodeIdentity(basePath: string): Promise; export declare function signEnvelope(basePath: string, env: SignedEnvelope): Promise; /** * Verify an envelope against a public key the caller already trusts. * * The key is passed in rather than read from the envelope on purpose: an * envelope-carried key proves only that the sender holds *a* key, not that they * are who the `origin` field claims. Callers pass the key pinned at peer-add. */ export declare function verifyEnvelope(env: SignedEnvelope, publicKeyHex: string): Promise; export declare function readPeers(basePath: string): FederationPeer[]; /** * Reject anything that is not a plain http(s) URL to a host. * * Blocks credentials-in-URL (they would be logged), and non-http schemes such * as `file:` which would turn a peer entry into a local file read. */ export declare function validatePeerUrl(raw: string): string; export declare function addPeer(basePath: string, input: { nodeId: string; url: string; publicKey: string; label?: string; }): FederationPeer; export declare function removePeer(basePath: string, nodeId: string): boolean; export declare function readEnvelopes(basePath: string, roomId: string): SignedEnvelope[]; export declare function validateRoomId(roomId: string): string; export interface MergeResult { merged: number; skippedDuplicate: number; skippedUnverified: number; skippedOversize: number; skippedHopLimit: number; } /** * Union-merge verified envelopes from a peer into the local room log. * * Idempotent: `envelopeId` is the merge key, so replaying the same batch is a * no-op. Anything that fails verification is dropped and counted rather than * quarantined — a receiver has no use for an envelope it cannot attribute. */ export declare function mergeEnvelopes(basePath: string, roomId: string, incoming: SignedEnvelope[], peerPublicKey: string): Promise; /** Pull one room from one peer and merge what verifies. */ export declare function syncRoomFromPeer(basePath: string, peer: FederationPeer, roomId: string, fetchImpl?: typeof fetch): Promise; /** * Serve this node's room logs for peers to pull. * * Binds loopback unless the caller explicitly asks otherwise, so starting a * server never silently exposes room contents on a routable interface. Read * only by construction: there is no route that mutates state, which removes the * whole class of unauthenticated-write attacks that a push design would open. */ export declare function serveFederation(basePath: string, opts?: { port?: number; bindHost?: string; }): Promise<{ server: Server; port: number; host: string; }>; /** Constant-time compare for any future shared-secret paths. */ export declare function safeEqual(a: string, b: string): boolean; //# sourceMappingURL=agentbbs-federation.d.ts.map