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
11 changes: 11 additions & 0 deletions control.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const (
CtrlKeyRotate ControlType = "key_rotate"
CtrlEntitlementUpdate ControlType = "entitlement_update"
CtrlEntitlementViolation ControlType = "entitlement_violation"
CtrlPushNotify ControlType = "push_notify"
)

// ControlMessage is the wire format for relay control protocol messages.
Expand Down Expand Up @@ -111,6 +112,16 @@ type ClientCountPayload struct {
SessionID string `json:"session_id"`
}

// PushNotifyPayload is sent by a daemon to the relay over its authenticated
// websocket to trigger a Web Push notification. It intentionally does not
// carry the relay session ID; the relay derives that from the registered daemon
// connection to prevent session spoofing.
type PushNotifyPayload struct {
NavSessionID string `json:"nav_session_id,omitempty"`
Type string `json:"type"`
Summary string `json:"summary"`
}

// KeyRotatePayload is sent by the daemon to the relay to update the session's
// auth KeyHMAC during automatic key rotation. The relay retains the previous
// HMAC until PrevExpiresAt so in-flight client tokens pass validation during
Expand Down
43 changes: 43 additions & 0 deletions protocol_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,48 @@ func TestControlMessageRoundtrip(t *testing.T) {
}
}

func TestPushNotifyPayloadRoundtrip(t *testing.T) {
t.Parallel()

payload := protocol.PushNotifyPayload{
NavSessionID: "agent-session-1",
Type: "approval_request",
Summary: "AgentD needs your approval",
}
raw, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal PushNotifyPayload: %v", err)
}
var rawFields map[string]json.RawMessage
if err := json.Unmarshal(raw, &rawFields); err != nil {
t.Fatalf("unmarshal raw PushNotifyPayload fields: %v", err)
}
if _, ok := rawFields["session_id"]; ok {
t.Fatalf("PushNotifyPayload must not carry relay session_id; relay derives it from daemon connection: %s", raw)
}

msg := protocol.ControlMessage{Type: protocol.CtrlPushNotify, Payload: raw}
encoded, err := json.Marshal(msg)
if err != nil {
t.Fatalf("marshal ControlMessage: %v", err)
}

var decoded protocol.ControlMessage
if err := json.Unmarshal(encoded, &decoded); err != nil {
t.Fatalf("unmarshal ControlMessage: %v", err)
}
if decoded.Type != protocol.CtrlPushNotify {
t.Fatalf("Type = %q, want %q", decoded.Type, protocol.CtrlPushNotify)
}
var got protocol.PushNotifyPayload
if err := json.Unmarshal(decoded.Payload, &got); err != nil {
t.Fatalf("unmarshal PushNotifyPayload: %v", err)
}
if got != payload {
t.Fatalf("payload = %+v, want %+v", got, payload)
}
}

func TestRegisterPayloadRoundtrip(t *testing.T) {
t.Parallel()
original := protocol.RegisterPayload{
Expand Down Expand Up @@ -455,6 +497,7 @@ func TestControlTypeConstants(t *testing.T) {
protocol.CtrlKeyRotate: "key_rotate",
protocol.CtrlEntitlementUpdate: "entitlement_update",
protocol.CtrlEntitlementViolation: "entitlement_violation",
protocol.CtrlPushNotify: "push_notify",
}
for ct, want := range expected {
if string(ct) != want {
Expand Down
Loading