feat(quartz): expose pending outbox events per relay for auth context

Add INostrClient.activeOutboxEvents(url) (backed by
PoolEventOutbox.activeOutboxEventsFor) returning the full events still
pending delivery to a relay, not just their ids like activeOutboxCache.
This lets a host explain *why* a relay is being authenticated with —
e.g. a pending kind-1059 gift wrap means we're sending a DM to its
recipient — by inspecting kind/tags. Combined with the existing
activeRequests(url) filters, it is the generic challenge context the
NIP-42 decision hook needs. Updates the INostrClient test fakes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
This commit is contained in:
Claude
2026-07-10 22:40:09 +00:00
parent 4bd73596e3
commit 38b2e65362
6 changed files with 30 additions and 0 deletions
@@ -127,6 +127,8 @@ class CoordinatorPipelineTest {
override fun activeCounts(url: NormalizedRelayUrl): Map<String, List<Filter>> = emptyMap()
override fun activeOutboxCache(url: NormalizedRelayUrl): Set<HexKey> = emptySet()
override fun activeOutboxEvents(url: NormalizedRelayUrl): List<Event> = emptyList()
}
private fun createCoordinator(
@@ -104,6 +104,9 @@ interface INostrClient : AutoCloseable {
fun activeCounts(url: NormalizedRelayUrl): Map<String, List<Filter>>
fun activeOutboxCache(url: NormalizedRelayUrl): Set<HexKey>
/** The events still pending delivery to [url] (full events, not just ids). */
fun activeOutboxEvents(url: NormalizedRelayUrl): List<Event>
}
class EmptyNostrClient : INostrClient {
@@ -158,5 +161,7 @@ class EmptyNostrClient : INostrClient {
override fun activeOutboxCache(url: NormalizedRelayUrl): Set<HexKey> = emptySet()
override fun activeOutboxEvents(url: NormalizedRelayUrl): List<Event> = emptyList()
override fun close() {}
}
@@ -369,6 +369,8 @@ class NostrClient(
override fun activeOutboxCache(url: NormalizedRelayUrl): Set<HexKey> = eventOutbox.activeOutboxCacheFor(url)
override fun activeOutboxEvents(url: NormalizedRelayUrl): List<Event> = eventOutbox.activeOutboxEventsFor(url)
override fun pendingPublishRelaysFor(eventId: HexKey): Set<NormalizedRelayUrl>? = eventOutbox.pendingRelaysFor(eventId)
override fun getReqFiltersOrNull(subId: String): Map<NormalizedRelayUrl, List<Filter>>? = activeRequests.getSubscriptionFiltersOrNull(subId)
@@ -96,6 +96,21 @@ class PoolEventOutbox {
return myEvents
}
/**
* The events still pending delivery to [url]. Unlike [activeOutboxCacheFor] (ids only), this
* returns the full events so callers can inspect kind/tags — e.g. to explain *why* a relay is
* being authenticated with (a pending gift wrap => sending a DM to its recipient).
*/
fun activeOutboxEventsFor(url: NormalizedRelayUrl): List<Event> {
val myEvents = mutableListOf<Event>()
eventOutbox.forEach { (_, outboxCache) ->
if (url in outboxCache.relaysRemaining) {
myEvents.add(outboxCache.event)
}
}
return myEvents
}
/**
* Returns the relays that have NOT yet acknowledged [eventId] with an OK, or
* null if the event is not currently tracked (never sent or already fully done).
@@ -104,6 +104,8 @@ private class TrackingNostrClient : INostrClient {
override fun activeOutboxCache(url: NormalizedRelayUrl): Set<String> = emptySet()
override fun activeOutboxEvents(url: NormalizedRelayUrl): List<Event> = emptyList()
override fun close() {}
}
@@ -431,6 +431,8 @@ private class CapturingNostrClient : INostrClient {
override fun activeOutboxCache(url: NormalizedRelayUrl): Set<HexKey> = emptySet()
override fun activeOutboxEvents(url: NormalizedRelayUrl): List<Event> = emptyList()
override fun close() {}
}
@@ -490,5 +492,7 @@ private class CountingNostrClient(
override fun activeOutboxCache(url: NormalizedRelayUrl): Set<HexKey> = emptySet()
override fun activeOutboxEvents(url: NormalizedRelayUrl): List<Event> = emptyList()
override fun close() {}
}