Files
waste-go/internal/proto/proto.go
Fredrik Johansson fcbd84f873 fix: compact timestamps, historical alias resolution, wider ts column
- Timestamps now show "Jun 28 10:58" for older messages (dropped
  "Yesterday" which overflowed the fixed-width column)
- Timestamp column widened from 52px to 72px to fit date+time
- state_snapshot now includes known_peers (historically seen, not
  currently connected) so the UI can resolve aliases in history
- MessagePane aliasFor falls back to knownPeers map

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 13:43:29 +02:00

372 lines
16 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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:<xid>".
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"`
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
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
}