import { type BootstrapResult } from "./compat/bootstrap.js"; import type { FriendRecord } from "./store/friends.js"; import type { CustomPacketEvent, FriendConnectionEvent, FriendRequest, FriendInfoEvent, InlineFileEvent, InviteEvent, InviteResponseEvent, LookupResult, NetworkNode, PeerOptions, SendTextUntilAckOptions, TextMessage } from "./types/peer.js"; export declare class Peer { #private; private constructor(); static create(opts: PeerOptions): Promise; start(): Promise; stop(): Promise; pubkey(): string; userid(): string; /** * Sign `message` with this identity's private key (XEdDSA over the X25519 * key). The signature verifies against the userid — `verifyDetached( * base58Decode(userid), message, sig)` — so a relying party that knows only * the userid can prove the signer holds the matching key. Used by "Sign in * with Decent". Returns a detached 64-byte signature. */ sign(message: Uint8Array): Uint8Array; address(): string; joinNetwork(): Promise; lookup(pubkey: string): Promise; announceSelf(timeoutMs?: number): Promise; /** * Aggregate DHT health snapshot — surfaces the layers that have to * work for UDP holepunch / route discovery to succeed. * bootstrapsConfigured – nodes the SDK is allowed to talk to * knownNodesCount – nodes discovered or persisted in our DHT view * lastSelfAnnounceMs – last time we ran a self-announce sweep * selfAnnounceStoredOn – how many nodes acknowledged STORING our * announce last time. **If 0, our outbound * DHT is broken** — peers can't find us, * #discoverFriendRoutes returns nothing, * UDP holepunch is impossible. Most common * failure mode when sessions stay on * tcp-relay forever. * udpLocalPort – the OS-assigned UDP port (null = no socket) * tcpRelayConnected – fallback relay path; if >0, sessions get * through even when DHT is dead. */ dhtHealth(): { bootstrapsConfigured: number; knownNodesCount: number; lastSelfAnnounceMs: number; selfAnnounceStoredOn: number; udpLocalPort: number | null; tcpRelayConnected: number; /** TCP-relay onion diagnostics: requests handed to relays / responses * routed back. If sent>0 but recv=0, relays aren't returning onion * responses (relay doesn't forward, or wire mismatch). */ tcpOnionSent: number; tcpOnionRecv: number; /** The specific relay endpoints this peer has live connections to. * Lets us compare two peers' relay sets — if there's NO overlap, * net_crypto handshake packets between them have nowhere to route * via the TCP fallback path. This is the most common "session * established=False forever on the WAN" failure mode after * restart waves: peers picked different relays from the bootstrap * pool. Surfacing this lets diag pinpoint it. */ tcpRelayEndpoints: Array<{ host: string; port: number; }>; }; addKnownNodes(nodes: NetworkNode[]): void; knownNodes(): NetworkNode[]; sendFriendRequest(pubkey: string, hello?: string): Promise; lastFriendRequestDispatch(): { transport: "onion" | "direct"; routes: number; targets: number; } | undefined; acceptFriendRequest(pubkey: string): Promise; rejectFriendRequest(pubkey: string): void; /** * Drop a friend entirely: tear down any active net_crypto session, forget * their cached endpoint, clear retry/backoff bookkeeping, and persist the * shrunk friend list to disk. Use this to remove stale persisted entries * (e.g. an old simulator pubkey) that are still hogging the friend * connection loop with cookie requests no peer is going to answer. * * `pubkey` accepts either the friend's userid (base58 of the real public * key) or full Carrier address — both resolve to the same friendId. */ removeFriend(pubkey: string): boolean; sendText(pubkey: string, text: string): Promise; /** * Send text and keep retransmitting until the peer explicitly ACKs it. * * The receiving peer sends that ACK only after every onText handler returns * successfully. If a handler persists to an inbox, return its write Promise * from the handler; then this method resolves only after that durable write * completed on the far side. Pass a stable deliveryId when retrying an item * from an application outbox across process restarts. */ sendTextUntilAck(pubkey: string, text: string, opts?: SendTextUntilAckOptions): Promise<{ deliveryId: string; }>; waitForFriendConnected(pubkey: string, timeoutMs?: number): Promise; onFriendRequest(cb: (req: FriendRequest) => void): void; onText(cb: (msg: TextMessage) => unknown | Promise): void; /** Files received inline over (bulk)messages — the iOS/C Carrier apps' * native way of sending images/audio online (FileModel JSON envelope). */ onInlineFile(cb: (evt: InlineFileEvent) => void): void; /** * Send a file INLINE over the message channel the way iOS/C Carrier apps * do: a FileModel JSON envelope ({data: base64, fileExtension, fileName, * type}) carried as a (bulk)message. This is the ONLY file path a native * Beagle client can receive online — the toxcore file transfer (80-82) * used by sendFile() is invisible to the Carrier C SDK. Best for images * and small files; the hard protocol cap is ~5MB (base64-inflated), keep * real use well below it. */ sendInlineFile(pubkey: string, opts: { name: string; data: Uint8Array; fileType?: "image" | "audio" | "text" | "unknown"; }): Promise; /** Fired when a peer sends a Carrier friend-invite (PACKET_TYPE_INVITE_REQUEST). * This is the channel the iOS/Android WebRTC SDK uses for call signaling — * each RtcSignal arrives as an "invite" with ext="carrier" and `data` the * reassembled JSON. */ onInvite(cb: (evt: InviteEvent) => void): void; /** Fired when a peer replies to one of our invites (PACKET_TYPE_INVITE_RESPONSE). * Not used by the one-way WebRTC signaling flow, but surfaced for peers that * do reply. */ onInviteResponse(cb: (evt: InviteResponseEvent) => void): void; /** * Send a Carrier friend-invite to a peer — the transport the iOS/Android * Elastos WebRTC SDK listens on (CarrierExtension.registerExtension). Used * for realtime call signaling: pass the RtcSignal JSON as `data` and the * remote's registerExtension handler fires with it. * * Wire-compatible with carrier_invite_friend: sends PACKET_TYPE_INVITE_REQUEST * packets over the reliable message channel, `ext` = "carrier" by default, * fragmented into INVITE_DATA_UNIT (1280-byte) chunks sharing one tid (first * fragment carries totalsz + optional bundle). Signaling is realtime, so this * requires a live session and does NOT fall back to offline express — a call * signal to an unreachable peer should fail fast, not queue. * * @returns the tid used (lets callers correlate a later invite-response). */ sendInvite(pubkey: string, data: Uint8Array | string, opts?: { ext?: string | null; bundle?: string; establishTimeoutMs?: number; }): Promise; /** * Send an application-defined custom packet to a friend. `id` selects the * channel and its delivery class by toxcore range: **160–191 lossless** * (reliable, ordered) or **192–254 lossy** (best-effort). Apps layered on * the peer use this to run their own protocols alongside chat without * collision — e.g. decentlan sends IP traffic on `192`. Throws if `pubkey` * is not a friend or the session has no transport (fail-fast; callers that * want offline delivery should use a message instead). */ sendCustomPacket(pubkey: string, id: number, data: Uint8Array): Promise; /** * Receive application custom packets (toxcore lossless 160–191 / lossy * 192–254). SDK-internal IDs are never delivered here. See * {@link sendCustomPacket}. */ onCustomPacket(cb: (evt: CustomPacketEvent) => void): void; /** * Offer a file to a friend (by userid). Returns the fileId (hex) or null if * the friend has no free transfer slot. The recipient gets an `onFile` offer; * once they `acceptFile`, chunks stream over the reliable net_crypto channel. */ sendFile(userid: string, data: Uint8Array, opts: { name: string; kind?: number; }): string | null; /** Accept an incoming file offer (the `fileNumber` from the onFile event). */ acceptFile(userid: string, fileNumber: number): void; /** Cancel a transfer (isSending = true if we're the sender). */ cancelFile(userid: string, fileNumber: number, isSending?: boolean): void; /** Cancel an in-flight transfer by its content fileId (hex). Returns true if found. */ cancelFileById(userid: string, fileId: string, isSending?: boolean): boolean; /** Cancel in-flight sends that match size/name (registry-desync fallback). */ cancelSendsMatching(userid: string, match: { name?: string; size: number; }): string[]; /** Incoming file offer: { friendId, fileNumber, fileId, name, size, kind }. */ onFile(cb: (offer: { friendId: string; fileNumber: number; fileId: string; name: string; size: number; kind: number; }) => void): void; /** Transfer progress: { friendId, fileId, received, total, sending? }. */ onFileProgress(cb: (p: { friendId: string; fileId: string; received: number; total: number; sending?: boolean; }) => void): void; /** Transfer complete: { friendId, fileId, name, size, data?, sending? } (data present on the receiver). */ onFileComplete(cb: (p: { friendId: string; fileId: string; name: string; size: number; data?: Uint8Array; sending?: boolean; }) => void): void; /** Transfer cancelled by the peer: { friendId, fileId, sending }. */ onFileCancel(cb: (p: { friendId: string; fileId: string; sending: boolean; }) => void): void; onFriendConnection(cb: (ev: FriendConnectionEvent) => void): void; onFriendInfo(cb: (ev: FriendInfoEvent) => void): void; friends(): FriendRecord[]; /** * Read-only snapshot of the live net_crypto session state for a * friend, or null if no session has been established yet. Lets * callers see whether a peer is reachable via direct UDP, only via * TCP relay, or not at all — the answer is the difference between * ~80ms RTT (UDP) and ~500ms+ RTT (relay) in practice, so the * caller (e.g. `agentnet diag`) can show an operator where their * latency is coming from. Returns: * * established — handshake complete on this side * udpRemote — direct UDP endpoint we've seen the peer at, * if any; null means UDP holepunch hasn't * succeeded (yet) so traffic falls back to TCP * relay * hasTcpRoute — true when a TCP relay has reported a route to * the peer * transport — convenience derived field: which path the next * outbound packet will actually use * lastPingRecvMs — for staleness; if older than ~32s the session * is on its way to timeout */ sessionStatus(pubkey: string): { established: boolean; udpRemote: { host: string; port: number; } | null; hasTcpRoute: boolean; transport: "udp" | "tcp-relay" | "both" | "none"; lastPingRecvMs: number | null; /** OS-assigned UDP port THIS node is listening on. Useful to confirm * we even have a UDP socket bound. */ ourLocalUdpPort: number | null; /** Cached UDP endpoint for the peer from a previous online state * (persisted across restarts). Set != null means UDP holepunch * has succeeded at some point — but null when we've only ever * reached them via TCP-relay. Tells us whether discovery has * ever found a usable UDP endpoint. */ friendUdpEndpoint: { host: string; port: number; } | null; /** UDP endpoints picked up by recent DHT discovery for this peer, * whether or not the cookie exchange has succeeded yet. If this * is empty AND friendUdpEndpoint is null AND we keep getting * tcp-relay, discovery itself is broken — we don't even know * where to send a cookie request. */ endpointCandidatesCount: number; /** The actual candidate endpoints (host:port) — lets diag see whether a * same-LAN host candidate (e.g. 10.0.0.x) was received but not adopted. */ endpointCandidates: string[]; /** Last time we sent an outbound cookie request via UDP for this * peer (null = never). Used to confirm Phase 1.2 retries are * actually firing. */ cookieRequestSentMs: number | null; /** The peer's physical-LAN host we've locked onto (set by the periodic * offer/maintenance LAN-adoption code). When set and equal to udpRemote's * host, the per-packet receive path refuses a public/hairpin downgrade. */ lanRemoteHost: string | null; } | null; waitForFriendRequest(timeoutMs?: number): Promise; /** * After a friend sends us PACKET_ID_ONLINE, push our nickname + status * message so their UI replaces "unknown" with our actual display name, * and optionally fire off a configured greeting message. Idempotent — * tracked per friend so we don't spam every keepalive cycle. * * Sends are sequenced with small delays so the receiving peer's UI layer * doesn't process three messenger packets in the same render frame — * iOS Beagle 1.8.6 has a SwiftUI use-after-free in * `_UIHostingView.beginTransaction()` when nickname / status / message * arrive in rapid succession at session establishment, observed as * EXC_BAD_ACCESS in the iOS app's main thread. */ /** * Update our own profile (display name / status-message description) at * runtime and re-push it to friends. `name` maps to the toxcore nickname * (PACKET_ID_NICKNAME) and the Carrier USERINFO `name`; `description` maps to * the USERINFO descr / status message. Re-arms the per-friend "profile sent" * flag so every friend receives the update, and proactively pushes to anyone * currently online. The greeting is NOT re-sent (its flag is left intact). */ setUserInfo(info: { name?: string; description?: string; }): void; /** Our current display name + status-message description. */ userInfo(): { name: string; description: string; }; /** * Tell a friend our public UDP endpoint over the (already-working, * possibly tcp-relay) messenger channel, so they can hole-punch to us. * This is the out-of-band substitute for the broken DHT/onion-announce * endpoint discovery. Both peers do this symmetrically; on receipt each * feeds the other's endpoint into endpointCandidates and punches. */ /** * Lazily allocate our node-level TURN relay on its own dedicated UDP * socket. Returns our public relay address (on the TURN server) which we * advertise to peers so they can reach us via the relay even when direct * UDP can't be punched. Relayed data is injected into #onDatagram tagged * `viaRelay` so it updates the relay freshness, not the direct endpoint. */ /** * Test-only seam for scripts/turn-socket-leak-selftest.mjs: drive one TURN * allocation attempt directly instead of waiting out the 120s-per-friend * relay keepalive that reaches this path in production. Not public API. */ __testForceTurnAllocation(): Promise<{ host: string; port: number; } | undefined>; }