Compare commits

...
Author SHA1 Message Date
sechmachine e58f1c7c48 feat(protocol): negotiate registered gateway profiles
Verify Protocol / verify (push) Canceled after 0s
Verify Protocol / module (push) Successful in 2m29s
2026-07-30 04:47:52 +07:00
sechmachine d26f8b60f8 feat(protocol): carry stream policy and gateway telemetry 2026-07-30 01:45:48 +07:00
sechmachine 59741761ce docs(openspec): archive gateway input feedback contract 2026-07-29 23:02:19 +07:00
26 changed files with 1297 additions and 105 deletions
+424 -40
View File
@@ -13,7 +13,7 @@ import (
"time"
)
const SchemaSHA256 = "e98c75ef81bbeac6be2b8f11202c1ffecec0aa515b48576a26756290e99d5dd8"
const SchemaSHA256 = "3aec8dd72bdbb6b9657c8df3160252c93034c7c1032d471e01eae2ef91e47716"
const ProtocolVersion = "1.0.0"
const CurrentWireVersion = "1"
const NMinus1WireVersion = "0"
@@ -68,12 +68,12 @@ type BrokerSession struct {
}
type CapabilityProfile struct {
Transport string `json:"transport"`
Framing string `json:"framing"`
Media string `json:"media"`
Audio string `json:"audio"`
SourceRateControl string `json:"source_rate_control"`
ClientDecode string `json:"client_decode"`
Transport string `json:"transport"`
Framing string `json:"framing"`
Media string `json:"media"`
Audio string `json:"audio"`
SourceRateControl string `json:"source_rate_control"`
ClientDecode []string `json:"client_decode"`
}
type ChannelFrame struct {
@@ -191,13 +191,14 @@ type GatewayDrain struct {
}
type GatewayHeartbeat struct {
Version string `json:"version"`
GatewayID string `json:"gateway_id"`
Sequence int64 `json:"sequence"`
ObservedAt string `json:"observed_at"`
ActiveConnections int64 `json:"active_connections"`
EgressKbps int64 `json:"egress_kbps"`
State string `json:"state"`
Version string `json:"version"`
GatewayID string `json:"gateway_id"`
Sequence int64 `json:"sequence"`
ObservedAt string `json:"observed_at"`
ActiveConnections int64 `json:"active_connections"`
EgressKbps int64 `json:"egress_kbps"`
State string `json:"state"`
Telemetry GatewayTelemetry `json:"telemetry"`
}
type GatewayRegistration struct {
@@ -216,6 +217,27 @@ type GatewayRegistration struct {
Capabilities CapabilityProfile `json:"capabilities"`
}
type GatewayTelemetry struct {
AdmittedSessions int64 `json:"admitted_sessions"`
AdmissionRejects int64 `json:"admission_rejects"`
Reconnects int64 `json:"reconnects"`
DrainTransitions int64 `json:"drain_transitions"`
MediaDrops int64 `json:"media_drops"`
MediaPackets int64 `json:"media_packets"`
MediaBytes int64 `json:"media_bytes"`
QueueDelayMicros int64 `json:"queue_delay_micros"`
ProcessingDelayMicros int64 `json:"processing_delay_micros"`
ProcessingSamples int64 `json:"processing_samples"`
PacingDelayMicros int64 `json:"pacing_delay_micros"`
ProviderErrors int64 `json:"provider_errors"`
InputRejected int64 `json:"input_rejected"`
ControlRttMicros int64 `json:"control_rtt_micros"`
ControlJitterMicros int64 `json:"control_jitter_micros"`
ControlLossPpm int64 `json:"control_loss_ppm"`
PendingReliable int64 `json:"pending_reliable"`
ProviderState string `json:"provider_state"`
}
type GrantReference struct {
OpaqueValue string `json:"opaque_value"`
ExpiresAt string `json:"expires_at"`
@@ -265,25 +287,26 @@ type PageInfo struct {
}
type ProviderSessionWork struct {
Version string `json:"version"`
SessionID string `json:"session_id"`
GatewayID string `json:"gateway_id"`
ReconnectSequence int64 `json:"reconnect_sequence"`
ExpiresAt string `json:"expires_at"`
ProviderProfile string `json:"provider_profile"`
ProviderIdentity string `json:"provider_identity"`
PolicyVersionID string `json:"policy_version_id"`
ApplicationID string `json:"application_id"`
ClientID string `json:"client_id"`
ManagementHost string `json:"management_host"`
ManagementPort int64 `json:"management_port"`
StreamHost string `json:"stream_host"`
StreamPort int64 `json:"stream_port"`
ClientCertificatePem string `json:"client_certificate_pem"`
ClientPrivateKeyPem string `json:"client_private_key_pem"`
ServerCertificatePem string `json:"server_certificate_pem"`
ClipboardPolicy ClipboardPolicy `json:"clipboard_policy"`
ProviderApplicationTerminationAllowed bool `json:"provider_application_termination_allowed"`
Version string `json:"version"`
SessionID string `json:"session_id"`
GatewayID string `json:"gateway_id"`
ReconnectSequence int64 `json:"reconnect_sequence"`
ExpiresAt string `json:"expires_at"`
ProviderProfile string `json:"provider_profile"`
ProviderIdentity string `json:"provider_identity"`
PolicyVersionID string `json:"policy_version_id"`
StreamPolicy ProviderStreamPolicy `json:"stream_policy"`
ApplicationID string `json:"application_id"`
ClientID string `json:"client_id"`
ManagementHost string `json:"management_host"`
ManagementPort int64 `json:"management_port"`
StreamHost string `json:"stream_host"`
StreamPort int64 `json:"stream_port"`
ClientCertificatePem string `json:"client_certificate_pem"`
ClientPrivateKeyPem string `json:"client_private_key_pem"`
ServerCertificatePem string `json:"server_certificate_pem"`
ClipboardPolicy ClipboardPolicy `json:"clipboard_policy"`
ProviderApplicationTerminationAllowed bool `json:"provider_application_termination_allowed"`
}
type ProviderState struct {
@@ -294,6 +317,15 @@ type ProviderState struct {
Channels []string `json:"channels"`
}
type ProviderStreamPolicy struct {
ResolutionWidth int64 `json:"resolution_width"`
ResolutionHeight int64 `json:"resolution_height"`
Fps int64 `json:"fps"`
Codec string `json:"codec"`
BitrateKbps int64 `json:"bitrate_kbps"`
AudioEnabled bool `json:"audio_enabled"`
}
type ReauthGrant struct {
Token string `json:"token"`
Purpose string `json:"purpose"`
@@ -852,14 +884,26 @@ func (v CapabilityProfile) Validate() error {
if len(v.SourceRateControl) > 64 {
violations = append(violations, FieldViolation{Field: "source_rate_control", Code: "max_length"})
}
if v.ClientDecode == "" {
if v.ClientDecode == nil {
violations = append(violations, FieldViolation{Field: "client_decode", Code: "required"})
}
if len(v.ClientDecode) < 1 && v.ClientDecode != "" {
violations = append(violations, FieldViolation{Field: "client_decode", Code: "min_length"})
if len(v.ClientDecode) < 1 {
violations = append(violations, FieldViolation{Field: "client_decode", Code: "min_items"})
}
if len(v.ClientDecode) > 64 {
violations = append(violations, FieldViolation{Field: "client_decode", Code: "max_length"})
if len(v.ClientDecode) > 2 {
violations = append(violations, FieldViolation{Field: "client_decode", Code: "max_items"})
}
for _, item := range v.ClientDecode {
if !(item == "h264-opus" || item == "hevc-opus") {
violations = append(violations, FieldViolation{Field: "client_decode", Code: "invalid_item"})
}
}
for index, item := range v.ClientDecode {
for prior := 0; prior < index; prior++ {
if item == v.ClientDecode[prior] {
violations = append(violations, FieldViolation{Field: "client_decode", Code: "duplicate_item"})
}
}
}
if len(violations) > 0 {
return ValidationError{Violations: violations}
@@ -2370,6 +2414,12 @@ func (v GatewayHeartbeat) Validate() error {
if v.State != "" && !(v.State == "ready" || v.State == "draining" || v.State == "offline") {
violations = append(violations, FieldViolation{Field: "state", Code: "invalid_value"})
}
if reflect.DeepEqual(v.Telemetry, GatewayTelemetry{}) {
violations = append(violations, FieldViolation{Field: "telemetry", Code: "required"})
}
if err := v.Telemetry.Validate(); err != nil {
violations = append(violations, FieldViolation{Field: "telemetry", Code: "invalid_object"})
}
if len(violations) > 0 {
return ValidationError{Violations: violations}
}
@@ -2403,6 +2453,9 @@ func DecodeGatewayHeartbeat(data []byte) (GatewayHeartbeat, error) {
if raw, ok := fields["state"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "state", Code: "required"}}}
}
if raw, ok := fields["telemetry"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "telemetry", Code: "required"}}}
}
if raw, ok := fields["version"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "version", Code: "required"}}}
}
@@ -2623,6 +2676,210 @@ func EncodeGatewayRegistration(value GatewayRegistration) ([]byte, error) {
return json.Marshal(value)
}
func (v GatewayTelemetry) Validate() error {
var violations []FieldViolation
if v.AdmittedSessions != 0 && v.AdmittedSessions < 0 {
violations = append(violations, FieldViolation{Field: "admitted_sessions", Code: "minimum"})
}
if v.AdmittedSessions > 9223372036854775807 {
violations = append(violations, FieldViolation{Field: "admitted_sessions", Code: "maximum"})
}
if v.AdmissionRejects != 0 && v.AdmissionRejects < 0 {
violations = append(violations, FieldViolation{Field: "admission_rejects", Code: "minimum"})
}
if v.AdmissionRejects > 9223372036854775807 {
violations = append(violations, FieldViolation{Field: "admission_rejects", Code: "maximum"})
}
if v.Reconnects != 0 && v.Reconnects < 0 {
violations = append(violations, FieldViolation{Field: "reconnects", Code: "minimum"})
}
if v.Reconnects > 9223372036854775807 {
violations = append(violations, FieldViolation{Field: "reconnects", Code: "maximum"})
}
if v.DrainTransitions != 0 && v.DrainTransitions < 0 {
violations = append(violations, FieldViolation{Field: "drain_transitions", Code: "minimum"})
}
if v.DrainTransitions > 9223372036854775807 {
violations = append(violations, FieldViolation{Field: "drain_transitions", Code: "maximum"})
}
if v.MediaDrops != 0 && v.MediaDrops < 0 {
violations = append(violations, FieldViolation{Field: "media_drops", Code: "minimum"})
}
if v.MediaDrops > 9223372036854775807 {
violations = append(violations, FieldViolation{Field: "media_drops", Code: "maximum"})
}
if v.MediaPackets != 0 && v.MediaPackets < 0 {
violations = append(violations, FieldViolation{Field: "media_packets", Code: "minimum"})
}
if v.MediaPackets > 9223372036854775807 {
violations = append(violations, FieldViolation{Field: "media_packets", Code: "maximum"})
}
if v.MediaBytes != 0 && v.MediaBytes < 0 {
violations = append(violations, FieldViolation{Field: "media_bytes", Code: "minimum"})
}
if v.MediaBytes > 9223372036854775807 {
violations = append(violations, FieldViolation{Field: "media_bytes", Code: "maximum"})
}
if v.QueueDelayMicros != 0 && v.QueueDelayMicros < 0 {
violations = append(violations, FieldViolation{Field: "queue_delay_micros", Code: "minimum"})
}
if v.QueueDelayMicros > 9223372036854775807 {
violations = append(violations, FieldViolation{Field: "queue_delay_micros", Code: "maximum"})
}
if v.ProcessingDelayMicros != 0 && v.ProcessingDelayMicros < 0 {
violations = append(violations, FieldViolation{Field: "processing_delay_micros", Code: "minimum"})
}
if v.ProcessingDelayMicros > 9223372036854775807 {
violations = append(violations, FieldViolation{Field: "processing_delay_micros", Code: "maximum"})
}
if v.ProcessingSamples != 0 && v.ProcessingSamples < 0 {
violations = append(violations, FieldViolation{Field: "processing_samples", Code: "minimum"})
}
if v.ProcessingSamples > 9223372036854775807 {
violations = append(violations, FieldViolation{Field: "processing_samples", Code: "maximum"})
}
if v.PacingDelayMicros != 0 && v.PacingDelayMicros < 0 {
violations = append(violations, FieldViolation{Field: "pacing_delay_micros", Code: "minimum"})
}
if v.PacingDelayMicros > 9223372036854775807 {
violations = append(violations, FieldViolation{Field: "pacing_delay_micros", Code: "maximum"})
}
if v.ProviderErrors != 0 && v.ProviderErrors < 0 {
violations = append(violations, FieldViolation{Field: "provider_errors", Code: "minimum"})
}
if v.ProviderErrors > 9223372036854775807 {
violations = append(violations, FieldViolation{Field: "provider_errors", Code: "maximum"})
}
if v.InputRejected != 0 && v.InputRejected < 0 {
violations = append(violations, FieldViolation{Field: "input_rejected", Code: "minimum"})
}
if v.InputRejected > 9223372036854775807 {
violations = append(violations, FieldViolation{Field: "input_rejected", Code: "maximum"})
}
if v.ControlRttMicros != 0 && v.ControlRttMicros < 0 {
violations = append(violations, FieldViolation{Field: "control_rtt_micros", Code: "minimum"})
}
if v.ControlRttMicros > 9223372036854775807 {
violations = append(violations, FieldViolation{Field: "control_rtt_micros", Code: "maximum"})
}
if v.ControlJitterMicros != 0 && v.ControlJitterMicros < 0 {
violations = append(violations, FieldViolation{Field: "control_jitter_micros", Code: "minimum"})
}
if v.ControlJitterMicros > 9223372036854775807 {
violations = append(violations, FieldViolation{Field: "control_jitter_micros", Code: "maximum"})
}
if v.ControlLossPpm != 0 && v.ControlLossPpm < 0 {
violations = append(violations, FieldViolation{Field: "control_loss_ppm", Code: "minimum"})
}
if v.ControlLossPpm > 1000000 {
violations = append(violations, FieldViolation{Field: "control_loss_ppm", Code: "maximum"})
}
if v.PendingReliable != 0 && v.PendingReliable < 0 {
violations = append(violations, FieldViolation{Field: "pending_reliable", Code: "minimum"})
}
if v.PendingReliable > 9223372036854775807 {
violations = append(violations, FieldViolation{Field: "pending_reliable", Code: "maximum"})
}
if v.ProviderState == "" {
violations = append(violations, FieldViolation{Field: "provider_state", Code: "required"})
}
if v.ProviderState != "" && !(v.ProviderState == "unknown" || v.ProviderState == "starting" || v.ProviderState == "ready" || v.ProviderState == "disconnected" || v.ProviderState == "terminating" || v.ProviderState == "terminated" || v.ProviderState == "cleanup_pending" || v.ProviderState == "failed") {
violations = append(violations, FieldViolation{Field: "provider_state", Code: "invalid_value"})
}
if len(violations) > 0 {
return ValidationError{Violations: violations}
}
return nil
}
func DecodeGatewayTelemetry(data []byte) (GatewayTelemetry, error) {
var value GatewayTelemetry
if len(data) > 1024*1024 {
return value, errors.New("protocol payload exceeds limit")
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(data, &fields); err != nil {
return value, err
}
if raw, ok := fields["admission_rejects"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "admission_rejects", Code: "required"}}}
}
if raw, ok := fields["admitted_sessions"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "admitted_sessions", Code: "required"}}}
}
if raw, ok := fields["control_jitter_micros"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "control_jitter_micros", Code: "required"}}}
}
if raw, ok := fields["control_loss_ppm"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "control_loss_ppm", Code: "required"}}}
}
if raw, ok := fields["control_rtt_micros"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "control_rtt_micros", Code: "required"}}}
}
if raw, ok := fields["drain_transitions"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "drain_transitions", Code: "required"}}}
}
if raw, ok := fields["input_rejected"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "input_rejected", Code: "required"}}}
}
if raw, ok := fields["media_bytes"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "media_bytes", Code: "required"}}}
}
if raw, ok := fields["media_drops"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "media_drops", Code: "required"}}}
}
if raw, ok := fields["media_packets"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "media_packets", Code: "required"}}}
}
if raw, ok := fields["pacing_delay_micros"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "pacing_delay_micros", Code: "required"}}}
}
if raw, ok := fields["pending_reliable"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "pending_reliable", Code: "required"}}}
}
if raw, ok := fields["processing_delay_micros"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "processing_delay_micros", Code: "required"}}}
}
if raw, ok := fields["processing_samples"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "processing_samples", Code: "required"}}}
}
if raw, ok := fields["provider_errors"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "provider_errors", Code: "required"}}}
}
if raw, ok := fields["provider_state"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "provider_state", Code: "required"}}}
}
if raw, ok := fields["queue_delay_micros"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "queue_delay_micros", Code: "required"}}}
}
if raw, ok := fields["reconnects"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "reconnects", Code: "required"}}}
}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil {
return value, err
}
var trailing any
if err := decoder.Decode(&trailing); err != io.EOF {
if err == nil {
return value, errors.New("trailing JSON value")
}
return value, err
}
if err := value.Validate(); err != nil {
return value, err
}
return value, nil
}
func EncodeGatewayTelemetry(value GatewayTelemetry) ([]byte, error) {
if err := value.Validate(); err != nil {
return nil, err
}
return json.Marshal(value)
}
func (v GrantReference) Validate() error {
var violations []FieldViolation
if v.OpaqueValue == "" {
@@ -3287,6 +3544,12 @@ func (v ProviderSessionWork) Validate() error {
if len(v.PolicyVersionID) > 128 {
violations = append(violations, FieldViolation{Field: "policy_version_id", Code: "max_length"})
}
if reflect.DeepEqual(v.StreamPolicy, ProviderStreamPolicy{}) {
violations = append(violations, FieldViolation{Field: "stream_policy", Code: "required"})
}
if err := v.StreamPolicy.Validate(); err != nil {
violations = append(violations, FieldViolation{Field: "stream_policy", Code: "invalid_object"})
}
if v.ApplicationID == "" {
violations = append(violations, FieldViolation{Field: "application_id", Code: "required"})
}
@@ -3440,6 +3703,9 @@ func DecodeProviderSessionWork(data []byte) (ProviderSessionWork, error) {
if raw, ok := fields["stream_host"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "stream_host", Code: "required"}}}
}
if raw, ok := fields["stream_policy"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "stream_policy", Code: "required"}}}
}
if raw, ok := fields["stream_port"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "stream_port", Code: "required"}}}
}
@@ -3555,6 +3821,108 @@ func EncodeProviderState(value ProviderState) ([]byte, error) {
return json.Marshal(value)
}
func (v ProviderStreamPolicy) Validate() error {
var violations []FieldViolation
if v.ResolutionWidth == 0 {
violations = append(violations, FieldViolation{Field: "resolution_width", Code: "required"})
}
if v.ResolutionWidth != 0 && v.ResolutionWidth < 320 {
violations = append(violations, FieldViolation{Field: "resolution_width", Code: "minimum"})
}
if v.ResolutionWidth > 16384 {
violations = append(violations, FieldViolation{Field: "resolution_width", Code: "maximum"})
}
if v.ResolutionHeight == 0 {
violations = append(violations, FieldViolation{Field: "resolution_height", Code: "required"})
}
if v.ResolutionHeight != 0 && v.ResolutionHeight < 200 {
violations = append(violations, FieldViolation{Field: "resolution_height", Code: "minimum"})
}
if v.ResolutionHeight > 8640 {
violations = append(violations, FieldViolation{Field: "resolution_height", Code: "maximum"})
}
if v.Fps == 0 {
violations = append(violations, FieldViolation{Field: "fps", Code: "required"})
}
if v.Fps != 0 && v.Fps < 1 {
violations = append(violations, FieldViolation{Field: "fps", Code: "minimum"})
}
if v.Fps > 240 {
violations = append(violations, FieldViolation{Field: "fps", Code: "maximum"})
}
if v.Codec == "" {
violations = append(violations, FieldViolation{Field: "codec", Code: "required"})
}
if v.Codec != "" && !(v.Codec == "H264" || v.Codec == "HEVC" || v.Codec == "AV1") {
violations = append(violations, FieldViolation{Field: "codec", Code: "invalid_value"})
}
if v.BitrateKbps == 0 {
violations = append(violations, FieldViolation{Field: "bitrate_kbps", Code: "required"})
}
if v.BitrateKbps != 0 && v.BitrateKbps < 100 {
violations = append(violations, FieldViolation{Field: "bitrate_kbps", Code: "minimum"})
}
if v.BitrateKbps > 1000000 {
violations = append(violations, FieldViolation{Field: "bitrate_kbps", Code: "maximum"})
}
if len(violations) > 0 {
return ValidationError{Violations: violations}
}
return nil
}
func DecodeProviderStreamPolicy(data []byte) (ProviderStreamPolicy, error) {
var value ProviderStreamPolicy
if len(data) > 1024*1024 {
return value, errors.New("protocol payload exceeds limit")
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(data, &fields); err != nil {
return value, err
}
if raw, ok := fields["audio_enabled"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "audio_enabled", Code: "required"}}}
}
if raw, ok := fields["bitrate_kbps"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "bitrate_kbps", Code: "required"}}}
}
if raw, ok := fields["codec"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "codec", Code: "required"}}}
}
if raw, ok := fields["fps"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "fps", Code: "required"}}}
}
if raw, ok := fields["resolution_height"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "resolution_height", Code: "required"}}}
}
if raw, ok := fields["resolution_width"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "resolution_width", Code: "required"}}}
}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil {
return value, err
}
var trailing any
if err := decoder.Decode(&trailing); err != io.EOF {
if err == nil {
return value, errors.New("trailing JSON value")
}
return value, err
}
if err := value.Validate(); err != nil {
return value, err
}
return value, nil
}
func EncodeProviderStreamPolicy(value ProviderStreamPolicy) ([]byte, error) {
if err := value.Validate(); err != nil {
return nil, err
}
return json.Marshal(value)
}
func (v ReauthGrant) Validate() error {
var violations []FieldViolation
if v.Token == "" {
@@ -4634,16 +5002,32 @@ func IntersectCapabilityProfiles(profiles ...CapabilityProfile) (CapabilityProfi
if err := selected.Validate(); err != nil {
return CapabilityProfile{}, ErrNoCapabilityOverlap
}
common := append([]string(nil), selected.ClientDecode...)
for _, profile := range profiles[1:] {
if err := profile.Validate(); err != nil || profile != selected {
if err := profile.Validate(); err != nil || profile.Transport != selected.Transport || profile.Framing != selected.Framing || profile.Media != selected.Media || profile.Audio != selected.Audio || profile.SourceRateControl != selected.SourceRateControl {
return CapabilityProfile{}, ErrNoCapabilityOverlap
}
next := common[:0]
for _, candidate := range common {
for _, offered := range profile.ClientDecode {
if candidate == offered {
next = append(next, candidate)
break
}
}
}
common = next
if len(common) == 0 {
return CapabilityProfile{}, ErrNoCapabilityOverlap
}
}
selected.ClientDecode = common
return selected, nil
}
func (v TunnelAdmissionRequest) DeviceAdmissionTranscript() []byte {
fields := []string{v.SessionID, v.GatewayID, v.Audience, v.Grant, fmt.Sprintf("%d", v.ReconnectSequence), v.ClientNonce, v.Capabilities.Transport, v.Capabilities.Framing, v.Capabilities.Media, v.Capabilities.Audio, v.Capabilities.SourceRateControl, v.Capabilities.ClientDecode}
fields := []string{v.SessionID, v.GatewayID, v.Audience, v.Grant, fmt.Sprintf("%d", v.ReconnectSequence), v.ClientNonce, v.Capabilities.Transport, v.Capabilities.Framing, v.Capabilities.Media, v.Capabilities.Audio, v.Capabilities.SourceRateControl, fmt.Sprintf("%d", len(v.Capabilities.ClientDecode))}
fields = append(fields, v.Capabilities.ClientDecode...)
var transcript strings.Builder
transcript.WriteString("versevdi/tunnel-admission/v1")
for _, field := range fields {
+2 -2
View File
@@ -12,7 +12,7 @@
"2"
]
},
"generator_sha256": "922983e07a8ecc559771778fbf139155b14664742d9873be062880102777dccb",
"generator_sha256": "00fdba050eb924a54dd3d63aac0a38560341b675f0de4e3e9631ee895057a9b6",
"protocol_version": "1.0.0",
"schema_sha256": "e98c75ef81bbeac6be2b8f11202c1ffecec0aa515b48576a26756290e99d5dd8"
"schema_sha256": "3aec8dd72bdbb6b9657c8df3160252c93034c7c1032d471e01eae2ef91e47716"
}
Binary file not shown.
+147 -14
View File
@@ -1,6 +1,6 @@
// Code generated by tools/generate.py; DO NOT EDIT.
#![allow(non_snake_case)]
pub const SCHEMA_SHA256: &str = "e98c75ef81bbeac6be2b8f11202c1ffecec0aa515b48576a26756290e99d5dd8";
pub const SCHEMA_SHA256: &str = "3aec8dd72bdbb6b9657c8df3160252c93034c7c1032d471e01eae2ef91e47716";
pub const CURRENT_WIRE_VERSION: &str = "1";
pub const N_MINUS_1_WIRE_VERSION: &str = "0";
pub const N_MINUS_2_WIRE_VERSION: &str = "-1";
@@ -210,11 +210,11 @@ pub struct CapabilityProfile {
media: String,
audio: String,
sourceRateControl: String,
clientDecode: String,
clientDecode: Vec<String>,
}
impl CapabilityProfile {
pub fn new(transport: String, framing: String, media: String, audio: String, sourceRateControl: String, clientDecode: String) -> Result<Self, ValidationError> {
pub fn new(transport: String, framing: String, media: String, audio: String, sourceRateControl: String, clientDecode: Vec<String>) -> Result<Self, ValidationError> {
let value = Self { transport, framing, media, audio, sourceRateControl, clientDecode };
value.validate()?;
Ok(value)
@@ -235,9 +235,10 @@ impl CapabilityProfile {
if self.sourceRateControl.is_empty() { return Err(ValidationError::new("source_rate_control", "required")); }
if !self.sourceRateControl.is_empty() && self.sourceRateControl.len() < 1 { return Err(ValidationError::new("source_rate_control", "min_length")); }
if self.sourceRateControl.len() > 64 { return Err(ValidationError::new("source_rate_control", "max_length")); }
if self.clientDecode.is_empty() { return Err(ValidationError::new("client_decode", "required")); }
if !self.clientDecode.is_empty() && self.clientDecode.len() < 1 { return Err(ValidationError::new("client_decode", "min_length")); }
if self.clientDecode.len() > 64 { return Err(ValidationError::new("client_decode", "max_length")); }
if self.clientDecode.len() < 1 { return Err(ValidationError::new("client_decode", "min_items")); }
if self.clientDecode.len() > 2 { return Err(ValidationError::new("client_decode", "max_items")); }
for item in self.clientDecode.iter() { if item != "h264-opus" && item != "hevc-opus" { return Err(ValidationError::new("client_decode", "invalid_item")); } }
for (index, item) in self.clientDecode.iter().enumerate() { if self.clientDecode[..index].contains(item) { return Err(ValidationError::new("client_decode", "duplicate_item")); } }
Ok(())
}
pub fn transport(&self) -> &String { &self.transport }
@@ -245,7 +246,7 @@ impl CapabilityProfile {
pub fn media(&self) -> &String { &self.media }
pub fn audio(&self) -> &String { &self.audio }
pub fn sourceRateControl(&self) -> &String { &self.sourceRateControl }
pub fn clientDecode(&self) -> &String { &self.clientDecode }
pub fn clientDecode(&self) -> &Vec<String> { &self.clientDecode }
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -771,11 +772,12 @@ pub struct GatewayHeartbeat {
activeConnections: i64,
egressKbps: i64,
state: String,
telemetry: GatewayTelemetry,
}
impl GatewayHeartbeat {
pub fn new(version: String, gatewayId: String, sequence: i64, observedAt: String, activeConnections: i64, egressKbps: i64, state: String) -> Result<Self, ValidationError> {
let value = Self { version, gatewayId, sequence, observedAt, activeConnections, egressKbps, state };
pub fn new(version: String, gatewayId: String, sequence: i64, observedAt: String, activeConnections: i64, egressKbps: i64, state: String, telemetry: GatewayTelemetry) -> Result<Self, ValidationError> {
let value = Self { version, gatewayId, sequence, observedAt, activeConnections, egressKbps, state, telemetry };
value.validate()?;
Ok(value)
}
@@ -791,6 +793,7 @@ impl GatewayHeartbeat {
if self.egressKbps < 0 { return Err(ValidationError::new("egress_kbps", "minimum")); }
if self.egressKbps > 1000000000 { return Err(ValidationError::new("egress_kbps", "maximum")); }
if self.state != "ready" && self.state != "draining" && self.state != "offline" { return Err(ValidationError::new("state", "invalid_value")); }
self.telemetry.validate().map_err(|_| ValidationError::new("telemetry", "invalid_object"))?;
Ok(())
}
pub fn version(&self) -> &String { &self.version }
@@ -800,6 +803,7 @@ impl GatewayHeartbeat {
pub fn activeConnections(&self) -> &i64 { &self.activeConnections }
pub fn egressKbps(&self) -> &i64 { &self.egressKbps }
pub fn state(&self) -> &String { &self.state }
pub fn telemetry(&self) -> &GatewayTelemetry { &self.telemetry }
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -873,6 +877,92 @@ impl GatewayRegistration {
pub fn capabilities(&self) -> &CapabilityProfile { &self.capabilities }
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GatewayTelemetry {
admittedSessions: i64,
admissionRejects: i64,
reconnects: i64,
drainTransitions: i64,
mediaDrops: i64,
mediaPackets: i64,
mediaBytes: i64,
queueDelayMicros: i64,
processingDelayMicros: i64,
processingSamples: i64,
pacingDelayMicros: i64,
providerErrors: i64,
inputRejected: i64,
controlRttMicros: i64,
controlJitterMicros: i64,
controlLossPpm: i64,
pendingReliable: i64,
providerState: String,
}
impl GatewayTelemetry {
pub fn new(admittedSessions: i64, admissionRejects: i64, reconnects: i64, drainTransitions: i64, mediaDrops: i64, mediaPackets: i64, mediaBytes: i64, queueDelayMicros: i64, processingDelayMicros: i64, processingSamples: i64, pacingDelayMicros: i64, providerErrors: i64, inputRejected: i64, controlRttMicros: i64, controlJitterMicros: i64, controlLossPpm: i64, pendingReliable: i64, providerState: String) -> Result<Self, ValidationError> {
let value = Self { admittedSessions, admissionRejects, reconnects, drainTransitions, mediaDrops, mediaPackets, mediaBytes, queueDelayMicros, processingDelayMicros, processingSamples, pacingDelayMicros, providerErrors, inputRejected, controlRttMicros, controlJitterMicros, controlLossPpm, pendingReliable, providerState };
value.validate()?;
Ok(value)
}
pub fn validate(&self) -> Result<(), ValidationError> {
if self.admittedSessions < 0 { return Err(ValidationError::new("admitted_sessions", "minimum")); }
if self.admittedSessions > 9223372036854775807 { return Err(ValidationError::new("admitted_sessions", "maximum")); }
if self.admissionRejects < 0 { return Err(ValidationError::new("admission_rejects", "minimum")); }
if self.admissionRejects > 9223372036854775807 { return Err(ValidationError::new("admission_rejects", "maximum")); }
if self.reconnects < 0 { return Err(ValidationError::new("reconnects", "minimum")); }
if self.reconnects > 9223372036854775807 { return Err(ValidationError::new("reconnects", "maximum")); }
if self.drainTransitions < 0 { return Err(ValidationError::new("drain_transitions", "minimum")); }
if self.drainTransitions > 9223372036854775807 { return Err(ValidationError::new("drain_transitions", "maximum")); }
if self.mediaDrops < 0 { return Err(ValidationError::new("media_drops", "minimum")); }
if self.mediaDrops > 9223372036854775807 { return Err(ValidationError::new("media_drops", "maximum")); }
if self.mediaPackets < 0 { return Err(ValidationError::new("media_packets", "minimum")); }
if self.mediaPackets > 9223372036854775807 { return Err(ValidationError::new("media_packets", "maximum")); }
if self.mediaBytes < 0 { return Err(ValidationError::new("media_bytes", "minimum")); }
if self.mediaBytes > 9223372036854775807 { return Err(ValidationError::new("media_bytes", "maximum")); }
if self.queueDelayMicros < 0 { return Err(ValidationError::new("queue_delay_micros", "minimum")); }
if self.queueDelayMicros > 9223372036854775807 { return Err(ValidationError::new("queue_delay_micros", "maximum")); }
if self.processingDelayMicros < 0 { return Err(ValidationError::new("processing_delay_micros", "minimum")); }
if self.processingDelayMicros > 9223372036854775807 { return Err(ValidationError::new("processing_delay_micros", "maximum")); }
if self.processingSamples < 0 { return Err(ValidationError::new("processing_samples", "minimum")); }
if self.processingSamples > 9223372036854775807 { return Err(ValidationError::new("processing_samples", "maximum")); }
if self.pacingDelayMicros < 0 { return Err(ValidationError::new("pacing_delay_micros", "minimum")); }
if self.pacingDelayMicros > 9223372036854775807 { return Err(ValidationError::new("pacing_delay_micros", "maximum")); }
if self.providerErrors < 0 { return Err(ValidationError::new("provider_errors", "minimum")); }
if self.providerErrors > 9223372036854775807 { return Err(ValidationError::new("provider_errors", "maximum")); }
if self.inputRejected < 0 { return Err(ValidationError::new("input_rejected", "minimum")); }
if self.inputRejected > 9223372036854775807 { return Err(ValidationError::new("input_rejected", "maximum")); }
if self.controlRttMicros < 0 { return Err(ValidationError::new("control_rtt_micros", "minimum")); }
if self.controlRttMicros > 9223372036854775807 { return Err(ValidationError::new("control_rtt_micros", "maximum")); }
if self.controlJitterMicros < 0 { return Err(ValidationError::new("control_jitter_micros", "minimum")); }
if self.controlJitterMicros > 9223372036854775807 { return Err(ValidationError::new("control_jitter_micros", "maximum")); }
if self.controlLossPpm < 0 { return Err(ValidationError::new("control_loss_ppm", "minimum")); }
if self.controlLossPpm > 1000000 { return Err(ValidationError::new("control_loss_ppm", "maximum")); }
if self.pendingReliable < 0 { return Err(ValidationError::new("pending_reliable", "minimum")); }
if self.pendingReliable > 9223372036854775807 { return Err(ValidationError::new("pending_reliable", "maximum")); }
if self.providerState != "unknown" && self.providerState != "starting" && self.providerState != "ready" && self.providerState != "disconnected" && self.providerState != "terminating" && self.providerState != "terminated" && self.providerState != "cleanup_pending" && self.providerState != "failed" { return Err(ValidationError::new("provider_state", "invalid_value")); }
Ok(())
}
pub fn admittedSessions(&self) -> &i64 { &self.admittedSessions }
pub fn admissionRejects(&self) -> &i64 { &self.admissionRejects }
pub fn reconnects(&self) -> &i64 { &self.reconnects }
pub fn drainTransitions(&self) -> &i64 { &self.drainTransitions }
pub fn mediaDrops(&self) -> &i64 { &self.mediaDrops }
pub fn mediaPackets(&self) -> &i64 { &self.mediaPackets }
pub fn mediaBytes(&self) -> &i64 { &self.mediaBytes }
pub fn queueDelayMicros(&self) -> &i64 { &self.queueDelayMicros }
pub fn processingDelayMicros(&self) -> &i64 { &self.processingDelayMicros }
pub fn processingSamples(&self) -> &i64 { &self.processingSamples }
pub fn pacingDelayMicros(&self) -> &i64 { &self.pacingDelayMicros }
pub fn providerErrors(&self) -> &i64 { &self.providerErrors }
pub fn inputRejected(&self) -> &i64 { &self.inputRejected }
pub fn controlRttMicros(&self) -> &i64 { &self.controlRttMicros }
pub fn controlJitterMicros(&self) -> &i64 { &self.controlJitterMicros }
pub fn controlLossPpm(&self) -> &i64 { &self.controlLossPpm }
pub fn pendingReliable(&self) -> &i64 { &self.pendingReliable }
pub fn providerState(&self) -> &String { &self.providerState }
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GrantReference {
opaqueValue: String,
@@ -1108,6 +1198,7 @@ pub struct ProviderSessionWork {
providerProfile: String,
providerIdentity: String,
policyVersionId: String,
streamPolicy: ProviderStreamPolicy,
applicationId: String,
clientId: String,
managementHost: String,
@@ -1122,8 +1213,8 @@ pub struct ProviderSessionWork {
}
impl ProviderSessionWork {
pub fn new(version: String, sessionId: String, gatewayId: String, reconnectSequence: i64, expiresAt: String, providerProfile: String, providerIdentity: String, policyVersionId: String, applicationId: String, clientId: String, managementHost: String, managementPort: i64, streamHost: String, streamPort: i64, clientCertificatePem: String, clientPrivateKeyPem: String, serverCertificatePem: String, clipboardPolicy: ClipboardPolicy, providerApplicationTerminationAllowed: bool) -> Result<Self, ValidationError> {
let value = Self { version, sessionId, gatewayId, reconnectSequence, expiresAt, providerProfile, providerIdentity, policyVersionId, applicationId, clientId, managementHost, managementPort, streamHost, streamPort, clientCertificatePem, clientPrivateKeyPem, serverCertificatePem, clipboardPolicy, providerApplicationTerminationAllowed };
pub fn new(version: String, sessionId: String, gatewayId: String, reconnectSequence: i64, expiresAt: String, providerProfile: String, providerIdentity: String, policyVersionId: String, streamPolicy: ProviderStreamPolicy, applicationId: String, clientId: String, managementHost: String, managementPort: i64, streamHost: String, streamPort: i64, clientCertificatePem: String, clientPrivateKeyPem: String, serverCertificatePem: String, clipboardPolicy: ClipboardPolicy, providerApplicationTerminationAllowed: bool) -> Result<Self, ValidationError> {
let value = Self { version, sessionId, gatewayId, reconnectSequence, expiresAt, providerProfile, providerIdentity, policyVersionId, streamPolicy, applicationId, clientId, managementHost, managementPort, streamHost, streamPort, clientCertificatePem, clientPrivateKeyPem, serverCertificatePem, clipboardPolicy, providerApplicationTerminationAllowed };
value.validate()?;
Ok(value)
}
@@ -1144,6 +1235,7 @@ impl ProviderSessionWork {
if self.policyVersionId.is_empty() { return Err(ValidationError::new("policy_version_id", "required")); }
if !self.policyVersionId.is_empty() && self.policyVersionId.len() < 1 { return Err(ValidationError::new("policy_version_id", "min_length")); }
if self.policyVersionId.len() > 128 { return Err(ValidationError::new("policy_version_id", "max_length")); }
self.streamPolicy.validate().map_err(|_| ValidationError::new("stream_policy", "invalid_object"))?;
if self.applicationId.is_empty() { return Err(ValidationError::new("application_id", "required")); }
if !self.applicationId.is_empty() && self.applicationId.len() < 1 { return Err(ValidationError::new("application_id", "min_length")); }
if self.applicationId.len() > 128 { return Err(ValidationError::new("application_id", "max_length")); }
@@ -1180,6 +1272,7 @@ impl ProviderSessionWork {
pub fn providerProfile(&self) -> &String { &self.providerProfile }
pub fn providerIdentity(&self) -> &String { &self.providerIdentity }
pub fn policyVersionId(&self) -> &String { &self.policyVersionId }
pub fn streamPolicy(&self) -> &ProviderStreamPolicy { &self.streamPolicy }
pub fn applicationId(&self) -> &String { &self.applicationId }
pub fn clientId(&self) -> &String { &self.clientId }
pub fn managementHost(&self) -> &String { &self.managementHost }
@@ -1224,6 +1317,42 @@ impl ProviderState {
pub fn channels(&self) -> &Vec<String> { &self.channels }
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderStreamPolicy {
resolutionWidth: i64,
resolutionHeight: i64,
fps: i64,
codec: String,
bitrateKbps: i64,
audioEnabled: bool,
}
impl ProviderStreamPolicy {
pub fn new(resolutionWidth: i64, resolutionHeight: i64, fps: i64, codec: String, bitrateKbps: i64, audioEnabled: bool) -> Result<Self, ValidationError> {
let value = Self { resolutionWidth, resolutionHeight, fps, codec, bitrateKbps, audioEnabled };
value.validate()?;
Ok(value)
}
pub fn validate(&self) -> Result<(), ValidationError> {
if self.resolutionWidth < 320 { return Err(ValidationError::new("resolution_width", "minimum")); }
if self.resolutionWidth > 16384 { return Err(ValidationError::new("resolution_width", "maximum")); }
if self.resolutionHeight < 200 { return Err(ValidationError::new("resolution_height", "minimum")); }
if self.resolutionHeight > 8640 { return Err(ValidationError::new("resolution_height", "maximum")); }
if self.fps < 1 { return Err(ValidationError::new("fps", "minimum")); }
if self.fps > 240 { return Err(ValidationError::new("fps", "maximum")); }
if self.codec != "H264" && self.codec != "HEVC" && self.codec != "AV1" { return Err(ValidationError::new("codec", "invalid_value")); }
if self.bitrateKbps < 100 { return Err(ValidationError::new("bitrate_kbps", "minimum")); }
if self.bitrateKbps > 1000000 { return Err(ValidationError::new("bitrate_kbps", "maximum")); }
Ok(())
}
pub fn resolutionWidth(&self) -> &i64 { &self.resolutionWidth }
pub fn resolutionHeight(&self) -> &i64 { &self.resolutionHeight }
pub fn fps(&self) -> &i64 { &self.fps }
pub fn codec(&self) -> &String { &self.codec }
pub fn bitrateKbps(&self) -> &i64 { &self.bitrateKbps }
pub fn audioEnabled(&self) -> &bool { &self.audioEnabled }
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReauthGrant {
token: String,
@@ -1601,7 +1730,9 @@ impl TunnelAdmissionRequest {
pub fn capabilities(&self) -> &CapabilityProfile { &self.capabilities }
pub fn device_admission_transcript(&self) -> Vec<u8> {
let reconnect_sequence = self.reconnectSequence.to_string();
let fields = [&self.sessionId, &self.gatewayId, &self.audience, &self.grant, &reconnect_sequence, &self.clientNonce, &self.capabilities.transport, &self.capabilities.framing, &self.capabilities.media, &self.capabilities.audio, &self.capabilities.sourceRateControl, &self.capabilities.clientDecode];
let client_decode_count = self.capabilities.clientDecode.len().to_string();
let mut fields = vec![self.sessionId.as_str(), self.gatewayId.as_str(), self.audience.as_str(), self.grant.as_str(), reconnect_sequence.as_str(), self.clientNonce.as_str(), self.capabilities.transport.as_str(), self.capabilities.framing.as_str(), self.capabilities.media.as_str(), self.capabilities.audio.as_str(), self.capabilities.sourceRateControl.as_str(), client_decode_count.as_str()];
fields.extend(self.capabilities.clientDecode.iter().map(String::as_str));
let mut transcript = String::from("versevdi/tunnel-admission/v1");
for field in fields { transcript.push_str(&format!("{}:{}", field.as_bytes().len(), field)); }
transcript.into_bytes()
@@ -1631,11 +1762,13 @@ impl VersionNegotiation {
}
pub fn intersect_capability_profiles(profiles: &[CapabilityProfile]) -> Result<CapabilityProfile, ValidationError> {
let selected = profiles.first().ok_or_else(|| ValidationError::new("capabilities", "no_overlap"))?.clone();
let mut selected = profiles.first().ok_or_else(|| ValidationError::new("capabilities", "no_overlap"))?.clone();
selected.validate().map_err(|_| ValidationError::new("capabilities", "no_overlap"))?;
for profile in &profiles[1..] {
profile.validate().map_err(|_| ValidationError::new("capabilities", "no_overlap"))?;
if profile != &selected { return Err(ValidationError::new("capabilities", "no_overlap")); }
if profile.transport != selected.transport || profile.framing != selected.framing || profile.media != selected.media || profile.audio != selected.audio || profile.sourceRateControl != selected.sourceRateControl { return Err(ValidationError::new("capabilities", "no_overlap")); }
selected.clientDecode.retain(|candidate| profile.clientDecode.contains(candidate));
if selected.clientDecode.is_empty() { return Err(ValidationError::new("capabilities", "no_overlap")); }
}
Ok(selected)
}
+187 -14
View File
@@ -1,7 +1,7 @@
// Code generated by tools/generate.py; DO NOT EDIT.
import Foundation
public typealias JSONObject = [String: String]
public let schemaSHA256 = "e98c75ef81bbeac6be2b8f11202c1ffecec0aa515b48576a26756290e99d5dd8"
public let schemaSHA256 = "3aec8dd72bdbb6b9657c8df3160252c93034c7c1032d471e01eae2ef91e47716"
public let currentWireVersion = "1"
public let nMinus1WireVersion = "0"
public let nMinus2WireVersion = "-1"
@@ -247,7 +247,7 @@ public struct CapabilityProfile: Codable, Equatable {
public let media: String
public let audio: String
public let sourceRateControl: String
public let clientDecode: String
public let clientDecode: [String]
enum CodingKeys: String, CodingKey {
case transport = "transport"
case framing = "framing"
@@ -257,7 +257,7 @@ public struct CapabilityProfile: Codable, Equatable {
case clientDecode = "client_decode"
}
public init(transport: String, framing: String, media: String, audio: String, sourceRateControl: String, clientDecode: String) throws {
public init(transport: String, framing: String, media: String, audio: String, sourceRateControl: String, clientDecode: [String]) throws {
self.transport = transport
self.framing = framing
self.media = media
@@ -271,7 +271,7 @@ public struct CapabilityProfile: Codable, Equatable {
let all = try decoder.container(keyedBy: AnyCodingKey.self)
for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") }
let c = try decoder.container(keyedBy: CodingKeys.self)
try self.init(transport: try c.decode(String.self, forKey: .transport), framing: try c.decode(String.self, forKey: .framing), media: try c.decode(String.self, forKey: .media), audio: try c.decode(String.self, forKey: .audio), sourceRateControl: try c.decode(String.self, forKey: .sourceRateControl), clientDecode: try c.decode(String.self, forKey: .clientDecode))
try self.init(transport: try c.decode(String.self, forKey: .transport), framing: try c.decode(String.self, forKey: .framing), media: try c.decode(String.self, forKey: .media), audio: try c.decode(String.self, forKey: .audio), sourceRateControl: try c.decode(String.self, forKey: .sourceRateControl), clientDecode: try c.decode([String].self, forKey: .clientDecode))
}
public func validate() throws {
@@ -290,9 +290,10 @@ public struct CapabilityProfile: Codable, Equatable {
if self.sourceRateControl.isEmpty { throw ContractValidationError(field: "source_rate_control", code: "required") }
if !self.sourceRateControl.isEmpty && self.sourceRateControl.utf8.count < 1 { throw ContractValidationError(field: "source_rate_control", code: "min_length") }
if self.sourceRateControl.utf8.count > 64 { throw ContractValidationError(field: "source_rate_control", code: "max_length") }
if self.clientDecode.isEmpty { throw ContractValidationError(field: "client_decode", code: "required") }
if !self.clientDecode.isEmpty && self.clientDecode.utf8.count < 1 { throw ContractValidationError(field: "client_decode", code: "min_length") }
if self.clientDecode.utf8.count > 64 { throw ContractValidationError(field: "client_decode", code: "max_length") }
if self.clientDecode.count < 1 { throw ContractValidationError(field: "client_decode", code: "min_items") }
if self.clientDecode.count > 2 { throw ContractValidationError(field: "client_decode", code: "max_items") }
for item in self.clientDecode where !["h264-opus", "hevc-opus"].contains(item) { throw ContractValidationError(field: "client_decode", code: "invalid_item") }
if Set(self.clientDecode).count != self.clientDecode.count { throw ContractValidationError(field: "client_decode", code: "duplicate_item") }
}
public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }
@@ -1003,6 +1004,7 @@ public struct GatewayHeartbeat: Codable, Equatable {
public let activeConnections: Int64
public let egressKbps: Int64
public let state: String
public let telemetry: GatewayTelemetry
enum CodingKeys: String, CodingKey {
case version = "version"
case gatewayId = "gateway_id"
@@ -1011,9 +1013,10 @@ public struct GatewayHeartbeat: Codable, Equatable {
case activeConnections = "active_connections"
case egressKbps = "egress_kbps"
case state = "state"
case telemetry = "telemetry"
}
public init(version: String, gatewayId: String, sequence: Int64, observedAt: String, activeConnections: Int64, egressKbps: Int64, state: String) throws {
public init(version: String, gatewayId: String, sequence: Int64, observedAt: String, activeConnections: Int64, egressKbps: Int64, state: String, telemetry: GatewayTelemetry) throws {
self.version = version
self.gatewayId = gatewayId
self.sequence = sequence
@@ -1021,6 +1024,7 @@ public struct GatewayHeartbeat: Codable, Equatable {
self.activeConnections = activeConnections
self.egressKbps = egressKbps
self.state = state
self.telemetry = telemetry
try validate()
}
@@ -1028,7 +1032,7 @@ public struct GatewayHeartbeat: Codable, Equatable {
let all = try decoder.container(keyedBy: AnyCodingKey.self)
for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") }
let c = try decoder.container(keyedBy: CodingKeys.self)
try self.init(version: try c.decode(String.self, forKey: .version), gatewayId: try c.decode(String.self, forKey: .gatewayId), sequence: try c.decode(Int64.self, forKey: .sequence), observedAt: try c.decode(String.self, forKey: .observedAt), activeConnections: try c.decode(Int64.self, forKey: .activeConnections), egressKbps: try c.decode(Int64.self, forKey: .egressKbps), state: try c.decode(String.self, forKey: .state))
try self.init(version: try c.decode(String.self, forKey: .version), gatewayId: try c.decode(String.self, forKey: .gatewayId), sequence: try c.decode(Int64.self, forKey: .sequence), observedAt: try c.decode(String.self, forKey: .observedAt), activeConnections: try c.decode(Int64.self, forKey: .activeConnections), egressKbps: try c.decode(Int64.self, forKey: .egressKbps), state: try c.decode(String.self, forKey: .state), telemetry: try c.decode(GatewayTelemetry.self, forKey: .telemetry))
}
public func validate() throws {
@@ -1044,6 +1048,7 @@ public struct GatewayHeartbeat: Codable, Equatable {
if self.egressKbps < 0 { throw ContractValidationError(field: "egress_kbps", code: "minimum") }
if self.egressKbps > 1000000000 { throw ContractValidationError(field: "egress_kbps", code: "maximum") }
if !["ready", "draining", "offline"].contains(self.state) { throw ContractValidationError(field: "state", code: "invalid_value") }
try self.telemetry.validate()
}
public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }
@@ -1141,6 +1146,117 @@ public struct GatewayRegistration: Codable, Equatable {
public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) }
}
public struct GatewayTelemetry: Codable, Equatable {
public let admittedSessions: Int64
public let admissionRejects: Int64
public let reconnects: Int64
public let drainTransitions: Int64
public let mediaDrops: Int64
public let mediaPackets: Int64
public let mediaBytes: Int64
public let queueDelayMicros: Int64
public let processingDelayMicros: Int64
public let processingSamples: Int64
public let pacingDelayMicros: Int64
public let providerErrors: Int64
public let inputRejected: Int64
public let controlRttMicros: Int64
public let controlJitterMicros: Int64
public let controlLossPpm: Int64
public let pendingReliable: Int64
public let providerState: String
enum CodingKeys: String, CodingKey {
case admittedSessions = "admitted_sessions"
case admissionRejects = "admission_rejects"
case reconnects = "reconnects"
case drainTransitions = "drain_transitions"
case mediaDrops = "media_drops"
case mediaPackets = "media_packets"
case mediaBytes = "media_bytes"
case queueDelayMicros = "queue_delay_micros"
case processingDelayMicros = "processing_delay_micros"
case processingSamples = "processing_samples"
case pacingDelayMicros = "pacing_delay_micros"
case providerErrors = "provider_errors"
case inputRejected = "input_rejected"
case controlRttMicros = "control_rtt_micros"
case controlJitterMicros = "control_jitter_micros"
case controlLossPpm = "control_loss_ppm"
case pendingReliable = "pending_reliable"
case providerState = "provider_state"
}
public init(admittedSessions: Int64, admissionRejects: Int64, reconnects: Int64, drainTransitions: Int64, mediaDrops: Int64, mediaPackets: Int64, mediaBytes: Int64, queueDelayMicros: Int64, processingDelayMicros: Int64, processingSamples: Int64, pacingDelayMicros: Int64, providerErrors: Int64, inputRejected: Int64, controlRttMicros: Int64, controlJitterMicros: Int64, controlLossPpm: Int64, pendingReliable: Int64, providerState: String) throws {
self.admittedSessions = admittedSessions
self.admissionRejects = admissionRejects
self.reconnects = reconnects
self.drainTransitions = drainTransitions
self.mediaDrops = mediaDrops
self.mediaPackets = mediaPackets
self.mediaBytes = mediaBytes
self.queueDelayMicros = queueDelayMicros
self.processingDelayMicros = processingDelayMicros
self.processingSamples = processingSamples
self.pacingDelayMicros = pacingDelayMicros
self.providerErrors = providerErrors
self.inputRejected = inputRejected
self.controlRttMicros = controlRttMicros
self.controlJitterMicros = controlJitterMicros
self.controlLossPpm = controlLossPpm
self.pendingReliable = pendingReliable
self.providerState = providerState
try validate()
}
public init(from decoder: Decoder) throws {
let all = try decoder.container(keyedBy: AnyCodingKey.self)
for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") }
let c = try decoder.container(keyedBy: CodingKeys.self)
try self.init(admittedSessions: try c.decode(Int64.self, forKey: .admittedSessions), admissionRejects: try c.decode(Int64.self, forKey: .admissionRejects), reconnects: try c.decode(Int64.self, forKey: .reconnects), drainTransitions: try c.decode(Int64.self, forKey: .drainTransitions), mediaDrops: try c.decode(Int64.self, forKey: .mediaDrops), mediaPackets: try c.decode(Int64.self, forKey: .mediaPackets), mediaBytes: try c.decode(Int64.self, forKey: .mediaBytes), queueDelayMicros: try c.decode(Int64.self, forKey: .queueDelayMicros), processingDelayMicros: try c.decode(Int64.self, forKey: .processingDelayMicros), processingSamples: try c.decode(Int64.self, forKey: .processingSamples), pacingDelayMicros: try c.decode(Int64.self, forKey: .pacingDelayMicros), providerErrors: try c.decode(Int64.self, forKey: .providerErrors), inputRejected: try c.decode(Int64.self, forKey: .inputRejected), controlRttMicros: try c.decode(Int64.self, forKey: .controlRttMicros), controlJitterMicros: try c.decode(Int64.self, forKey: .controlJitterMicros), controlLossPpm: try c.decode(Int64.self, forKey: .controlLossPpm), pendingReliable: try c.decode(Int64.self, forKey: .pendingReliable), providerState: try c.decode(String.self, forKey: .providerState))
}
public func validate() throws {
if self.admittedSessions < 0 { throw ContractValidationError(field: "admitted_sessions", code: "minimum") }
if self.admittedSessions > 9223372036854775807 { throw ContractValidationError(field: "admitted_sessions", code: "maximum") }
if self.admissionRejects < 0 { throw ContractValidationError(field: "admission_rejects", code: "minimum") }
if self.admissionRejects > 9223372036854775807 { throw ContractValidationError(field: "admission_rejects", code: "maximum") }
if self.reconnects < 0 { throw ContractValidationError(field: "reconnects", code: "minimum") }
if self.reconnects > 9223372036854775807 { throw ContractValidationError(field: "reconnects", code: "maximum") }
if self.drainTransitions < 0 { throw ContractValidationError(field: "drain_transitions", code: "minimum") }
if self.drainTransitions > 9223372036854775807 { throw ContractValidationError(field: "drain_transitions", code: "maximum") }
if self.mediaDrops < 0 { throw ContractValidationError(field: "media_drops", code: "minimum") }
if self.mediaDrops > 9223372036854775807 { throw ContractValidationError(field: "media_drops", code: "maximum") }
if self.mediaPackets < 0 { throw ContractValidationError(field: "media_packets", code: "minimum") }
if self.mediaPackets > 9223372036854775807 { throw ContractValidationError(field: "media_packets", code: "maximum") }
if self.mediaBytes < 0 { throw ContractValidationError(field: "media_bytes", code: "minimum") }
if self.mediaBytes > 9223372036854775807 { throw ContractValidationError(field: "media_bytes", code: "maximum") }
if self.queueDelayMicros < 0 { throw ContractValidationError(field: "queue_delay_micros", code: "minimum") }
if self.queueDelayMicros > 9223372036854775807 { throw ContractValidationError(field: "queue_delay_micros", code: "maximum") }
if self.processingDelayMicros < 0 { throw ContractValidationError(field: "processing_delay_micros", code: "minimum") }
if self.processingDelayMicros > 9223372036854775807 { throw ContractValidationError(field: "processing_delay_micros", code: "maximum") }
if self.processingSamples < 0 { throw ContractValidationError(field: "processing_samples", code: "minimum") }
if self.processingSamples > 9223372036854775807 { throw ContractValidationError(field: "processing_samples", code: "maximum") }
if self.pacingDelayMicros < 0 { throw ContractValidationError(field: "pacing_delay_micros", code: "minimum") }
if self.pacingDelayMicros > 9223372036854775807 { throw ContractValidationError(field: "pacing_delay_micros", code: "maximum") }
if self.providerErrors < 0 { throw ContractValidationError(field: "provider_errors", code: "minimum") }
if self.providerErrors > 9223372036854775807 { throw ContractValidationError(field: "provider_errors", code: "maximum") }
if self.inputRejected < 0 { throw ContractValidationError(field: "input_rejected", code: "minimum") }
if self.inputRejected > 9223372036854775807 { throw ContractValidationError(field: "input_rejected", code: "maximum") }
if self.controlRttMicros < 0 { throw ContractValidationError(field: "control_rtt_micros", code: "minimum") }
if self.controlRttMicros > 9223372036854775807 { throw ContractValidationError(field: "control_rtt_micros", code: "maximum") }
if self.controlJitterMicros < 0 { throw ContractValidationError(field: "control_jitter_micros", code: "minimum") }
if self.controlJitterMicros > 9223372036854775807 { throw ContractValidationError(field: "control_jitter_micros", code: "maximum") }
if self.controlLossPpm < 0 { throw ContractValidationError(field: "control_loss_ppm", code: "minimum") }
if self.controlLossPpm > 1000000 { throw ContractValidationError(field: "control_loss_ppm", code: "maximum") }
if self.pendingReliable < 0 { throw ContractValidationError(field: "pending_reliable", code: "minimum") }
if self.pendingReliable > 9223372036854775807 { throw ContractValidationError(field: "pending_reliable", code: "maximum") }
if !["unknown", "starting", "ready", "disconnected", "terminating", "terminated", "cleanup_pending", "failed"].contains(self.providerState) { throw ContractValidationError(field: "provider_state", code: "invalid_value") }
}
public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }
public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) }
}
public struct GrantReference: Codable, Equatable {
public let opaqueValue: String
public let expiresAt: String
@@ -1458,6 +1574,7 @@ public struct ProviderSessionWork: Codable, Equatable {
public let providerProfile: String
public let providerIdentity: String
public let policyVersionId: String
public let streamPolicy: ProviderStreamPolicy
public let applicationId: String
public let clientId: String
public let managementHost: String
@@ -1478,6 +1595,7 @@ public struct ProviderSessionWork: Codable, Equatable {
case providerProfile = "provider_profile"
case providerIdentity = "provider_identity"
case policyVersionId = "policy_version_id"
case streamPolicy = "stream_policy"
case applicationId = "application_id"
case clientId = "client_id"
case managementHost = "management_host"
@@ -1491,7 +1609,7 @@ public struct ProviderSessionWork: Codable, Equatable {
case providerApplicationTerminationAllowed = "provider_application_termination_allowed"
}
public init(version: String, sessionId: String, gatewayId: String, reconnectSequence: Int64, expiresAt: String, providerProfile: String, providerIdentity: String, policyVersionId: String, applicationId: String, clientId: String, managementHost: String, managementPort: Int64, streamHost: String, streamPort: Int64, clientCertificatePem: String, clientPrivateKeyPem: String, serverCertificatePem: String, clipboardPolicy: ClipboardPolicy, providerApplicationTerminationAllowed: Bool) throws {
public init(version: String, sessionId: String, gatewayId: String, reconnectSequence: Int64, expiresAt: String, providerProfile: String, providerIdentity: String, policyVersionId: String, streamPolicy: ProviderStreamPolicy, applicationId: String, clientId: String, managementHost: String, managementPort: Int64, streamHost: String, streamPort: Int64, clientCertificatePem: String, clientPrivateKeyPem: String, serverCertificatePem: String, clipboardPolicy: ClipboardPolicy, providerApplicationTerminationAllowed: Bool) throws {
self.version = version
self.sessionId = sessionId
self.gatewayId = gatewayId
@@ -1500,6 +1618,7 @@ public struct ProviderSessionWork: Codable, Equatable {
self.providerProfile = providerProfile
self.providerIdentity = providerIdentity
self.policyVersionId = policyVersionId
self.streamPolicy = streamPolicy
self.applicationId = applicationId
self.clientId = clientId
self.managementHost = managementHost
@@ -1518,7 +1637,7 @@ public struct ProviderSessionWork: Codable, Equatable {
let all = try decoder.container(keyedBy: AnyCodingKey.self)
for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") }
let c = try decoder.container(keyedBy: CodingKeys.self)
try self.init(version: try c.decode(String.self, forKey: .version), sessionId: try c.decode(String.self, forKey: .sessionId), gatewayId: try c.decode(String.self, forKey: .gatewayId), reconnectSequence: try c.decode(Int64.self, forKey: .reconnectSequence), expiresAt: try c.decode(String.self, forKey: .expiresAt), providerProfile: try c.decode(String.self, forKey: .providerProfile), providerIdentity: try c.decode(String.self, forKey: .providerIdentity), policyVersionId: try c.decode(String.self, forKey: .policyVersionId), applicationId: try c.decode(String.self, forKey: .applicationId), clientId: try c.decode(String.self, forKey: .clientId), managementHost: try c.decode(String.self, forKey: .managementHost), managementPort: try c.decode(Int64.self, forKey: .managementPort), streamHost: try c.decode(String.self, forKey: .streamHost), streamPort: try c.decode(Int64.self, forKey: .streamPort), clientCertificatePem: try c.decode(String.self, forKey: .clientCertificatePem), clientPrivateKeyPem: try c.decode(String.self, forKey: .clientPrivateKeyPem), serverCertificatePem: try c.decode(String.self, forKey: .serverCertificatePem), clipboardPolicy: try c.decode(ClipboardPolicy.self, forKey: .clipboardPolicy), providerApplicationTerminationAllowed: try c.decode(Bool.self, forKey: .providerApplicationTerminationAllowed))
try self.init(version: try c.decode(String.self, forKey: .version), sessionId: try c.decode(String.self, forKey: .sessionId), gatewayId: try c.decode(String.self, forKey: .gatewayId), reconnectSequence: try c.decode(Int64.self, forKey: .reconnectSequence), expiresAt: try c.decode(String.self, forKey: .expiresAt), providerProfile: try c.decode(String.self, forKey: .providerProfile), providerIdentity: try c.decode(String.self, forKey: .providerIdentity), policyVersionId: try c.decode(String.self, forKey: .policyVersionId), streamPolicy: try c.decode(ProviderStreamPolicy.self, forKey: .streamPolicy), applicationId: try c.decode(String.self, forKey: .applicationId), clientId: try c.decode(String.self, forKey: .clientId), managementHost: try c.decode(String.self, forKey: .managementHost), managementPort: try c.decode(Int64.self, forKey: .managementPort), streamHost: try c.decode(String.self, forKey: .streamHost), streamPort: try c.decode(Int64.self, forKey: .streamPort), clientCertificatePem: try c.decode(String.self, forKey: .clientCertificatePem), clientPrivateKeyPem: try c.decode(String.self, forKey: .clientPrivateKeyPem), serverCertificatePem: try c.decode(String.self, forKey: .serverCertificatePem), clipboardPolicy: try c.decode(ClipboardPolicy.self, forKey: .clipboardPolicy), providerApplicationTerminationAllowed: try c.decode(Bool.self, forKey: .providerApplicationTerminationAllowed))
}
public func validate() throws {
@@ -1539,6 +1658,7 @@ public struct ProviderSessionWork: Codable, Equatable {
if self.policyVersionId.isEmpty { throw ContractValidationError(field: "policy_version_id", code: "required") }
if !self.policyVersionId.isEmpty && self.policyVersionId.utf8.count < 1 { throw ContractValidationError(field: "policy_version_id", code: "min_length") }
if self.policyVersionId.utf8.count > 128 { throw ContractValidationError(field: "policy_version_id", code: "max_length") }
try self.streamPolicy.validate()
if self.applicationId.isEmpty { throw ContractValidationError(field: "application_id", code: "required") }
if !self.applicationId.isEmpty && self.applicationId.utf8.count < 1 { throw ContractValidationError(field: "application_id", code: "min_length") }
if self.applicationId.utf8.count > 128 { throw ContractValidationError(field: "application_id", code: "max_length") }
@@ -1614,6 +1734,55 @@ public struct ProviderState: Codable, Equatable {
public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) }
}
public struct ProviderStreamPolicy: Codable, Equatable {
public let resolutionWidth: Int64
public let resolutionHeight: Int64
public let fps: Int64
public let codec: String
public let bitrateKbps: Int64
public let audioEnabled: Bool
enum CodingKeys: String, CodingKey {
case resolutionWidth = "resolution_width"
case resolutionHeight = "resolution_height"
case fps = "fps"
case codec = "codec"
case bitrateKbps = "bitrate_kbps"
case audioEnabled = "audio_enabled"
}
public init(resolutionWidth: Int64, resolutionHeight: Int64, fps: Int64, codec: String, bitrateKbps: Int64, audioEnabled: Bool) throws {
self.resolutionWidth = resolutionWidth
self.resolutionHeight = resolutionHeight
self.fps = fps
self.codec = codec
self.bitrateKbps = bitrateKbps
self.audioEnabled = audioEnabled
try validate()
}
public init(from decoder: Decoder) throws {
let all = try decoder.container(keyedBy: AnyCodingKey.self)
for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") }
let c = try decoder.container(keyedBy: CodingKeys.self)
try self.init(resolutionWidth: try c.decode(Int64.self, forKey: .resolutionWidth), resolutionHeight: try c.decode(Int64.self, forKey: .resolutionHeight), fps: try c.decode(Int64.self, forKey: .fps), codec: try c.decode(String.self, forKey: .codec), bitrateKbps: try c.decode(Int64.self, forKey: .bitrateKbps), audioEnabled: try c.decode(Bool.self, forKey: .audioEnabled))
}
public func validate() throws {
if self.resolutionWidth < 320 { throw ContractValidationError(field: "resolution_width", code: "minimum") }
if self.resolutionWidth > 16384 { throw ContractValidationError(field: "resolution_width", code: "maximum") }
if self.resolutionHeight < 200 { throw ContractValidationError(field: "resolution_height", code: "minimum") }
if self.resolutionHeight > 8640 { throw ContractValidationError(field: "resolution_height", code: "maximum") }
if self.fps < 1 { throw ContractValidationError(field: "fps", code: "minimum") }
if self.fps > 240 { throw ContractValidationError(field: "fps", code: "maximum") }
if !["H264", "HEVC", "AV1"].contains(self.codec) { throw ContractValidationError(field: "codec", code: "invalid_value") }
if self.bitrateKbps < 100 { throw ContractValidationError(field: "bitrate_kbps", code: "minimum") }
if self.bitrateKbps > 1000000 { throw ContractValidationError(field: "bitrate_kbps", code: "maximum") }
}
public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }
public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) }
}
public struct ReauthGrant: Codable, Equatable {
public let token: String
public let purpose: String
@@ -2153,7 +2322,8 @@ public struct VersionNegotiation: Codable, Equatable {
public extension TunnelAdmissionRequest {
func deviceAdmissionTranscript() -> Data {
let fields = [sessionId, gatewayId, audience, grant, String(reconnectSequence), clientNonce, capabilities.transport, capabilities.framing, capabilities.media, capabilities.audio, capabilities.sourceRateControl, capabilities.clientDecode]
var fields = [sessionId, gatewayId, audience, grant, String(reconnectSequence), clientNonce, capabilities.transport, capabilities.framing, capabilities.media, capabilities.audio, capabilities.sourceRateControl, String(capabilities.clientDecode.count)]
fields.append(contentsOf: capabilities.clientDecode)
var transcript = "versevdi/tunnel-admission/v1"
for field in fields { transcript += "\(field.utf8.count):\(field)" }
return Data(transcript.utf8)
@@ -2164,10 +2334,13 @@ public extension CapabilityProfile {
static func intersection(_ profiles: [CapabilityProfile]) throws -> CapabilityProfile {
guard let selected = profiles.first else { throw ContractValidationError(field: "capabilities", code: "no_overlap") }
try selected.validate()
var common = selected.clientDecode
for profile in profiles.dropFirst() {
try profile.validate()
if profile != selected { throw ContractValidationError(field: "capabilities", code: "no_overlap") }
if profile.transport != selected.transport || profile.framing != selected.framing || profile.media != selected.media || profile.audio != selected.audio || profile.sourceRateControl != selected.sourceRateControl { throw ContractValidationError(field: "capabilities", code: "no_overlap") }
common = common.filter { profile.clientDecode.contains($0) }
if common.isEmpty { throw ContractValidationError(field: "capabilities", code: "no_overlap") }
}
return selected
return try CapabilityProfile(transport: selected.transport, framing: selected.framing, media: selected.media, audio: selected.audio, sourceRateControl: selected.sourceRateControl, clientDecode: common)
}
}
@@ -14,16 +14,16 @@
- [x] 2.1 Run Protocol validation and the Go, Rust, and Swift conformance
consumers against the new fixtures.
- [ ] 2.2 Advance the Data Plane to the final immutable Protocol revision and
- [x] 2.2 Advance the Data Plane to the final immutable Protocol revision and
translate only the typed envelopes to provider control packets.
- [x] 2.3 Add deterministic host-feedback forwarding and input-release tests
without provider endpoint or credential disclosure.
- [ ] 2.4 Advance the Data Plane and Connection Server to the final clipboard
- [x] 2.4 Advance the Data Plane and Connection Server to the final clipboard
contract and prove disabled direction, malformed/oversized text, rate, loop,
and file/binary rejection against the authenticated provider path.
## 3. Freeze
- [ ] 3.1 Reconcile the canonical specification, OpenSpec tasks, source
- [x] 3.1 Reconcile the canonical specification, OpenSpec tasks, source
provenance, and consumer fixture digest before creating a new immutable
Protocol release candidate.
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-29
@@ -0,0 +1,38 @@
## Context
The Data Plane already collects process-wide atomic counters and gauges. Only active connections and egress Kbps cross the authenticated heartbeat boundary.
## Goals / Non-Goals
**Goals:**
- Carry the existing low-cardinality observations with explicit units.
- Bound every numeric field and enumerate provider state.
- Keep registration capacity distinct from measured traffic.
**Non-Goals:**
- Add session, route, endpoint, credential, label, or payload fields.
- Define a new telemetry transport.
- Publish a Protocol version.
## Decisions
- Nest the values in required `GatewayTelemetry` so heartbeat telemetry is one strict atomic contract.
- Use cumulative counters and microsecond delay totals plus one processing sample per complete provider media unit; consumers can derive rates/averages without losing raw observations.
- Define queue delay as residence in the bounded provider queue, processing as active recovery/framing/QUIC work excluding queue and scheduler waits, and pacing as scheduler wait only.
- Derive measured egress from transmitted-byte deltas over monotonic elapsed time; configured capacity remains registration data.
- Keep loss as parts per million and provider state as a bounded enum.
## Risks / Trade-offs
- [Cumulative counters approach signed integer limits] → Bound at signed 64-bit and saturate consumer conversions.
- [New required object breaks RC6] → Test locally and publish only under separate immutable-version authorization.
## Migration Plan
Regenerate all bindings locally, update both consumers through the temporary workspace, and stop at the immutable publication boundary.
## Open Questions
None.
@@ -0,0 +1,23 @@
## Why
Gateway heartbeat currently carries active sessions and one egress rate but cannot transport the required observed process and provider-path telemetry.
## What Changes
- Add one required bounded low-cardinality telemetry object to authenticated gateway heartbeat.
- Cover counters, delay totals/samples, control RTT/loss/jitter, pending reliability, reconnects, and provider state.
- Keep configured capacity exclusively in registration and measured egress in heartbeat.
## Capabilities
### New Capabilities
- `gateway-heartbeat-telemetry`: Authenticated heartbeats carry bounded observed gateway telemetry without routes, sessions, credentials, or payload data.
### Modified Capabilities
None.
## Impact
The control-v1 schema, generated Go/Rust/Swift bindings, conformance checks, and both unpublished consumers require coordinated local updates. RC6 remains unchanged.
@@ -0,0 +1,22 @@
## ADDED Requirements
### Requirement: Heartbeat carries observed gateway telemetry
Every authenticated `GatewayHeartbeat` SHALL carry the bounded process-level counters, delay totals and samples, control RTT/loss/jitter, pending reliable work, reconnect count, and provider state defined by `GatewayTelemetry`.
#### Scenario: Valid telemetry heartbeat
- **WHEN** a gateway reports its current observed snapshot
- **THEN** Go, Rust, and Swift bindings accept the same bounded low-cardinality values and units
### Requirement: Heartbeat telemetry excludes sensitive dimensions
Heartbeat telemetry MUST reject unknown fields and MUST NOT include session, route, endpoint, credential, label, or payload values.
#### Scenario: Secret or high-cardinality field is attempted
- **WHEN** a heartbeat contains an unregistered session, route, endpoint, credential, or payload field
- **THEN** strict contract validation rejects it before authenticated transport
### Requirement: Delay and egress observations have one canonical meaning
Queue delay SHALL measure provider-queue residence, processing delay SHALL measure active gateway recovery/framing/QUIC work excluding queue and pacing, and pacing delay SHALL measure scheduler waiting only. Processing samples SHALL count complete provider media units rather than Verse fragments. Measured egress SHALL derive from transmitted-byte deltas over monotonic elapsed time and MUST NOT be copied from configured capacity.
#### Scenario: One provider unit becomes multiple Verse frames
- **WHEN** one complete provider unit waits in the queue, traverses gateway processing, waits for pacing, and fragments into multiple Verse frames
- **THEN** each delay total includes only its defined interval and the heartbeat advances processing samples exactly once
@@ -0,0 +1,12 @@
## 1. Contract
- [x] 1.1 Add bounded GatewayTelemetry to every heartbeat
- [x] 1.2 Add Go, Rust, and Swift strict conformance checks
- [x] 1.3 Regenerate bindings and prove deterministic output
- [x] 1.4 Specify queue, processing, pacing, sample, and measured-egress semantics
## 2. Consumer Boundary
- [x] 2.1 Verify local Data Plane and Server consumers through a temporary workspace
- [ ] 2.2 Publish one new never-reused immutable Protocol version under separate authorization
- [ ] 2.3 Resolve from empty caches and pin exact checksums in both consumers
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-29
@@ -0,0 +1,42 @@
## Context
The Server resolves an immutable stream-policy version, but RC6 provider work carries only its identifier. The Data Plane consequently cannot distinguish the authorized settings from local defaults.
## Goals / Non-Goals
**Goals:**
- Carry only the effective launch settings required by the provider boundary.
- Express client decode support as an ordered set of existing registered profiles.
- Generate the same ordered registered-profile intersection for every consumer.
- Generate identical validation from the canonical schema for all bindings.
- Preserve the policy-version identifier for audit correlation.
**Non-Goals:**
- Publish or mutate RC6.
- Add a provider-specific token grammar or generic capability framework.
- Expose provider work or policy internals to Verse clients.
## Decisions
- Use one required nested `ProviderStreamPolicy` value in `ProviderSessionWork`; this keeps the policy settings atomic and avoids repeating validation.
- Carry the Server-selected target bitrate rather than all policy bounds because Apollo ANNOUNCE consumes one configured bitrate.
- Permit canonical `H264`, `HEVC`, and `AV1` values in the contract. A provider implementation must reject values it cannot honor rather than silently downgrade them.
- Carry `audio_enabled` even though the current Apollo path cannot truthfully disable audio; the Data Plane must fail closed for that combination.
- Change `client_decode` from one opaque string to a non-empty ordered unique array of registered profile identifiers. Preference belongs to the first peer's order.
- Generate `IntersectCapabilityProfiles` from the canonical schema so Protocol, Server, and Data Plane do not maintain separate interpretations.
## Risks / Trade-offs
- [New required field breaks RC6 consumers] → Publish only under a separately authorized new immutable version and pin both consumers after empty-cache resolution.
- [Provider capabilities differ] → Validate the effective policy against the selected provider before readiness.
- [Peers advertise no common registered profile] → Reject admission instead of inventing a combined token or silently downgrading.
## Migration Plan
Regenerate and verify bindings locally, update both consumers through a temporary workspace only, then stop at the publication boundary. RC6 remains unchanged.
## Open Questions
None.
@@ -0,0 +1,24 @@
## Why
Provider work identifies an immutable stream-policy version but omits the effective settings, allowing a gateway to launch Apollo with unrelated hard-coded media parameters.
## What Changes
- Add the effective resolution, frame rate, codec, selected bitrate, and audio policy to authenticated provider work.
- Represent decode support as an ordered set of registered profiles and generate one canonical intersection operation for consumers.
- Require generated Go, Rust, and Swift bindings to validate the same bounded stream-policy contract.
- Keep the new contract unpublished until a new immutable Protocol version is separately authorized.
## Capabilities
### New Capabilities
- `provider-stream-policy`: Authenticated provider work carries the exact effective stream policy consumed by the provider launch, and registered peers negotiate that policy through the shared ordered profile intersection.
### Modified Capabilities
None.
## Impact
The control-v1 schema, generated bindings, conformance fixtures, and downstream Server and Data Plane consumers require coordinated local updates. RC6 remains immutable and unchanged.
@@ -0,0 +1,33 @@
## ADDED Requirements
### Requirement: Provider work carries the effective stream policy
Authenticated `ProviderSessionWork` SHALL carry the immutable policy version and its effective resolution, frame rate, codec, target bitrate, and audio-enabled decision.
#### Scenario: Gateway receives an effective policy
- **WHEN** the Server issues provider work for an admitted session
- **THEN** the work identifies the policy version and includes the effective bounded stream-policy values
### Requirement: Stream-policy bindings share one strict contract
Generated Go, Rust, and Swift bindings MUST reject missing, unknown, out-of-range, or unsupported stream-policy wire values according to the canonical schema.
#### Scenario: Invalid policy is rejected consistently
- **WHEN** provider work contains an unknown codec or a value outside the canonical bounds
- **THEN** every generated binding rejects the work before it can reach provider setup
### Requirement: Decode capabilities use registered ordered profiles
`CapabilityProfile.client_decode` SHALL be a non-empty ordered unique set containing only registered `h264-opus` and `hevc-opus` profile identifiers. It MUST NOT encode multiple capabilities in an opaque private token.
#### Scenario: Independent peer advertises one registered profile
- **WHEN** an independent peer advertises one registered decode profile
- **THEN** canonical validation accepts that profile without requiring a combined private token
### Requirement: Consumers share one ordered registered-profile intersection
Generated Protocol behavior SHALL select common registered profiles in the first peer's preference order. Provider consumers SHALL separately reject the resulting intersection when it cannot honor the immutable stream policy.
#### Scenario: Policy-compatible profile overlaps
- **WHEN** the gateway advertises HEVC then H.264 and the client advertises only H.264
- **THEN** the shared intersection selects `h264-opus`
#### Scenario: No policy-compatible profile overlaps
- **WHEN** peers have no registered common profile
- **THEN** the shared intersection rejects admission without inventing a private combined token
@@ -0,0 +1,13 @@
## 1. Contract
- [x] 1.1 Add bounded effective stream policy to ProviderSessionWork
- [x] 1.2 Add Go, Rust, and Swift conformance coverage
- [x] 1.3 Regenerate bindings and prove deterministic output
- [x] 1.4 Replace the opaque decode token with an ordered unique set of registered profiles
- [x] 1.5 Generate and cross-check canonical ordered registered-profile intersection behavior
## 2. Consumer Boundary
- [x] 2.1 Verify local Server and Data Plane consumers through a temporary workspace
- [ ] 2.2 Publish one new never-reused immutable Protocol version under separate authorization
- [ ] 2.3 Resolve from empty caches and pin exact checksums in both consumers
@@ -0,0 +1,83 @@
# gateway-input-feedback Specification
## Purpose
Define the provider-neutral typed input, feedback, and text-clipboard envelopes
used across the authenticated Verse gateway boundary.
## Requirements
### Requirement: Typed sequenced input envelope
The `input.sequenced.v1` payload SHALL begin with ASCII `VGI1`, a one-byte
event kind, and one-byte payload length. It SHALL contain exactly one bounded
keyboard, mouse-button, relative-mouse, UTF-8 scalar, or controller-state
event. False keyboard/mouse state and zeroed controller state are explicit
releases. Multibyte integer fields SHALL be big-endian. Unknown kinds,
length mismatches, malformed UTF-8, unsupported controller indices, and
reserved fields SHALL be rejected before provider translation.
#### Scenario: Keyboard state change
- **WHEN** a client sends a valid keyboard press or release envelope
- **THEN** the gateway forwards the corresponding typed provider input on its
reliable keyboard channel and records the pressed state for cleanup.
#### Scenario: Invalid input envelope
- **WHEN** a client sends an envelope with an unknown event kind, invalid
length, malformed UTF-8 scalar, or nonzero reserved field
- **THEN** the gateway rejects it without sending provider input or changing
pressed state.
### Requirement: Explicit input release
The typed input envelope SHALL represent release of each keyboard key,
mouse button, and controller state. Gateway cleanup SHALL send a typed release
for every accepted pressed state before provider disconnect; it SHALL NOT use
an implementation-specific release-all provider command.
#### Scenario: Tunnel cleanup with pressed input
- **WHEN** a tunnel closes after accepted pressed keyboard, mouse, or
controller input
- **THEN** the gateway emits the corresponding individual provider release
packets reliably before starting provider disconnect.
### Requirement: Bounded provider feedback control envelope
The registered bidirectional reliable `control.ack.v1` flow SHALL define an ASCII `VGF1` envelope
with a direction byte, type byte, big-endian payload length, and exact payload
bytes. Only host termination, rumble, and HDR feedback SHALL be valid from the
gateway to the client; only IDR and FEC/loss feedback SHALL be valid from the
client to the gateway. The envelope SHALL contain no provider address,
certificate, credential, or opaque provider packet.
#### Scenario: Host termination forwarding
- **WHEN** the Apollo adapter receives an authenticated host termination
packet
- **THEN** the gateway forwards a bounded `VGF1` termination envelope over
reliable Verse control and reports the provider state separately.
#### Scenario: Unauthorized or malformed feedback
- **WHEN** feedback is disabled by policy, has an invalid direction/type/length,
or contains a forbidden provider field
- **THEN** the gateway rejects it without forwarding or provider mutation.
### Requirement: Policy-bound text clipboard envelope
The reliable `clipboard.text.v1` flow SHALL carry only a typed UTF-8 text
envelope with exact direction and a 16--128 character canonical unpadded ASCII
base64url loop token. The Server SHALL mint
the enabled directions, maximum text bytes, and maximum updates per minute in
authenticated provider work. The gateway SHALL reject disabled direction,
unknown fields, files, file URLs, client folders, binary data, malformed UTF-8,
oversized values, rates above policy, and reflected/replayed loop tokens. It
SHALL not put clipboard content, provider routes, or credentials in telemetry,
audit, state, or errors.
#### Scenario: Clipboard audit metadata
- **WHEN** the gateway successfully delivers, suppresses, or rejects a clipboard update
- **THEN** it sends an authenticated Server audit record with only direction,
bounded byte count, outcome, and a fixed reason; it never includes text or
the loop token.
#### Scenario: Clipboard delivery failure
- **WHEN** provider-to-client control delivery fails
- **THEN** the gateway does not report the update as forwarded.
#### Scenario: Reflected clipboard value
- **WHEN** a client-originated text value returns from the provider with the
matching retained token/value pair
- **THEN** the gateway suppresses the reflected update without a second
provider mutation or client delivery.
+1 -1
View File
@@ -56,7 +56,7 @@ message CapabilityProfile {
string media = 3;
string audio = 4;
string source_rate_control = 5;
string client_decode = 6;
repeated string client_decode = 6;
}
message GatewayRegistration {
+50 -4
View File
@@ -376,7 +376,13 @@
"media": {"type": "string", "minLength": 1, "maxLength": 64},
"audio": {"type": "string", "minLength": 1, "maxLength": 64},
"source_rate_control": {"type": "string", "minLength": 1, "maxLength": 64},
"client_decode": {"type": "string", "minLength": 1, "maxLength": 64}
"client_decode": {
"type": "array",
"minItems": 1,
"maxItems": 2,
"uniqueItems": true,
"items": {"type": "string", "enum": ["h264-opus", "hevc-opus"]}
}
}
},
"GatewayRegistration": {
@@ -399,10 +405,35 @@
"capabilities": {"$ref": "#/$defs/CapabilityProfile"}
}
},
"GatewayTelemetry": {
"type": "object",
"additionalProperties": false,
"required": ["admitted_sessions", "admission_rejects", "reconnects", "drain_transitions", "media_drops", "media_packets", "media_bytes", "queue_delay_micros", "processing_delay_micros", "processing_samples", "pacing_delay_micros", "provider_errors", "input_rejected", "control_rtt_micros", "control_jitter_micros", "control_loss_ppm", "pending_reliable", "provider_state"],
"properties": {
"admitted_sessions": {"type": "integer", "minimum": 0, "maximum": 9223372036854775807},
"admission_rejects": {"type": "integer", "minimum": 0, "maximum": 9223372036854775807},
"reconnects": {"type": "integer", "minimum": 0, "maximum": 9223372036854775807},
"drain_transitions": {"type": "integer", "minimum": 0, "maximum": 9223372036854775807},
"media_drops": {"type": "integer", "minimum": 0, "maximum": 9223372036854775807},
"media_packets": {"type": "integer", "minimum": 0, "maximum": 9223372036854775807},
"media_bytes": {"type": "integer", "minimum": 0, "maximum": 9223372036854775807},
"queue_delay_micros": {"type": "integer", "minimum": 0, "maximum": 9223372036854775807},
"processing_delay_micros": {"type": "integer", "minimum": 0, "maximum": 9223372036854775807},
"processing_samples": {"type": "integer", "minimum": 0, "maximum": 9223372036854775807},
"pacing_delay_micros": {"type": "integer", "minimum": 0, "maximum": 9223372036854775807},
"provider_errors": {"type": "integer", "minimum": 0, "maximum": 9223372036854775807},
"input_rejected": {"type": "integer", "minimum": 0, "maximum": 9223372036854775807},
"control_rtt_micros": {"type": "integer", "minimum": 0, "maximum": 9223372036854775807},
"control_jitter_micros": {"type": "integer", "minimum": 0, "maximum": 9223372036854775807},
"control_loss_ppm": {"type": "integer", "minimum": 0, "maximum": 1000000},
"pending_reliable": {"type": "integer", "minimum": 0, "maximum": 9223372036854775807},
"provider_state": {"type": "string", "enum": ["unknown", "starting", "ready", "disconnected", "terminating", "terminated", "cleanup_pending", "failed"]}
}
},
"GatewayHeartbeat": {
"type": "object",
"additionalProperties": false,
"required": ["version", "gateway_id", "sequence", "observed_at", "active_connections", "egress_kbps", "state"],
"required": ["version", "gateway_id", "sequence", "observed_at", "active_connections", "egress_kbps", "state", "telemetry"],
"properties": {
"version": {"type": "string", "const": "1"},
"gateway_id": {"type": "string", "minLength": 1, "maxLength": 128},
@@ -410,7 +441,8 @@
"observed_at": {"type": "string", "format": "date-time", "maxLength": 64},
"active_connections": {"type": "integer", "minimum": 0, "maximum": 1000000},
"egress_kbps": {"type": "integer", "minimum": 0, "maximum": 1000000000},
"state": {"type": "string", "enum": ["ready", "draining", "offline"]}
"state": {"type": "string", "enum": ["ready", "draining", "offline"]},
"telemetry": {"$ref": "#/$defs/GatewayTelemetry"}
}
},
"GatewayDrain": {
@@ -457,10 +489,23 @@
"provider_identity": {"type": "string", "minLength": 1, "maxLength": 256}
}
},
"ProviderStreamPolicy": {
"type": "object",
"additionalProperties": false,
"required": ["resolution_width", "resolution_height", "fps", "codec", "bitrate_kbps", "audio_enabled"],
"properties": {
"resolution_width": {"type": "integer", "minimum": 320, "maximum": 16384},
"resolution_height": {"type": "integer", "minimum": 200, "maximum": 8640},
"fps": {"type": "integer", "minimum": 1, "maximum": 240},
"codec": {"type": "string", "enum": ["H264", "HEVC", "AV1"]},
"bitrate_kbps": {"type": "integer", "minimum": 100, "maximum": 1000000},
"audio_enabled": {"type": "boolean"}
}
},
"ProviderSessionWork": {
"type": "object",
"additionalProperties": false,
"required": ["version", "session_id", "gateway_id", "reconnect_sequence", "expires_at", "provider_profile", "provider_identity", "policy_version_id", "application_id", "client_id", "management_host", "management_port", "stream_host", "stream_port", "client_certificate_pem", "client_private_key_pem", "server_certificate_pem", "clipboard_policy", "provider_application_termination_allowed"],
"required": ["version", "session_id", "gateway_id", "reconnect_sequence", "expires_at", "provider_profile", "provider_identity", "policy_version_id", "stream_policy", "application_id", "client_id", "management_host", "management_port", "stream_host", "stream_port", "client_certificate_pem", "client_private_key_pem", "server_certificate_pem", "clipboard_policy", "provider_application_termination_allowed"],
"properties": {
"version": {"type": "string", "const": "1"},
"session_id": {"type": "string", "minLength": 1, "maxLength": 128},
@@ -470,6 +515,7 @@
"provider_profile": {"type": "string", "const": "apollo"},
"provider_identity": {"type": "string", "minLength": 1, "maxLength": 256},
"policy_version_id": {"type": "string", "minLength": 1, "maxLength": 128},
"stream_policy": {"$ref": "#/$defs/ProviderStreamPolicy"},
"application_id": {"type": "string", "minLength": 1, "maxLength": 128},
"client_id": {"type": "string", "minLength": 1, "maxLength": 128},
"management_host": {"type": "string", "minLength": 1, "maxLength": 256},
+68 -10
View File
@@ -1,6 +1,7 @@
package protocol_test
import (
"reflect"
"strings"
"testing"
@@ -37,7 +38,7 @@ func TestGeneratedDecodersRejectMissingRequiredFieldsAndTrailingValues(t *testin
}
func TestGatewayContractsRejectUnknownVersionsAndFields(t *testing.T) {
registration := `{"version":"1","gateway_id":"gateway-1","instance_identity":"instance-1","certificate_identity":"cert-1","public_identity":"public-1","address":"gateway.test:443","provider_identity":"apollo-provider-1","protocol_min_version":1,"protocol_max_version":1,"connection_capacity":8,"bandwidth_capacity_kbps":100000,"features":["datagram.media"],"capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":"h264-opus"}}`
registration := `{"version":"1","gateway_id":"gateway-1","instance_identity":"instance-1","certificate_identity":"cert-1","public_identity":"public-1","address":"gateway.test:443","provider_identity":"apollo-provider-1","protocol_min_version":1,"protocol_max_version":1,"connection_capacity":8,"bandwidth_capacity_kbps":100000,"features":["datagram.media"],"capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":["h264-opus"]}}`
if _, err := protocol.DecodeGatewayRegistration([]byte(registration)); err != nil {
t.Fatalf("valid gateway registration rejected: %v", err)
}
@@ -56,31 +57,78 @@ func TestGatewayContractsRejectUnknownVersionsAndFields(t *testing.T) {
}
func TestGatewayRegistrationRejectsInvertedProtocolBounds(t *testing.T) {
registration := `{"version":"1","gateway_id":"gateway-1","instance_identity":"instance-1","certificate_identity":"cert-1","public_identity":"public-1","address":"gateway.test:443","provider_identity":"apollo-provider-1","protocol_min_version":2,"protocol_max_version":1,"connection_capacity":8,"bandwidth_capacity_kbps":100000,"features":["datagram.media"],"capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":"h264-opus"}}`
registration := `{"version":"1","gateway_id":"gateway-1","instance_identity":"instance-1","certificate_identity":"cert-1","public_identity":"public-1","address":"gateway.test:443","provider_identity":"apollo-provider-1","protocol_min_version":2,"protocol_max_version":1,"connection_capacity":8,"bandwidth_capacity_kbps":100000,"features":["datagram.media"],"capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":["h264-opus"]}}`
if _, err := protocol.DecodeGatewayRegistration([]byte(registration)); err == nil {
t.Fatal("DecodeGatewayRegistration accepted inverted protocol bounds")
}
}
func TestGatewayHeartbeatCarriesBoundedObservedTelemetry(t *testing.T) {
valid := `{"version":"1","gateway_id":"gateway-1","sequence":1,"observed_at":"2099-01-01T00:00:00Z","active_connections":1,"egress_kbps":64,"state":"ready","telemetry":{"admitted_sessions":2,"admission_rejects":3,"reconnects":4,"drain_transitions":5,"media_drops":6,"media_packets":7,"media_bytes":8000,"queue_delay_micros":9,"processing_delay_micros":10,"processing_samples":11,"pacing_delay_micros":12,"provider_errors":13,"input_rejected":14,"control_rtt_micros":15,"control_jitter_micros":16,"control_loss_ppm":17,"pending_reliable":18,"provider_state":"ready"}}`
if _, err := protocol.DecodeGatewayHeartbeat([]byte(valid)); err != nil {
t.Fatalf("valid gateway heartbeat rejected: %v", err)
}
for _, invalid := range []string{
strings.Replace(valid, `,"telemetry":{`, `,"session_id":"forbidden","telemetry":{`, 1),
strings.Replace(valid, `"control_loss_ppm":17`, `"control_loss_ppm":1000001`, 1),
strings.Replace(valid, `"provider_state":"ready"`, `"provider_state":"provider.example:47984"`, 1),
} {
if _, err := protocol.DecodeGatewayHeartbeat([]byte(invalid)); err == nil {
t.Fatalf("invalid gateway heartbeat accepted: %s", invalid)
}
}
}
func TestCapabilityIntersectionRejectsNoOverlap(t *testing.T) {
first := protocol.CapabilityProfile{Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded", SourceRateControl: "server", ClientDecode: "h264-opus"}
if got, err := protocol.IntersectCapabilityProfiles(first, first); err != nil || got != first {
first := protocol.CapabilityProfile{Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded", SourceRateControl: "server", ClientDecode: []string{"h264-opus"}}
if got, err := protocol.IntersectCapabilityProfiles(first, first); err != nil || !reflect.DeepEqual(got, first) {
t.Fatalf("IntersectCapabilityProfiles matching profiles = %+v, %v", got, err)
}
second := first
second.ClientDecode = "hevc-opus"
second.ClientDecode = []string{"hevc-opus"}
if _, err := protocol.IntersectCapabilityProfiles(first, second); err == nil {
t.Fatal("IntersectCapabilityProfiles accepted profiles without a common codec profile")
}
}
func TestCapabilityIntersectionSelectsRegisteredOrderedProfiles(t *testing.T) {
gateway := protocol.CapabilityProfile{
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded",
SourceRateControl: "server", ClientDecode: []string{"hevc-opus", "h264-opus"},
}
h264Client := gateway
h264Client.ClientDecode = []string{"h264-opus"}
selected, err := protocol.IntersectCapabilityProfiles(gateway, h264Client)
if err != nil || !reflect.DeepEqual(selected.ClientDecode, []string{"h264-opus"}) {
t.Fatalf("H.264 profile intersection = %+v, %v", selected, err)
}
hevcClient := gateway
hevcClient.ClientDecode = []string{"hevc-opus"}
selected, err = protocol.IntersectCapabilityProfiles(gateway, hevcClient)
if err != nil || !reflect.DeepEqual(selected.ClientDecode, []string{"hevc-opus"}) {
t.Fatalf("HEVC profile intersection = %+v, %v", selected, err)
}
noOverlap := gateway
noOverlap.ClientDecode = []string{"h264-opus"}
if _, err := protocol.IntersectCapabilityProfiles(noOverlap, hevcClient); err == nil {
t.Fatal("intersection accepted registered profiles without overlap")
}
for _, invalid := range [][]string{{"h264-hevc-opus"}, {"h264-opus", "h264-opus"}} {
profile := gateway
profile.ClientDecode = invalid
if err := profile.Validate(); err == nil {
t.Fatalf("CapabilityProfile accepted invalid registered profile set %q", invalid)
}
}
}
func TestTunnelAdmissionRequiresDeviceSignature(t *testing.T) {
request := protocol.TunnelAdmissionRequest{
Version: "1", SessionID: "session-1", GatewayID: "gateway-1", Audience: "versevdi-gateway",
Grant: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_", ReconnectSequence: 0,
ClientNonce: "0123456789abcdef", Capabilities: protocol.CapabilityProfile{
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded",
SourceRateControl: "server", ClientDecode: "h264-opus",
SourceRateControl: "server", ClientDecode: []string{"h264-opus"},
},
}
if _, err := protocol.EncodeTunnelAdmissionRequest(request); err == nil {
@@ -94,17 +142,17 @@ func TestTunnelAdmissionTranscriptIsDomainSeparatedAndLengthDelimited(t *testing
Grant: strings.Repeat("g", 43), ReconnectSequence: 0, ClientNonce: strings.Repeat("n", 16),
DeviceSignature: strings.Repeat("s", 86), Capabilities: protocol.CapabilityProfile{
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded",
SourceRateControl: "server", ClientDecode: "h264-opus",
SourceRateControl: "server", ClientDecode: []string{"h264-opus"},
},
}
want := "versevdi/tunnel-admission/v17:session7:gateway8:audience43:" + strings.Repeat("g", 43) + "1:016:" + strings.Repeat("n", 16) + "10:quic-tls1311:datagram-v17:encoded7:encoded6:server9:h264-opus"
want := "versevdi/tunnel-admission/v17:session7:gateway8:audience43:" + strings.Repeat("g", 43) + "1:016:" + strings.Repeat("n", 16) + "10:quic-tls1311:datagram-v17:encoded7:encoded6:server1:19:h264-opus"
if got := string(request.DeviceAdmissionTranscript()); got != want {
t.Fatalf("DeviceAdmissionTranscript() = %q, want %q", got, want)
}
}
func TestSessionAuthorityRejectsProviderRoute(t *testing.T) {
valid := `{"version":"1","session_id":"session-1","gateway_id":"gateway-1","audience":"versevdi-gateway","reconnect_sequence":0,"expires_at":"2099-01-01T00:00:00Z","capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":"h264-opus"},"provider_profile":"apollo","provider_identity":"provider-1"}`
valid := `{"version":"1","session_id":"session-1","gateway_id":"gateway-1","audience":"versevdi-gateway","reconnect_sequence":0,"expires_at":"2099-01-01T00:00:00Z","capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":["h264-opus"]},"provider_profile":"apollo","provider_identity":"provider-1"}`
if _, err := protocol.DecodeSessionAuthority([]byte(valid)); err != nil {
t.Fatalf("valid session authority rejected: %v", err)
}
@@ -114,7 +162,7 @@ func TestSessionAuthorityRejectsProviderRoute(t *testing.T) {
}
func TestProviderSessionWorkIsStrictAndSessionBound(t *testing.T) {
valid := `{"version":"1","session_id":"session-1","gateway_id":"gateway-1","reconnect_sequence":0,"expires_at":"2099-01-01T00:00:00Z","provider_profile":"apollo","provider_identity":"provider-1","policy_version_id":"policy-1","application_id":"42","client_id":"paired-client-1","management_host":"apollo.test","management_port":47990,"stream_host":"apollo.test","stream_port":47984,"client_certificate_pem":"certificate","client_private_key_pem":"private-key","server_certificate_pem":"server-certificate","clipboard_policy":{"client_to_provider_enabled":false,"provider_to_client_enabled":false,"max_text_bytes":65536,"max_updates_per_minute":30},"provider_application_termination_allowed":false}`
valid := `{"version":"1","session_id":"session-1","gateway_id":"gateway-1","reconnect_sequence":0,"expires_at":"2099-01-01T00:00:00Z","provider_profile":"apollo","provider_identity":"provider-1","policy_version_id":"policy-1","stream_policy":{"resolution_width":2560,"resolution_height":1440,"fps":120,"codec":"HEVC","bitrate_kbps":40000,"audio_enabled":true},"application_id":"42","client_id":"paired-client-1","management_host":"apollo.test","management_port":47990,"stream_host":"apollo.test","stream_port":47984,"client_certificate_pem":"certificate","client_private_key_pem":"private-key","server_certificate_pem":"server-certificate","clipboard_policy":{"client_to_provider_enabled":false,"provider_to_client_enabled":false,"max_text_bytes":65536,"max_updates_per_minute":30},"provider_application_termination_allowed":false}`
if _, err := protocol.DecodeProviderSessionWork([]byte(valid)); err != nil {
t.Fatalf("valid provider work rejected: %v", err)
}
@@ -124,6 +172,16 @@ func TestProviderSessionWorkIsStrictAndSessionBound(t *testing.T) {
if _, err := protocol.DecodeProviderSessionWork([]byte(strings.Replace(valid, `,"clipboard_policy":{"client_to_provider_enabled":false,"provider_to_client_enabled":false,"max_text_bytes":65536,"max_updates_per_minute":30}`, "", 1))); err == nil {
t.Fatal("provider work accepted missing clipboard policy")
}
for _, invalid := range []string{
strings.Replace(valid, `,"stream_policy":{"resolution_width":2560,"resolution_height":1440,"fps":120,"codec":"HEVC","bitrate_kbps":40000,"audio_enabled":true}`, "", 1),
strings.Replace(valid, `"fps":120`, `"fps":241`, 1),
strings.Replace(valid, `"codec":"HEVC"`, `"codec":"VP9"`, 1),
strings.Replace(valid, `"audio_enabled":true`, `"audio_enabled":true,"unknown":false`, 1),
} {
if _, err := protocol.DecodeProviderSessionWork([]byte(invalid)); err == nil {
t.Fatalf("provider work accepted invalid stream policy: %s", invalid)
}
}
}
func TestGatewayClipboardAuditIsMetadataOnlyAndStrict(t *testing.T) {
+44 -11
View File
@@ -149,7 +149,13 @@ def go_validation(definition: dict[str, Any]) -> list[str]:
lines.append(f"\tif len(v.{field}) < {prop['minItems']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"min_items\"}}) }}")
if "maxItems" in prop:
lines.append(f"\tif len(v.{field}) > {prop['maxItems']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"max_items\"}}) }}")
item_ref = ref_name(prop.get("items", {}))
items = prop.get("items", {})
if "enum" in items:
allowed = " || ".join(f'item == "{value}"' for value in items["enum"])
lines.append(f"\tfor _, item := range v.{field} {{ if !({allowed}) {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_item\"}}) }} }}")
if prop.get("uniqueItems") and items.get("type") == "string":
lines.append(f"\tfor index, item := range v.{field} {{ for prior := 0; prior < index; prior++ {{ if item == v.{field}[prior] {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"duplicate_item\"}}) }} }} }}")
item_ref = ref_name(items)
if item_ref:
lines.append(f"\tfor index := range v.{field} {{ if err := v.{field}[index].Validate(); err != nil {{ violations = append(violations, FieldViolation{{Field: fmt.Sprintf(\"{prop_name}[%d]\", index), Code: \"invalid_item\"}}) }} }}")
reference = ref_name(prop)
@@ -254,16 +260,23 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
"\tif len(profiles) == 0 { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
"\tselected := profiles[0]",
"\tif err := selected.Validate(); err != nil { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
"\tcommon := append([]string(nil), selected.ClientDecode...)",
"\tfor _, profile := range profiles[1:] {",
"\t\tif err := profile.Validate(); err != nil || profile != selected { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
"\t\tif err := profile.Validate(); err != nil || profile.Transport != selected.Transport || profile.Framing != selected.Framing || profile.Media != selected.Media || profile.Audio != selected.Audio || profile.SourceRateControl != selected.SourceRateControl { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
"\t\tnext := common[:0]",
"\t\tfor _, candidate := range common { for _, offered := range profile.ClientDecode { if candidate == offered { next = append(next, candidate); break } } }",
"\t\tcommon = next",
"\t\tif len(common) == 0 { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
"\t}",
"\tselected.ClientDecode = common",
"\treturn selected, nil",
"}",
"",
])
out.extend([
"func (v TunnelAdmissionRequest) DeviceAdmissionTranscript() []byte {",
"\tfields := []string{v.SessionID, v.GatewayID, v.Audience, v.Grant, fmt.Sprintf(\"%d\", v.ReconnectSequence), v.ClientNonce, v.Capabilities.Transport, v.Capabilities.Framing, v.Capabilities.Media, v.Capabilities.Audio, v.Capabilities.SourceRateControl, v.Capabilities.ClientDecode}",
"\tfields := []string{v.SessionID, v.GatewayID, v.Audience, v.Grant, fmt.Sprintf(\"%d\", v.ReconnectSequence), v.ClientNonce, v.Capabilities.Transport, v.Capabilities.Framing, v.Capabilities.Media, v.Capabilities.Audio, v.Capabilities.SourceRateControl, fmt.Sprintf(\"%d\", len(v.Capabilities.ClientDecode))}",
"\tfields = append(fields, v.Capabilities.ClientDecode...)",
"\tvar transcript strings.Builder",
"\ttranscript.WriteString(\"versevdi/tunnel-admission/v1\")",
"\tfor _, field := range fields { fmt.Fprintf(&transcript, \"%d:%s\", len(field), field) }",
@@ -346,7 +359,13 @@ def rust_validation(definition: dict[str, Any]) -> list[str]:
lines.append(f" {prefix}if {value}.len() < {prop['minItems']} {{ return Err(ValidationError::new(\"{prop_name}\", \"min_items\")); }}")
if "maxItems" in prop:
lines.append(f" {prefix}if {value}.len() > {prop['maxItems']} {{ return Err(ValidationError::new(\"{prop_name}\", \"max_items\")); }}")
item_ref = ref_name(prop.get("items", {}))
items = prop.get("items", {})
if "enum" in items:
allowed = " && ".join(f'item != \"{item}\"' for item in items["enum"])
lines.append(f" {prefix}for item in {value}.iter() {{ if {allowed} {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_item\")); }} }}")
if prop.get("uniqueItems") and items.get("type") == "string":
lines.append(f" {prefix}for (index, item) in {value}.iter().enumerate() {{ if {value}[..index].contains(item) {{ return Err(ValidationError::new(\"{prop_name}\", \"duplicate_item\")); }} }}")
item_ref = ref_name(items)
if item_ref:
lines.append(f" {prefix}for item in {value}.iter() {{ item.validate().map_err(|_| ValidationError::new(\"{prop_name}\", \"invalid_item\"))?; }}")
reference = ref_name(prop)
@@ -440,7 +459,9 @@ def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibili
out.extend([
" pub fn device_admission_transcript(&self) -> Vec<u8> {",
" let reconnect_sequence = self.reconnectSequence.to_string();",
" let fields = [&self.sessionId, &self.gatewayId, &self.audience, &self.grant, &reconnect_sequence, &self.clientNonce, &self.capabilities.transport, &self.capabilities.framing, &self.capabilities.media, &self.capabilities.audio, &self.capabilities.sourceRateControl, &self.capabilities.clientDecode];",
" let client_decode_count = self.capabilities.clientDecode.len().to_string();",
" let mut fields = vec![self.sessionId.as_str(), self.gatewayId.as_str(), self.audience.as_str(), self.grant.as_str(), reconnect_sequence.as_str(), self.clientNonce.as_str(), self.capabilities.transport.as_str(), self.capabilities.framing.as_str(), self.capabilities.media.as_str(), self.capabilities.audio.as_str(), self.capabilities.sourceRateControl.as_str(), client_decode_count.as_str()];",
" fields.extend(self.capabilities.clientDecode.iter().map(String::as_str));",
" let mut transcript = String::from(\"versevdi/tunnel-admission/v1\");",
" for field in fields { transcript.push_str(&format!(\"{}:{}\", field.as_bytes().len(), field)); }",
" transcript.into_bytes()",
@@ -449,11 +470,13 @@ def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibili
out.extend(["}", ""])
out.extend([
"pub fn intersect_capability_profiles(profiles: &[CapabilityProfile]) -> Result<CapabilityProfile, ValidationError> {",
" let selected = profiles.first().ok_or_else(|| ValidationError::new(\"capabilities\", \"no_overlap\"))?.clone();",
" let mut selected = profiles.first().ok_or_else(|| ValidationError::new(\"capabilities\", \"no_overlap\"))?.clone();",
" selected.validate().map_err(|_| ValidationError::new(\"capabilities\", \"no_overlap\"))?;",
" for profile in &profiles[1..] {",
" profile.validate().map_err(|_| ValidationError::new(\"capabilities\", \"no_overlap\"))?;",
" if profile != &selected { return Err(ValidationError::new(\"capabilities\", \"no_overlap\")); }",
" if profile.transport != selected.transport || profile.framing != selected.framing || profile.media != selected.media || profile.audio != selected.audio || profile.sourceRateControl != selected.sourceRateControl { return Err(ValidationError::new(\"capabilities\", \"no_overlap\")); }",
" selected.clientDecode.retain(|candidate| profile.clientDecode.contains(candidate));",
" if selected.clientDecode.is_empty() { return Err(ValidationError::new(\"capabilities\", \"no_overlap\")); }",
" }",
" Ok(selected)",
"}",
@@ -502,7 +525,13 @@ def swift_validation(definition: dict[str, Any]) -> list[str]:
lines.append(f" {prefix}if {value}.count < {prop['minItems']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"min_items\") }}")
if "maxItems" in prop:
lines.append(f" {prefix}if {value}.count > {prop['maxItems']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"max_items\") }}")
item_ref = ref_name(prop.get("items", {}))
items = prop.get("items", {})
if "enum" in items:
allowed = ", ".join(f'\"{item}\"' for item in items["enum"])
lines.append(f" {prefix}for item in {value} where ![{allowed}].contains(item) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_item\") }}")
if prop.get("uniqueItems") and items.get("type") == "string":
lines.append(f" {prefix}if Set({value}).count != {value}.count {{ throw ContractValidationError(field: \"{prop_name}\", code: \"duplicate_item\") }}")
item_ref = ref_name(items)
if item_ref:
lines.append(f" {prefix}for item in {value} {{ try item.validate() }}")
reference = ref_name(prop)
@@ -583,7 +612,8 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
out.extend([
"public extension TunnelAdmissionRequest {",
" func deviceAdmissionTranscript() -> Data {",
" let fields = [sessionId, gatewayId, audience, grant, String(reconnectSequence), clientNonce, capabilities.transport, capabilities.framing, capabilities.media, capabilities.audio, capabilities.sourceRateControl, capabilities.clientDecode]",
" var fields = [sessionId, gatewayId, audience, grant, String(reconnectSequence), clientNonce, capabilities.transport, capabilities.framing, capabilities.media, capabilities.audio, capabilities.sourceRateControl, String(capabilities.clientDecode.count)]",
" fields.append(contentsOf: capabilities.clientDecode)",
" var transcript = \"versevdi/tunnel-admission/v1\"",
" for field in fields { transcript += \"\\(field.utf8.count):\\(field)\" }",
" return Data(transcript.utf8)",
@@ -594,11 +624,14 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
" static func intersection(_ profiles: [CapabilityProfile]) throws -> CapabilityProfile {",
" guard let selected = profiles.first else { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
" try selected.validate()",
" var common = selected.clientDecode",
" for profile in profiles.dropFirst() {",
" try profile.validate()",
" if profile != selected { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
" if profile.transport != selected.transport || profile.framing != selected.framing || profile.media != selected.media || profile.audio != selected.audio || profile.sourceRateControl != selected.sourceRateControl { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
" common = common.filter { profile.clientDecode.contains($0) }",
" if common.isEmpty { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
" }",
" return selected",
" return try CapabilityProfile(transport: selected.transport, framing: selected.framing, media: selected.media, audio: selected.audio, sourceRateControl: selected.sourceRateControl, clientDecode: common)",
" }",
"}",
"",
+77 -6
View File
@@ -33,7 +33,7 @@ def main() -> int:
let capability = try CapabilityProfile(
transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
audio: "encoded", sourceRateControl: "server", clientDecode: "h264-opus"
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"]
)
let request = try TunnelAdmissionRequest(
version: "1", sessionId: "session", gatewayId: "gateway", audience: "audience",
@@ -41,19 +41,26 @@ let request = try TunnelAdmissionRequest(
clientNonce: String(repeating: "n", count: 16), deviceSignature: String(repeating: "s", count: 86), capabilities: capability
)
_ = request
let transcript = "versevdi/tunnel-admission/v17:session7:gateway8:audience43:" + String(repeating: "g", count: 43) + "1:016:" + String(repeating: "n", count: 16) + "10:quic-tls1311:datagram-v17:encoded7:encoded6:server9:h264-opus"
let transcript = "versevdi/tunnel-admission/v17:session7:gateway8:audience43:" + String(repeating: "g", count: 43) + "1:016:" + String(repeating: "n", count: 16) + "10:quic-tls1311:datagram-v17:encoded7:encoded6:server1:19:h264-opus"
guard String(data: request.deviceAdmissionTranscript(), encoding: .utf8) == transcript else {
fatalError("unexpected device admission transcript")
}
let incompatible = try CapabilityProfile(
transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
audio: "encoded", sourceRateControl: "server", clientDecode: "hevc-opus"
audio: "encoded", sourceRateControl: "server", clientDecode: ["hevc-opus"]
)
let gatewayCapability = try CapabilityProfile(
transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
audio: "encoded", sourceRateControl: "server", clientDecode: ["hevc-opus", "h264-opus"]
)
do {
guard try CapabilityProfile.intersection([capability, capability]) == capability else {
fatalError("matching capability profiles did not intersect")
}
} catch { fatalError("matching capability profiles did not intersect") }
guard try CapabilityProfile.intersection([gatewayCapability, capability]).clientDecode == ["h264-opus"] else {
fatalError("ordered registered profile intersection changed")
}
do {
_ = try CapabilityProfile.intersection([capability, incompatible])
fatalError("profiles without overlap were accepted")
@@ -85,6 +92,41 @@ do {
)
fatalError("invalid allocation bounds were accepted")
} catch { }
let streamPolicy = try ProviderStreamPolicy(
resolutionWidth: 2560, resolutionHeight: 1440, fps: 120,
codec: "HEVC", bitrateKbps: 40000, audioEnabled: true
)
guard streamPolicy.codec == "HEVC" else { fatalError("stream policy changed") }
for invalid in [
{ try ProviderStreamPolicy(resolutionWidth: 319, resolutionHeight: 1440, fps: 120, codec: "HEVC", bitrateKbps: 40000, audioEnabled: true) },
{ try ProviderStreamPolicy(resolutionWidth: 2560, resolutionHeight: 1440, fps: 241, codec: "HEVC", bitrateKbps: 40000, audioEnabled: true) },
{ try ProviderStreamPolicy(resolutionWidth: 2560, resolutionHeight: 1440, fps: 120, codec: "VP9", bitrateKbps: 40000, audioEnabled: true) },
] {
do {
_ = try invalid()
fatalError("invalid stream policy was accepted")
} catch { }
}
let telemetry = try GatewayTelemetry(
admittedSessions: 1, admissionRejects: 2, reconnects: 3, drainTransitions: 4,
mediaDrops: 5, mediaPackets: 6, mediaBytes: 7, queueDelayMicros: 8,
processingDelayMicros: 9, processingSamples: 10, pacingDelayMicros: 11,
providerErrors: 12, inputRejected: 13, controlRttMicros: 14,
controlJitterMicros: 15, controlLossPpm: 16, pendingReliable: 17,
providerState: "ready"
)
guard telemetry.mediaBytes == 7 else { fatalError("gateway telemetry changed") }
do {
_ = try GatewayTelemetry(
admittedSessions: 1, admissionRejects: 2, reconnects: 3, drainTransitions: 4,
mediaDrops: 5, mediaPackets: 6, mediaBytes: 7, queueDelayMicros: 8,
processingDelayMicros: 9, processingSamples: 10, pacingDelayMicros: 11,
providerErrors: 12, inputRejected: 13, controlRttMicros: 14,
controlJitterMicros: 15, controlLossPpm: 1000001, pendingReliable: 17,
providerState: "ready"
)
fatalError("invalid gateway telemetry was accepted")
} catch { }
for text in [
String(repeating: "a", count: 65536),
String(repeating: "é", count: 32768),
@@ -118,7 +160,7 @@ do {
fn main() {
let capabilities = CapabilityProfile::new(
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
"encoded".into(), "server".into(), "h264-opus".into(),
"encoded".into(), "server".into(), vec!["h264-opus".into()],
).unwrap();
let request = TunnelAdmissionRequest::new(
"1".into(), "session".into(), "gateway".into(), "audience".into(),
@@ -126,7 +168,7 @@ fn main() {
).unwrap();
let transcript = "versevdi/tunnel-admission/v17:session7:gateway8:audience43:".to_string()
+ &"g".repeat(43) + "1:016:" + &"n".repeat(16)
+ "10:quic-tls1311:datagram-v17:encoded7:encoded6:server9:h264-opus";
+ "10:quic-tls1311:datagram-v17:encoded7:encoded6:server1:19:h264-opus";
assert_eq!(request.device_admission_transcript(), transcript.into_bytes());
assert!(TunnelAdmissionRequest::new(
"2".into(), "session".into(), "gateway".into(), "audience".into(),
@@ -143,12 +185,41 @@ fn main() {
assert!(intersect_capability_profiles(&[capabilities.clone(), capabilities.clone()]).is_ok());
let incompatible = CapabilityProfile::new(
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
"encoded".into(), "server".into(), "hevc-opus".into(),
"encoded".into(), "server".into(), vec!["hevc-opus".into()],
).unwrap();
let gateway_capability = CapabilityProfile::new(
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
"encoded".into(), "server".into(), vec!["hevc-opus".into(), "h264-opus".into()],
).unwrap();
assert_eq!(
intersect_capability_profiles(&[gateway_capability, capabilities.clone()]).unwrap().clientDecode(),
&vec!["h264-opus".to_string()],
);
assert!(intersect_capability_profiles(&[capabilities, incompatible]).is_err());
assert!(AllocationPolicy::new(
100, 50, 25, "standard".into(), "audience".into(), "verse".into(), 1, 60, 300,
).is_err());
assert!(ProviderStreamPolicy::new(
2560, 1440, 120, "HEVC".into(), 40000, true,
).is_ok());
assert!(ProviderStreamPolicy::new(
319, 1440, 120, "HEVC".into(), 40000, true,
).is_err());
assert!(ProviderStreamPolicy::new(
2560, 1440, 241, "HEVC".into(), 40000, true,
).is_err());
assert!(ProviderStreamPolicy::new(
2560, 1440, 120, "VP9".into(), 40000, true,
).is_err());
assert!(GatewayTelemetry::new(
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, "ready".into(),
).is_ok());
assert!(GatewayTelemetry::new(
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 1000001, 17, "ready".into(),
).is_err());
assert!(GatewayTelemetry::new(
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, "provider.example:47984".into(),
).is_err());
for text in [
"a".repeat(65536),
"é".repeat(32768),