From 4ab549787aa43dd87dfdbc07d744d14525a34f00 Mon Sep 17 00:00:00 2001 From: "courtmanr@gmail.com" Date: Tue, 11 Aug 2026 15:21:07 +0100 Subject: [PATCH] Add push preferences to relay connect frames --- .../v6/internal/subsystems/relay-runtime.md | 4 ++++ internal/relay/protocol.go | 16 +++++++++---- internal/relay/protocol_test.go | 23 +++++++++++++++++++ 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/relay-runtime.md b/docs/release-control/v6/internal/subsystems/relay-runtime.md index 8478e45f9..c24d6baf5 100644 --- a/docs/release-control/v6/internal/subsystems/relay-runtime.md +++ b/docs/release-control/v6/internal/subsystems/relay-runtime.md @@ -19,6 +19,10 @@ Own the desktop and mobile relay runtimes, their persisted relay state boundaries, the server-owned mobile relay capability boundary, and the canonical reconnect, encryption, protocol, proxy, and relay-trust behavior for Pulse instance bridging. +Mobile CONNECT frames may carry explicit push preferences for critical, +warning, approval, and fix-result notifications. The relay protocol preserves +each boolean so server-side delivery can enforce the paired device's choices; +legacy clients omit the entire preferences object and remain wire-compatible. ## Canonical Files diff --git a/internal/relay/protocol.go b/internal/relay/protocol.go index 3b4c993b1..9341cfe2b 100644 --- a/internal/relay/protocol.go +++ b/internal/relay/protocol.go @@ -149,10 +149,18 @@ type RegisterAckPayload struct { // ConnectPayload is sent by the app in CONNECT frames. type ConnectPayload struct { - InstanceID string `json:"instance_id"` - AuthToken string `json:"auth_token"` - DeviceToken string `json:"device_token,omitempty"` // push notification device token (mobile apps only) - Platform string `json:"platform,omitempty"` // "ios" or "android" (mobile apps only) + InstanceID string `json:"instance_id"` + AuthToken string `json:"auth_token"` + DeviceToken string `json:"device_token,omitempty"` // push notification device token (mobile apps only) + Platform string `json:"platform,omitempty"` // "ios" or "android" (mobile apps only) + NotificationPreferences *PushNotificationPreferences `json:"notification_preferences,omitempty"` // server-enforced delivery preferences +} + +type PushNotificationPreferences struct { + CriticalEnabled bool `json:"critical_enabled"` + WarningEnabled bool `json:"warning_enabled"` + ApprovalEnabled bool `json:"approval_enabled"` + FixResultEnabled bool `json:"fix_result_enabled"` } // ConnectAckPayload is sent by the relay in CONNECT_ACK frames. diff --git a/internal/relay/protocol_test.go b/internal/relay/protocol_test.go index 0939a7992..59a12d239 100644 --- a/internal/relay/protocol_test.go +++ b/internal/relay/protocol_test.go @@ -122,6 +122,12 @@ func TestControlFrameRoundTrip(t *testing.T) { AuthToken: "auth-token-xyz", DeviceToken: "device-token-123", Platform: "ios", + NotificationPreferences: &PushNotificationPreferences{ + CriticalEnabled: true, + WarningEnabled: false, + ApprovalEnabled: true, + FixResultEnabled: false, + }, } frame, err := NewControlFrame(FrameConnect, 0, orig) if err != nil { @@ -155,6 +161,23 @@ func TestControlFrameRoundTrip(t *testing.T) { if got.Platform != orig.Platform { t.Errorf("Platform: got %q, want %q", got.Platform, orig.Platform) } + if got.NotificationPreferences == nil { + t.Fatal("NotificationPreferences was omitted from CONNECT round trip") + } + if *got.NotificationPreferences != *orig.NotificationPreferences { + t.Errorf("NotificationPreferences: got %#v, want %#v", got.NotificationPreferences, orig.NotificationPreferences) + } + + legacyFrame, err := NewControlFrame(FrameConnect, 0, ConnectPayload{ + InstanceID: "relay_legacy", + AuthToken: "legacy-auth", + }) + if err != nil { + t.Fatalf("NewControlFrame(legacy CONNECT) error = %v", err) + } + if bytes.Contains(legacyFrame.Payload, []byte("notification_preferences")) { + t.Fatalf("legacy CONNECT unexpectedly serialized notification preferences: %s", legacyFrame.Payload) + } }) t.Run("channel open payload", func(t *testing.T) {