1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
|
"""
MeshBay protocol constants and message type definitions.
MNP (Mesh Node Protocol) — v0.1
MHP (Mesh Bay Hub Protocol) — v0.1
All wire messages are length-prefixed msgpack (4-byte big-endian length header).
Every message carries a "v" field for protocol version.
"""
from dataclasses import dataclass, field
from typing import Any
# The wire versions live in meshbay_common/__init__.py — one source, because a
# second copy here said "0.1" while every message on the wire carried "0.2".
# Nothing imported it, which is the only reason it was harmless.
from meshbay_common import MNP_VERSION, MHP_VERSION # noqa: F401 (re-export)
# ── MNP message types ─────────────────────────────────────────────────────────
class MNP:
HANDSHAKE = "handshake"
HANDSHAKE_ACK = "handshake_ack"
INDEX_SYNC = "index_sync" # full Mesh Group Index
INDEX_DELTA = "index_delta" # incremental update
FILE_REQUEST = "file_req" # request chunk(s)
FILE_CHUNK = "file_chunk" # encrypted chunk response
STREAM_SEGMENT = "stream_seg" # HLS/DASH segment
CHAT_MESSAGE = "chat_msg" # Double Ratchet message
CHAT_ATTACHMENT = "chat_attach" # attachment metadata
CHAT_HISTORY = "chat_hist" # request message history (newest, or before a cursor)
CHAT_HISTORY_RESPONSE = "chat_hist_resp" # history response with messages
# Liveness on an *already open* channel. A peer that goes away without
# closing leaves a DataChannel that still reads as connected until the next
# real request hangs, and there was no way to ask. This is not a discovery
# mechanism: opening a connection in order to ping costs a full ICE/DTLS
# handshake (measured at 0.6-7 s across two ISPs), so presence in the group
# list comes from the hub's socket registry instead.
PING = "ping"
PONG = "pong"
# GEK_REQUEST / GEK_RESPONSE were removed (NS3, and finding L1): the node must
# never serve the GEK in plaintext. Members obtain it by unwrapping their own
# ECIES bundle. The constants lingered after the handlers were deleted, leaving
# the wire contract looking as though the endpoint still existed.
FILE_UPLOAD = "file_upload" # client pushes file chunk to node
FILE_UPLOAD_ACK = "file_upload_ack" # node acknowledges chunk receipt
DIR_CREATE = "dir_create" # client → node: make a directory
DIR_CREATE_ACK = "dir_create_ack" # node → client: created
DIR_DELETE = "dir_delete" # client → node: remove an empty directory
DIR_DELETE_ACK = "dir_delete_ack" # node → client: removed
FILE_DELETE = "file_delete" # client requests file deletion
FILE_DELETE_ACK = "file_delete_ack" # node confirms deletion
STREAM_REQUEST = "stream_req" # client requests MSE video stream
STREAM_INIT = "stream_init" # node sends codec info + signals stream start
STREAM_DATA = "stream_data" # node sends encrypted fMP4 segment
STREAM_END = "stream_end" # node signals end of stream
STREAM_MORE = "stream_more" # client → node: room for N more segments
STREAM_STOP = "stream_stop" # client → node: nobody is watching any more
EPHEMERAL_STREAM = "ephemeral_stream" # reserved — mobile live push
HANDSHAKE_CHALLENGE = "handshake_challenge" # node → client: GEK proof nonce
HANDSHAKE_RESPONSE = "handshake_response" # client → node: HMAC(GEK, nonce)
ADMIN_CHALLENGE = "admin_challenge" # node → client: Ed25519 sign challenge
ADMIN_RESPONSE = "admin_response" # client → node: Ed25519 signature
# GEK_BUNDLE_STORE was removed with the invite redesign: the node wraps the GEK
# itself, for a key the recipient proved possession of, so no member ever hands
# the node key material (C5b, and the H3 substitution it enabled).
GEK_BUNDLE_FETCH = "gek_bundle_fetch" # client → node: request own wrapped GEK
GEK_BUNDLE_RESP = "gek_bundle_resp" # node → client: wrapped GEK bundle
KEYPAIR_BUNDLE_STORE = "keypair_bundle_store" # client → node: store encrypted keypair bundle
KEYPAIR_BUNDLE_FETCH = "keypair_bundle_fetch" # client → node: request own keypair bundle
KEYPAIR_BUNDLE_RESP = "keypair_bundle_resp" # node → client: encrypted keypair bundle
KEYPAIR_BUNDLE_DELETE = "keypair_bundle_delete" # client → node: withdraw own backup
JOIN_REQUEST = "join_request" # client → node: pair/recognise this identity
JOIN_RESULT = "join_result" # node → client: outcome + wrapped GEK
INVITE_CREATE = "invite_create" # operator → node: issue a pairing code
MEMBER_REVOKE = "member_revoke" # operator → node: stop serving the key
MEMBER_REVOKE_ACK = "member_revoke_ack"
MEMBER_UNPIN = "member_unpin" # operator → node: forget an identity
MEMBER_UNPIN_ACK = "member_unpin_ack"
MEMBER_UPLOAD = "member_upload" # operator → node: may members upload?
MEMBER_UPLOAD_ACK = "member_upload_ack"
# Device linking. A new device files a request bound to a code it displays;
# an already-pinned device of the same account approves it. Neither the hub
# nor the node can produce the countersignature.
DEVICE_REQUEST = "device_add_request" # new device → node
DEVICE_REQUEST_ACK = "device_add_request_ack"
DEVICE_LOOKUP = "device_lookup" # approver → node: find by code
DEVICE_LOOKUP_RESULT = "device_lookup_result"
DEVICE_ADD = "device_add" # approver → node: countersigned
DEVICE_ADD_ACK = "device_add_ack"
DEVICE_LIST = "device_list" # anyone → node: my devices
DEVICE_LIST_RESULT = "device_list_result"
DEVICE_REVOKE = "device_revoke" # a device retires another
# Rotation is the half of revocation that revocation cannot do: the node
# generates a fresh key itself, so no key material crosses the wire.
GEK_ROTATE = "gek_rotate" # operator → node: new group key
GEK_ROTATE_ACK = "gek_rotate_ack"
INVITE_RESULT = "invite_result" # node → operator: the code, once
# ── Index entry ───────────────────────────────────────────────────────────────
@dataclass
class IndexEntry:
id: str # blake3 hash of file (hex)
name: str # filename
path: str # path relative to shared directory
size: int # bytes
type: str # video | audio | image | document | archive | other
added_at: int # unix timestamp
duration: int | None = None # seconds, for media
thumb_hash: str | None = None # blake3 of thumbnail
uploader_id: str | None = None # user_id of who uploaded (None = pre-existing on disk)
uploader_pk: str | None = None # Ed25519 public key of uploader (base64 raw 32 bytes)
@dataclass
class IndexDelta:
base_version: int
version: int
additions: list[IndexEntry] = field(default_factory=list)
deletions: list[str] = field(default_factory=list) # list of ids
# ── Chunk request/response ────────────────────────────────────────────────────
@dataclass
class ChunkRequest:
file_id: str # blake3 hash of file (hex)
chunk_index: int
@dataclass
class ChunkResponse:
chunk_index: int
plaintext_size: int
nonce_b64: str
ct_b64: str
ct_hash_b64: str
pt_hash_b64: str
sig_b64: str
pk_node_b64: str
file_hash_b64: str
|