Bring wire protocol into full YAW/2 spec compliance

Five interop issues fixed against PROTOCOL.md:

- Join signature now covers nonce_raw || net_ascii (64-char UTF-8 hex
  string) as specified in §5.1, not net_raw_bytes. Both anchor server
  and client updated to match.

- Chat wire fields renamed to spec names: text/ts (Unix ms int64)
  replacing body/sent_at (ISO timestamp). Flat layout on PeerMessage
  matches §8 exactly; store and TUI updated accordingly.

- Direct messages now use the spec "pm" type (flat {type,mid,text,ts})
  instead of chat+to. Receiver reconstructs a ChatMessage with
  dm:<short-id> room for IPC/storage. §8 compliant.

- File transfer message types changed to spec hyphenated names:
  file-offer, file-accept, file-cancel, file-done with spec field
  names (name/size not filename/size_bytes). §9 compliant.

- DataChannel open-race (§14 gotcha #3) fixed with sync.Once: doOpen
  fires on OnOpen callback or immediately if the channel is already
  open when WireDataChannel is called (answerer race).

Also fixes two bugs found during testing:

- mid was missing from outgoing wire messages, causing all received
  messages to arrive with mid="" and collide on the UNIQUE DB
  constraint. mid is now included on all sent chat/pm messages; a
  random mid is generated for any received message that omits it.

- Test scripts hardened: kill -9 + active lsof polling replaces blind
  sleep for port cleanup; join_network sent before peer_field queries
  (local_peer is now network-scoped and nil until joined).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Fredrik Johansson
2026-06-22 11:38:01 +02:00
parent 13fb7ba1fe
commit b87f14a361
11 changed files with 623 additions and 126 deletions

View File

@@ -35,40 +35,64 @@ 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"
MsgFileResp MsgType = "file_response"
MsgFileDone MsgType = "file_done"
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"
)
// 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"`
// Only one of these will be set, depending on Type.
Chat *ChatMessage `json:"chat,omitempty"`
// 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"`
FileOffer *FileOffer `json:"file_offer,omitempty"`
FileResp *FileResponse `json:"file_response,omitempty"`
FileDone *FileDone `json:"file_done,omitempty"`
Seq *uint64 `json:"seq,omitempty"` // for ping/pong
// 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"`
// 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
}
// ChatMessage is a message to a room or a DM.
// 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"` // random 16-byte hex, for deduplication (YAW/2 §8)
ID string `json:"id"` // internal uuid, kept for local use
From PeerID `json:"from"`
To *PeerID `json:"to,omitempty"` // nil = broadcast to room
Room string `json:"room"`
Body string `json:"body"`
SentAt time.Time `json:"sent_at"`
Mid string `json:"mid,omitempty"` // optional dedup id (required when relay hops > 0)
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
}
// PeerGossip shares known peer addresses.
@@ -95,27 +119,13 @@ type FileListResp struct {
Files []FileEntry `json:"files"`
}
// FileOffer initiates a file transfer.
// 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 {
Mid string `json:"mid"` // dedup id
Xid string `json:"xid"` // transfer id, used as DataChannel label "f:<xid>"
Filename string `json:"filename"`
SizeBytes int64 `json:"size_bytes"`
SHA256 string `json:"sha256"` // hex
}
// FileResponse accepts or declines a FileOffer.
type FileResponse struct {
Mid string `json:"mid"`
Xid string `json:"xid"`
Accepted bool `json:"accepted"`
}
// FileDone signals that all chunks have been sent. Receiver verifies SHA256.
type FileDone struct {
Mid string `json:"mid"`
Xid string `json:"xid"`
SHA256 string `json:"sha256"` // hex
Name string `json:"name"`
Size int64 `json:"size"`
SHA256 string `json:"sha256"`
}
// ── DataChannel hello (YAW/2 §6) ─────────────────────────────────────────────