// Package proto defines all wire types shared between the daemon and anchor. // Everything on the wire is newline-delimited JSON. // Binary data (keys, signatures) is hex-encoded; signaling boxes are base64. package proto import ( "crypto/sha256" "fmt" "time" ) // ── Identity ────────────────────────────────────────────────────────────────── // PeerID is a peer's stable identity: lowercase hex of the 32-byte Ed25519 public key (64 chars). // This IS the peer — display names are advisory only and unauthenticated. type PeerID string // Short returns the first 16 hex chars grouped in 4s: "a1b2 c3d4 e5f6 0718". func (p PeerID) Short() string { s := string(p) if len(s) < 16 { return s } return s[0:4] + " " + s[4:8] + " " + s[8:12] + " " + s[12:16] } // PeerInfo is a peer's self-description, included in the hello confirmation. type PeerInfo struct { ID PeerID `json:"id"` Alias string `json:"alias"` // advisory, not authenticated PublicKey string `json:"public_key"` // Ed25519 pubkey, hex CreatedAt time.Time `json:"created_at"` } // ── Peer-to-peer message types (over the "yaw" DataChannel) ────────────────── // MsgType identifies the kind of peer message. 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" ) // PmMessage is a private message sent directly over a single peer link (§8 "pm"). // The sender/receiver are implicit from the DataChannel; no room or from fields on the wire. type PmMessage struct { Text string `json:"text"` Ts int64 `json:"ts"` // Unix milliseconds } // PeerMessage is the top-level container sent over the "yaw" DataChannel. // The spec types (hello, chat, pm, file-offer …) are flat JSON objects; we // embed the fields directly using inline structs where needed, but for structured // types we include the payload pointer. Unknown fields are ignored (forward compat). // File chunks go over a separate binary DataChannel labeled "f:". type PeerMessage struct { Type MsgType `json:"type"` // chat / pm fields (flat on the wire per spec §8) Mid string `json:"mid,omitempty"` // optional dedup id; required when relay hops > 0 Room string `json:"room,omitempty"` // chat only Text string `json:"text,omitempty"` // chat and pm Ts int64 `json:"ts,omitempty"` // chat and pm (Unix ms) // Non-spec extensions (unknown types are silently ignored by other impls) Gossip *PeerGossip `json:"gossip,omitempty"` FileListResp *FileListResp `json:"file_list_resp,omitempty"` // file transfer (§9) — fields are flat on the wire Xid string `json:"xid,omitempty"` Name string `json:"name,omitempty"` Size int64 `json:"size,omitempty"` SHA256 string `json:"sha256,omitempty"` ResumeOffset int64 `json:"resume_offset,omitempty"` // waste-go ext: non-zero in file-accept when resuming // file-done / file-cancel / file-accept just need xid (already above) Reason string `json:"reason,omitempty"` // file-cancel Seq *uint64 `json:"seq,omitempty"` // ping/pong // history_request fields Since int64 `json:"since,omitempty"` // Unix ms; 0 = no lower bound Limit int `json:"limit,omitempty"` // history_chunk fields History []HistoryEntry `json:"history,omitempty"` HistoryDone bool `json:"history_done,omitempty"` } // ResumableFile describes a partially-downloaded file found on daemon startup. type ResumableFile struct { Name string `json:"name"` SHA256 string `json:"sha256"` From string `json:"from"` // peer ID hex Size int64 `json:"size"` Offset int64 `json:"offset"` // bytes already received } // HistoryEntry is one message in a history_chunk response. type HistoryEntry struct { Mid string `json:"mid"` From string `json:"from"` // peer ID hex FromAlias string `json:"from_alias"` // advisory Text string `json:"text"` Ts int64 `json:"ts"` // Unix ms } // 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) 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 Room string `json:"room"` Text string `json:"text"` Ts int64 `json:"ts"` // Unix milliseconds } // ComputeMsgID returns the EXT-007 content-addressed ID for a message. // sha256(fromID \x00 room \x00 ts_decimal \x00 text) func ComputeMsgID(fromID PeerID, room string, ts int64, text string) string { h := sha256.New() fmt.Fprintf(h, "%s\x00%s\x00%d\x00%s", string(fromID), room, ts, text) return fmt.Sprintf("sha256:%x", h.Sum(nil)) } // PeerGossip shares known peer addresses. type PeerGossip struct { Peers []GossipEntry `json:"peers"` } // GossipEntry is one peer hint shared via gossip. type GossipEntry struct { Peer PeerInfo `json:"peer"` AddrHint string `json:"addr_hint"` // IP:port hint, may be behind NAT LastSeen time.Time `json:"last_seen"` } // FileEntry describes a single file in a peer's shared directory. type FileEntry struct { Name string `json:"name"` SizeBytes int64 `json:"size_bytes"` Path string `json:"path,omitempty"` // relative path including filename } // ShareEntry describes one persistent share root. type ShareEntry struct { Path string `json:"path"` Networks []string `json:"networks"` // ["*"] = global } // FileListResp is the payload for MsgFileListResp. // MsgFileListReq carries no payload — it is a zero-field request. type FileListResp struct { Files []FileEntry `json:"files"` } // FileOffer is used internally when emitting EvtIncomingFile to the IPC layer. // On the wire, file-offer fields are flat inside PeerMessage (xid/name/size/sha256). type FileOffer struct { Xid string `json:"xid"` Name string `json:"name"` Size int64 `json:"size"` SHA256 string `json:"sha256"` } // ── DataChannel hello (YAW/2 §6) ───────────────────────────────────────────── // HelloMessage is the first message sent on the "yaw" DataChannel. // The signature binds this identity to the specific DTLS session. // The Invite field is a waste-go extension (§ waste-go/extensions.md): // when RequireInvite is enabled on a network, peers that omit or present // an invalid signed invite are disconnected. YAW/2-only peers ignore this field. type HelloMessage struct { Type string `json:"type"` // always "hello" ID string `json:"id"` // hex pubkey Nick string `json:"nick"` // alias Caps []string `json:"caps"` // capability list, e.g. ["chat","file"] Sig string `json:"sig"` // hex ed25519 sig over HelloBindString Invite string `json:"invite,omitempty"` // waste-go ext: signed waste: invite string } // HelloBindString returns the bytes the hello signature covers: // "yaw/2 bind" || localDTLSFingerprint(32 bytes) || remoteDTLSFingerprint(32 bytes) func HelloBindString(localFP, remoteFP []byte) []byte { buf := []byte("yaw/2 bind") buf = append(buf, localFP...) buf = append(buf, remoteFP...) return buf } // ── Signaling payload (sealed inside nacl/box, exchanged via anchor) ────────── // SignalingKind identifies the kind of sealed signaling payload. type SignalingKind string const ( SigOffer SignalingKind = "offer" SigAnswer SignalingKind = "answer" SigCandidate SignalingKind = "candidate" SigBye SignalingKind = "bye" SigEkey SignalingKind = "ekey" // YAW/2.1: ephemeral key exchange ) // SignalingPayload is the JSON plaintext sealed inside a crypto_box (YAW/2 §5 / §5.4′). type SignalingPayload struct { Kind SignalingKind `json:"kind"` SDP string `json:"sdp,omitempty"` // offer / answer Cand string `json:"cand,omitempty"` // trickle ICE candidate line Mid string `json:"mid,omitempty"` // media stream id for candidate MLine int `json:"mline,omitempty"` // media line index // YAW/2.1 ekey fields (sealed under static keys) V string `json:"v,omitempty"` // "yaw/2.1" EPK string `json:"epk,omitempty"` // hex-encoded ephemeral X25519 pubkey (32 bytes) EkeySig string `json:"ekey_sig,omitempty"` // hex Ed25519 sig over ekey bind bytes } // ── Anchor WebSocket wire types (YAW/2 §5) ──────────────────────────────────── // AnchorMsgType identifies anchor WebSocket messages. 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" ) // 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 } // ── IPC protocol (daemon ↔ local UI) ───────────────────────────────────────── // IpcMsgType identifies IPC messages. 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" CmdSendFile IpcMsgType = "send_file" 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 // 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 ) // NetworkInfo summarises one joined network for state_snapshot and network_joined events. type NetworkInfo struct { NetworkID string `json:"network_id"` NetworkName string `json:"network_name"` LocalPeer *PeerInfo `json:"local_peer,omitempty"` ShareDir string `json:"share_dir,omitempty"` // absolute path; empty = not sharing DownloadDir string `json:"download_dir,omitempty"` // absolute path for received files } // IpcMessage covers both commands and events. type IpcMessage struct { Type IpcMsgType `json:"type"` // optional: scopes a command/event to a specific network. // When absent, defaults to the first (or only) joined network. NetworkID string `json:"network_id,omitempty"` // send_message Room string `json:"room,omitempty"` To *PeerID `json:"to,omitempty"` Body string `json:"body,omitempty"` // 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) // 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"` // 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"` 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 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 // peer_status — ICE connection quality ConnState string `json:"conn_state,omitempty"` // pion PeerConnectionState string CandidateType string `json:"candidate_type,omitempty"` // host | srflx | relay | unknown RemoteAddress string `json:"remote_address,omitempty"` // remote IP:port of active candidate pair }