Relabel private Nostr envelopes honestly

This commit is contained in:
jack
2026-07-26 05:41:28 +02:00
committed by jack
parent 6aeef1c00e
commit 1c5dbb677b
28 changed files with 1161 additions and 475 deletions
+25 -18
View File
@@ -106,10 +106,11 @@ private extension NostrRelayManagerDependencies {
@MainActor
final class NostrRelayManager: ObservableObject {
static let shared = NostrRelayManager()
// Track gift-wraps (kind 1059) we initiated so we can log OK acks at info
private(set) static var pendingGiftWrapIDs = Set<String>()
static func registerPendingGiftWrap(id: String) {
pendingGiftWrapIDs.insert(id)
// Track BitChat private envelopes we initiated so relay rejections can be
// reported without misclassifying ordinary public-event failures.
private(set) static var pendingPrivateEnvelopeIDs = Set<String>()
static func registerPendingPrivateEnvelope(id: String) {
pendingPrivateEnvelopeIDs.insert(id)
}
struct Relay: Identifiable {
@@ -124,7 +125,7 @@ final class NostrRelayManager: ObservableObject {
var nextReconnectTime: Date?
}
// Default relays carry NIP-17 gift wraps, so avoid relays known to reject kind 1059.
// Default relays carry persisted BitChat private-envelope events.
private static let defaultRelays = [
"wss://relay.damus.io",
"wss://nos.lol",
@@ -137,7 +138,7 @@ final class NostrRelayManager: ObservableObject {
@Published private(set) var relays: [Relay] = []
@Published private(set) var isConnected = false
/// Whether a relay that carries private messages is connected. DMs
/// target the default (gift-wrap-capable) relay set, so a connected
/// target the default private-envelope relay set, so a connected
/// geohash/custom relay alone must not count sends would still queue.
@Published private(set) var isDMRelayConnected = false
@@ -217,7 +218,7 @@ final class NostrRelayManager: ObservableObject {
private var messageQueue: [PendingSend] = []
private let messageQueueLock = NSLock()
/// Non-queued sends whose callers require relay durability. A WebSocket
/// write only proves bytes left this process; NIP-20 OK is the relay's
/// write only proves bytes left this process; NIP-01 `OK` is the relay's
/// accept/reject acknowledgment.
private struct ConfirmedSendState {
let token: UUID
@@ -356,7 +357,7 @@ final class NostrRelayManager: ObservableObject {
duplicateInboundEventDropCount = 0
duplicateInboundEventDropCountBySubscription.removeAll()
inboundEventLogCount = 0
Self.pendingGiftWrapIDs.removeAll()
Self.pendingPrivateEnvelopeIDs.removeAll()
confirmedSends.removeAll()
messageQueueLock.lock()
@@ -433,8 +434,8 @@ final class NostrRelayManager: ObservableObject {
}
/// Attempts an event only on currently connected target relays and
/// reports whether at least one relay explicitly accepted it via NIP-20
/// OK. A successful WebSocket write alone is not durable acceptance.
/// reports whether at least one relay explicitly accepted it via NIP-01
/// `OK`. A successful WebSocket write alone is not durable acceptance.
/// Unlike `sendEvent`, this never enters the process-local pending queue;
/// callers use it when success unlocks durable state or user-visible
/// delivery progress.
@@ -1139,7 +1140,7 @@ final class NostrRelayManager: ObservableObject {
guard shouldDeliverInboundEvent(subscriptionID: subId, eventID: event.id) else {
return
}
if event.kind != 1059 {
if !NostrProtocol.acceptedPrivateEnvelopeKinds.contains(event.kind) {
// Per-event logging floods dev builds in busy geohashes; sample it.
inboundEventLogCount += 1
if inboundEventLogCount == 1 || inboundEventLogCount.isMultiple(of: TransportConfig.nostrInboundEventLogInterval) {
@@ -1168,11 +1169,11 @@ final class NostrRelayManager: ObservableObject {
case .ok(let eventId, let success, let reason):
resolveConfirmedSend(eventID: eventId, relayURL: relayUrl, accepted: success)
if success {
_ = Self.pendingGiftWrapIDs.remove(eventId)
_ = Self.pendingPrivateEnvelopeIDs.remove(eventId)
SecureLogger.debug("✅ Accepted id=\(eventId.prefix(16))… relay=\(relayUrl)", category: .session)
} else {
let isGiftWrap = Self.pendingGiftWrapIDs.remove(eventId) != nil
if isGiftWrap {
let isPrivateEnvelope = Self.pendingPrivateEnvelopeIDs.remove(eventId) != nil
if isPrivateEnvelope {
SecureLogger.warning("📮 Rejected id=\(eventId.prefix(16))… relay=\(relayUrl) reason=\(reason)", category: .session)
} else {
SecureLogger.error("📮 Rejected id=\(eventId.prefix(16))… relay=\(relayUrl) reason=\(reason)", category: .session)
@@ -1610,13 +1611,19 @@ struct NostrFilter: Encodable {
}
}
// For NIP-17 gift wraps
static func giftWrapsFor(pubkey: String, since: Date? = nil) -> NostrFilter {
// BitChat private envelopes, plus compatibility legacy envelopes emitted
// during the bounded migration and stored by older releases as kind 1059.
static func privateEnvelopesFor(pubkey: String, since: Date? = nil) -> NostrFilter {
var filter = NostrFilter()
filter.kinds = [1059] // Gift wrap kind
filter.kinds = NostrProtocol.acceptedPrivateEnvelopeKinds
filter.since = since?.timeIntervalSince1970.toInt()
filter.tagFilters = ["p": [pubkey]]
filter.limit = TransportConfig.nostrRelayDefaultFetchLimit // reasonable limit
// Before the migration deadline each logical payload is stored once
// per accepted wire kind. Scale the combined filter so compatibility
// copies do not halve the number of logical messages/acks recovered
// after a reconnect.
filter.limit = TransportConfig.nostrRelayDefaultFetchLimit
* NostrProtocol.acceptedPrivateEnvelopeKinds.count
return filter
}