Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Clave/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,12 @@ final class AppState {
}

do {
let result = try await LightSigner.handleRequest(privateKey: privateKey, requestEvent: requestEvent, skipProtection: true)
let result = try await LightSigner.handleRequest(
privateKey: privateKey,
requestEvent: requestEvent,
skipProtection: true,
responseRelayUrl: request.responseRelayUrl
)
SharedStorage.removePendingRequest(id: request.id)
refreshPendingRequests()
return result.status == "signed"
Expand Down
56 changes: 35 additions & 21 deletions Clave/ClaveApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,32 +103,46 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
let relayUrlString = (userInfo["relay_url"] as? String) ?? SharedConstants.relayURL

do {
let relay = LightRelay(url: relayUrlString)
try await relay.connect()

let now = Int(Date().timeIntervalSince1970)
// NIP-46 spec: clients MUST include ["p", <signer-pubkey>] on kind:24133
let filter: [String: Any] = [
"kinds": [24133],
"#p": [signerPubkey],
"since": now - 60,
"limit": 5
]

var events = try await relay.fetchEvents(filter: filter, timeout: 10.0)

if events.isEmpty {
try await Task.sleep(nanoseconds: 2_000_000_000)
let retryFilter: [String: Any] = [
var events: [[String: Any]] = []

// Prefer embedded event from the push payload. See NotificationService.swift
// for rationale — same fix, same race, same fallback.
if let embedded = userInfo["event"] as? [String: Any] {
logger.notice("[App] Using embedded event from push payload")
events = [embedded]
} else {
if userInfo["event"] != nil {
logger.warning("[App] event key present but not castable to [String: Any] — falling back to relay fetch")
} else {
logger.notice("[App] No embedded event; fetching from \(relayUrlString, privacy: .public)")
}
let relay = LightRelay(url: relayUrlString)
try await relay.connect()

let now = Int(Date().timeIntervalSince1970)
// NIP-46 spec: clients MUST include ["p", <signer-pubkey>] on kind:24133
let filter: [String: Any] = [
"kinds": [24133],
"#p": [signerPubkey],
"since": now - 120,
"since": now - 60,
"limit": 5
]
events = try await relay.fetchEvents(filter: retryFilter, timeout: 10.0)
}

relay.disconnect()
events = try await relay.fetchEvents(filter: filter, timeout: 10.0)

if events.isEmpty {
try await Task.sleep(nanoseconds: 2_000_000_000)
let retryFilter: [String: Any] = [
"kinds": [24133],
"#p": [signerPubkey],
"since": now - 120,
"limit": 5
]
events = try await relay.fetchEvents(filter: retryFilter, timeout: 10.0)
}

relay.disconnect()
}

let processedKey = "processedEventIDs"
var processedIDs = Set(SharedConstants.sharedDefaults.stringArray(forKey: processedKey) ?? [])
Expand Down
56 changes: 36 additions & 20 deletions ClaveNSE/NotificationService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,32 +110,48 @@ class NotificationService: UNNotificationServiceExtension {
let relayUrlString = (userInfo["relay_url"] as? String) ?? SharedConstants.relayURL

do {
let relay = LightRelay(url: relayUrlString)
try await relay.connect()

let now = Int(Date().timeIntervalSince1970)
// NIP-46 spec: clients MUST include ["p", <signer-pubkey>] on kind:24133
let filter: [String: Any] = [
"kinds": [24133],
"#p": [signerPubkey],
"since": now - 60,
"limit": 5
]

var events = try await relay.fetchEvents(filter: filter, timeout: 10.0)
var events: [[String: Any]] = []

// Prefer the embedded event from the push payload (build 22+). This
// bypasses the ephemeral-fetch race where the relay drops kind:24133
// before NSE can REQ for it. If absent (older proxy or oversized
// event), fall through to the existing fetch-from-relay path.
if let embedded = userInfo["event"] as? [String: Any] {
logger.notice("[ClaveNSE] Using embedded event from push payload")
events = [embedded]
} else {
if userInfo["event"] != nil {
logger.warning("[ClaveNSE] event key present but not castable to [String: Any] — falling back to relay fetch")
} else {
logger.notice("[ClaveNSE] No embedded event; fetching from \(relayUrlString, privacy: .public)")
}
let relay = LightRelay(url: relayUrlString)
try await relay.connect()

if events.isEmpty {
try await Task.sleep(nanoseconds: 2_000_000_000)
let retryFilter: [String: Any] = [
let now = Int(Date().timeIntervalSince1970)
// NIP-46 spec: clients MUST include ["p", <signer-pubkey>] on kind:24133
let filter: [String: Any] = [
"kinds": [24133],
"#p": [signerPubkey],
"since": now - 120,
"since": now - 60,
"limit": 5
]
events = try await relay.fetchEvents(filter: retryFilter, timeout: 10.0)
}

relay.disconnect()
events = try await relay.fetchEvents(filter: filter, timeout: 10.0)

if events.isEmpty {
try await Task.sleep(nanoseconds: 2_000_000_000)
let retryFilter: [String: Any] = [
"kinds": [24133],
"#p": [signerPubkey],
"since": now - 120,
"limit": 5
]
events = try await relay.fetchEvents(filter: retryFilter, timeout: 10.0)
}

relay.disconnect()
}

if events.isEmpty {
return SigningResult(status: .noEvents)
Expand Down
3 changes: 2 additions & 1 deletion Shared/LightSigner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,8 @@ enum LightSigner {
method: method,
eventKind: eventKind ?? 0,
clientPubkey: senderPubkey,
timestamp: Date().timeIntervalSince1970
timestamp: Date().timeIntervalSince1970,
responseRelayUrl: responseRelayUrl
)
SharedStorage.queuePendingRequest(pending)
}
Expand Down
10 changes: 10 additions & 0 deletions Shared/SharedModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ struct PendingRequest: Codable, Identifiable {
let eventKind: Int?
let clientPubkey: String
let timestamp: Double
/// Relay URL the request was received on. Threaded back into
/// LightSigner.handleRequest at approval-time so the response publishes
/// to the same relay the client is actually subscribed on — not the
/// powr.build fallback.
///
/// Optional for backward compatibility — Codable decodes missing keys on
/// Optional properties as nil without throwing, so pre-build-22 rows in
/// UserDefaults decode cleanly and fall back to SharedConstants.relayURL
/// at publish time.
let responseRelayUrl: String?
}

struct ConnectedClient: Codable, Identifiable {
Expand Down
14 changes: 14 additions & 0 deletions relay-proxy/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,20 @@ async function dispatchCaughtEvent({ event, sourceUrl, classification }) {
event_id: event.id,
};

// Embed the caught event so NSE doesn't have to race the relay's ephemeral
// retention window. APNs alert payload cap is 4KB; 3500B leaves ~300B for
// the aps container. Oversized events fall through to NSE's existing
// fetch-from-relay path (same broken behavior as build 21, no regression).
const eventJSON = JSON.stringify(event);
const eventBytes = Buffer.byteLength(eventJSON);
if (eventBytes <= 3500) {
pushPayload.event = event;
} else {
console.log(
`[Push] Event ${event.id.slice(0, 8)} too large (${eventBytes}B), omitting embed — NSE will fall back to relay fetch`
);
}

for (const entry of matchingTokens) {
try {
const status = await sendPush(entry.token, pushPayload);
Expand Down