From 20a824118c09af15d6c338db4c9480ffe5cbcdb6 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sat, 19 Sep 2026 02:12:47 +0200 Subject: docs: cite MESHBAY_DESIGN.md and a section instead of the merged notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-feature design notes were merged into docs/MESHBAY_DESIGN.md and deleted from the tree on 2026-09-11, but ~230 comments across the three packages still named them — usually written `docs/musicbay.md §3.2`, as though the file were still in docs/. A reader had to know §16 existed to resolve any of them. They now name the section directly. Every mapping comes from §16, the concordance, which already records where each old section landed: musicbay -> §9.8, mediacenter -> §9.7 for the Videos app and §6.5 where the subject is derived data, photos -> §9.9, auth-confirm -> §3.6, refactoring-search -> §9.11, invite-pairing-v1 -> §3.4, per-node-identity-v1 -> §3.2, captcha -> §7.7, chat-sender-keys -> §4.5, apps/refactor-groups -> §9.1–§9.4, desktop-client-v1 per section. Bare citations of the same documents (`draft-v6 §2.11`, `§4.8`, `§3.4`) are retargeted too: those collide with real section numbers in the design document, so leaving them would have been worse than the named form. Four cases the concordance does not cover, each decided rather than guessed: Sub-item references into documents that no longer exist — mediacenter's `§3.3 row 4`, `§3.4b/c`, `apps.md §3b` — name rows and sub-items §9.7 and §9.2 do not reproduce. The module-level citation stays; the sub-item pointer is dropped. The V-findings keep their labels but lose the dead `§10.1/` prefix. §13.8 lists V1–V13 as per-application open items, which is not what the labels mean in these comments, so pointing them at §13.8 would have been a false citation. `apps.md §5`'s virtualization requirement has no counterpart anywhere in the design document. The requirement is stated in the comment itself, so the citation is dropped rather than aimed at a section that does not say it. Comments that attributed a *sentence* to an old note — musicbay's "several thousand files" example, its "what got measured" note, its measured ~11%/~26% cover-art figures, the "original no root, whole shared tree" call — state the fact without attribution now. §9.8 does not contain those sentences and citing it for them would have been wrong. CLAUDE.md's "a reference to a document that no longer exists" row now says the concordance is for git history and out-of-tree material; the code cites sections directly. Verified: 2851 passed, 4 skipped. The 12 errors in the run are the Firefox leg of test_sticky_header.py's browser harness, which is broken at the browser level on this machine — headless Firefox (snap) dies with `[GFX1-]: RenderCompositorSWGL failed mapping default framebuffer`, renders nothing, and the probe exits `{"error": "no measurement"}` after its full 90s wait. Chrome runs the same 12 assertions in 3.2s and passes. Nothing here can affect it: every changed line in style.css is inside a comment. Also checked: ast.parse on every changed .py, `node --check` on every changed .js, the /* */ balance in style.css, and that no changed line exceeds the width its file already used. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 2 +- packages/meshbay-client/src/main.js | 4 +- .../meshbay-common/src/meshbay_common/__init__.py | 25 +++---- .../meshbay-common/src/meshbay_common/adminop.py | 4 +- .../meshbay-common/src/meshbay_common/chatbox.py | 2 +- .../meshbay-common/src/meshbay_common/device.py | 2 +- packages/meshbay-common/src/meshbay_common/join.py | 2 +- .../meshbay-common/src/meshbay_common/protocol.py | 16 ++--- packages/meshbay-hub/src/meshbay_hub/api/users.py | 6 +- packages/meshbay-hub/src/meshbay_hub/config.py | 2 +- packages/meshbay-hub/src/meshbay_hub/db/models.py | 2 +- packages/meshbay-hub/src/meshbay_hub/mail.py | 4 +- .../meshbay-hub/src/meshbay_hub/static/apps.js | 2 +- .../src/meshbay_hub/static/auth-page.js | 4 +- .../src/meshbay_hub/static/file-utils.js | 4 +- .../src/meshbay_hub/static/files-app.js | 2 +- .../src/meshbay_hub/static/group-name.js | 4 +- .../src/meshbay_hub/static/group-page.js | 12 ++-- .../meshbay_hub/static/helloworld-app-settings.js | 7 +- .../src/meshbay_hub/static/helloworld-app.js | 4 +- .../src/meshbay_hub/static/hub-client.js | 11 ++-- .../src/meshbay_hub/static/keyderive.js | 10 +-- .../src/meshbay_hub/static/music-app-settings.js | 2 +- .../src/meshbay_hub/static/music-app.js | 39 ++++++----- .../src/meshbay_hub/static/music-player.js | 18 ++--- .../src/meshbay_hub/static/photos-app-settings.js | 2 +- .../src/meshbay_hub/static/photos-app.js | 4 +- .../meshbay-hub/src/meshbay_hub/static/platform.js | 5 +- .../src/meshbay_hub/static/profile-page.js | 4 +- .../src/meshbay_hub/static/search-page.js | 8 +-- .../src/meshbay_hub/static/settings-page.js | 2 +- .../src/meshbay_hub/static/source-merge.js | 2 +- .../meshbay-hub/src/meshbay_hub/static/style.css | 12 ++-- .../src/meshbay_hub/static/transport.js | 34 +++++----- .../src/meshbay_hub/static/video-app-settings.js | 2 +- .../src/meshbay_hub/static/video-app.js | 32 ++++----- packages/meshbay-hub/tests/test_desktop_shell.py | 11 ++-- packages/meshbay-hub/tests/test_downloads.py | 6 +- .../test_helloworld_proves_the_plugin_claim.py | 2 +- packages/meshbay-hub/tests/test_hook_ordering.py | 2 +- packages/meshbay-hub/tests/test_password_change.py | 2 +- packages/meshbay-hub/tests/test_password_reset.py | 2 +- packages/meshbay-hub/tests/test_recovery_email.py | 2 +- packages/meshbay-hub/tests/test_recovery_key.py | 2 +- packages/meshbay-hub/tests/test_rewrap_fanout.py | 2 +- .../tests/test_search_files_unmerged.py | 4 +- .../meshbay-hub/tests/test_search_media_merge.py | 2 +- .../meshbay-hub/tests/test_search_source_merge.py | 2 +- .../meshbay-hub/tests/test_sticky_band_ring.py | 7 +- .../meshbay-hub/tests/test_transport_contracts.py | 2 +- packages/meshbay-hub/tests/test_zip_size_limit.py | 2 +- .../meshbay-node/src/meshbay_node/bundle_store.py | 4 +- .../meshbay-node/src/meshbay_node/chat/store.py | 2 +- packages/meshbay-node/src/meshbay_node/daemon.py | 77 ++++++++++++---------- .../src/meshbay_node/indexer/enrich.py | 64 +++++++++--------- .../src/meshbay_node/indexer/enrich_audio.py | 7 +- .../src/meshbay_node/indexer/enrich_photo.py | 7 +- .../src/meshbay_node/indexer/indexer.py | 13 ++-- .../src/meshbay_node/indexer/title_parse.py | 31 ++++----- .../meshbay-node/src/meshbay_node/media_cache.py | 36 +++++----- .../meshbay-node/src/meshbay_node/media_probe.py | 2 +- .../meshbay-node/src/meshbay_node/musicbrainz.py | 6 +- packages/meshbay-node/src/meshbay_node/ops.py | 13 ++-- packages/meshbay-node/src/meshbay_node/roster.py | 16 ++--- packages/meshbay-node/src/meshbay_node/tmdb.py | 10 +-- .../src/meshbay_node/transport/webrtc_server.py | 72 ++++++++++---------- packages/meshbay-node/src/meshbay_node/ui/app.py | 2 +- packages/meshbay-node/tests/conftest.py | 4 +- .../tests/test_audio_root_gates_enrichment.py | 9 ++- .../meshbay-node/tests/test_audio_transcode.py | 2 +- .../tests/test_bundle_store_recovery.py | 2 +- .../meshbay-node/tests/test_chat_encryption.py | 2 +- .../meshbay-node/tests/test_chat_history_binary.py | 2 +- .../meshbay-node/tests/test_chat_multidevice.py | 5 +- packages/meshbay-node/tests/test_cli_dispatch.py | 2 +- .../tests/test_device_on_connection.py | 7 +- packages/meshbay-node/tests/test_enrich.py | 2 +- packages/meshbay-node/tests/test_enrich_photo.py | 2 +- packages/meshbay-node/tests/test_group_roster.py | 6 +- packages/meshbay-node/tests/test_media_cache.py | 2 +- packages/meshbay-node/tests/test_musicbrainz.py | 5 +- .../tests/test_musicbrainz_enabled_policy.py | 7 +- packages/meshbay-node/tests/test_roster_pairing.py | 2 +- .../tests/test_season_and_search_requests.py | 3 +- .../tests/test_security_regressions.py | 12 ++-- .../tests/test_startup_scan_enrichment.py | 4 +- .../tests/test_stream_video_transcode.py | 4 +- packages/meshbay-node/tests/test_title_parse.py | 12 ++-- packages/meshbay-node/tests/test_tmdb.py | 2 +- .../meshbay-node/tests/test_tmdb_config_policy.py | 5 +- .../meshbay-node/tests/test_tmdb_enabled_policy.py | 2 +- .../meshbay-node/tests/test_tmdb_rematch_policy.py | 2 +- .../meshbay-node/tests/test_tmdb_search_ladder.py | 4 +- .../meshbay-node/tests/test_transfer_settings.py | 3 +- .../tests/test_video_root_gates_enrichment.py | 2 +- .../meshbay-node/tests/test_webrtc_transport.py | 3 +- packaging/caddy/meshbay.org.Caddyfile | 8 +-- packaging/conf/hub.toml.example | 4 +- packaging/systemd/meshbay-node-user.service | 4 +- packaging/systemd/meshbay-node.service | 14 ++-- 100 files changed, 447 insertions(+), 411 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1f837eb..0199979 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -141,7 +141,7 @@ that produced it. | The node, the hub, the clients, the applications | §6, §7, §8, §9 | | Structural decisions that are not revisited | §14 | | What is built, what is not, what is open | §15 | -| A reference to a document that no longer exists (`draft-v5 §5.2`, `apps.md §3`, …) | §16, the concordance — it maps every one onto its replacement section | +| A reference to a document that no longer exists (`draft-v5 §5.2`, `apps.md §3`, …) — in git history, or in a document outside this repository | §16, the concordance — it maps every one onto its replacement section. The code itself cites `MESHBAY_DESIGN.md` and a section directly | The documents under `docs/` that this replaced are kept and carry a banner saying where their content went. **Where any of them disagrees with diff --git a/packages/meshbay-client/src/main.js b/packages/meshbay-client/src/main.js index 5e00d0b..794477b 100644 --- a/packages/meshbay-client/src/main.js +++ b/packages/meshbay-client/src/main.js @@ -15,7 +15,7 @@ * That value is realised by reproducible builds and published hashes (18.7), * not by the packaging format. * - * See docs/desktop-client-v1.md §2 and §3. + * See docs/MESHBAY_DESIGN.md §8.1, §8.2. */ 'use strict'; @@ -466,7 +466,7 @@ function secretsBackend() { // // Note what this key is *not*: it is not a per-node identity key. Those are // generated per node, pinned there, and never leave that relationship -// (docs/per-node-identity-v1.md). Nothing here correlates a person across +// (docs/MESHBAY_DESIGN.md §3.2). Nothing here correlates a person across // operators, and nothing wraps a group key for it. const DEVICE_KEY = 'device_auth_ed25519'; diff --git a/packages/meshbay-common/src/meshbay_common/__init__.py b/packages/meshbay-common/src/meshbay_common/__init__.py index c5b0279..1d857fd 100644 --- a/packages/meshbay-common/src/meshbay_common/__init__.py +++ b/packages/meshbay-common/src/meshbay_common/__init__.py @@ -26,7 +26,8 @@ __version__ = "0.14.0" # 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/musicbay.md). +# `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 @@ -38,29 +39,29 @@ __version__ = "0.14.0" # 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 musicbay.md's 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. Additive at the protocol level: an older client -# never sends the op and never expects the field. +# 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/photos.md). 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 +# 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 -# (§10.1/V13). Additive: an older node logs "unknown type", the client's +# (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/auth-confirm.md §4.3). Additive: an older node ignores the -# field on store and never returns one; an older client never sends it. +# 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 diff --git a/packages/meshbay-common/src/meshbay_common/adminop.py b/packages/meshbay-common/src/meshbay_common/adminop.py index 3e9fe11..20b5080 100644 --- a/packages/meshbay-common/src/meshbay_common/adminop.py +++ b/packages/meshbay-common/src/meshbay_common/adminop.py @@ -63,7 +63,7 @@ OP_SET_SCAN_SETTINGS = "set_scan_settings" # shown before signing names the outcome and not the operation. OP_TRANSFER_LIMITS = "transfer_limits" # Whether the node uses the operator's own API token/language instead of the -# shipped default — node-wide (docs/mediacenter.md §5.5), one credential +# shipped default — node-wide (docs/MESHBAY_DESIGN.md §9.7), one credential # shared by every group. Signed like the rest: it turns on outbound # third-party network traffic that did not exist before the Videos app # (§8) — an unsigned change would let any member alter egress the operator @@ -119,7 +119,7 @@ OP_GROUP_ATTACH = "group_attach" OP_GROUP_DETACH = "group_detach" # OP_GEK_BUNDLE_STORE is gone. Members no longer hand the node key material at # all: the node holds the GEK and wraps it itself, for a key the recipient proved -# they hold (see `join.py` and docs/invite-pairing-v1.md). The operation existed +# they hold (see `join.py` and docs/MESHBAY_DESIGN.md §3.4). The operation existed # only to make member-supplied bundles safe, and deleting the message is a # stronger guarantee than authorizing it. diff --git a/packages/meshbay-common/src/meshbay_common/chatbox.py b/packages/meshbay-common/src/meshbay_common/chatbox.py index 740e829..f0534fa 100644 --- a/packages/meshbay-common/src/meshbay_common/chatbox.py +++ b/packages/meshbay-common/src/meshbay_common/chatbox.py @@ -1,7 +1,7 @@ """ Chat message encryption and sender authentication. -Design A of `docs/chat-sender-keys.md`, decided 2026-09-07. What it is, and what +Design A of `docs/MESHBAY_DESIGN.md` §4.5, decided 2026-09-07. What it is, and what it deliberately is not, in the order the decisions were made: **Not a ratchet, and not sender keys.** With per-sender chains distributed under diff --git a/packages/meshbay-common/src/meshbay_common/device.py b/packages/meshbay-common/src/meshbay_common/device.py index cfa8dd6..3a598d0 100644 --- a/packages/meshbay-common/src/meshbay_common/device.py +++ b/packages/meshbay-common/src/meshbay_common/device.py @@ -27,7 +27,7 @@ Fields are length-prefixed and domain-separated, per L4 — the same rule as of the connection carrying the message, so neither signature can be lifted onto another connection, and `node_pk` binds an authorization to one node. -See `docs/desktop-client-v1.md` §4. +See `docs/MESHBAY_DESIGN.md` §3.3. """ from __future__ import annotations diff --git a/packages/meshbay-common/src/meshbay_common/join.py b/packages/meshbay-common/src/meshbay_common/join.py index 6ee543f..84ea18a 100644 --- a/packages/meshbay-common/src/meshbay_common/join.py +++ b/packages/meshbay-common/src/meshbay_common/join.py @@ -11,7 +11,7 @@ from the hub and wrap the group key for whatever came back. The hub is the key directory, so a hub answering with its own key was handed the GEK by an honest inviter following the protocol exactly. The key now comes from the peer over an authenticated channel and is bound to an identity by a one-time pairing code the -hub never sees. See `docs/invite-pairing-v1.md`. +hub never sees. See `docs/MESHBAY_DESIGN.md` §3.4. Fields are length-prefixed and domain-separated, per L4 — the same rule as `handshake.py` and `adminop.py`. `nonce_node` is the handshake nonce the node just diff --git a/packages/meshbay-common/src/meshbay_common/protocol.py b/packages/meshbay-common/src/meshbay_common/protocol.py index c3cb099..c444711 100644 --- a/packages/meshbay-common/src/meshbay_common/protocol.py +++ b/packages/meshbay-common/src/meshbay_common/protocol.py @@ -69,11 +69,11 @@ class MNP: # never once invoked. A live handler on both transports, plaintext media, # and no client: removed rather than repaired. # - # Not a Double Ratchet message, and never was — `first-review.md` C1 - # rejected exactly that for groups. Since MNP 2.0 it is AES-256-GCM under a + # Not a Double Ratchet message, and never was — finding C1 + # (`docs/MESHBAY_DESIGN.md` §13.1) rejected exactly that for groups. Since MNP 2.0 it is AES-256-GCM under a # per-device subkey of the group's chat epoch key, signed over the # ciphertext with the sending device's pinned Ed25519 key. There is no - # plaintext form on the wire (`chatbox.py`, docs/chat-sender-keys.md); + # plaintext form on the wire (`chatbox.py`, docs/MESHBAY_DESIGN.md §4.5); # `format` distinguishes a *stored* pre-2.0 row, which is still served. CHAT_MESSAGE = "chat_msg" # one chat message, sealed and signed CHAT_ATTACHMENT = "chat_attach" # attachment metadata @@ -136,7 +136,7 @@ class MNP: GEK_BUNDLE_RESP = "gek_bundle_resp" # node → client: wrapped GEK bundle KEYPAIR_BUNDLE_STORE = "keypair_bundle_store" # client → node: store encrypted keypair bundle # optional `bundle_enc_recovery` (MNP 0.14): a second copy wrapped under the - # account's recovery key (docs/auth-confirm.md §4.3) + # account's recovery key (docs/MESHBAY_DESIGN.md §3.6) KEYPAIR_BUNDLE_FETCH = "keypair_bundle_fetch" # client → node: request own keypair bundle KEYPAIR_BUNDLE_RESP = "keypair_bundle_resp" # node → client: encrypted keypair bundle # carries `bundle_enc_recovery` too when the node has one stored @@ -178,9 +178,9 @@ class MNP: TMDB_OVERRIDE_ACK = "tmdb_override_ack" TMDB_REMATCH = "tmdb_rematch" # operator → node: drop one file's match TMDB_REMATCH_ACK = "tmdb_rematch_ack" - # Music app (docs/musicbay.md). Contact is derived from the owner's hub - # email at login — no config/ack pair needed. Only the per-group toggle - # remains. + # Music app (docs/MESHBAY_DESIGN.md §9.8). Contact is derived from the + # owner's hub email at login — no config/ack pair needed. Only the + # per-group toggle remains. MUSICBRAINZ_ENABLED = "musicbrainz_enabled" # operator → node: enable/disable MUSICBRAINZ_ENABLED_ACK = "musicbrainz_enabled_ack" # node → this group: new enabled state MUSIC_META_REQ = "music_meta_req" # client → node: metadata for a path @@ -268,7 +268,7 @@ class MNP: # below, which is the operator's view of the whole node: this is scoped to # one group and answers any member of it, because the point is that a member # verifies another member's device *for themselves* rather than trusting the - # node's `sender_id` (Tier 2, docs/desktop-client-v1.md §4.8). + # node's `sender_id` (Tier 2, docs/MESHBAY_DESIGN.md §3.3). GROUP_ROSTER_REQ = "group_roster_req" GROUP_ROSTER_RESP = "group_roster_resp" ROSTER_READ = "roster_read" # operator → node: list pinned identities + members diff --git a/packages/meshbay-hub/src/meshbay_hub/api/users.py b/packages/meshbay-hub/src/meshbay_hub/api/users.py index 7b2fc8c..0394f53 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/users.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/users.py @@ -107,7 +107,7 @@ class RegisterRequest(BaseModel): email: str password: str | None = None # deprecated — legacy native clients auth_key: str | None = None # PBKDF2-derived, new clients - # Client-generated account recovery key (docs/auth-confirm.md §4.4). Pure + # Client-generated account recovery key (docs/MESHBAY_DESIGN.md §3.6). Pure # pass-through: appended to the verification e-mail so the user's mailbox # backs it up, then dropped. Never written to any table, never logged. recovery_key: str | None = None @@ -901,7 +901,7 @@ async def verify_email_change( # ── Passphrase change (Flow A) ────────────────────────────────────────────── # -# docs/auth-confirm.md §3. The passphrase derives two independent values on the +# docs/MESHBAY_DESIGN.md §3.6. The passphrase derives two independent values on the # client: auth_key (verified here) and bundle_key (AES-GCM key for the per-node # identity bundles, which live on nodes and never on the hub). The client # re-wraps those bundles from the old bundle_key to the new one on every @@ -1023,7 +1023,7 @@ async def change_password( # ── Passphrase reset (Flow B) ────────────────────────────────────────────── # -# docs/auth-confirm.md §4.2. An e-mail code re-opens hub login for someone who +# docs/MESHBAY_DESIGN.md §3.6. An e-mail code re-opens hub login for someone who # has lost their passphrase. It recovers no group content — that needs the # recovery key, which the client applies on its own after the reset. # reset-request never reveals whether an account exists. diff --git a/packages/meshbay-hub/src/meshbay_hub/config.py b/packages/meshbay-hub/src/meshbay_hub/config.py index 876583e..f2212de 100644 --- a/packages/meshbay-hub/src/meshbay_hub/config.py +++ b/packages/meshbay-hub/src/meshbay_hub/config.py @@ -75,7 +75,7 @@ class CaptchaConfig: # Set this when that check is turned off in the reCAPTCHA console, which is # what the desktop client needs: its page is served from `app://meshbay`, # so the hostname Google sees is not the hub's and never can be. See - # docs/captcha.md §6. + # docs/MESHBAY_DESIGN.md §7.7. allowed_hosts: list[str] = field(default_factory=list) # A solve from a page Google cannot attribute to a domain reports an empty # hostname — `app://meshbay` does, measured live, and so does any other diff --git a/packages/meshbay-hub/src/meshbay_hub/db/models.py b/packages/meshbay-hub/src/meshbay_hub/db/models.py index f1fff41..62e4a11 100644 --- a/packages/meshbay-hub/src/meshbay_hub/db/models.py +++ b/packages/meshbay-hub/src/meshbay_hub/db/models.py @@ -201,7 +201,7 @@ class UserDevice(Base): directory *others* read from, where a substituted key was handed the GEK by an honest member. * **It is not a node identity key.** Those are generated per node, pinned - there, and never leave that relationship (`docs/per-node-identity-v1.md`). + there, and never leave that relationship (`docs/MESHBAY_DESIGN.md` §3.2). A device holds one of these *plus* a different key per node, so nothing here correlates a person across operators. diff --git a/packages/meshbay-hub/src/meshbay_hub/mail.py b/packages/meshbay-hub/src/meshbay_hub/mail.py index aa6b74e..126ed45 100644 --- a/packages/meshbay-hub/src/meshbay_hub/mail.py +++ b/packages/meshbay-hub/src/meshbay_hub/mail.py @@ -315,7 +315,7 @@ def send_verification_code(to: str, code: str, recovery_key: str | None = None) """ Registration verification e-mail. When `recovery_key` is given it is appended to the body so the recipient's mailbox becomes the backup for it - (docs/auth-confirm.md §4.4). + (docs/MESHBAY_DESIGN.md §3.6). `recovery_key` is a **pass-through**: it is generated on the client, never stored anywhere on the hub, and never logged — only whether one was present. @@ -374,7 +374,7 @@ def send_email_change_code(to: str, code: str) -> None: def send_password_reset_code(to: str, code: str) -> None: """ - Passphrase-reset code (docs/auth-confirm.md §4.2). This only re-opens hub + Passphrase-reset code (docs/MESHBAY_DESIGN.md §3.6). This only re-opens hub login; it recovers no group content — that needs the recovery key. """ msg = EmailMessage() diff --git a/packages/meshbay-hub/src/meshbay_hub/static/apps.js b/packages/meshbay-hub/src/meshbay_hub/static/apps.js index 461bd57..32f2314 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/apps.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/apps.js @@ -70,7 +70,7 @@ const APPS = [ Component: MusicApp, Settings: MusicSettings }, { key: 'photo', icon: 'image', labelKey: 'group.tab_photos', Component: PhotosApp, Settings: PhotoSettings }, - // The reference implementation (docs/refactor-groups.md §4.1). `dev` keeps + // The reference implementation (docs/MESHBAY_DESIGN.md §9.4). `dev` keeps // it out of an operator's way; everything else about it is an ordinary // entry, which is the point. { key: 'helloworld', icon: 'chat', labelKey: 'group.tab_helloworld', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js b/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js index 04a00af..8958d44 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js @@ -301,7 +301,7 @@ export function RegisterPage() { setLoading(true); try { if (window.MeshBayKeys) { - // The account recovery key (docs/auth-confirm.md §4.3/§4.4): generated + // The account recovery key (docs/MESHBAY_DESIGN.md §3.6): generated // here, shown once on the next screen. When the user leaves "e-mail it" // checked, the mnemonic goes in the register body so the hub appends it // to the verification e-mail (and stores it nowhere); otherwise it is @@ -511,7 +511,7 @@ export function RegisterPage() { } -// ── Passphrase reset — Flow B (docs/auth-confirm.md §4) ───────────────────── +// ── Passphrase reset — Flow B (docs/MESHBAY_DESIGN.md §3.6) ───────────────── // // An e-mail code restores hub login. A recovery key, if the user still has one, // restores the per-node identities in the same step: the fan-out reads each diff --git a/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js b/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js index b732734..decf916 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js @@ -507,11 +507,11 @@ async function downloadEntry(transfers, transport, gek, entry) { * only alternative is to build the whole thing in memory — so that path is * offered but says what it costs first. * - * Lifted out of files-app.js (docs/photos.md §3) so photos-app.js's own + * Lifted out of files-app.js (docs/MESHBAY_DESIGN.md §9.9) so photos-app.js's own * "zip this album" button calls the same implementation rather than a * second one — nothing here is Files-specific once `entries`/`transport`/ * `gek`/`setError` are passed in, the same shared-context shape every app - * already receives (apps.md §2). + * already receives (docs/MESHBAY_DESIGN.md §9.2). */ async function downloadDirectory(transfers, transport, gek, entries, dir, { setError }) { if (!transport || !transport.connected) return; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/files-app.js b/packages/meshbay-hub/src/meshbay_hub/static/files-app.js index 7629459..3f4212e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/files-app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/files-app.js @@ -314,7 +314,7 @@ function FilesPanel({ } }, [currentPath]); - // The implementation lives in file-utils.js (docs/photos.md §3) so + // The implementation lives in file-utils.js (docs/MESHBAY_DESIGN.md §9.9) so // photos-app.js's own "zip this album" button can call the same code // rather than a second one. const downloadDirectory = useCallback(async (dir) => { diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-name.js b/packages/meshbay-hub/src/meshbay_hub/static/group-name.js index f109da7..f0ace6a 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-name.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-name.js @@ -8,7 +8,7 @@ import { sourceLabel } from './source-merge.js'; * Group names are unique only per owner account (the hub enforces that), so the * `@handle` is what tells two groups called "photos" apart. `owner` is the * owner's username for a local group, or the source hub for a federated one - * (decision 5 in ~/next/groupnames.md); the caller decides which. + * (docs/MESHBAY_DESIGN.md §7.3); the caller decides which. * * `inline` renders "name@owner" on one line, for places that cannot take a * block — a badge, a `confirm()` string built elsewhere. @@ -29,7 +29,7 @@ export function GroupName({ name, owner, inline = false }) { * The Search view merges a file several groups share into one entry, so the * badge under a card cannot always name a group. One source keeps naming it * and keeps linking to it; more than one becomes a count, and *which* one was - * picked is deliberately not shown (docs/refactoring-search.md §5.5). + * picked is deliberately not shown (docs/MESHBAY_DESIGN.md §9.11). * * `entries` is the whole unit — every episode of a show, every track of an * album — not the entry the card was drawn from; `sourceLabel` explains why. diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js index 205fce2..9a86f2a 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js @@ -154,10 +154,10 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // that). Null until the handshake ack arrives. const [scanSettings, setScanSettings] = useState(null); // TMDB on/off + whether a custom token is set, node-wide (not per-group) — - // docs/mediacenter.md §5.5. Null until the handshake ack arrives. + // docs/MESHBAY_DESIGN.md §9.7. Null until the handshake ack arrives. const [tmdbConfig, setTmdbConfig] = useState(null); // Which folders each app works over. One shape for all of them — a list, - // always, even where an app only wants one (docs/refactor-groups.md §1.6): + // always, even where an app only wants one (docs/MESHBAY_DESIGN.md §9.3): // Videos and Music were single values, which meant a library spread over two // drives could not be described at all. Empty means nothing configured yet, // which every app reads as "show nothing", never "the whole group index". @@ -171,7 +171,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // Whether members' cross-group Search lists this group. Not an app setting: // it is about the group as a whole, and it hides nothing from this page. const [searchListed, setSearchListed] = useState(true); - // MusicBrainz on/off (per-group) — docs/musicbay.md §3.2. + // MusicBrainz on/off (per-group) — docs/MESHBAY_DESIGN.md §9.8. const [musicbrainzConfig, setMusicbrainzConfig] = useState(null); // `op` rides through to the shell — 'replace', 'next' or 'append' // (docs/playlists.md §9.2). It has to be named here: a wrapper that takes @@ -320,7 +320,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, setDeviceReady(false); gekRef.current = null; if (!session.bundleKey) session.bundleKey = await _loadBundleKey(); - // Persisted (docs/auth-confirm.md §4.3) so a group joined in a later + // Persisted (docs/MESHBAY_DESIGN.md §3.6) so a group joined in a later // session still leaves a recovery-wrapped identity copy on its node. if (!session.recoveryKey) session.recoveryKey = await _loadRecoveryKey(); if (!session.bundleKey && window.MeshBayKeys) { @@ -669,7 +669,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // the only unambiguous answer once a group can have several writable roots. // Chat has no folder to browse, so it needs one picked for it, and this is // the same rule the node applies when a client names no root at all. It - // becomes an operator-chosen directory in phase 2 (refactor-groups.md §1.7). + // becomes an operator-chosen directory in phase 2 (docs/MESHBAY_DESIGN.md §9.6). // const writableRoots = useMemo( () => nodeRoots.filter((r) => r.writable && r.available !== false), @@ -735,7 +735,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // rather than written out. Naming them here would mean adding an app // required editing this file, which is the one thing the plugin // architecture is supposed to have removed — and the reference app - // (docs/refactor-groups.md §4.1) is what made the difference visible. + // (docs/MESHBAY_DESIGN.md §9.4) is what made the difference visible. const perAppDirectories = useMemo(() => { const out = {}; for (const app of APPS) out[`${app.key}Directories`] = appDirs(app.key); diff --git a/packages/meshbay-hub/src/meshbay_hub/static/helloworld-app-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/helloworld-app-settings.js index 9058199..be26fbc 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/helloworld-app-settings.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/helloworld-app-settings.js @@ -11,9 +11,10 @@ import { FolderPickerField } from './folder-tree.js'; * app's registry key by the page, and `ops.set_app_directories` stores the row * under that name without knowing what it is. * - * It takes the shared props and no others (`docs/apps.md` §3b), which is what - * `test_app_settings_plugin.py` checks of every pane — including this one, so - * the reference implementation is held to the contract it demonstrates. + * It takes the shared props and no others (`docs/MESHBAY_DESIGN.md` §9.2), + * which is what `test_app_settings_plugin.py` checks of every pane — including + * this one, so the reference implementation is held to the contract it + * demonstrates. */ function HelloWorldSettings({ roots, dirs, settings, saveDirectories }) { const { busy, msg, run } = useSaver(); diff --git a/packages/meshbay-hub/src/meshbay_hub/static/helloworld-app.js b/packages/meshbay-hub/src/meshbay_hub/static/helloworld-app.js index 01b4ed4..98ca8fa 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/helloworld-app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/helloworld-app.js @@ -7,7 +7,7 @@ import { formatSize } from './file-utils.js'; * The smallest application this platform can host, and the proof that adding * one costs nothing outside its own two files. * - * Everything else in `docs/refactor-groups.md` §3 is asserted by tests that + * Everything else in `docs/MESHBAY_DESIGN.md` §9.4 is asserted by tests that * read source. This is the other kind of evidence: an app nobody wrote a line * of plumbing for, that stores directories, appears as a tab, and lists files — * because the registry entry beside it is genuinely all there is. @@ -18,7 +18,7 @@ import { formatSize } from './file-utils.js'; * put a toy app in everybody's group; deleting the app would leave the claim * resting entirely on tests that read text. * - * It takes the standard props — see `docs/apps.md` §2 — and reads + * It takes the standard props — see `docs/MESHBAY_DESIGN.md` §9.2 — and reads * `helloworldDirectories`, which nothing on the node knows about by name: * `ops.set_app_directories` keys the row by whatever the app is called. */ diff --git a/packages/meshbay-hub/src/meshbay_hub/static/hub-client.js b/packages/meshbay-hub/src/meshbay_hub/static/hub-client.js index 81185a5..f467720 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/hub-client.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/hub-client.js @@ -136,7 +136,7 @@ async function clearAllCachedIndexes() { // so importing modules can update either field without this module handing // out a rebindable export. // -// `recoveryKey` (docs/auth-confirm.md §4.3) is the AES key that wraps the +// `recoveryKey` (docs/MESHBAY_DESIGN.md §3.6) is the AES key that wraps the // *recovery* copy of an identity bundle. In-memory only, and set only when the // user has just generated or entered the recovery secret (registration, or the // Flow B screen) — it cannot be re-derived from the passphrase. @@ -169,10 +169,11 @@ async function _loadKey(slot) { return val || null; } catch { return null; } } -// 'bk' = passphrase-derived bundle key; 'rk' = recovery key (docs/auth-confirm.md -// §4.3). Persisting 'rk' is what lets a group joined in a *later* session still -// get a recovery-wrapped identity copy, instead of only groups joined in the -// unbroken session that generated it. Cleared with everything else on sign-out. +// 'bk' = passphrase-derived bundle key; 'rk' = recovery key +// (docs/MESHBAY_DESIGN.md §3.6). Persisting 'rk' is what lets a group joined +// in a *later* session still get a recovery-wrapped identity copy, instead of +// only groups joined in the unbroken session that generated it. Cleared with +// everything else on sign-out. const _storeBundleKey = (key) => _storeKey('bk', key); const _loadBundleKey = () => _loadKey('bk'); const _storeRecoveryKey = (key) => _storeKey('rk', key); diff --git a/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js b/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js index edfa109..879f56f 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js @@ -191,7 +191,7 @@ async function deriveBundleKeys(password, username) { // ── Account recovery key ───────────────────────────────────────────────────── // -// docs/auth-confirm.md §4.3. A full-entropy secret the user keeps outside the +// docs/MESHBAY_DESIGN.md §3.6. A full-entropy secret the user keeps outside the // passphrase — in their password manager, or (step 3) e-mailed to them. It // wraps a *second* copy of every per-node identity bundle, so a forgotten // passphrase does not strand the account's group identities. @@ -323,7 +323,7 @@ async function registerUser(username, email, password, recoveryMnemonic, captcha const payload = { username, email, auth_key: authKey }; // The recovery mnemonic, when the user opted to have it e-mailed: the hub // appends it to the verification e-mail and stores it nowhere - // (docs/auth-confirm.md §4.4). Omitted when they chose to save it themselves. + // (docs/MESHBAY_DESIGN.md §3.6). Omitted when they chose to save it themselves. if (recoveryMnemonic) payload.recovery_key = recoveryMnemonic; // reCAPTCHA response, when the hub has a captcha configured. The widget lives // in RegisterPage (auth-page.js); this function just forwards its token. A @@ -347,7 +347,7 @@ async function registerUser(username, email, password, recoveryMnemonic, captcha * bundle goes to that node and nowhere else, and is what any other browser * fetches to become the same person there. When `recoveryKey` is supplied a * second copy wrapped under it rides along, so a forgotten passphrase does not - * strand this identity (docs/auth-confirm.md §4.3). + * strand this identity (docs/MESHBAY_DESIGN.md §3.6). */ async function generateNodeIdentity(bundleKey, recoveryKey) { const { skEdRaw, pkEdRaw, skXRaw, pkXRaw } = await generateKeypairs(); @@ -465,7 +465,7 @@ async function _bundleKeyPairFields(password, username) { window.MeshBayKeys = { registerUser, loginAndRecover, generateNodeIdentity, generateKeypairs, signBytes, deriveAuthKey, decryptBundleWithKey, encryptBundleWithKey, bundleVersion, - // Exposed for the passphrase change (docs/auth-confirm.md §3): re-wrapping a + // Exposed for the passphrase change (docs/MESHBAY_DESIGN.md §3.6): re-wrapping a // node's identity bundle needs the old key (a {v2,v1} pair, since an old // bundle may be v1) to read it and the new v2 key to write it back. deriveEncryptionKey, deriveEncryptionKeyV1, @@ -473,6 +473,6 @@ window.MeshBayKeys = { // `session.bundleKey` uses this, so `v2hkdf` is never the field one sign-in // path forgot (docs/playlists.md §3.4). deriveBundleKeys, bundleKeyPairFields: _bundleKeyPairFields, - // Account recovery key (docs/auth-confirm.md §4.3). + // Account recovery key (docs/MESHBAY_DESIGN.md §3.6). generateRecoveryKey, deriveRecoveryKey, }; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/music-app-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/music-app-settings.js index 8ce2ab6..a9a3c95 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/music-app-settings.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/music-app-settings.js @@ -8,7 +8,7 @@ import { FolderPickerField } from './folder-tree.js'; * * Same shape as Videos, minus a credential: MusicBrainz's read endpoints need * no API key, only a descriptive User-Agent, and that is one operator identity - * held node-wide rather than a per-group setting (docs/musicbay.md §3.2). + * held node-wide rather than a per-group setting (docs/MESHBAY_DESIGN.md §9.8). * * Several folders, for the same reason Videos has several: a music library * that lives on two drives had no way to say so. diff --git a/packages/meshbay-hub/src/meshbay_hub/static/music-app.js b/packages/meshbay-hub/src/meshbay_hub/static/music-app.js index baa3520..e0308cb 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/music-app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/music-app.js @@ -15,13 +15,13 @@ import * as P from './playlists.js'; // // An album-grid (MusicBrainz-enriched, when a track has no usable embedded // cover) or flat (tag/filename-only) browser for a group's audio files, per -// docs/musicbay.md. Grouping is by `artist`/`album` -- already resolved at -// index time from embedded tags, falling back to filename/folder parsing -// (indexer/enrich_audio.py) -- never guessed here. +// docs/MESHBAY_DESIGN.md §9.8. Grouping is by `artist`/`album` -- already +// resolved at index time from embedded tags, falling back to filename/folder +// parsing (indexer/enrich_audio.py) -- never guessed here. // // Unlike Videos, MusicBrainz is looked up only when a track has no embedded -// cover art at all (musicbay.md section 2.1's tiered trust: tags first, -// filename parsing second, MusicBrainz last) -- most of a real, well-ripped +// cover art at all (docs/MESHBAY_DESIGN.md §9.8's order of trust: tags +// first, filename parsing second, MusicBrainz last) -- most of a real, well-ripped // library already carries good artist/album text and often its own cover, so // this avoids a network round trip most tiles never need. Playback never // touches this file: clicking a track calls the `onPlayQueue` prop the shell @@ -52,7 +52,7 @@ function foldKey(s) { } // Same shape as video-app.js's underVideoRoot: an unset root means "show -// nothing" (docs/musicbay.md's amended §2.1 — the node itself runs no +// nothing" (docs/MESHBAY_DESIGN.md §9.8 — the node itself runs no // tag/cover enrichment for this group before a root is chosen either, // daemon.py's _enrich_new_audio_entries), not "the whole shared tree" — // falling back to that would just show files nothing has enriched. @@ -212,17 +212,16 @@ function useMusicMeta(transportRef, fileId, active) { } // A drawn CD standing in for a cover nothing supplied one for -- most tiles -// in a real, older/well-ripped library land here (musicbay.md's own -// measurement: ~11% embedded art, ~26% once sibling image files are counted -// too), so this is the *default* look of the grid, not a rare fallback, and -// needed to read as a deliberate piece of art rather than a broken image. -// A flat single-color icon (the first version of this) looked exactly like -// "missing", not "no cover" -- an actual disc, with the iridescent sheen a -// real CD's data side has, reads as intentional at a glance. Genuinely -// unique gradient ids: a `` id is a plain DOM id, and a grid -// full of these renders many instances at once -- reusing one literal id -// would leave every disc after the first pointing at whichever def the -// browser resolves first. +// in a real, older/well-ripped library land here (measured: ~11% embedded +// art, ~26% once sibling image files are counted too), so this is the +// *default* look of the grid, not a rare fallback, and needed to read as a +// deliberate piece of art rather than a broken image. A flat single-color icon +// (the first version of this) looked exactly like "missing", not "no cover" -- +// an actual disc, with the iridescent sheen a real CD's data side has, reads +// as intentional at a glance. Genuinely unique gradient ids: a +// `` id is a plain DOM id, and a grid full of these renders +// many instances at once -- reusing one literal id would leave every disc +// after the first pointing at whichever def the browser resolves first. let _discIdSeq = 0; function DiscPlaceholder({ cls }) { @@ -517,8 +516,8 @@ function FlatArtistFolder({ artist, onPlayQueue, onMenu }) { ${open && html`
${/* A real artist folder with no album layer at all is common here - -- a pile of loose singles, not one release (musicbay.md - section 2.1's "flat per-artist folder" case). Nesting them + -- a pile of loose singles, not one release (a flat + per-artist folder, docs/MESHBAY_DESIGN.md §9.8). Nesting them one more level behind their own always-empty "Unknown album" row was exactly the friction reported live: an extra, pointless expand before reaching a track that's playable @@ -748,7 +747,7 @@ function MusicApp({ } // foldKey rides along for the Search page's merge unit keys -// (docs/refactoring-search.md §5.2). An album's *display* strings are the +// (docs/MESHBAY_DESIGN.md §9.11). An album's *display* strings are the // first-seen spelling, and which group is seen first is the order its index // happened to arrive in — so keying a unit on them would let the chosen source // change between page loads. The folded key is the one grouping actually used, diff --git a/packages/meshbay-hub/src/meshbay_hub/static/music-player.js b/packages/meshbay-hub/src/meshbay_hub/static/music-player.js index c3a2868..7c81f4e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/music-player.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/music-player.js @@ -8,7 +8,7 @@ import { useStickyBand } from './sticky.js'; import { CHUNK_SIZE, pipelinedDownload } from './file-utils.js'; /** - * The Music app's persistent player bar (docs/musicbay.md §2.3, §7.2). + * The Music app's persistent player bar (docs/MESHBAY_DESIGN.md §9.8). * * Owned and rendered by app.js — the router's parent — so playback survives * navigating between groups, search, and other pages. Both group-page.js @@ -23,11 +23,12 @@ import { CHUNK_SIZE, pipelinedDownload } from './file-utils.js'; * track is a few megabytes, so it is downloaded and decrypted once through * the same chunk pipeline Files already uses (file-utils.js's * pipelinedDownload), then played from a blob URL — the deliberate - * simplification recorded in musicbay.md §2.2. WMA and Musepack are the one - * exception (`NEEDS_TRANSCODE_RE` below): neither decodes in a browser's - *