aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-common/src/meshbay_common/__init__.py
blob: f10302f9edd5aa5ff22a4c64e2949dde1d8870cc (plain) (blame)
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
"""MeshBay common — shared crypto primitives and protocol types."""

__version__ = "0.15.0"
# 0.2: added PING/PONG, and `before`/`has_more` on chat history. Both are
# additive — an 0.1 peer sends no `before` and gets the newest page, which is
# what it wanted — so this is a MINOR bump, not a MAJOR one.
# 0.4: added `apps_enabled`/`apps_enabled_ack`, and `enabled_apps` on the
# handshake ack, for the group-applications registry. Additive — a node that
# predates it is never sent the op, and a client that predates it never looks
# for the field — so this is a MINOR bump too.
# 0.5: added `width`/`height`/`display_title`/`season`/`episode` to
# `IndexEntry`, started populating the already-declared `duration`/
# `thumb_hash`, and added `media_meta_req`/`media_meta_resp`, for the Videos
# group app. Additive — an older client simply doesn't render the new
# fields — so this is a MINOR bump too.
# 0.6: added `season_meta_req`/`season_meta_resp` (per-season TMDB overview,
# rather than one static show-level summary applied to every season alike)
# and `tmdb_search_req`/`tmdb_search_resp` + `tmdb_override`/`tmdb_override_ack`
# (an operator correcting a wrong automatic TMDB match, found live against a
# real show fragmented across TMDB entries per season). All additive.
# 0.7: added `tmdb_enabled`/`tmdb_enabled_ack` — whether TMDB is used moved
# from a node-wide setting to per-group (an operator running a real media
# library alongside test/demo groups on one node wants outbound TMDB traffic
# for one, not all of them). `tmdb_config`/`tmdb_config_ack` keep their name
# but now only cover the token/language, which stay node-wide. Additive: an
# older client simply never sends/handles the new pair.
# 0.8: added `artist`/`album`/`track_no` to `IndexEntry`, and
# `musicbrainz_config`/`_ack`, `musicbrainz_enabled`/`_ack`,
# `music_meta_req`/`_resp`, for the Music group app
# (docs/MESHBAY_DESIGN.md §9.8).
# Same shape as 0.5-0.7's Videos additions, and reuses `duration`/
# `thumb_hash`/`display_title` rather than declaring new ones. Additive:
# an older client simply doesn't render the new fields or send the new
# messages.
# 0.9: added `audio_transcode_req`/`_resp` — WMA and Musepack tag/cover
# fine but decode in no mainstream browser's <audio> element at all, so
# the node transcodes to AAC/M4A on request and caches the result the same
# way a TMDB poster or MusicBrainz cover is cached. Additive: an older
# client never sends the request, and simply can't play those two formats,
# same as before this existed.
# 0.10: added `audio_root`/`_ack` — the Music app's own entry point, same
# shape as `video_root`/`_ack`. Reverses the original "no root, whole
# shared tree" call: a real messy library showed that call was wrong, and
# Music now requires one before showing/enriching anything, exactly like
# Videos (docs/MESHBAY_DESIGN.md §9.8). Additive at the protocol level: an
# older client never sends the op and never expects the field.
# 0.11: added `taken_at`/`camera` to `IndexEntry` (best-effort, from a photo's
# own EXIF block) and `photo_roots`/`photo_roots_ack`, for the Photos group
# app (docs/MESHBAY_DESIGN.md §9.9). Unlike `video_root`/`audio_root`,
# `photo_roots` is a *set*, replaced whole in one signed op — a photo library
# is routinely scattered across several folders, not one. Additive: an older client
# never sends the op and never expects either field.
# 0.12: added `link_preview_req`/`link_preview_resp` — the node unfurls a URL
# pasted in chat into an OpenGraph card. Additive: an older node logs "unknown
# type" and the client just shows the bare link, as it always did.
# 0.13: added `tmdb_rematch`/`tmdb_rematch_ack` — an operator dropping one
# file's cached TMDB match so it re-resolves with the current matcher
# (V13). Additive: an older node logs "unknown type", the client's
# button just does nothing.
# 0.14: added an optional `bundle_enc_recovery` field on `keypair_bundle_store`
# and `keypair_bundle_resp` — a second copy of the identity bundle wrapped
# under the account's recovery key, so a forgotten passphrase does not strand
# the identity (docs/MESHBAY_DESIGN.md §3.6). Additive: an older node ignores
# the field on store and never returns one; an older client never sends it.
# 0.15: `file_chunk` and `index_sync` had forked between the transports — WebRTC
# sent binary, unsigned chunks and plain index entries, QUIC sent base64 chunks
# with a per-chunk Ed25519 signature and a `GroupIndex.serialize()` envelope. One
# type, two shapes, a single consumer each and no test that they agreed. Both now
# come from one encoder (`protocol.file_chunk_wire`, `transport/wire.py`), which is
# QUIC adopting what WebRTC already sent. **Breaking on the QUIC wire**, and only
# there: the WebRTC shape — the one every deployed client speaks — is byte for byte
# what it was, and no QUIC client ships. Recorded as a MINOR bump for that reason;
# a deployed QUIC peer would have made it a MAJOR one.
# 1.0: `index_sync`, `index_delta` and the `handshake_ack` configuration
# fields now travel **sealed under a GEK-derived subkey**
# (`meshbay_common.groupbox`), and the handshake negotiates a supported
# version range instead of writing a `v` nobody reads.
#
# **Breaking, on the wire every deployed client speaks**, and there is no way
# to describe it as additive: an old client sends `index_sync` and gets a
# message with no `entries`; it reads `ack.enabled_apps`, finds nothing, and
# applies its documented fallback — "show every app" — rather than reporting
# an error; a new client against an old node finds `entries` it does not
# expect and no `ct`. 0.15 stayed MINOR because only the QUIC wire changed and
# no QUIC client ships; that argument is not available here, and MAJOR is what
# the project's own rule says. Hub and every node deploy together; the SPA is
# served by the hub, so a browser picks up the new client on reload.
#
# Version negotiation ships in the same flag day rather than after it (phase
# 15.6): the coordinated deployment is already being paid for, and it is what
# makes the *next* breaking change cost a refusal message instead of a second
# flag day. `MNP_MIN_SUPPORTED` in `handshake.py` is the other half.
#
# **2.0 (2026-09-07): the write path is sealed, chat is encrypted, and the last
# unencrypted content message is gone.** Three changes that landed together and
# share one version, because they share one flag day.
#
#   - `file_upload` and `file_upload_ack` travel sealed under a GEK-derived
#     subkey (`groupbox.PURPOSE_UPLOAD`). The filename, the destination folder
#     and the bytes all ride inside the seal; `upload_id` — a fresh
#     client-chosen correlation id — and `chunk_index` stay in clear because
#     the node routes and orders on them. `filename` used to be the
#     correlation key and cannot be any more, which is what forced `upload_id`.
#   - `chat_msg` is sealed under a per-device subkey of the group's chat epoch
#     key (`meshbay_common.chatbox`) and signed over the ciphertext with the
#     device key the node pinned. There is deliberately **no per-group switch**:
#     every node in existence is a test node, so an opt-in flag would buy
#     nothing and leave a plaintext branch reachable. Existing node data is
#     migrated by `QE/migration/migrate_chat_encryption.py`.
#   - `stream_seg` is **removed**. It answered with an MPEG-TS segment as
#     base64 with no encryption at all, on both transports, to any
#     authenticated member — the one content-plane message that never went
#     through a GEK-derived key. `stream_data` has done the job properly since
#     Phase 12, and `fetchStreamSegment`, its only browser caller, was defined
#     and never once invoked.
#
# **Breaking, on the wire every deployed client speaks**, and MAJOR by the same
# rule 1.0 was.
#
# **`MNP_MIN_SUPPORTED` moves to "2.0" with it, and that is a change of plan
# worth reading.** The sealed upload alone was a *confined* break: a 1.x peer
# could still connect, browse, download, stream and chat, and only its uploads
# were refused — so the floor stayed at "1.0" and the refusal was per message
# (`upload_not_sealed`). Mandatory chat encryption ends that confinement. A 1.x
# peer can neither produce a sealed chat message nor read one, so it would
# connect, appear to work, and then be unable to say anything or read anything
# anyone else said. Refusing it at the handshake with `version_too_old` and a
# sentence saying so is the honest form: a stated refusal is a bug report, a
# chat that quietly does not work is a support case. The per-message
# `upload_not_sealed` path stays, unchanged — it is still the right answer if
# the floor is ever lowered again.
#
# Still deliberately in clear, and none of it is content: the handshake itself,
# `index_progress` (counters only — see daemon.py `_push_index_progress`), the
# admin and configuration acks, and the media-metadata replies. The index at
# rest and file content on the operator's disk are unchanged.
# **3.0 (2026-09-09): a transfer needs a lease, and a peer that cannot ask for
# one is refused at the handshake.**
#
# `transfer_open` / `transfer_close` / `transfer_state` carry the lease a
# download or an upload runs under; `file_req` gains an optional `tr` and
# `file_upload` gains one beside the `upload_id` already in clear. All three are
# in clear, like `index_progress` and for the same stated reason: `tr` is
# opaque, `bytes` and `chunks` are numbers, and there is no filename and no path
# anywhere in them. Putting one there to make a log line prettier is exactly the
# trade `groupbox.py` exists to refuse.
#
# **The messages are additive; the requirement is not, and that is what makes
# this MAJOR.** A 2.0 client sends no `tr`, so it is a leaseless reader — and a
# leaseless reader is either refused as soon as it opens a third file, or it is
# not refused and transfers outside every cap the operator set. An opt-in switch
# ("enforce leases only for clients that speak 3.0") leaves that branch
# reachable on every node, which is finding C6's lesson — a transport that
# accepted a bare JWT — one feature later. It was already refused once, for chat
# encryption, on 2026-09-07.
#
# Browsing is deliberately **not** leased and never will be: not the poster
# grid, not the covers, not opening a photo to look at it. That exemption is
# bounded rather than open (`transfers.LeaselessReads`, two files in flight per
# session), because an exemption with no bound is the leaseless branch under
# another name.
#
# **What it costs, stated plainly.** The SPA is served by the hub, so a browser
# picks up the new client on reload. The desktop client ships its own UI, so an
# un-updated one is locked out — which is why `GET /v1/hub/version` carries
# `client.minimum` and the client checks it *before* connecting, and says "this
# version can no longer connect" rather than showing a handshake refusal nobody
# can act on.
#
# **3.1 (2026-09-16): per-account blobs the node holds and cannot read.**
#
# `user_blob_store` / `_fetch` / `_list` / `_delete`, modelled on
# `keypair_bundle_*` in every respect including that the node stores and returns
# an opaque byte string. Playlists are the first payload (docs/playlists.md
# §8.1); `kind` is validated against a pattern rather than an enum, so the table
# does not become an arbitrary key/value store for whatever a client feels like
# writing.
#
# **Additive, and MINOR because nothing is required.** A 3.0 peer never sends
# any of these and loses nothing it had; a 3.0 *node* answers `unknown message
# type`, and a client that cannot store a playlist on one node stores it on the
# next one it reaches and keeps its own copy meanwhile (§6.4). `MNP_MIN_SUPPORTED`
# does not move. That is the difference from 3.0, where the requirement — not the
# messages — is what made it MAJOR.
# **3.2 (2026-09-17): the viewer picks the audio track.**
#
# `stream_init` carries `audio_tracks` (ordinal, language, title, codec,
# channels) and the `audio_track` actually used; `stream_req` gains an optional
# `audio_track`. Until now the streaming path mapped `0:a:0` unconditionally,
# so a dubbed film played in whichever language was muxed first — across a real
# library that is one language, and the others were unreachable.
#
# **Additive, and MINOR because nothing is required, but the reason differs
# from 3.1's.** A playlist a node cannot store is a feature a client keeps to
# itself; an `audio_track` a node ignores is the *wrong language*, served
# silently, which is a wrong answer and not a missing one. What makes this
# MINOR anyway is that the client cannot get into that position: the selector
# is drawn from `audio_tracks` in the node's own `stream_init`, so a 3.1 node
# sends no list, the client shows no selector, and no `audio_track` is ever
# sent to a peer that would ignore it. **The capability is discovered from the
# answer, never from the version number** — and that is not the opt-in
# compatibility switch 3.0 refused, because there is no second branch on the
# node: a 3.2 node always enumerates, always honours what it is asked for, and
# always says which track it used. `MNP_MIN_SUPPORTED` does not move.
#
# **3.3 (2026-09-17): the viewer picks a subtitle track.**
#
# `stream_init` carries `subtitle_tracks` (ordinal, language, title, codec);
# `subtitle_req` asks for one by ordinal and `subtitle_resp` names a cache hash
# to pull through the ordinary `file_req`/`file_chunk` path, exactly as
# `audio_transcode_resp` does. Subtitles travel beside the stream because MSE
# decodes no in-band text track, and whole-file because the extraction is then
# absolute in time: a seek re-extracts nothing and the `<track>` is untouched.
#
# **Additive, MINOR, and for once nothing can be served wrongly.** An
# `audio_track` a 3.2 node ignores is the wrong language played in silence;
# `subtitle_req` has no such failure — a node too old to know the message
# answers `unknown message type` and the client shows no subtitles, which is
# what it showed before. The selector is still drawn from `subtitle_tracks` in
# the node's own `stream_init` and from no version number, so the request is
# never sent to a peer that could not answer it. `MNP_MIN_SUPPORTED` does not
# move.
MNP_VERSION = "3.3"
MHP_VERSION = "0.1"