fix(archive): "Load archive" served the wrong article — gate on identity#185
Merged
Merged
Conversation
The reconstruct handler asked relays for {kinds:[30023], '#r':[url]} and
rendered the newest hit, with no authors filter and no identity check.
But `#r` is not an identity index: buildArticleEvent co-emits an indexed
`r` for responds-to targets and for the first 25 OUTBOUND LINKS of every
article, so an event matches merely by LINKING to the probed URL. Every
article you publish poisons the `#r` index for up to 25 URLs it links to,
and a cross-linked corpus maximizes it.
The worst case needs no race: with no published capture of X, a linking
article is the ONLY candidate — it wins 100% of the time, and altCount 0
means the "N relay versions found" tell never fires. loadArchivedArticle
pins url: state.article.url, so the foreign body renders under X's
address and claims made on it key to X. NIP_DRAFT.md already mandated the
disambiguation this handler never did.
articleAnswersTo (url-identity.js, pure + tested): an article answers to
its identity URL (the first `r`, per the reconstruct invariant) and its
capture_url mirror, and to nothing else. The handler reconstructs first,
keeps only events that ARE the requested URL, and reports found: false
when none survive. Probing from a mirror address still resolves.
Normalization is applied only to an article's own addresses, on both
sides. Normalizing the probe against the raw `#r` set instead would widen
the over-match — the identity check and the normalization are one change.
No wire-format change. 1737 tests green; lint 0 errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 17, 2026
…L entries kept from both sides
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the bug you hit: "Once, I went ahead and clicked 'Load Archive' and it loaded a completely different article."
It reproduces, it needs no race, and the mechanism is entirely ours.
tests/archive-identity.test.mjspins it.The mechanism
xray:archive:reconstructasked relays for{kinds:[30023], '#r':[url], limit:20}, sorted bycreated_atdesc, tookevents[0], and reconstructed it — no authors filter, no identity check.But
#ris not an identity index.buildArticleEventco-emits an indexedrtag forresponds-totargets and for the first 25 outbound links of every article:So every article you publish poisons the
#rindex for up to 25 URLs it merely links to. A cross-linked corpus — the whole point of the tool — maximizes the over-match.The worst case needs no concurrency. If you never published a capture of X, a linking article is the only candidate → it wins 100% of the time, and
altCountis 0, so the "N relay versions found" tell never fires. It reads as an ordinary archive offer.And the swap is disguised.
loadArchivedArticlepinsurl: state.article.url, so the foreign article's title and body render under your URL — and any claims or comments you make key to X. Foreign content gets annotated as if it were your article.docs/NIP_DRAFT.mdalready mandated this disambiguation. The handler did none of it.The fix
articleAnswersToinurl-identity.js— pure, tested, and the natural home (that module is already about mirror-vs-original identity). An article answers to exactly two addresses:r, per thereconstructArticleFromEventinvariantcapture_urlmirror — so probing from an archive.is address still resolvesEverything else in the
rset is a reference to someone else's article. The handler now reconstructs first, keeps only events that ARE the requested URL, and reportsfound: falsewhen none survive.The trap worth knowing about
Link
rtargets are normalized (content-extractor.js) while the primaryrandcapture-urlare emitted raw, and the probe URL is raw. Normalizing the probe against the raw#rset — the obvious "fix" — would have made this fire MORE often. Normalization here is applied only to an article's own addresses, on both sides. The identity check and the normalization are one change and must not be split.Verification
The repro test runs the real builder and asserts the poisoned
rtag IS present (so the relay filter really would return the event) before asserting the gate rejects it — the bug is pinned, not just the fix. Both directions covered: the linking article is rejected for the victim URL and still findable as itself; a genuine capture still resolves.npm test— 1737 pass, 0 failnpm run build— clean ·npm run lint— 0 errorsCompanion PR (banner flood) follows separately. Until both land, Options → Advanced → Archive banner →
richersilences the false banners with no code.🤖 Generated with Claude Code