From f362ca6fc66b422c8b5187ba3aa3c893c1b50879 Mon Sep 17 00:00:00 2001 From: Hesham Karm <24391550+hishamkaram@users.noreply.github.com> Date: Sun, 17 May 2026 17:21:14 +0200 Subject: [PATCH] feat(protocol): add session termination controls --- control.go | 19 +++++++++++++++++++ protocol_test.go | 8 ++++++++ 2 files changed, 27 insertions(+) diff --git a/control.go b/control.go index b3d1fd0..88067c1 100644 --- a/control.go +++ b/control.go @@ -26,6 +26,8 @@ const ( CtrlEntitlementViolation ControlType = "entitlement_violation" CtrlPushNotify ControlType = "push_notify" CtrlPushNotifyResult ControlType = "push_notify_result" + CtrlTerminateSession ControlType = "terminate_session" + CtrlTerminateSessionAck ControlType = "terminate_session_ack" ) // ControlMessage is the wire format for relay control protocol messages. @@ -99,6 +101,23 @@ type DeactivateDeveloperPayload struct { DeveloperID string `json:"developer_id"` } +// TerminateSessionPayload is sent by the relay to a daemon when an operator +// terminates a single relay session from the dashboard. Unlike developer +// deactivation, this is scoped to one relay session and should stop reconnects +// for that relay session identity. +type TerminateSessionPayload struct { + SessionID string `json:"session_id"` + Reason string `json:"reason,omitempty"` +} + +// TerminateSessionAckPayload is sent by the daemon after it has accepted a +// terminate_session control message. The relay treats it as observability; the +// durable termination tombstone is the reconnect-blocking authority. +type TerminateSessionAckPayload struct { + SessionID string `json:"session_id"` + Stopped int `json:"stopped,omitempty"` +} + // ClientConnectedPayload is sent by the relay to the daemon when a PWA client // connects or reconnects. The daemon uses this to replay message history. type ClientConnectedPayload struct { diff --git a/protocol_test.go b/protocol_test.go index 28e06ab..b136ff4 100644 --- a/protocol_test.go +++ b/protocol_test.go @@ -343,6 +343,12 @@ func TestDeactivateDeveloperPayloadRoundtrip(t *testing.T) { assertRoundtrip(t, protocol.DeactivateDeveloperPayload{DeveloperID: "dev-1"}) } +func TestTerminateSessionPayloadRoundtrip(t *testing.T) { + t.Parallel() + assertRoundtrip(t, protocol.TerminateSessionPayload{SessionID: "relay-1", Reason: "operator"}) + assertRoundtrip(t, protocol.TerminateSessionAckPayload{SessionID: "relay-1", Stopped: 2}) +} + func TestClientConnectedPayloadRoundtrip(t *testing.T) { t.Parallel() assertRoundtrip(t, protocol.ClientConnectedPayload{SessionID: "sess-123"}) @@ -557,6 +563,8 @@ func TestControlTypeConstants(t *testing.T) { protocol.CtrlEntitlementViolation: "entitlement_violation", protocol.CtrlPushNotify: "push_notify", protocol.CtrlPushNotifyResult: "push_notify_result", + protocol.CtrlTerminateSession: "terminate_session", + protocol.CtrlTerminateSessionAck: "terminate_session_ack", } for ct, want := range expected { if string(ct) != want {