revert(amethyst): drop GitStatusIndex auth change; defer to separate proposal

The isAuthoritative guard added to GitStatusIndex read the repository owner
from the status event's own `a` tag (GitStatusEvent.repositoryAddress() ->
first a tag, no kind filter, no cross-check against the target's repo). That
value is attacker-controlled: a forged kind:1632 carrying
`["a", "30617:<attacker-pubkey>:anything"]` makes `status.pubKey ==
repoAddress.pubKeyHex` pass, so the spoof it meant to block still succeeds.

It also regressed reads: reduceLatestByTarget only re-runs on a new
kind 1630-1633, so a status dropped while the 30617 was uncached stayed
dropped, leaving genuinely-closed items in the Open tab with wrong counts.

Keep this series focused on the quartz + cli NIP-34 parity work. The Android
status-authorization hardening (resolve the repo from the target item, load
the cached 30617, authorize against repo.pubKey + maintainers() + the target
author, and re-reduce when a 30617 arrives) will land as its own proposal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UKMaNoK5M2PQKCAxhxWzPr
This commit is contained in:
Claude
2026-07-21 03:38:20 +00:00
parent 5f5356c0fe
commit 062ab0f012

View File

@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.model
import com.vitorpamplona.amethyst.model.LocalCache.observeEvents
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
import com.vitorpamplona.quartz.nip34Git.status.GitStatusAppliedEvent
import com.vitorpamplona.quartz.nip34Git.status.GitStatusClosedEvent
import com.vitorpamplona.quartz.nip34Git.status.GitStatusEvent
@@ -72,7 +71,6 @@ object GitStatusIndex {
val latest = HashMap<HexKey, GitStatusEvent>()
for (event in events) {
val target = event.rootEventId() ?: continue
if (!isAuthoritative(event, target)) continue
val current = latest[target]
if (current == null || event.createdAt > current.createdAt) {
latest[target] = event
@@ -81,29 +79,6 @@ object GitStatusIndex {
return latest
}
/**
* NIP-34: only the repository owner, a declared maintainer, or the target
* item's own author may set its status — "the newest Status from the root
* author or a maintainer is valid". Without this filter any pubkey could
* publish a kind-1632 and make someone else's issue render closed.
*
* The owner is read from the status's own `a` tag (so it holds even before
* the announcement is cached); the maintainer set needs the cached kind-30617
* (falling back to owner/author-only until it arrives — the safe default is to
* treat an unverifiable status as absent, i.e. the item stays open).
*/
private fun isAuthoritative(
status: GitStatusEvent,
targetId: HexKey,
): Boolean {
status.repositoryAddress()?.let { repoAddress ->
if (status.pubKey == repoAddress.pubKeyHex) return true
val repo = LocalCache.getAddressableNoteIfExists(repoAddress)?.event as? GitRepositoryEvent
if (repo != null && status.pubKey in repo.maintainers()) return true
}
return status.pubKey == LocalCache.getNoteIfExists(targetId)?.event?.pubKey
}
/**
* Whether the latest status for [targetId] marks it as closed (kind 1632)
* or applied/resolved/merged (kind 1631). Items with no status event, or