From b8a0eb6b7885fd87a77052df0a0e71e3ee05589c Mon Sep 17 00:00:00 2001 From: Tom Brow Date: Sun, 18 Oct 2026 11:27:21 -0700 Subject: [PATCH] fix(push-gateway): request default notification sound Signed-off-by: Tom Brow --- crates/buzz-push-gateway/src/apns.rs | 17 +++++++++++++++++ crates/buzz-push-gateway/src/model.rs | 2 +- docs/formal/nip-pl/delivery.py | 2 +- docs/formal/nip-pl/fixed_payload.py | 2 +- docs/nips/NIP-PL.md | 2 +- 5 files changed, 21 insertions(+), 4 deletions(-) diff --git a/crates/buzz-push-gateway/src/apns.rs b/crates/buzz-push-gateway/src/apns.rs index 8fe125b576..6b65ffcb1d 100644 --- a/crates/buzz-push-gateway/src/apns.rs +++ b/crates/buzz-push-gateway/src/apns.rs @@ -239,6 +239,18 @@ mod tests { StatusCode::OK } + #[test] + fn apns_payload_matches_normative_contract_and_models() { + let body = std::str::from_utf8(APNS_RECONNECT_PAYLOAD).unwrap(); + for contract in [ + include_str!("../../../docs/nips/NIP-PL.md"), + include_str!("../../../docs/formal/nip-pl/delivery.py"), + include_str!("../../../docs/formal/nip-pl/fixed_payload.py"), + ] { + assert!(contract.contains(body), "APNs fixed-body contract drift"); + } + } + #[tokio::test] async fn certificate_transport_sends_no_bearer_and_exact_body_for_every_attempt() { let requests = Arc::new(Mutex::new(Vec::new())); @@ -274,6 +286,11 @@ mod tests { } let captured = requests.lock().unwrap(); assert_eq!(captured.len(), 2); + for request in captured.iter() { + let payload: serde_json::Value = serde_json::from_slice(&request.body).unwrap(); + assert_eq!(payload["aps"]["sound"], "default"); + assert_eq!(payload["aps"]["mutable-content"], 1); + } assert!(captured .iter() .all(|request| request.body.as_slice() == APNS_RECONNECT_PAYLOAD)); diff --git a/crates/buzz-push-gateway/src/model.rs b/crates/buzz-push-gateway/src/model.rs index 441966bd20..05f97ef3d5 100644 --- a/crates/buzz-push-gateway/src/model.rs +++ b/crates/buzz-push-gateway/src/model.rs @@ -13,7 +13,7 @@ pub const MAX_ENROLL_REQUEST_BYTES: usize = pub const MAX_GRANT_BYTES: usize = 4096; pub const MAX_ENDPOINT_HEX_BYTES: usize = 512; pub const APNS_RECONNECT_PAYLOAD: &[u8] = - br#"{"aps":{"alert":{"body":"Reconnect to your relay now"},"mutable-content":1}}"#; + br#"{"aps":{"alert":{"body":"Reconnect to your relay now"},"sound":"default","mutable-content":1}}"#; pub const WIRE_VERSION: u8 = 1; /// Relay request. It deliberately has no application-payload field: diff --git a/docs/formal/nip-pl/delivery.py b/docs/formal/nip-pl/delivery.py index 2c8c5ac350..39a1ca9d34 100644 --- a/docs/formal/nip-pl/delivery.py +++ b/docs/formal/nip-pl/delivery.py @@ -7,7 +7,7 @@ transient release, revocation ordering, custody, and the constant APNs body. """ from itertools import permutations, product -FIXED_BODY = b'{"aps":{"alert":{"body":"Reconnect to your relay now"},"mutable-content":1}}' +FIXED_BODY = b'{"aps":{"alert":{"body":"Reconnect to your relay now"},"sound":"default","mutable-content":1}}' class Gateway: def __init__(self): diff --git a/docs/formal/nip-pl/fixed_payload.py b/docs/formal/nip-pl/fixed_payload.py index e69af9c434..5b424cd21f 100644 --- a/docs/formal/nip-pl/fixed_payload.py +++ b/docs/formal/nip-pl/fixed_payload.py @@ -5,7 +5,7 @@ caller-controlled or capability-derived category; none is an argument to body(). """ from itertools import product -C = b'{"aps":{"alert":{"body":"Reconnect to your relay now"},"mutable-content":1}}' +C = b'{"aps":{"alert":{"body":"Reconnect to your relay now"},"sound":"default","mutable-content":1}}' DOMAINS = [ (b"request-a", b"request-b"), # exact signed body (b"auth-a", b"auth-b"), # NIP-98 event/header diff --git a/docs/nips/NIP-PL.md b/docs/nips/NIP-PL.md index 1ee9ed2f16..38ee0eca4c 100644 --- a/docs/nips/NIP-PL.md +++ b/docs/nips/NIP-PL.md @@ -224,7 +224,7 @@ Common invariant, all transports: the application payload is a transport-owned r ### APNs -The APNs application body is the exact UTF-8 byte constant `{"aps":{"alert":{"body":"Reconnect to your relay now"},"mutable-content":1}}`. It has no custom member, event identifier, unread count, or relay-supplied byte. The constant mutable-content flag lets the Buzz Notification Service Extension compute a local badge and, when separately authorized data is available, replace the generic text; the gateway does not carry that data. The gateway MUST send that exact body for every accepted APNs attempt; it MUST NOT serialize any relay request, endpoint grant, provider response, or generic JSON value into the body. `apns-topic`, environment, credentials, push type `alert`, and priority `10` come only from gateway configuration. `apns-id` is a canonical UUID and `apns-expiration` is bounded by the endpoint capability and a gateway-local ceiling. +The APNs application body is the exact UTF-8 byte constant `{"aps":{"alert":{"body":"Reconnect to your relay now"},"sound":"default","mutable-content":1}}`. It has no custom member, event identifier, unread count, or relay-supplied byte. The constant sound requests the system default alert sound, subject to notification authorization and user sound, Focus, and haptics settings. It does not select critical alerts or guarantee vibration. Sound is present even when the Notification Service Extension does not run. The constant mutable-content flag lets the Buzz Notification Service Extension compute a local badge and, when separately authorized data is available, replace the generic text; the gateway does not carry that data. The gateway MUST send that exact body for every accepted APNs attempt; it MUST NOT serialize any relay request, endpoint grant, provider response, or generic JSON value into the body. `apns-topic`, environment, credentials, push type `alert`, and priority `10` come only from gateway configuration. `apns-id` is a canonical UUID and `apns-expiration` is bounded by the endpoint capability and a gateway-local ceiling. ### FCM