Skip to content

Publish the data archive under node_ref, not node_id - #609

Merged
Babissimo merged 3 commits into
mainfrom
archive-publishes-node-ref
Sep 24, 2026
Merged

Babissimo merged 3 commits into
mainfrom
archive-publishes-node-ref

Conversation

@Babissimo

@Babissimo Babissimo commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

/api/data/archive was the one public route still handing out the private node_id: in every listed key, in its filter, in the download path and in the downloaded body. The Data Explorer on app.retina.fm showed those ids as its file tree. Its node picker is built from /api/radar/nodes, which is keyed on node_ref, so on prod every node appeared twice (16 known nodes, 32 rows on the timeline).

What changes

The store stays keyed on node_id (re-keying it is phase 2 of the archive programme plan in claude-shared). The route translates at the publication boundary, as every other public feed does:

Before After
Listed key …/node_id=ret…/part-….parquet …/node_ref=nde…/part-….parquet (bare <ref> for the legacy layout)
Filter ?node_id= ?node_ref=
Download stored key published key, resolved back to the stored one
Body "node_id": "ret…" "node_ref": "nde…"
Node with no published identity listed not listed
  • Only published keys download. A stored node_id= key now 404s, so the route no longer confirms a guessed id.
  • A filter naming a private node's ref gets the same answer as an unknown ref, total included.
  • The Data Explorer sends node_ref, reads node_ref from the preview, and its search box says "node ref".
  • The admin Storage page reads the same listing. It maps refs back to ids through the admin-only node-refs map, so its file table matches the per-node table beside it.

A second commit fixes the reverse lookup for mirrored nodes. These are nodes relayed from another environment, with no registry row here, whose ref arrives with their detections (#373). public_identity published them under that ref, but id_for_identity could not resolve it back. That left their archive files listed but impossible to download or filter, and left /api/radar/analytics/{node_ref} and the custody routes unable to answer for them.

#610 (the owner pages: no Node ID column, and names that fall back to the ref rather than the id) is merged into this branch as its third commit, so both ship in one deploy.

Breaking for scripts: anything holding an old node_id= key or passing ?node_id= gets 404s or an unscoped listing. The route is public and appears in the API reference.

Verification

  • New route tests in test_publication.py cover listing, filtering, both key layouts, download, stored-key refusal and private nodes. All of them fail against the old route.
  • Full backend suite (-m "not external" -n 4 --dist worksteal): 5078 passed. Dashboard: 1394 passed (both on the combined head, rebased on current main). Typecheck, eslint and pre-commit are clean.
  • Tested locally against the real app on a seeded SQLite DB, with a public node, a private node, a synthetic node and one with no registry row. The listing showed only the ref and the synthetic id. A stored key, a private node's key and the id as a filter each got 404 or an empty listing. In the browser, the Data Explorer file tree, the preview's "Reported node" and a ?node= scoped listing all used the ref, and no ret… id appeared on the page.

🤖 Generated with Claude Code

@claude

This comment has been minimized.

@Babissimo

Copy link
Copy Markdown
Contributor Author
  1. Fixed in the amended commit: the download now names the node from the key it has already resolved, and falls back to the body's own field only for a key with no node segment. test_an_empty_file_is_named_by_its_key covers the zero-row case.
  2. Left as it is. _key_node_id reads stored keys and mirrors storage._partition_value. The two new helpers deliberately accept different spellings: _published_key rewrites only node_id=, and _stored_key accepts only node_ref=, so a stored key is never taken as a published one. A shared parser would have to carry that asymmetry as a parameter. Phase 2 of the archive plan replaces this layout anyway.

@claude

This comment has been minimized.

@claude

This comment has been minimized.

@Babissimo
Babissimo force-pushed the archive-publishes-node-ref branch from 796cdd8 to a332b89 Compare September 24, 2026 15:34
@claude

This comment has been minimized.

@claude

This comment has been minimized.

@Babissimo
Babissimo force-pushed the archive-publishes-node-ref branch from 779dfd4 to da8f2ad Compare September 24, 2026 15:44
@claude

This comment has been minimized.

@Babissimo
Babissimo force-pushed the archive-publishes-node-ref branch from da8f2ad to ddd3a28 Compare September 24, 2026 16:04
@Babissimo

Copy link
Copy Markdown
Contributor Author
  • StoragePage node segment: fixed in the archive commit. The Storage page now reads the segment before the filename, as the route and the Data Explorer do, and its test uses a key with an extra leading segment, which fails against parts[3].
  • _mirrored_id scan: accepted. It runs only when the registry map has no entry for a ref, and it walks the connected nodes (tens) once, which costs less than serialising the response it guards.
  • Nodes with neither a name nor a ref: as intended. Such a node is on no public surface, and the console doesn't fall back to the private id.

@claude

This comment has been minimized.

@Babissimo
Babissimo force-pushed the archive-publishes-node-ref branch from ddd3a28 to 4fb47b3 Compare September 24, 2026 16:09
@claude

This comment has been minimized.

Babissimo and others added 3 commits September 24, 2026 17:21
/api/data/archive was the one public route still handing out the private
node_id: in every listed key (year=/month=/day=/node_id=<id>/), in its
node_id filter, in the download path and as the downloaded body's node_id
field. The Data Explorer on app.retina.fm showed those ids as its file
tree, and because its node picker is built from /api/radar/nodes, which is
keyed on node_ref, every node appeared twice (16 known nodes, 32 on the
timeline on prod).

The store stays keyed on node_id; re-keying it is phase 2 of the archive
programme plan. The route translates at the publication boundary instead,
the same place every other feed does: listed keys carry node_ref=<ref>
(bare <ref> for the legacy layout), the filter is node_ref, a download
resolves the ref back to the stored key, and the body's node_id becomes
node_ref. A node with no published identity is not listed. Only published
keys download: a stored node_id= key now 404s, which also stops the route
confirming a guessed id. A filter naming a private node's ref answers like
an unknown one, total included.

The admin Storage page reads the same listing, so it maps the refs back to
node_ids through the admin-only node-refs map, keeping its file table in
the id space of the per-node table beside it.

Breaks any script holding an old node_id key or passing node_id=; the
route is public and documented in the API reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
public_identity names a mirrored node, one whose detections arrive from
another environment and which has no registry row here, by the ref that
environment sends with them (#373). id_for_identity, the inverse every
public path parameter resolves through, predates that and reads only the
registry, so a mirrored node was published under a ref that no route
would take back.

It showed first in the archive: a mirrored node's files are listed under
its ref, but downloading one or filtering by the ref found nothing. The
same gap sits under /api/radar/analytics/{node_ref} and the custody
routes, which this fixes too.

The reverse scans the connected nodes, where a mirrored ref lives, and
counts a match only if owner_identity names that node by the ref, so a
registry row that outranks a mirrored ref on the way out does here as
well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owner pages on app.retina.fm still put the private node_id on screen.
Connect your node listed it in a Node ID column beside the Node ref one,
and /api/auth/me/nodes filled an unnamed node's name with its node_id,
which Tunnel and My Nodes then displayed; they also fell back to the id
themselves when a node had no ref.

The owner route now names a node through public_name, as the public
listing does: a node with no name of its own, or one whose name is a node
id, is called by its ref. The route keeps node_id for the owner's own
calls (release, location privacy, the node's <id>.retnode.com site), but
no page displays it: the Node ID column goes, Tunnel searches and labels
by name or ref, and a node with neither shows a dash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Babissimo
Babissimo force-pushed the archive-publishes-node-ref branch from 4fb47b3 to 7431f57 Compare September 24, 2026 16:22
@Babissimo

Copy link
Copy Markdown
Contributor Author

Both fixed, each folded into the commit it belongs to:

  1. _mirrored_id now snapshots connected_nodes under connected_nodes_lock and iterates outside it, as node_sites.py does. No caller of id_for_identity holds that lock.
  2. A key with no node segment now checks the node its body names against the private set before responding, and 404s if the node is private. test_a_file_with_no_node_in_its_key_is_withheld_if_its_body_names_a_private_node covers both outcomes and fails without the check.

@claude

claude Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Claude finished @Babissimo's task in 3m 49s —— View job


Code Review: PR #609 — Publish the data archive under node_ref, not node_id

  • Gather context (PR body, prior review thread, CLAUDE.md)
  • Review the diff against origin/main (all 3 commits: cfd26c3, 4a7a436, 7431f57)
  • Post review findings

Summary

This re-review checked the current HEAD against the two issues raised in the prior automated review round (unlocked _mirrored_id scan, and the download route's body-fallback path skipping the privacy check). Both are already resolved in the code currently on the branch:

_mirrored_id locking — backend/services/node_refs.py:219-220 takes the snapshot under state.connected_nodes_lock before iterating:

with state.connected_nodes_lock:
    snapshot = list(state.connected_nodes.items())
for node_id, known in snapshot:
    ...

This matches the snapshot-under-lock pattern used elsewhere (node_sites.py, analytics_refresh.py, frame_processor.py). No unlocked iteration over connected_nodes remains.

Download route's body-fallback privacy check — backend/routes/archive.py:131-140 checks the resolved node_id (key segment or body fallback) against private before building the response:

node_id = stored[1] or body_id
if node_id in private:
    raise HTTPException(status_code=404, detail="Archive file not found")
data["node_ref"] = public_identity(node_id)

and this exact path is covered by test_a_file_with_no_node_in_its_key_is_withheld_if_its_body_names_a_private_node in test_publication.py, which asserts a 404 for a path-less key whose body names a private node. The route's stated invariant ("no key, parameter or body it hands out names the private id") holds for this case.

Rest of the diff

Went through the remaining changed surface (auth.py's my_nodes/public_name wiring, the OverviewPage/OnboardingPage/TunnelLinkPage renames, StoragePage's node-segment parsing and ref→id mapping, and the mechanical node_id→node_ref rename across the Data Explorer) — nothing new stood out. The node_ids closure variable used in public_name(cfg.get("name"), ref, node_ids) (auth.py:427) resolves correctly to the function-scoped list from get_user_nodes, and ref is correctly scoped in each of OverviewPage's three render blocks that use it.

Test coverage for this diff (test_publication.py, test_node_refs.py, test_auth_routes.py) exercises listing, filtering, both key layouts, download, stored-key refusal, private nodes, mirrored nodes in both directions, and the name-fallback behavior — consistent with what the PR description claims.

Confirmed clean

No outstanding issues found. Both concerns from the previous review round are already fixed and tested on this branch.

Branch: archive-publishes-node-ref

@Babissimo
Babissimo merged commit aea23bd into main Sep 24, 2026
21 checks passed
@Babissimo
Babissimo deleted the archive-publishes-node-ref branch September 24, 2026 16:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant