<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/docs/devel-phases-next.md, branch 0.13</title>
<subtitle>MeshBay — read-only public mirror</subtitle>
<id>https://git.meshbay.org/meshbay.git/atom?h=0.13</id>
<link rel='self' href='https://git.meshbay.org/meshbay.git/atom?h=0.13'/>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/'/>
<updated>2026-09-10T22:19:06Z</updated>
<entry>
<title>docs: remove the documents MESHBAY_DESIGN.md replaces</title>
<updated>2026-09-10T22:19:06Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-10T22:19:06Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=f059cb118c556d1f0279350507f74b8a47d5a98a'/>
<id>urn:sha1:f059cb118c556d1f0279350507f74b8a47d5a98a</id>
<content type='text'>
Twenty-four files, about 17 000 lines: the two architecture drafts, the three
security reviews, eleven design notes, the roadmap, the decisions file, the v1–v4
archive, the deprecated user guide and the stale quickstart. Their content is in
MESHBAY_DESIGN.md, and git history holds the originals.

The reason to delete rather than keep bannered: a document that is superseded but
present still gets read, and a reader cannot always tell which of two accounts of
one mechanism is the live one. That was the argument for retiring the user guide
rather than repairing it, and it applies to the whole set.

What made this safe is the concordance. Roughly 290 comments and docstrings cite
these files by section — `musicbay.md §6`, `mediacenter.md §5.5`, `draft-v6 §2.11`
— and section 16 maps every one onto its replacement, so not a single comment
needs editing to stay followable. It now says plainly that the files are gone and
where to recover them, and it gained rows for the three reviews (their findings
are section 13), and for the two guides.

Four kept documents pointed into the set and were repointed first: `playlists.md`
(nine references — it is a live proposal and must not dangle), `WINDOWS-PORT.md`,
and CLAUDE.md's example. No dangling reference remains outside section 16.

Two files were dropped from the list after checking what they hold. `HTTPS.md` is
an operational runbook — Caddy, certificate renewal, DNS, troubleshooting — and
MESHBAY_DESIGN.md deliberately covers no operations, so nothing would replace it;
the versioned Caddyfile is the config, not the procedure. `cast-smart-tv.md` is
the plan for the unbuilt DLNA phase of a feature whose first two phases ship, and
section 11.4 summarises it in four lines rather than carrying the SSDP/UPnP work.

There is no user guide now, and section 0.1 says so rather than leaving a reader
to discover it.

Suites green: 2258 passed, 4 skipped.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01YVoHVCcfBqud6ZjG4db3y7
</content>
</entry>
<entry>
<title>docs: point the superseded drafts at the design document</title>
<updated>2026-09-10T16:45:03Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-10T16:45:03Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=d82a0e7eec97844cd5eaf8aa49315146d5ed3d33'/>
<id>urn:sha1:d82a0e7eec97844cd5eaf8aa49315146d5ed3d33</id>
<content type='text'>
Twenty-three documents that MESHBAY_DESIGN.md absorbs gain a header saying so
and naming the sections their content went to. None is deleted: code comments,
tests and the documents themselves cite their sections and their labels, and
each records reasoning a synthesis compresses.

The header states the precedence, because two documents describing one system
will disagree eventually: where a draft disagrees with MESHBAY_DESIGN.md the
design document is right, and where either disagrees with the code the code is.

Seven status lines were corrected on the way through, all of them claiming less
than the truth. Videos, Music, Photos, partial-read hashing and account recovery
were headed "proposal, not implemented" months after they shipped; the desktop
client said "nothing here is implemented" with stages A through D running; draft
v6 still called itself the current specification.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01YVoHVCcfBqud6ZjG4db3y7
</content>
</entry>
<entry>
<title>Merge origin/main into the chat encryption work</title>
<updated>2026-09-07T16:03:52Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-07T16:03:52Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=e1383e1d545b994f4ad61694f868339defb0bdef'/>
<id>urn:sha1:e1383e1d545b994f4ad61694f868339defb0bdef</id>
<content type='text'>
Both sides landed a breaking MNP change and both called it 2.0, which is right:
the sealed upload, the removal of `stream_seg` and mandatory chat encryption
share one flag day. They are recorded as one version in `__init__.py` rather
than as a race between two.

The resolutions that were decisions rather than mechanics:

* **`MNP_MIN_SUPPORTED` moves to "2.0".** The sealed upload alone was a
  *confined* break — a 1.x peer could still connect, browse, download, stream
  and chat, with only its uploads refused by `upload_not_sealed` — so the floor
  deliberately stayed at "1.0". Mandatory chat encryption ends that
  confinement: a 1.x peer can neither produce a sealed chat message nor read
  one, so it would connect, look fine, and be unable to say anything. Refusing
  it at the handshake is the honest form. The per-message `upload_not_sealed`
  path is untouched and still right if the floor is ever lowered.
* **`sendChat` throws on an `error` reply**, from origin, applied to the sealed
  send. It matters more after this change, not less: the node now refuses a
  stale epoch, a malformed envelope and a device claim that is not the
  connection's own, so there are three new ways for a message to be rejected
  and none of them may look like a message that was sent.
* **`req_id` supersedes the per-type routing** this branch added for
  `chat_keys_resp` and `device_hello_ack`. Both blocks are kept beside the
  existing `chat_hist_resp` one, for the same stated reason — a node too old to
  stamp — and their comments no longer claim to be the mechanism that closes
  the class. `req_id` is.
* **`chat_send_probe.py` is rebuilt on origin's structure**, not beside it: two
  scenarios, a stub that stamps `req_id`, `music_meta_req` as the older pending
  request. The encrypted path is layered on — a real Ed25519 device key
  generated in the page, and a `chat_keys_resp` sealed by the shipped Python,
  because a payload the page built itself would prove only that the page agrees
  with the page.
* **`test_reply_correlation.py` now sends a sealed message.** Its subject is
  which of the two messages leaving that handler carries the id; plaintext chat
  was only the fixture, and the node refuses one now.
* `groupbox` keeps both new purposes (`upload`, `chat_keys`); `protocol.py`
  keeps origin's removal of `STREAM_SEGMENT` and this branch's correction of
  the "Double Ratchet message" comment on `CHAT_MESSAGE`, which was wrong when
  it was written and is wrong differently now.

Full suite on the merged tree: 1993 passed, 11 failed — the same 11 that fail
on a pristine checkout (2 Windows service tests, 1 apps-enabled policy, 7
transcode tests that pass in isolation, and the WebRTC invite test that hangs
on its own).

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01TZZxYjz8YeWRz13xDi8LJr
</content>
</entry>
<entry>
<title>feat(chat): encrypt group chat under per-device epoch keys (MNP 2.0)</title>
<updated>2026-09-07T15:50:28Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-07T15:50:28Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=36cebf25d0e0f24cf63be4380ccb5d03da726a74'/>
<id>urn:sha1:36cebf25d0e0f24cf63be4380ccb5d03da726a74</id>
<content type='text'>
Chat messages are sealed with AES-256-GCM under a key derived per group, per
epoch, per *device*, and signed over the ciphertext with the device key the
node pinned. The node relays and archives; it cannot read a message.

There is no switch. MNP goes to 2.0 and MNP_MIN_SUPPORTED moves with it, so a
1.x peer is refused at the handshake with `version_too_old` rather than
admitted and then unable to speak. An opt-in flag was designed and rejected:
every node is a test node, so it would have bought nothing and left a plaintext
branch reachable — C6's lesson one feature later. A test reads the source and
refuses any code that consults a `chat_encrypted` setting.

Not Sender Keys, and `senderkeys.py` is now documented as unused. With
distribution under the group key and a node that serves history to devices
which were not present, the node must retain each chain's earliest key, and a
chain key at iteration i yields every message key from i on by pure HKDF —
forward secrecy is zero either way. What the ratchet was left buying was
stateful client code with silent failure modes, three of them reproduced: any
member could sign as any other, a second device dropped the first's chain, and
the skipped-key cache grew without bound. The reasoning is in
docs/chat-sender-keys.md, which is the specification and the decision record.

Epochs, not rotation: the epoch key is wrapped under the group key at delivery
and never stored under it, so `gek_rotate` is a re-wrap. A group-key-derived
archive key would have made every message ever sent unreadable on the first
`member unpin`, which is the documented step after removing a member. A new
epoch opens on member revoke/unpin, device revoke and `gek_rotate`; old epochs
are kept and still delivered, so history stays readable to everyone who could
already read it, and nothing anywhere deletes one.

Three prerequisites this needed, each a live defect on its own:

* The peer registry was keyed by user_id, so one account's second device
  evicted the first and the broadcast skipped recipients by account — a
  person's phone never saw what they typed on their laptop.
* The handshake authenticated an account, never a device. `device_hello`
  (additive, signed, refused unless the key is a live device of this account in
  the node's own roster) is what lets the node refuse a member claiming
  somebody else's key.
* `_admin_exec_file_delete` authorized against the exact uploading key, so
  device linking had already broken deleting your own file from your other
  device. It now authorizes against any non-revoked device of `uploader_id`.

Found by driving the real panel over the real transport, not by reading source:
`chat_keys_resp` was routed by arrival order and handed to an unanswered
`media_meta_req` — the original frozen-tab defect in a message type that did
not exist when that probe was written. And `_asText` had been deleted with an
unrelated helper beside it; its only caller sits inside a promise the panel
catches, so every conversation rendered empty with nothing in the console.

Existing node data is migrated by QE/migration/migrate_chat_encryption.py
(not versioned, per the QE rule), run with the node stopped.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01TZZxYjz8YeWRz13xDi8LJr
</content>
</entry>
<entry>
<title>refactor(mnp)!: remove stream_seg, the last unencrypted content message</title>
<updated>2026-09-07T15:45:54Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-07T15:45:54Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=77dd077491aea50e71e21e0d17555a2f91cf818b'/>
<id>urn:sha1:77dd077491aea50e71e21e0d17555a2f91cf818b</id>
<content type='text'>
`stream_seg` answered with an MPEG-TS segment as base64 with no encryption at
all — the one message on the content plane that never went through a
GEK-derived key. Live on both transports, answering any authenticated member.

It predates `stream_data`, which does the same job properly (`chunk_ciphertext`,
keyed per segment, AES-256-GCM) and has since Phase 12. Its only browser caller,
`fetchStreamSegment`, was defined and never once invoked — a plaintext media
endpoint with no client. Removed rather than repaired.

Gone with it: `_extract_segment` and the ffmpeg semaphore in quic_server, the
`fetch_stream_segment` QUIC client method, and `_b64decode` in transport.js,
which had no other caller.

The H6 regression test lived on this handler — it pinned `_do_stream_segment_async`
to a coroutine so `subprocess.run` could not stall the event loop for thirty
seconds per request. It is replaced by the property that outlives the handler:
no transport carries media outside an AEAD, asserted on `stream_seg` and
`data_b64` across all three transport modules. The half of H6 that survives —
the live streaming path still spawns ffmpeg — keeps its own test.

BREAKING CHANGE: `stream_seg` is no longer answered on either transport. No
shipping client sends it. Recorded as part of MNP 2.0, whose other half — the
sealed upload — carries the version bump.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01AsoWC3GmhNdwVFomW3QjH3
</content>
</entry>
<entry>
<title>feat!: MNP 1.0 — seal index and handshake_ack under the group key</title>
<updated>2026-09-03T14:16:55Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-03T14:16:55Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=675beed6ff688733a9598f9d82d41578f48316be'/>
<id>urn:sha1:675beed6ff688733a9598f9d82d41578f48316be</id>
<content type='text'>
`index_sync`, `index_delta` and the `handshake_ack` config payload now travel
sealed under a GEK-derived subkey (`meshbay_common/groupbox.py`, mirrored by
`sealGroup`/`openGroup` in `crypto.js`). Only `type`, `v`, `group_id` and the
ack's `node_pk`/`proof`/`sig` stay in clear — a receiver must route and
authenticate before it would trust a decryption. Verify, then decrypt.

The ack line is integrity, not confidentiality: the signed handshake transcript
names no ack field, so `is_node_admin`, `enabled_apps`, `video_root` and the
rest were authenticated by the DTLS channel alone. The index line is defence in
depth against a repeat of C1/C6 — a peer served before the handshake completes
now gets ciphertext, not filenames. Nothing against an observer, the hub, or a
member; that is the whole claim. `index_progress` stays clear (D3, counters
only). Chat is out of scope.

Failure is fatal: a payload that does not open ends the session naming the
message type — never an empty index or an empty `enabled_apps`, both of which
are legitimate states.

Version negotiation ships here too (phase 15.6, brought forward): `v` + `v_min`
on `handshake` and `handshake_challenge`, refused with `version_too_old` /
`version_too_new` / `version_unreadable`. The flag day was already being paid
for; the next breaking change now costs a refusal message.

BREAKING CHANGE: breaks the WebRTC wire every deployed client speaks. Hub and
every node must deploy together; the SPA is served by the hub, so a browser
picks up the new client on reload. See MESHBAY_NODE_PROTOCOL.md §11.1a, §13.1.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01HkzbhmMmK8PqQBtGz5zCvY
</content>
</entry>
<entry>
<title>docs: Windows port audit and sender key distribution decision</title>
<updated>2026-09-03T13:34:33Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-03T12:09:18Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=15087b0e8fdb872602310119f14680aaa443fd93'/>
<id>urn:sha1:15087b0e8fdb872602310119f14680aaa443fd93</id>
<content type='text'>
Add docs/WINDOWS-PORT.md with the full portability audit (what is
already portable, what blocks, implementation plan W1-W7).

Reverse structural decision 20: sender keys are distributed
GEK-wrapped, not pairwise to identity keys. The GEK is the group
secret; files and chat share the same access boundary. Per-device
chains (15.0b) remain required for correctness.

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>docs: move root docs into docs/ and archive superseded drafts</title>
<updated>2026-09-01T14:05:16Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-01T14:05:16Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=8a6294b0412a86f378c6e2e937c28de64a903c91'/>
<id>urn:sha1:8a6294b0412a86f378c6e2e937c28de64a903c91</id>
<content type='text'>
Move the remaining root-level .md files (except CLAUDE.md) into docs/:
devel-phases.md, devel-phases-next.md, first-review.md, second-review.md,
tmp-decisions.md. Update all inbound references in CLAUDE.md (now docs/-prefixed)
and strip the now-redundant docs/ prefix from links inside the moved files.

Consolidate the superseded material into docs/old-draft.md: architecture
drafts v1-v4, POC v1, and the Phase 1-12 development log, each under an
ARCHIVED banner with a preamble pointing at the current specs. Delete the
merged originals plus the unreferenced French translations (v1-fr, v2-fr,
poc-v1-fr). Repoint the surviving file-links in first-review.md,
second-review.md and meshbay-draft-v5.md at old-draft.md; prose "draft-v3 §x"
mentions are left as-is since the content now lives in the archive.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01J74kj44q6REczub8XR3DRy
</content>
</entry>
</feed>
