diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-13 15:40:34 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-13 15:40:34 +0200 |
| commit | d917bb61e42336c38782b22da604d7ca923d484a (patch) | |
| tree | 3ad99072f02d621ca4a54f4fcce65a2c530c4b79 /packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py | |
| parent | c5fff4ce8366b08669c0c8b6d30b99b94b9fefca (diff) | |
| download | meshbay-d917bb61e42336c38782b22da604d7ca923d484a.tar.gz | |
fix(node): an uploaded file records who sent it
`_register_uploader` walked the index for the entry it had just written, at a
moment when no such entry can exist: the file was a `.part` until the rename on
the line above, which is not indexable, and the watchdog that will index it
debounces for two seconds and then hashes. The walk matched nothing, silently,
so every uploaded file in every group was owned by nobody — and `file_delete`
refuses a caller with no admin authority when the entry records no uploader, so
a member could not delete what they had just sent. MESHBAY_DESIGN.md §5.4
grants that to any non-revoked device of the uploading account.
The record is now written when the last chunk lands (`indexer.record_upload`)
and the entry is stamped from it in `_hash_or_cached`, the one funnel every
entry passes through — initial scan, watchdog, reconcile and replug alike. It
lives in the index cache rather than on the entry alone, because the index is
rebuilt from disk at every start and an owner the node forgets on restart is a
right quietly taken away. It is validated against a live `stat()`, so whatever
later occupies that path inherits nothing; and `_rescan_root`'s carry-over no
longer copies over it, or memory would beat the durable record.
§5.4 also claimed ownership was *provable* — a transcript the uploader signs,
stored with the entry. No such signature has ever existed; `meshbay:upload:v1`
in the code is the groupbox purpose that seals the envelope. The section now
states what the code does, and the transcript is an open item in §15.3.
`test_upload_attribution.py` drives the real handler and a real indexer across
that seam. Against the previous source its two positive cases fail on the
property, not on a missing method — an upload, then a rebuild from disk, then
a different file at the same path inheriting nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMxEQadpzPkYLFf5CYKhpW
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py | 25 |
1 files changed, 14 insertions, 11 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py index 29d6e7f..8df88d8 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py @@ -4914,26 +4914,29 @@ class WebRTCPeerSession: log.info("Upload complete: %s (%d chunks, %d bytes)", stored_name, total_chunks, state.bytes) self._audit("file_upload", f"{rel_dir}/{stored_name}") - self._register_uploader(ctx, rel_dir, stored_name) + self._register_uploader(ctx, final_path) - def _register_uploader(self, ctx: dict, rel_dir: str, filename: str) -> None: + def _register_uploader(self, ctx: dict, file_path: Path) -> None: """ - Tag the index entry with the uploader's identity after upload completes. + Record who sent this file, for the index entry that does not exist yet. - The key recorded here is the one this node pinned, not the one the token + The key recorded is the one this node pinned, not the one the token carried. `pk_user` was a hub-chosen claim, and it decided who could later delete the file: a hub issuing a token naming its own key could delete anyone's uploads on any node. Deletion is supposed to be authorized by the node, and this closes the last place where it was not. + + **The entry is not here to be tagged.** This used to walk `ctx["index"]` + for the name just written and set the fields on it; at this point the + watchdog has not fired (it debounces for two seconds and then hashes) + and the file was a `.part` until the line above, which is not indexable + — so the walk matched nothing, every time, and said nothing about it. + The indexer stamps the entry from this record when it creates it. """ - idx = ctx.get("index") - if not idx: + record = ctx.get("record_upload") + if record is None: return - for entry in idx.entries: - if entry.name == filename and entry.path == rel_dir: - entry.uploader_id = self._user_id - entry.uploader_pk = self._pinned_pk - return + self._spawn(record(file_path, self._user_id or "", self._pinned_pk or "")) def _do_file_delete(self, msg: dict) -> None: ctx = self._group_ctx() |