Anchor: add EXT-009 scoped presence query

Lets a client ask "is peer X currently online in network Y?" without
joining that network first — a stateless O(1) lookup against the
anchor's existing client registry. Scoped to (net, id) rather than a
global lookup by id alone, so it can't be used as a cross-network
"is this pubkey online anywhere" oracle: the caller must already know
a network the peer belongs to, matching the knowledge already required
to join it and observe presence the slow way via peer-join/peer-leave.

Motivated by flit's known-devices list, where each pairing is its own
isolated anchor network and the app holds no persistent connection
while idle — today the only way to know if a device is reachable is
to attempt a full connect.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Fredrik Johansson
2026-07-09 15:06:11 +02:00
parent b649f4d012
commit 586d39e3b8
3 changed files with 256 additions and 97 deletions

View File

@@ -38,20 +38,20 @@ type PeerInfo struct {
type MsgType string
const (
MsgChat MsgType = "chat"
MsgPm MsgType = "pm" // private message, §8
MsgPeerGossip MsgType = "peer_gossip"
MsgFileListReq MsgType = "file_list_req"
MsgFileListResp MsgType = "file_list_resp"
MsgFileOffer MsgType = "file-offer" // §9, hyphenated per spec
MsgFileAccept MsgType = "file-accept"
MsgFileCancel MsgType = "file-cancel"
MsgFileDone MsgType = "file-done"
MsgPing MsgType = "ping"
MsgPong MsgType = "pong"
MsgHistoryRequest MsgType = "history_request"
MsgHistoryChunk MsgType = "history_chunk"
MsgReaction MsgType = "reaction"
MsgChat MsgType = "chat"
MsgPm MsgType = "pm" // private message, §8
MsgPeerGossip MsgType = "peer_gossip"
MsgFileListReq MsgType = "file_list_req"
MsgFileListResp MsgType = "file_list_resp"
MsgFileOffer MsgType = "file-offer" // §9, hyphenated per spec
MsgFileAccept MsgType = "file-accept"
MsgFileCancel MsgType = "file-cancel"
MsgFileDone MsgType = "file-done"
MsgPing MsgType = "ping"
MsgPong MsgType = "pong"
MsgHistoryRequest MsgType = "history_request"
MsgHistoryChunk MsgType = "history_chunk"
MsgReaction MsgType = "reaction"
)
// PmMessage is a private message sent directly over a single peer link (§8 "pm").
@@ -108,9 +108,9 @@ type PeerMessage struct {
type ResumableFile struct {
Name string `json:"name"`
SHA256 string `json:"sha256"`
From string `json:"from"` // peer ID hex
From string `json:"from"` // peer ID hex
Size int64 `json:"size"`
Offset int64 `json:"offset"` // bytes already received
Offset int64 `json:"offset"` // bytes already received
}
// HistoryEntry is one message in a history_chunk response.
@@ -125,10 +125,10 @@ type HistoryEntry struct {
// ChatMessage is a group chat message (wire type "chat", §8).
// Also used internally for persisting PMs after they are received.
type ChatMessage struct {
Mid string `json:"mid,omitempty"` // optional dedup id (required when relay hops > 0)
Mid string `json:"mid,omitempty"` // optional dedup id (required when relay hops > 0)
MsgID string `json:"msg_id,omitempty"` // EXT-007: content-addressed gossip ID
From PeerID `json:"from,omitempty"` // set by receiver from DC context; not on wire for pm
To *PeerID `json:"to,omitempty"` // internal only — not transmitted; set for DMs
From PeerID `json:"from,omitempty"` // set by receiver from DC context; not on wire for pm
To *PeerID `json:"to,omitempty"` // internal only — not transmitted; set for DMs
Room string `json:"room"`
Text string `json:"text"`
Ts int64 `json:"ts"` // Unix milliseconds
@@ -240,27 +240,30 @@ type SignalingPayload struct {
type AnchorMsgType string
const (
AnchorChallenge AnchorMsgType = "challenge"
AnchorJoin AnchorMsgType = "join"
AnchorJoined AnchorMsgType = "joined"
AnchorPeerJoin AnchorMsgType = "peer-join"
AnchorPeerLeave AnchorMsgType = "peer-leave"
AnchorTo AnchorMsgType = "to"
AnchorFrom AnchorMsgType = "from"
AnchorNoPeer AnchorMsgType = "no-peer"
AnchorChallenge AnchorMsgType = "challenge"
AnchorJoin AnchorMsgType = "join"
AnchorJoined AnchorMsgType = "joined"
AnchorPeerJoin AnchorMsgType = "peer-join"
AnchorPeerLeave AnchorMsgType = "peer-leave"
AnchorTo AnchorMsgType = "to"
AnchorFrom AnchorMsgType = "from"
AnchorNoPeer AnchorMsgType = "no-peer"
AnchorPresenceQuery AnchorMsgType = "presence_query" // waste-go ext EXT-009
AnchorPresence AnchorMsgType = "presence" // waste-go ext EXT-009
)
// AnchorMessage covers all WebSocket frames to/from the anchor.
type AnchorMessage struct {
Type AnchorMsgType `json:"type"`
Nonce string `json:"nonce,omitempty"` // challenge nonce, hex
ID string `json:"id,omitempty"` // peer hex id
Net string `json:"net,omitempty"` // hashed network name
Sig string `json:"sig,omitempty"` // ed25519 sig over (nonce||net), hex
Peers []string `json:"peers,omitempty"` // joined: list of peer hex ids in network
To string `json:"to,omitempty"` // target peer hex id
From string `json:"from,omitempty"` // sender peer hex id
Box string `json:"box,omitempty"` // base64 nacl/box sealed payload
Type AnchorMsgType `json:"type"`
Nonce string `json:"nonce,omitempty"` // challenge nonce, hex
ID string `json:"id,omitempty"` // peer hex id
Net string `json:"net,omitempty"` // hashed network name
Sig string `json:"sig,omitempty"` // ed25519 sig over (nonce||net), hex
Peers []string `json:"peers,omitempty"` // joined: list of peer hex ids in network
To string `json:"to,omitempty"` // target peer hex id
From string `json:"from,omitempty"` // sender peer hex id
Box string `json:"box,omitempty"` // base64 nacl/box sealed payload
Online *bool `json:"online,omitempty"` // presence: true iff (net, id) is currently connected
}
// ── IPC protocol (daemon ↔ local UI) ─────────────────────────────────────────
@@ -270,45 +273,45 @@ type IpcMsgType string
const (
// Commands (UI → daemon)
CmdSendMessage IpcMsgType = "send_message"
CmdJoinNetwork IpcMsgType = "join_network" // fields: network_name (plaintext)
CmdLeaveNetwork IpcMsgType = "leave_network"
CmdGetState IpcMsgType = "get_state"
CmdSendMessage IpcMsgType = "send_message"
CmdJoinNetwork IpcMsgType = "join_network" // fields: network_name (plaintext)
CmdLeaveNetwork IpcMsgType = "leave_network"
CmdGetState IpcMsgType = "get_state"
CmdSendFile IpcMsgType = "send_file"
CmdSetShareDir IpcMsgType = "set_share_dir" // set per-network share directory at runtime
CmdSetShareDir IpcMsgType = "set_share_dir" // set per-network share directory at runtime
CmdGenerateInvite IpcMsgType = "generate_invite"
CmdGetFileList IpcMsgType = "get_file_list"
CmdExportIdentity IpcMsgType = "export_identity" // returns encrypted backup blob
CmdImportIdentity IpcMsgType = "import_identity" // replaces identity from backup blob
CmdAddShare IpcMsgType = "add_share" // add a share root; fields: path, networks
CmdRemoveShare IpcMsgType = "remove_share" // remove a share root; field: path
CmdListShares IpcMsgType = "list_shares" // returns shares_list event
CmdCreateRoom IpcMsgType = "create_room" // field: room (name)
CmdSetDownloadDir IpcMsgType = "set_download_dir" // set per-network download directory at runtime; fields: network_id, path
CmdSendReaction IpcMsgType = "send_reaction" // fields: network_id, reaction_mid, reaction_emoji
CmdRemoveShare IpcMsgType = "remove_share" // remove a share root; field: path
CmdListShares IpcMsgType = "list_shares" // returns shares_list event
CmdCreateRoom IpcMsgType = "create_room" // field: room (name)
CmdSetDownloadDir IpcMsgType = "set_download_dir" // set per-network download directory at runtime; fields: network_id, path
CmdSendReaction IpcMsgType = "send_reaction" // fields: network_id, reaction_mid, reaction_emoji
// Events (daemon → UI)
EvtMessageReceived IpcMsgType = "message_received"
EvtPeerConnected IpcMsgType = "peer_connected"
EvtPeerDisconnected IpcMsgType = "peer_disconnected"
EvtSessionReady IpcMsgType = "session_ready" // DataChannel open + hello verified
EvtPeerStatus IpcMsgType = "peer_status" // ICE connection state + candidate type
EvtIncomingFile IpcMsgType = "incoming_file"
EvtFileProgress IpcMsgType = "file_progress"
EvtStateSnapshot IpcMsgType = "state_snapshot"
EvtError IpcMsgType = "error"
EvtInviteGenerated IpcMsgType = "invite_generated"
EvtFileList IpcMsgType = "file_list"
EvtFileComplete IpcMsgType = "file_complete"
EvtNetworkJoined IpcMsgType = "network_joined"
EvtNetworkLeft IpcMsgType = "network_left"
EvtIdentityExported IpcMsgType = "identity_exported"
EvtIdentityImported IpcMsgType = "identity_imported"
EvtSharesList IpcMsgType = "shares_list"
EvtRoomCreated IpcMsgType = "room_created" // field: room (name)
EvtHistoryLoaded IpcMsgType = "history_loaded" // fields: room, messages
EvtResumableTransfers IpcMsgType = "resumable_transfers" // field: resumable_files
EvtReaction IpcMsgType = "reaction" // fields: reaction_mid, reaction_emoji, peer_id
EvtMessageReceived IpcMsgType = "message_received"
EvtPeerConnected IpcMsgType = "peer_connected"
EvtPeerDisconnected IpcMsgType = "peer_disconnected"
EvtSessionReady IpcMsgType = "session_ready" // DataChannel open + hello verified
EvtPeerStatus IpcMsgType = "peer_status" // ICE connection state + candidate type
EvtIncomingFile IpcMsgType = "incoming_file"
EvtFileProgress IpcMsgType = "file_progress"
EvtStateSnapshot IpcMsgType = "state_snapshot"
EvtError IpcMsgType = "error"
EvtInviteGenerated IpcMsgType = "invite_generated"
EvtFileList IpcMsgType = "file_list"
EvtFileComplete IpcMsgType = "file_complete"
EvtNetworkJoined IpcMsgType = "network_joined"
EvtNetworkLeft IpcMsgType = "network_left"
EvtIdentityExported IpcMsgType = "identity_exported"
EvtIdentityImported IpcMsgType = "identity_imported"
EvtSharesList IpcMsgType = "shares_list"
EvtRoomCreated IpcMsgType = "room_created" // field: room (name)
EvtHistoryLoaded IpcMsgType = "history_loaded" // fields: room, messages
EvtResumableTransfers IpcMsgType = "resumable_transfers" // field: resumable_files
EvtReaction IpcMsgType = "reaction" // fields: reaction_mid, reaction_emoji, peer_id
)
// NetworkInfo summarises one joined network for state_snapshot and network_joined events.
@@ -335,44 +338,44 @@ type IpcMessage struct {
// join_network / leave_network
NetworkName string `json:"network_name,omitempty"`
NetworkHash string `json:"network_hash,omitempty"` // 64-char hex (yaw2 `net` field); alternative to network_name
ShareDir string `json:"share_dir,omitempty"` // optional per-network share directory
RequireInvite bool `json:"require_invite,omitempty"` // waste-go ext: reject peers without valid signed invite
InviteString string `json:"invite_string,omitempty"` // waste-go ext: the invite used to join (stored in mesh)
NetworkHash string `json:"network_hash,omitempty"` // 64-char hex (yaw2 `net` field); alternative to network_name
ShareDir string `json:"share_dir,omitempty"` // optional per-network share directory
RequireInvite bool `json:"require_invite,omitempty"` // waste-go ext: reject peers without valid signed invite
InviteString string `json:"invite_string,omitempty"` // waste-go ext: the invite used to join (stored in mesh)
// send_file / set_share_dir / file_complete path
Path string `json:"path,omitempty"`
// events
Peer *PeerInfo `json:"peer,omitempty"`
PeerID *PeerID `json:"peer_id,omitempty"`
Nick string `json:"nick,omitempty"`
Message *ChatMessage `json:"message,omitempty"`
Offer *FileOffer `json:"offer,omitempty"`
TransferID string `json:"transfer_id,omitempty"`
BytesReceived int64 `json:"bytes_received,omitempty"`
TotalBytes int64 `json:"total_bytes,omitempty"`
Peer *PeerInfo `json:"peer,omitempty"`
PeerID *PeerID `json:"peer_id,omitempty"`
Nick string `json:"nick,omitempty"`
Message *ChatMessage `json:"message,omitempty"`
Offer *FileOffer `json:"offer,omitempty"`
TransferID string `json:"transfer_id,omitempty"`
BytesReceived int64 `json:"bytes_received,omitempty"`
TotalBytes int64 `json:"total_bytes,omitempty"`
// state_snapshot fields (existing shape preserved for backward compat)
MasterAlias string `json:"master_alias,omitempty"` // daemon's alias (available before any network join)
MasterID string `json:"master_id,omitempty"` // daemon's master public key hex
LocalPeer *PeerInfo `json:"local_peer,omitempty"`
ConnectedPeers []PeerInfo `json:"connected_peers,omitempty"`
KnownPeers []PeerInfo `json:"known_peers,omitempty"` // historically seen, not currently connected
Rooms []string `json:"rooms,omitempty"`
MasterAlias string `json:"master_alias,omitempty"` // daemon's alias (available before any network join)
MasterID string `json:"master_id,omitempty"` // daemon's master public key hex
LocalPeer *PeerInfo `json:"local_peer,omitempty"`
ConnectedPeers []PeerInfo `json:"connected_peers,omitempty"`
KnownPeers []PeerInfo `json:"known_peers,omitempty"` // historically seen, not currently connected
Rooms []string `json:"rooms,omitempty"`
// multi-network: all joined networks (additive)
Networks []NetworkInfo `json:"networks,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
InviteGenerated string `json:"invite,omitempty"`
Files []FileEntry `json:"files,omitempty"`
Messages []ChatMessage `json:"messages,omitempty"` // history_loaded
ResumableFiles []ResumableFile `json:"resumable_files,omitempty"` // resumable_transfers
ReactionMID string `json:"reaction_mid,omitempty"` // reaction
ReactionEmoji string `json:"reaction_emoji,omitempty"` // reaction
Shares []ShareEntry `json:"shares,omitempty"`
ShareNetworks []string `json:"network_ids,omitempty"` // for add_share command: scope to specific network IDs, or ["*"] for global
Networks []NetworkInfo `json:"networks,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
InviteGenerated string `json:"invite,omitempty"`
Files []FileEntry `json:"files,omitempty"`
Messages []ChatMessage `json:"messages,omitempty"` // history_loaded
ResumableFiles []ResumableFile `json:"resumable_files,omitempty"` // resumable_transfers
ReactionMID string `json:"reaction_mid,omitempty"` // reaction
ReactionEmoji string `json:"reaction_emoji,omitempty"` // reaction
Shares []ShareEntry `json:"shares,omitempty"`
ShareNetworks []string `json:"network_ids,omitempty"` // for add_share command: scope to specific network IDs, or ["*"] for global
// export_identity / import_identity
Passphrase string `json:"passphrase,omitempty"` // import only; never echoed back
Backup string `json:"backup,omitempty"` // JSON backup blob
Passphrase string `json:"passphrase,omitempty"` // import only; never echoed back
Backup string `json:"backup,omitempty"` // JSON backup blob
// peer_status — ICE connection quality
ConnState string `json:"conn_state,omitempty"` // pion PeerConnectionState string
CandidateType string `json:"candidate_type,omitempty"` // host | srflx | relay | unknown