diff --git a/fixtures/conformance/control-v1.tsv b/fixtures/conformance/control-v1.tsv new file mode 100644 index 0000000..ffc5d2a --- /dev/null +++ b/fixtures/conformance/control-v1.tsv @@ -0,0 +1,11 @@ +id version kind input expected +version-current 1 version 1 valid +version-n-minus-1 0 version 0 valid +version-n-minus-2 -1 version -1 valid +version-unsupported 2 version 2 invalid:unsupported_version +page-valid 1 page limit=20;cursor=opaque valid +page-limit-high 1 page limit=101 invalid:invalid_limit +manifest-valid 1 manifest version=1;gateway_id=g-1;grant=opaque-one-time-grant-value-with-at-least-43-bytes;audience=versevdi-gateway;purpose=launch;protocol=verse-gateway-v1;expires_at=2099-01-01T00:00:00Z valid +manifest-provider-field 1 manifest gateway_id=g-1;grant=g-1;audience=versevdi-gateway;purpose=launch;provider_url=https://provider.invalid invalid:forbidden_field +clipboard-text-valid 1 clipboard encoding=utf-8;text=hello%20world valid +clipboard-file 1 clipboard encoding=octet-stream;file=/tmp/a invalid:unsupported_clipboard diff --git a/fixtures/conformance/datagram-v1.tsv b/fixtures/conformance/datagram-v1.tsv new file mode 100644 index 0000000..699f0f0 --- /dev/null +++ b/fixtures/conformance/datagram-v1.tsv @@ -0,0 +1,6 @@ +id version kind input expected +valid-empty-control 1 datagram hex=564401010000000000000000000000000000010000 valid +invalid-short 1 datagram hex=564401 invalid:truncated +invalid-version 1 datagram hex=564402010000000000000000000000000000010000 invalid:unsupported_version +invalid-channel 1 datagram hex=564401990000000000000000000000000000010000 invalid:unknown_channel +invalid-length 1 datagram hex=564401010000000000000000000000000000010001 invalid:length_mismatch diff --git a/fixtures/conformance/events-v1.tsv b/fixtures/conformance/events-v1.tsv new file mode 100644 index 0000000..40d94a4 --- /dev/null +++ b/fixtures/conformance/events-v1.tsv @@ -0,0 +1,6 @@ +id version kind input expected +event-valid 1 event version=1;sequence=42;correlation_id=corr-1;payload_bytes=128 valid +event-missing-correlation 1 event version=1;sequence=42;payload_bytes=128 invalid:required +event-gap 1 event version=1;sequence=42;correlation_id=corr-1;after=1;earliest=5;payload_bytes=128 invalid:gap +event-oversized 1 event version=1;sequence=42;correlation_id=corr-1;payload_bytes=16385 invalid:payload_limit +event-unsupported-version 1 event version=2;sequence=42;correlation_id=corr-1;payload_bytes=128 invalid:unsupported_version diff --git a/fixtures/conformance/tunnel-v1.tsv b/fixtures/conformance/tunnel-v1.tsv new file mode 100644 index 0000000..c4cd4cb --- /dev/null +++ b/fixtures/conformance/tunnel-v1.tsv @@ -0,0 +1,6 @@ +id version kind input expected +tunnel-current 1 tunnel offered=1;feature=control.v1 valid +tunnel-n-minus-1 0 tunnel offered=0;feature=control.v1 valid +tunnel-n-minus-2 -1 tunnel offered=-1;feature=control.v1 valid +tunnel-unsupported 1 tunnel offered=2;feature=control.v1 invalid:unsupported_version +tunnel-no-control 1 tunnel offered=1;feature=media.video invalid:unsupported_feature diff --git a/fixtures/invalid/manifest-provider-field.json b/fixtures/invalid/manifest-provider-field.json new file mode 100644 index 0000000..59ddff3 --- /dev/null +++ b/fixtures/invalid/manifest-provider-field.json @@ -0,0 +1,30 @@ +{ + "version": "1", + "purpose": "launch", + "session_id": "session-1", + "reconnect_sequence": 0, + "gateway": { + "id": "gateway-1", + "addresses": ["gateway.control.test:443"], + "public_identity": "gateway-1" + }, + "tunnel": { + "versions": ["verse-gateway-v1/1"], + "features": ["control.v1"] + }, + "profile": { + "id": "standard", + "bounds": { + "minimum_kbps": 1000, + "target_kbps": 5000, + "maximum_kbps": 10000 + } + }, + "grant": { + "opaque_value": "opaque-one-time-grant-value-with-at-least-43-bytes", + "expires_at": "2099-01-01T00:00:00Z", + "audience": "versevdi-gateway" + }, + "correlation_id": "correlation-1", + "provider_url": "https://provider.invalid" +} diff --git a/fixtures/manifest.json b/fixtures/manifest.json new file mode 100644 index 0000000..ee365c2 --- /dev/null +++ b/fixtures/manifest.json @@ -0,0 +1,10 @@ +{ + "algorithm": "sha256(path\\0bytes\\0 sorted by path)", + "files": [ + "fixtures/conformance/control-v1.tsv", + "fixtures/conformance/datagram-v1.tsv", + "fixtures/conformance/events-v1.tsv", + "fixtures/conformance/tunnel-v1.tsv" + ], + "corpus_sha256": "c91a512dc67aa9912b31b21144be2adfeacf0dc80dd8515bd3b4a8f52977e761" +} diff --git a/fixtures/valid/manifest.json b/fixtures/valid/manifest.json new file mode 100644 index 0000000..245812d --- /dev/null +++ b/fixtures/valid/manifest.json @@ -0,0 +1,29 @@ +{ + "version": "1", + "purpose": "launch", + "session_id": "session-1", + "reconnect_sequence": 0, + "gateway": { + "id": "gateway-1", + "addresses": ["gateway.control.test:443"], + "public_identity": "gateway-1" + }, + "tunnel": { + "versions": ["verse-gateway-v1/1"], + "features": ["control.v1", "clipboard.text"] + }, + "profile": { + "id": "standard", + "bounds": { + "minimum_kbps": 1000, + "target_kbps": 5000, + "maximum_kbps": 10000 + } + }, + "grant": { + "opaque_value": "opaque-one-time-grant-value-with-at-least-43-bytes", + "expires_at": "2099-01-01T00:00:00Z", + "audience": "versevdi-gateway" + }, + "correlation_id": "correlation-1" +} diff --git a/gen/go/protocol/protocol.go b/gen/go/protocol/protocol.go new file mode 100644 index 0000000..0eb098b --- /dev/null +++ b/gen/go/protocol/protocol.go @@ -0,0 +1,2897 @@ +// Code generated by tools/generate.py; DO NOT EDIT. +package protocol + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "reflect" + "time" +) + +const SchemaSHA256 = "36e4c8bac2eae674c1eba551c6ca8c64bf80fcc092ca63ec89a2c71c2bec86e1" +const ProtocolVersion = "1.0.0" +const CurrentWireVersion = "1" +const NMinus1WireVersion = "0" +const NMinus2WireVersion = "-1" + +type FieldViolation struct { + Field string `json:"field"` + Code string `json:"code"` +} + +type ValidationError struct { + Violations []FieldViolation +} + +func (e ValidationError) Error() string { return "protocol validation failed" } + +type AllocationPolicy struct { + MinimumKbps int64 `json:"minimum_kbps"` + TargetKbps int64 `json:"target_kbps"` + MaximumKbps int64 `json:"maximum_kbps"` + Tier string `json:"tier"` + Audience string `json:"audience"` + Protocol string `json:"protocol"` + ProtocolVersion int64 `json:"protocol_version"` + GrantTTLSeconds int64 `json:"grant_ttl_seconds"` + ReservationLeaseSeconds int64 `json:"reservation_lease_seconds"` +} + +type AssignedDesktop struct { + AssignmentID string `json:"assignment_id"` + PoolID string `json:"pool_id"` + Name string `json:"name"` + Availability string `json:"availability"` +} + +type BrokerSession struct { + ID string `json:"id"` + PrincipalID string `json:"principal_id"` + PoolID string `json:"pool_id"` + AssignmentID string `json:"assignment_id,omitempty"` + State string `json:"state"` + PolicySnapshot AllocationPolicy `json:"policy_snapshot"` + ReconnectDeadline string `json:"reconnect_deadline,omitempty"` + Outcome string `json:"outcome,omitempty"` + FailureCode string `json:"failure_code,omitempty"` + CleanupState string `json:"cleanup_state"` + IdempotencyKey string `json:"idempotency_key"` + CorrelationID string `json:"correlation_id"` + RequestedAt string `json:"requested_at"` + EndedAt string `json:"ended_at,omitempty"` + Version int64 `json:"version"` +} + +type ClipboardText struct { + Text string `json:"text"` + Encoding string `json:"encoding"` +} + +type ConnectionManifest struct { + Version string `json:"version"` + Purpose string `json:"purpose"` + SessionID string `json:"session_id"` + ReconnectSequence int64 `json:"reconnect_sequence"` + Gateway ManifestGateway `json:"gateway"` + Tunnel ManifestTunnel `json:"tunnel"` + Profile ManifestProfile `json:"profile"` + Grant GrantReference `json:"grant"` + CorrelationID string `json:"correlation_id"` +} + +type DeviceChallenge struct { + DeviceID string `json:"device_id"` + ServerID string `json:"server_id"` + PrincipalID string `json:"principal_id"` + Challenge string `json:"challenge"` + ExpiresAt string `json:"expires_at"` + Algorithm string `json:"algorithm"` + SignatureFormat string `json:"signature_format"` +} + +type DeviceProofRequest struct { + Challenge string `json:"challenge"` + Signature string `json:"signature"` +} + +type DeviceRegistrationRequest struct { + Name string `json:"name"` + Platform string `json:"platform"` + DeviceSubject string `json:"device_subject"` + Algorithm string `json:"algorithm"` + PublicKey string `json:"public_key"` +} + +type EntitledPool struct { + PoolID string `json:"pool_id"` + Name string `json:"name"` + AssignmentState string `json:"assignment_state"` +} + +type ErrorEnvelope struct { + Status bool `json:"status"` + Error string `json:"error"` + Code string `json:"code"` + Message string `json:"message"` + Resolution string `json:"resolution"` + RequestID string `json:"request_id"` + Violations []FieldViolation `json:"violations"` +} + +type EventEnvelope struct { + EventID string `json:"event_id"` + Sequence int64 `json:"sequence"` + Type string `json:"type"` + Version int64 `json:"version"` + Resource ResourceLink `json:"resource"` + OccurredAt string `json:"occurred_at"` + CorrelationID string `json:"correlation_id"` + Payload map[string]any `json:"payload"` +} + +type EventResume struct { + Cursor string `json:"cursor"` + LastSequence int64 `json:"last_sequence"` +} + +type GrantReference struct { + OpaqueValue string `json:"opaque_value"` + ExpiresAt string `json:"expires_at"` + Audience string `json:"audience"` +} + +type LoginRequest struct { + Provider string `json:"provider,omitempty"` + Username string `json:"username"` + Password string `json:"password"` +} + +type ManifestBounds struct { + MinimumKbps int64 `json:"minimum_kbps"` + TargetKbps int64 `json:"target_kbps"` + MaximumKbps int64 `json:"maximum_kbps"` +} + +type ManifestGateway struct { + ID string `json:"id"` + Addresses []string `json:"addresses"` + PublicIdentity string `json:"public_identity"` +} + +type ManifestProfile struct { + ID string `json:"id"` + Bounds ManifestBounds `json:"bounds"` +} + +type ManifestTunnel struct { + Versions []string `json:"versions"` + Features []string `json:"features"` +} + +type NativeCredential struct { + DeviceID string `json:"device_id,omitempty"` + FamilyID string `json:"family_id"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpiresAt string `json:"expires_at"` + RefreshExpiresAt string `json:"refresh_expires_at,omitempty"` +} + +type PageInfo struct { + Limit int64 `json:"limit"` + NextCursor string `json:"next_cursor"` +} + +type ReauthGrant struct { + Token string `json:"token"` + Purpose string `json:"purpose"` + ExpiresAt string `json:"expires_at"` +} + +type ReauthRequest struct { + Password string `json:"password"` + Purpose string `json:"purpose"` +} + +type ReconnectRequest struct { + ClientDeviceID string `json:"client_device_id"` + DeviceKeyID string `json:"device_key_id"` + ExpectedVersion int64 `json:"expected_version"` +} + +type RefreshRequest struct { + FamilyID string `json:"family_id"` + RefreshToken string `json:"refresh_token"` +} + +type Resource struct { + ID string `json:"id"` + Kind string `json:"kind"` + Name string `json:"name"` + State string `json:"state"` + AssignmentState string `json:"assignment_state,omitempty"` + Version int64 `json:"version"` + Links []ResourceLink `json:"links"` +} + +type ResourceLink struct { + Type string `json:"type"` + ID string `json:"id"` + Version int64 `json:"version"` +} + +type ResourceList struct { + AssignedDesktops []AssignedDesktop `json:"assigned_desktops"` + EntitledPools []EntitledPool `json:"entitled_pools"` + Page PageInfo `json:"page"` +} + +type SessionRequest struct { + ClientDeviceID string `json:"client_device_id"` + DeviceKeyID string `json:"device_key_id"` + PoolID string `json:"pool_id"` + IdempotencyKey string `json:"idempotency_key"` + PolicySnapshot AllocationPolicy `json:"policy_snapshot"` +} + +type VersionNegotiation struct { + SupportedVersions []string `json:"supported_versions"` + Features []string `json:"features"` +} + +func (v AllocationPolicy) Validate() error { + var violations []FieldViolation + if v.MinimumKbps == 0 { + violations = append(violations, FieldViolation{Field: "minimum_kbps", Code: "required"}) + } + if v.MinimumKbps != 0 && v.MinimumKbps < 1 { + violations = append(violations, FieldViolation{Field: "minimum_kbps", Code: "minimum"}) + } + if v.MinimumKbps > 100000000 { + violations = append(violations, FieldViolation{Field: "minimum_kbps", Code: "maximum"}) + } + if v.TargetKbps == 0 { + violations = append(violations, FieldViolation{Field: "target_kbps", Code: "required"}) + } + if v.TargetKbps != 0 && v.TargetKbps < 1 { + violations = append(violations, FieldViolation{Field: "target_kbps", Code: "minimum"}) + } + if v.TargetKbps > 100000000 { + violations = append(violations, FieldViolation{Field: "target_kbps", Code: "maximum"}) + } + if v.MaximumKbps == 0 { + violations = append(violations, FieldViolation{Field: "maximum_kbps", Code: "required"}) + } + if v.MaximumKbps != 0 && v.MaximumKbps < 1 { + violations = append(violations, FieldViolation{Field: "maximum_kbps", Code: "minimum"}) + } + if v.MaximumKbps > 100000000 { + violations = append(violations, FieldViolation{Field: "maximum_kbps", Code: "maximum"}) + } + if v.Tier == "" { + violations = append(violations, FieldViolation{Field: "tier", Code: "required"}) + } + if v.Tier != "" && !(v.Tier == "standard" || v.Tier == "priority" || v.Tier == "premium") { + violations = append(violations, FieldViolation{Field: "tier", Code: "invalid_value"}) + } + if v.Audience == "" { + violations = append(violations, FieldViolation{Field: "audience", Code: "required"}) + } + if len(v.Audience) < 1 && v.Audience != "" { + violations = append(violations, FieldViolation{Field: "audience", Code: "min_length"}) + } + if len(v.Audience) > 256 { + violations = append(violations, FieldViolation{Field: "audience", Code: "max_length"}) + } + if v.Protocol == "" { + violations = append(violations, FieldViolation{Field: "protocol", Code: "required"}) + } + if len(v.Protocol) < 1 && v.Protocol != "" { + violations = append(violations, FieldViolation{Field: "protocol", Code: "min_length"}) + } + if len(v.Protocol) > 64 { + violations = append(violations, FieldViolation{Field: "protocol", Code: "max_length"}) + } + if v.ProtocolVersion == 0 { + violations = append(violations, FieldViolation{Field: "protocol_version", Code: "required"}) + } + if v.ProtocolVersion != 0 && v.ProtocolVersion < 1 { + violations = append(violations, FieldViolation{Field: "protocol_version", Code: "minimum"}) + } + if v.ProtocolVersion > 100 { + violations = append(violations, FieldViolation{Field: "protocol_version", Code: "maximum"}) + } + if v.GrantTTLSeconds == 0 { + violations = append(violations, FieldViolation{Field: "grant_ttl_seconds", Code: "required"}) + } + if v.GrantTTLSeconds != 0 && v.GrantTTLSeconds < 5 { + violations = append(violations, FieldViolation{Field: "grant_ttl_seconds", Code: "minimum"}) + } + if v.GrantTTLSeconds > 300 { + violations = append(violations, FieldViolation{Field: "grant_ttl_seconds", Code: "maximum"}) + } + if v.ReservationLeaseSeconds == 0 { + violations = append(violations, FieldViolation{Field: "reservation_lease_seconds", Code: "required"}) + } + if v.ReservationLeaseSeconds != 0 && v.ReservationLeaseSeconds < 5 { + violations = append(violations, FieldViolation{Field: "reservation_lease_seconds", Code: "minimum"}) + } + if v.ReservationLeaseSeconds > 3600 { + violations = append(violations, FieldViolation{Field: "reservation_lease_seconds", Code: "maximum"}) + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeAllocationPolicy(data []byte) (AllocationPolicy, error) { + var value AllocationPolicy + 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["audience"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "audience", Code: "required"}}} + } + if raw, ok := fields["grant_ttl_seconds"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "grant_ttl_seconds", Code: "required"}}} + } + if raw, ok := fields["maximum_kbps"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "maximum_kbps", Code: "required"}}} + } + if raw, ok := fields["minimum_kbps"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "minimum_kbps", Code: "required"}}} + } + if raw, ok := fields["protocol"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "protocol", Code: "required"}}} + } + if raw, ok := fields["protocol_version"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "protocol_version", Code: "required"}}} + } + if raw, ok := fields["reservation_lease_seconds"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "reservation_lease_seconds", Code: "required"}}} + } + if raw, ok := fields["target_kbps"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "target_kbps", Code: "required"}}} + } + if raw, ok := fields["tier"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "tier", 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 EncodeAllocationPolicy(value AllocationPolicy) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} + +func (v AssignedDesktop) Validate() error { + var violations []FieldViolation + if v.AssignmentID == "" { + violations = append(violations, FieldViolation{Field: "assignment_id", Code: "required"}) + } + if len(v.AssignmentID) < 1 && v.AssignmentID != "" { + violations = append(violations, FieldViolation{Field: "assignment_id", Code: "min_length"}) + } + if len(v.AssignmentID) > 128 { + violations = append(violations, FieldViolation{Field: "assignment_id", Code: "max_length"}) + } + if v.PoolID == "" { + violations = append(violations, FieldViolation{Field: "pool_id", Code: "required"}) + } + if len(v.PoolID) < 1 && v.PoolID != "" { + violations = append(violations, FieldViolation{Field: "pool_id", Code: "min_length"}) + } + if len(v.PoolID) > 128 { + violations = append(violations, FieldViolation{Field: "pool_id", Code: "max_length"}) + } + if v.Name == "" { + violations = append(violations, FieldViolation{Field: "name", Code: "required"}) + } + if len(v.Name) < 1 && v.Name != "" { + violations = append(violations, FieldViolation{Field: "name", Code: "min_length"}) + } + if len(v.Name) > 256 { + violations = append(violations, FieldViolation{Field: "name", Code: "max_length"}) + } + if v.Availability == "" { + violations = append(violations, FieldViolation{Field: "availability", Code: "required"}) + } + if len(v.Availability) < 1 && v.Availability != "" { + violations = append(violations, FieldViolation{Field: "availability", Code: "min_length"}) + } + if len(v.Availability) > 64 { + violations = append(violations, FieldViolation{Field: "availability", Code: "max_length"}) + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeAssignedDesktop(data []byte) (AssignedDesktop, error) { + var value AssignedDesktop + 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["assignment_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "assignment_id", Code: "required"}}} + } + if raw, ok := fields["availability"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "availability", Code: "required"}}} + } + if raw, ok := fields["name"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "name", Code: "required"}}} + } + if raw, ok := fields["pool_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "pool_id", 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 EncodeAssignedDesktop(value AssignedDesktop) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} + +func (v BrokerSession) Validate() error { + var violations []FieldViolation + if v.ID == "" { + violations = append(violations, FieldViolation{Field: "id", Code: "required"}) + } + if len(v.ID) < 1 && v.ID != "" { + violations = append(violations, FieldViolation{Field: "id", Code: "min_length"}) + } + if len(v.ID) > 128 { + violations = append(violations, FieldViolation{Field: "id", Code: "max_length"}) + } + if v.PrincipalID == "" { + violations = append(violations, FieldViolation{Field: "principal_id", Code: "required"}) + } + if len(v.PrincipalID) < 1 && v.PrincipalID != "" { + violations = append(violations, FieldViolation{Field: "principal_id", Code: "min_length"}) + } + if len(v.PrincipalID) > 128 { + violations = append(violations, FieldViolation{Field: "principal_id", Code: "max_length"}) + } + if v.PoolID == "" { + violations = append(violations, FieldViolation{Field: "pool_id", Code: "required"}) + } + if len(v.PoolID) < 1 && v.PoolID != "" { + violations = append(violations, FieldViolation{Field: "pool_id", Code: "min_length"}) + } + if len(v.PoolID) > 128 { + violations = append(violations, FieldViolation{Field: "pool_id", Code: "max_length"}) + } + if len(v.AssignmentID) > 128 { + violations = append(violations, FieldViolation{Field: "assignment_id", Code: "max_length"}) + } + if v.State == "" { + violations = append(violations, FieldViolation{Field: "state", Code: "required"}) + } + if len(v.State) < 1 && v.State != "" { + violations = append(violations, FieldViolation{Field: "state", Code: "min_length"}) + } + if len(v.State) > 64 { + violations = append(violations, FieldViolation{Field: "state", Code: "max_length"}) + } + if reflect.DeepEqual(v.PolicySnapshot, AllocationPolicy{}) { + violations = append(violations, FieldViolation{Field: "policy_snapshot", Code: "required"}) + } + if err := v.PolicySnapshot.Validate(); err != nil { + violations = append(violations, FieldViolation{Field: "policy_snapshot", Code: "invalid_object"}) + } + if len(v.ReconnectDeadline) > 64 { + violations = append(violations, FieldViolation{Field: "reconnect_deadline", Code: "max_length"}) + } + if v.ReconnectDeadline != "" { + if parsed, err := time.Parse(time.RFC3339Nano, v.ReconnectDeadline); err != nil || parsed.UTC().Format(time.RFC3339Nano) != v.ReconnectDeadline { + violations = append(violations, FieldViolation{Field: "reconnect_deadline", Code: "invalid_time"}) + } + } + if len(v.Outcome) > 64 { + violations = append(violations, FieldViolation{Field: "outcome", Code: "max_length"}) + } + if len(v.FailureCode) > 128 { + violations = append(violations, FieldViolation{Field: "failure_code", Code: "max_length"}) + } + if v.CleanupState == "" { + violations = append(violations, FieldViolation{Field: "cleanup_state", Code: "required"}) + } + if len(v.CleanupState) < 1 && v.CleanupState != "" { + violations = append(violations, FieldViolation{Field: "cleanup_state", Code: "min_length"}) + } + if len(v.CleanupState) > 64 { + violations = append(violations, FieldViolation{Field: "cleanup_state", Code: "max_length"}) + } + if v.IdempotencyKey == "" { + violations = append(violations, FieldViolation{Field: "idempotency_key", Code: "required"}) + } + if len(v.IdempotencyKey) < 1 && v.IdempotencyKey != "" { + violations = append(violations, FieldViolation{Field: "idempotency_key", Code: "min_length"}) + } + if len(v.IdempotencyKey) > 256 { + violations = append(violations, FieldViolation{Field: "idempotency_key", Code: "max_length"}) + } + if v.CorrelationID == "" { + violations = append(violations, FieldViolation{Field: "correlation_id", Code: "required"}) + } + if len(v.CorrelationID) < 1 && v.CorrelationID != "" { + violations = append(violations, FieldViolation{Field: "correlation_id", Code: "min_length"}) + } + if len(v.CorrelationID) > 128 { + violations = append(violations, FieldViolation{Field: "correlation_id", Code: "max_length"}) + } + if v.RequestedAt == "" { + violations = append(violations, FieldViolation{Field: "requested_at", Code: "required"}) + } + if len(v.RequestedAt) > 64 { + violations = append(violations, FieldViolation{Field: "requested_at", Code: "max_length"}) + } + if v.RequestedAt != "" { + if parsed, err := time.Parse(time.RFC3339Nano, v.RequestedAt); err != nil || parsed.UTC().Format(time.RFC3339Nano) != v.RequestedAt { + violations = append(violations, FieldViolation{Field: "requested_at", Code: "invalid_time"}) + } + } + if len(v.EndedAt) > 64 { + violations = append(violations, FieldViolation{Field: "ended_at", Code: "max_length"}) + } + if v.EndedAt != "" { + if parsed, err := time.Parse(time.RFC3339Nano, v.EndedAt); err != nil || parsed.UTC().Format(time.RFC3339Nano) != v.EndedAt { + violations = append(violations, FieldViolation{Field: "ended_at", Code: "invalid_time"}) + } + } + if v.Version == 0 { + violations = append(violations, FieldViolation{Field: "version", Code: "required"}) + } + if v.Version != 0 && v.Version < 1 { + violations = append(violations, FieldViolation{Field: "version", Code: "minimum"}) + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeBrokerSession(data []byte) (BrokerSession, error) { + var value BrokerSession + 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["cleanup_state"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "cleanup_state", Code: "required"}}} + } + if raw, ok := fields["correlation_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "correlation_id", Code: "required"}}} + } + if raw, ok := fields["id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "id", Code: "required"}}} + } + if raw, ok := fields["idempotency_key"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "idempotency_key", Code: "required"}}} + } + if raw, ok := fields["policy_snapshot"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "policy_snapshot", Code: "required"}}} + } + if raw, ok := fields["pool_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "pool_id", Code: "required"}}} + } + if raw, ok := fields["principal_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "principal_id", Code: "required"}}} + } + if raw, ok := fields["requested_at"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "requested_at", Code: "required"}}} + } + 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["version"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "version", 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 EncodeBrokerSession(value BrokerSession) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} + +func (v ClipboardText) Validate() error { + var violations []FieldViolation + if v.Text == "" { + violations = append(violations, FieldViolation{Field: "text", Code: "required"}) + } + if len(v.Text) > 65536 { + violations = append(violations, FieldViolation{Field: "text", Code: "max_length"}) + } + if v.Encoding == "" { + violations = append(violations, FieldViolation{Field: "encoding", Code: "required"}) + } + if v.Encoding != "utf-8" && v.Encoding != "" { + violations = append(violations, FieldViolation{Field: "encoding", Code: "invalid_value"}) + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeClipboardText(data []byte) (ClipboardText, error) { + var value ClipboardText + 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["encoding"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "encoding", Code: "required"}}} + } + if raw, ok := fields["text"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "text", 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 EncodeClipboardText(value ClipboardText) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} + +func (v ConnectionManifest) Validate() error { + var violations []FieldViolation + if v.Version == "" { + violations = append(violations, FieldViolation{Field: "version", Code: "required"}) + } + if v.Version != "1" && v.Version != "" { + violations = append(violations, FieldViolation{Field: "version", Code: "invalid_value"}) + } + if v.Purpose == "" { + violations = append(violations, FieldViolation{Field: "purpose", Code: "required"}) + } + if v.Purpose != "" && !(v.Purpose == "launch" || v.Purpose == "reconnect") { + violations = append(violations, FieldViolation{Field: "purpose", Code: "invalid_value"}) + } + if v.SessionID == "" { + violations = append(violations, FieldViolation{Field: "session_id", Code: "required"}) + } + if len(v.SessionID) < 1 && v.SessionID != "" { + violations = append(violations, FieldViolation{Field: "session_id", Code: "min_length"}) + } + if len(v.SessionID) > 128 { + violations = append(violations, FieldViolation{Field: "session_id", Code: "max_length"}) + } + if v.ReconnectSequence != 0 && v.ReconnectSequence < 0 { + violations = append(violations, FieldViolation{Field: "reconnect_sequence", Code: "minimum"}) + } + if reflect.DeepEqual(v.Gateway, ManifestGateway{}) { + violations = append(violations, FieldViolation{Field: "gateway", Code: "required"}) + } + if err := v.Gateway.Validate(); err != nil { + violations = append(violations, FieldViolation{Field: "gateway", Code: "invalid_object"}) + } + if reflect.DeepEqual(v.Tunnel, ManifestTunnel{}) { + violations = append(violations, FieldViolation{Field: "tunnel", Code: "required"}) + } + if err := v.Tunnel.Validate(); err != nil { + violations = append(violations, FieldViolation{Field: "tunnel", Code: "invalid_object"}) + } + if reflect.DeepEqual(v.Profile, ManifestProfile{}) { + violations = append(violations, FieldViolation{Field: "profile", Code: "required"}) + } + if err := v.Profile.Validate(); err != nil { + violations = append(violations, FieldViolation{Field: "profile", Code: "invalid_object"}) + } + if reflect.DeepEqual(v.Grant, GrantReference{}) { + violations = append(violations, FieldViolation{Field: "grant", Code: "required"}) + } + if err := v.Grant.Validate(); err != nil { + violations = append(violations, FieldViolation{Field: "grant", Code: "invalid_object"}) + } + if v.CorrelationID == "" { + violations = append(violations, FieldViolation{Field: "correlation_id", Code: "required"}) + } + if len(v.CorrelationID) < 1 && v.CorrelationID != "" { + violations = append(violations, FieldViolation{Field: "correlation_id", Code: "min_length"}) + } + if len(v.CorrelationID) > 128 { + violations = append(violations, FieldViolation{Field: "correlation_id", Code: "max_length"}) + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeConnectionManifest(data []byte) (ConnectionManifest, error) { + var value ConnectionManifest + 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["correlation_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "correlation_id", Code: "required"}}} + } + if raw, ok := fields["gateway"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "gateway", Code: "required"}}} + } + if raw, ok := fields["grant"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "grant", Code: "required"}}} + } + if raw, ok := fields["profile"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "profile", Code: "required"}}} + } + if raw, ok := fields["purpose"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "purpose", Code: "required"}}} + } + if raw, ok := fields["reconnect_sequence"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "reconnect_sequence", Code: "required"}}} + } + if raw, ok := fields["session_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "session_id", Code: "required"}}} + } + if raw, ok := fields["tunnel"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "tunnel", Code: "required"}}} + } + if raw, ok := fields["version"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "version", 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 EncodeConnectionManifest(value ConnectionManifest) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} + +func (v DeviceChallenge) Validate() error { + var violations []FieldViolation + if v.DeviceID == "" { + violations = append(violations, FieldViolation{Field: "device_id", Code: "required"}) + } + if len(v.DeviceID) < 1 && v.DeviceID != "" { + violations = append(violations, FieldViolation{Field: "device_id", Code: "min_length"}) + } + if len(v.DeviceID) > 128 { + violations = append(violations, FieldViolation{Field: "device_id", Code: "max_length"}) + } + if v.ServerID == "" { + violations = append(violations, FieldViolation{Field: "server_id", Code: "required"}) + } + if len(v.ServerID) < 1 && v.ServerID != "" { + violations = append(violations, FieldViolation{Field: "server_id", Code: "min_length"}) + } + if len(v.ServerID) > 128 { + violations = append(violations, FieldViolation{Field: "server_id", Code: "max_length"}) + } + if v.PrincipalID == "" { + violations = append(violations, FieldViolation{Field: "principal_id", Code: "required"}) + } + if len(v.PrincipalID) < 1 && v.PrincipalID != "" { + violations = append(violations, FieldViolation{Field: "principal_id", Code: "min_length"}) + } + if len(v.PrincipalID) > 128 { + violations = append(violations, FieldViolation{Field: "principal_id", Code: "max_length"}) + } + if v.Challenge == "" { + violations = append(violations, FieldViolation{Field: "challenge", Code: "required"}) + } + if len(v.Challenge) < 1 && v.Challenge != "" { + violations = append(violations, FieldViolation{Field: "challenge", Code: "min_length"}) + } + if len(v.Challenge) > 256 { + violations = append(violations, FieldViolation{Field: "challenge", Code: "max_length"}) + } + if v.ExpiresAt == "" { + violations = append(violations, FieldViolation{Field: "expires_at", Code: "required"}) + } + if len(v.ExpiresAt) > 64 { + violations = append(violations, FieldViolation{Field: "expires_at", Code: "max_length"}) + } + if v.ExpiresAt != "" { + if parsed, err := time.Parse(time.RFC3339Nano, v.ExpiresAt); err != nil || parsed.UTC().Format(time.RFC3339Nano) != v.ExpiresAt { + violations = append(violations, FieldViolation{Field: "expires_at", Code: "invalid_time"}) + } + } + if v.Algorithm == "" { + violations = append(violations, FieldViolation{Field: "algorithm", Code: "required"}) + } + if v.Algorithm != "ed25519" && v.Algorithm != "" { + violations = append(violations, FieldViolation{Field: "algorithm", Code: "invalid_value"}) + } + if v.SignatureFormat == "" { + violations = append(violations, FieldViolation{Field: "signature_format", Code: "required"}) + } + if v.SignatureFormat != "ed25519-domain-separated-v1" && v.SignatureFormat != "" { + violations = append(violations, FieldViolation{Field: "signature_format", Code: "invalid_value"}) + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeDeviceChallenge(data []byte) (DeviceChallenge, error) { + var value DeviceChallenge + 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["algorithm"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "algorithm", Code: "required"}}} + } + if raw, ok := fields["challenge"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "challenge", Code: "required"}}} + } + if raw, ok := fields["device_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "device_id", Code: "required"}}} + } + if raw, ok := fields["expires_at"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "expires_at", Code: "required"}}} + } + if raw, ok := fields["principal_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "principal_id", Code: "required"}}} + } + if raw, ok := fields["server_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "server_id", Code: "required"}}} + } + if raw, ok := fields["signature_format"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "signature_format", 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 EncodeDeviceChallenge(value DeviceChallenge) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} + +func (v DeviceProofRequest) Validate() error { + var violations []FieldViolation + if v.Challenge == "" { + violations = append(violations, FieldViolation{Field: "challenge", Code: "required"}) + } + if len(v.Challenge) < 1 && v.Challenge != "" { + violations = append(violations, FieldViolation{Field: "challenge", Code: "min_length"}) + } + if len(v.Challenge) > 256 { + violations = append(violations, FieldViolation{Field: "challenge", Code: "max_length"}) + } + if v.Signature == "" { + violations = append(violations, FieldViolation{Field: "signature", Code: "required"}) + } + if len(v.Signature) < 1 && v.Signature != "" { + violations = append(violations, FieldViolation{Field: "signature", Code: "min_length"}) + } + if len(v.Signature) > 256 { + violations = append(violations, FieldViolation{Field: "signature", Code: "max_length"}) + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeDeviceProofRequest(data []byte) (DeviceProofRequest, error) { + var value DeviceProofRequest + 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["challenge"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "challenge", Code: "required"}}} + } + if raw, ok := fields["signature"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "signature", 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 EncodeDeviceProofRequest(value DeviceProofRequest) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} + +func (v DeviceRegistrationRequest) Validate() error { + var violations []FieldViolation + if v.Name == "" { + violations = append(violations, FieldViolation{Field: "name", Code: "required"}) + } + if len(v.Name) < 1 && v.Name != "" { + violations = append(violations, FieldViolation{Field: "name", Code: "min_length"}) + } + if len(v.Name) > 128 { + violations = append(violations, FieldViolation{Field: "name", Code: "max_length"}) + } + if v.Platform == "" { + violations = append(violations, FieldViolation{Field: "platform", Code: "required"}) + } + if len(v.Platform) < 1 && v.Platform != "" { + violations = append(violations, FieldViolation{Field: "platform", Code: "min_length"}) + } + if len(v.Platform) > 64 { + violations = append(violations, FieldViolation{Field: "platform", Code: "max_length"}) + } + if v.DeviceSubject == "" { + violations = append(violations, FieldViolation{Field: "device_subject", Code: "required"}) + } + if len(v.DeviceSubject) < 1 && v.DeviceSubject != "" { + violations = append(violations, FieldViolation{Field: "device_subject", Code: "min_length"}) + } + if len(v.DeviceSubject) > 256 { + violations = append(violations, FieldViolation{Field: "device_subject", Code: "max_length"}) + } + if v.Algorithm == "" { + violations = append(violations, FieldViolation{Field: "algorithm", Code: "required"}) + } + if v.Algorithm != "ed25519" && v.Algorithm != "" { + violations = append(violations, FieldViolation{Field: "algorithm", Code: "invalid_value"}) + } + if v.PublicKey == "" { + violations = append(violations, FieldViolation{Field: "public_key", Code: "required"}) + } + if len(v.PublicKey) < 1 && v.PublicKey != "" { + violations = append(violations, FieldViolation{Field: "public_key", Code: "min_length"}) + } + if len(v.PublicKey) > 256 { + violations = append(violations, FieldViolation{Field: "public_key", Code: "max_length"}) + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeDeviceRegistrationRequest(data []byte) (DeviceRegistrationRequest, error) { + var value DeviceRegistrationRequest + 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["algorithm"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "algorithm", Code: "required"}}} + } + if raw, ok := fields["device_subject"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "device_subject", Code: "required"}}} + } + if raw, ok := fields["name"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "name", Code: "required"}}} + } + if raw, ok := fields["platform"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "platform", Code: "required"}}} + } + if raw, ok := fields["public_key"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "public_key", 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 EncodeDeviceRegistrationRequest(value DeviceRegistrationRequest) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} + +func (v EntitledPool) Validate() error { + var violations []FieldViolation + if v.PoolID == "" { + violations = append(violations, FieldViolation{Field: "pool_id", Code: "required"}) + } + if len(v.PoolID) < 1 && v.PoolID != "" { + violations = append(violations, FieldViolation{Field: "pool_id", Code: "min_length"}) + } + if len(v.PoolID) > 128 { + violations = append(violations, FieldViolation{Field: "pool_id", Code: "max_length"}) + } + if v.Name == "" { + violations = append(violations, FieldViolation{Field: "name", Code: "required"}) + } + if len(v.Name) < 1 && v.Name != "" { + violations = append(violations, FieldViolation{Field: "name", Code: "min_length"}) + } + if len(v.Name) > 256 { + violations = append(violations, FieldViolation{Field: "name", Code: "max_length"}) + } + if v.AssignmentState == "" { + violations = append(violations, FieldViolation{Field: "assignment_state", Code: "required"}) + } + if len(v.AssignmentState) < 1 && v.AssignmentState != "" { + violations = append(violations, FieldViolation{Field: "assignment_state", Code: "min_length"}) + } + if len(v.AssignmentState) > 64 { + violations = append(violations, FieldViolation{Field: "assignment_state", Code: "max_length"}) + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeEntitledPool(data []byte) (EntitledPool, error) { + var value EntitledPool + 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["assignment_state"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "assignment_state", Code: "required"}}} + } + if raw, ok := fields["name"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "name", Code: "required"}}} + } + if raw, ok := fields["pool_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "pool_id", 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 EncodeEntitledPool(value EntitledPool) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} + +func (v ErrorEnvelope) Validate() error { + var violations []FieldViolation + if v.Error == "" { + violations = append(violations, FieldViolation{Field: "error", Code: "required"}) + } + if len(v.Error) < 1 && v.Error != "" { + violations = append(violations, FieldViolation{Field: "error", Code: "min_length"}) + } + if len(v.Error) > 512 { + violations = append(violations, FieldViolation{Field: "error", Code: "max_length"}) + } + if v.Code == "" { + violations = append(violations, FieldViolation{Field: "code", Code: "required"}) + } + if len(v.Code) < 1 && v.Code != "" { + violations = append(violations, FieldViolation{Field: "code", Code: "min_length"}) + } + if len(v.Code) > 128 { + violations = append(violations, FieldViolation{Field: "code", Code: "max_length"}) + } + if v.Message == "" { + violations = append(violations, FieldViolation{Field: "message", Code: "required"}) + } + if len(v.Message) < 1 && v.Message != "" { + violations = append(violations, FieldViolation{Field: "message", Code: "min_length"}) + } + if len(v.Message) > 512 { + violations = append(violations, FieldViolation{Field: "message", Code: "max_length"}) + } + if v.Resolution == "" { + violations = append(violations, FieldViolation{Field: "resolution", Code: "required"}) + } + if len(v.Resolution) < 1 && v.Resolution != "" { + violations = append(violations, FieldViolation{Field: "resolution", Code: "min_length"}) + } + if len(v.Resolution) > 128 { + violations = append(violations, FieldViolation{Field: "resolution", Code: "max_length"}) + } + if v.RequestID == "" { + violations = append(violations, FieldViolation{Field: "request_id", Code: "required"}) + } + if len(v.RequestID) < 1 && v.RequestID != "" { + violations = append(violations, FieldViolation{Field: "request_id", Code: "min_length"}) + } + if len(v.RequestID) > 128 { + violations = append(violations, FieldViolation{Field: "request_id", Code: "max_length"}) + } + if v.Violations == nil { + violations = append(violations, FieldViolation{Field: "violations", Code: "required"}) + } + if len(v.Violations) > 16 { + violations = append(violations, FieldViolation{Field: "violations", Code: "max_items"}) + } + for index := range v.Violations { + if err := v.Violations[index].Validate(); err != nil { + violations = append(violations, FieldViolation{Field: fmt.Sprintf("violations[%d]", index), Code: "invalid_item"}) + } + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeErrorEnvelope(data []byte) (ErrorEnvelope, error) { + var value ErrorEnvelope + 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["code"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "code", Code: "required"}}} + } + if raw, ok := fields["error"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "error", Code: "required"}}} + } + if raw, ok := fields["message"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "message", Code: "required"}}} + } + if raw, ok := fields["request_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "request_id", Code: "required"}}} + } + if raw, ok := fields["resolution"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "resolution", Code: "required"}}} + } + if raw, ok := fields["status"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "status", Code: "required"}}} + } + if raw, ok := fields["violations"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "violations", 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 EncodeErrorEnvelope(value ErrorEnvelope) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} + +func (v EventEnvelope) Validate() error { + var violations []FieldViolation + if v.EventID == "" { + violations = append(violations, FieldViolation{Field: "event_id", Code: "required"}) + } + if len(v.EventID) < 1 && v.EventID != "" { + violations = append(violations, FieldViolation{Field: "event_id", Code: "min_length"}) + } + if len(v.EventID) > 128 { + violations = append(violations, FieldViolation{Field: "event_id", Code: "max_length"}) + } + if v.Sequence == 0 { + violations = append(violations, FieldViolation{Field: "sequence", Code: "required"}) + } + if v.Sequence != 0 && v.Sequence < 1 { + violations = append(violations, FieldViolation{Field: "sequence", Code: "minimum"}) + } + if v.Type == "" { + violations = append(violations, FieldViolation{Field: "type", Code: "required"}) + } + if len(v.Type) < 1 && v.Type != "" { + violations = append(violations, FieldViolation{Field: "type", Code: "min_length"}) + } + if len(v.Type) > 128 { + violations = append(violations, FieldViolation{Field: "type", Code: "max_length"}) + } + if v.Version == 0 { + violations = append(violations, FieldViolation{Field: "version", Code: "required"}) + } + if v.Version != 0 && v.Version < 1 { + violations = append(violations, FieldViolation{Field: "version", Code: "minimum"}) + } + if reflect.DeepEqual(v.Resource, ResourceLink{}) { + violations = append(violations, FieldViolation{Field: "resource", Code: "required"}) + } + if err := v.Resource.Validate(); err != nil { + violations = append(violations, FieldViolation{Field: "resource", Code: "invalid_object"}) + } + if v.OccurredAt == "" { + violations = append(violations, FieldViolation{Field: "occurred_at", Code: "required"}) + } + if len(v.OccurredAt) > 64 { + violations = append(violations, FieldViolation{Field: "occurred_at", Code: "max_length"}) + } + if v.OccurredAt != "" { + if parsed, err := time.Parse(time.RFC3339Nano, v.OccurredAt); err != nil || parsed.UTC().Format(time.RFC3339Nano) != v.OccurredAt { + violations = append(violations, FieldViolation{Field: "occurred_at", Code: "invalid_time"}) + } + } + if v.CorrelationID == "" { + violations = append(violations, FieldViolation{Field: "correlation_id", Code: "required"}) + } + if len(v.CorrelationID) < 1 && v.CorrelationID != "" { + violations = append(violations, FieldViolation{Field: "correlation_id", Code: "min_length"}) + } + if len(v.CorrelationID) > 128 { + violations = append(violations, FieldViolation{Field: "correlation_id", Code: "max_length"}) + } + if v.Payload == nil { + violations = append(violations, FieldViolation{Field: "payload", Code: "required"}) + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeEventEnvelope(data []byte) (EventEnvelope, error) { + var value EventEnvelope + 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["correlation_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "correlation_id", Code: "required"}}} + } + if raw, ok := fields["event_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "event_id", Code: "required"}}} + } + if raw, ok := fields["occurred_at"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "occurred_at", Code: "required"}}} + } + if raw, ok := fields["payload"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "payload", Code: "required"}}} + } + if raw, ok := fields["resource"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "resource", Code: "required"}}} + } + if raw, ok := fields["sequence"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "sequence", Code: "required"}}} + } + if raw, ok := fields["type"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "type", Code: "required"}}} + } + if raw, ok := fields["version"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "version", Code: "required"}}} + } + if raw, ok := fields["payload"]; ok && len(raw) > 16384 { + return value, ValidationError{Violations: []FieldViolation{{Field: "payload", Code: "max_bytes"}}} + } + 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 EncodeEventEnvelope(value EventEnvelope) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} + +func (v EventResume) Validate() error { + var violations []FieldViolation + if v.Cursor == "" { + violations = append(violations, FieldViolation{Field: "cursor", Code: "required"}) + } + if len(v.Cursor) > 512 { + violations = append(violations, FieldViolation{Field: "cursor", Code: "max_length"}) + } + if v.LastSequence != 0 && v.LastSequence < 0 { + violations = append(violations, FieldViolation{Field: "last_sequence", Code: "minimum"}) + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeEventResume(data []byte) (EventResume, error) { + var value EventResume + 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["cursor"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "cursor", Code: "required"}}} + } + if raw, ok := fields["last_sequence"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "last_sequence", 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 EncodeEventResume(value EventResume) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} + +func (v FieldViolation) Validate() error { + var violations []FieldViolation + if v.Field == "" { + violations = append(violations, FieldViolation{Field: "field", Code: "required"}) + } + if len(v.Field) < 1 && v.Field != "" { + violations = append(violations, FieldViolation{Field: "field", Code: "min_length"}) + } + if len(v.Field) > 128 { + violations = append(violations, FieldViolation{Field: "field", Code: "max_length"}) + } + if v.Code == "" { + violations = append(violations, FieldViolation{Field: "code", Code: "required"}) + } + if len(v.Code) < 1 && v.Code != "" { + violations = append(violations, FieldViolation{Field: "code", Code: "min_length"}) + } + if len(v.Code) > 64 { + violations = append(violations, FieldViolation{Field: "code", Code: "max_length"}) + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeFieldViolation(data []byte) (FieldViolation, error) { + var value FieldViolation + 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["code"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "code", Code: "required"}}} + } + if raw, ok := fields["field"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "field", 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 EncodeFieldViolation(value FieldViolation) ([]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 == "" { + violations = append(violations, FieldViolation{Field: "opaque_value", Code: "required"}) + } + if len(v.OpaqueValue) < 43 && v.OpaqueValue != "" { + violations = append(violations, FieldViolation{Field: "opaque_value", Code: "min_length"}) + } + if len(v.OpaqueValue) > 256 { + violations = append(violations, FieldViolation{Field: "opaque_value", Code: "max_length"}) + } + if v.ExpiresAt == "" { + violations = append(violations, FieldViolation{Field: "expires_at", Code: "required"}) + } + if len(v.ExpiresAt) > 64 { + violations = append(violations, FieldViolation{Field: "expires_at", Code: "max_length"}) + } + if v.ExpiresAt != "" { + if parsed, err := time.Parse(time.RFC3339Nano, v.ExpiresAt); err != nil || parsed.UTC().Format(time.RFC3339Nano) != v.ExpiresAt { + violations = append(violations, FieldViolation{Field: "expires_at", Code: "invalid_time"}) + } + } + if v.Audience == "" { + violations = append(violations, FieldViolation{Field: "audience", Code: "required"}) + } + if len(v.Audience) < 1 && v.Audience != "" { + violations = append(violations, FieldViolation{Field: "audience", Code: "min_length"}) + } + if len(v.Audience) > 128 { + violations = append(violations, FieldViolation{Field: "audience", Code: "max_length"}) + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeGrantReference(data []byte) (GrantReference, error) { + var value GrantReference + 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["audience"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "audience", Code: "required"}}} + } + if raw, ok := fields["expires_at"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "expires_at", Code: "required"}}} + } + if raw, ok := fields["opaque_value"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "opaque_value", 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 EncodeGrantReference(value GrantReference) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} + +func (v LoginRequest) Validate() error { + var violations []FieldViolation + if v.Provider != "" && !(v.Provider == "ldap" || v.Provider == "local") { + violations = append(violations, FieldViolation{Field: "provider", Code: "invalid_value"}) + } + if v.Username == "" { + violations = append(violations, FieldViolation{Field: "username", Code: "required"}) + } + if len(v.Username) < 1 && v.Username != "" { + violations = append(violations, FieldViolation{Field: "username", Code: "min_length"}) + } + if len(v.Username) > 256 { + violations = append(violations, FieldViolation{Field: "username", Code: "max_length"}) + } + if v.Password == "" { + violations = append(violations, FieldViolation{Field: "password", Code: "required"}) + } + if len(v.Password) < 1 && v.Password != "" { + violations = append(violations, FieldViolation{Field: "password", Code: "min_length"}) + } + if len(v.Password) > 1024 { + violations = append(violations, FieldViolation{Field: "password", Code: "max_length"}) + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeLoginRequest(data []byte) (LoginRequest, error) { + var value LoginRequest + 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["password"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "password", Code: "required"}}} + } + if raw, ok := fields["username"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "username", 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 EncodeLoginRequest(value LoginRequest) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} + +func (v ManifestBounds) Validate() error { + var violations []FieldViolation + if v.MinimumKbps == 0 { + violations = append(violations, FieldViolation{Field: "minimum_kbps", Code: "required"}) + } + if v.MinimumKbps != 0 && v.MinimumKbps < 1 { + violations = append(violations, FieldViolation{Field: "minimum_kbps", Code: "minimum"}) + } + if v.MinimumKbps > 100000000 { + violations = append(violations, FieldViolation{Field: "minimum_kbps", Code: "maximum"}) + } + if v.TargetKbps == 0 { + violations = append(violations, FieldViolation{Field: "target_kbps", Code: "required"}) + } + if v.TargetKbps != 0 && v.TargetKbps < 1 { + violations = append(violations, FieldViolation{Field: "target_kbps", Code: "minimum"}) + } + if v.TargetKbps > 100000000 { + violations = append(violations, FieldViolation{Field: "target_kbps", Code: "maximum"}) + } + if v.MaximumKbps == 0 { + violations = append(violations, FieldViolation{Field: "maximum_kbps", Code: "required"}) + } + if v.MaximumKbps != 0 && v.MaximumKbps < 1 { + violations = append(violations, FieldViolation{Field: "maximum_kbps", Code: "minimum"}) + } + if v.MaximumKbps > 100000000 { + violations = append(violations, FieldViolation{Field: "maximum_kbps", Code: "maximum"}) + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeManifestBounds(data []byte) (ManifestBounds, error) { + var value ManifestBounds + 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["maximum_kbps"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "maximum_kbps", Code: "required"}}} + } + if raw, ok := fields["minimum_kbps"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "minimum_kbps", Code: "required"}}} + } + if raw, ok := fields["target_kbps"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "target_kbps", 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 EncodeManifestBounds(value ManifestBounds) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} + +func (v ManifestGateway) Validate() error { + var violations []FieldViolation + if v.ID == "" { + violations = append(violations, FieldViolation{Field: "id", Code: "required"}) + } + if len(v.ID) < 1 && v.ID != "" { + violations = append(violations, FieldViolation{Field: "id", Code: "min_length"}) + } + if len(v.ID) > 128 { + violations = append(violations, FieldViolation{Field: "id", Code: "max_length"}) + } + if v.Addresses == nil { + violations = append(violations, FieldViolation{Field: "addresses", Code: "required"}) + } + if len(v.Addresses) < 1 { + violations = append(violations, FieldViolation{Field: "addresses", Code: "min_items"}) + } + if len(v.Addresses) > 4 { + violations = append(violations, FieldViolation{Field: "addresses", Code: "max_items"}) + } + if v.PublicIdentity == "" { + violations = append(violations, FieldViolation{Field: "public_identity", Code: "required"}) + } + if len(v.PublicIdentity) < 1 && v.PublicIdentity != "" { + violations = append(violations, FieldViolation{Field: "public_identity", Code: "min_length"}) + } + if len(v.PublicIdentity) > 256 { + violations = append(violations, FieldViolation{Field: "public_identity", Code: "max_length"}) + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeManifestGateway(data []byte) (ManifestGateway, error) { + var value ManifestGateway + 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["addresses"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "addresses", Code: "required"}}} + } + if raw, ok := fields["id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "id", Code: "required"}}} + } + if raw, ok := fields["public_identity"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "public_identity", 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 EncodeManifestGateway(value ManifestGateway) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} + +func (v ManifestProfile) Validate() error { + var violations []FieldViolation + if v.ID == "" { + violations = append(violations, FieldViolation{Field: "id", Code: "required"}) + } + if len(v.ID) < 1 && v.ID != "" { + violations = append(violations, FieldViolation{Field: "id", Code: "min_length"}) + } + if len(v.ID) > 128 { + violations = append(violations, FieldViolation{Field: "id", Code: "max_length"}) + } + if reflect.DeepEqual(v.Bounds, ManifestBounds{}) { + violations = append(violations, FieldViolation{Field: "bounds", Code: "required"}) + } + if err := v.Bounds.Validate(); err != nil { + violations = append(violations, FieldViolation{Field: "bounds", Code: "invalid_object"}) + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeManifestProfile(data []byte) (ManifestProfile, error) { + var value ManifestProfile + 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["bounds"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "bounds", Code: "required"}}} + } + if raw, ok := fields["id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "id", 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 EncodeManifestProfile(value ManifestProfile) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} + +func (v ManifestTunnel) Validate() error { + var violations []FieldViolation + if v.Versions == nil { + violations = append(violations, FieldViolation{Field: "versions", Code: "required"}) + } + if len(v.Versions) < 1 { + violations = append(violations, FieldViolation{Field: "versions", Code: "min_items"}) + } + if len(v.Versions) > 4 { + violations = append(violations, FieldViolation{Field: "versions", Code: "max_items"}) + } + if v.Features == nil { + violations = append(violations, FieldViolation{Field: "features", Code: "required"}) + } + if len(v.Features) > 32 { + violations = append(violations, FieldViolation{Field: "features", Code: "max_items"}) + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeManifestTunnel(data []byte) (ManifestTunnel, error) { + var value ManifestTunnel + 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["features"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "features", Code: "required"}}} + } + if raw, ok := fields["versions"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "versions", 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 EncodeManifestTunnel(value ManifestTunnel) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} + +func (v NativeCredential) Validate() error { + var violations []FieldViolation + if len(v.DeviceID) > 128 { + violations = append(violations, FieldViolation{Field: "device_id", Code: "max_length"}) + } + if v.FamilyID == "" { + violations = append(violations, FieldViolation{Field: "family_id", Code: "required"}) + } + if len(v.FamilyID) < 1 && v.FamilyID != "" { + violations = append(violations, FieldViolation{Field: "family_id", Code: "min_length"}) + } + if len(v.FamilyID) > 128 { + violations = append(violations, FieldViolation{Field: "family_id", Code: "max_length"}) + } + if v.AccessToken == "" { + violations = append(violations, FieldViolation{Field: "access_token", Code: "required"}) + } + if len(v.AccessToken) < 1 && v.AccessToken != "" { + violations = append(violations, FieldViolation{Field: "access_token", Code: "min_length"}) + } + if len(v.AccessToken) > 256 { + violations = append(violations, FieldViolation{Field: "access_token", Code: "max_length"}) + } + if v.RefreshToken == "" { + violations = append(violations, FieldViolation{Field: "refresh_token", Code: "required"}) + } + if len(v.RefreshToken) < 1 && v.RefreshToken != "" { + violations = append(violations, FieldViolation{Field: "refresh_token", Code: "min_length"}) + } + if len(v.RefreshToken) > 256 { + violations = append(violations, FieldViolation{Field: "refresh_token", Code: "max_length"}) + } + if v.ExpiresAt == "" { + violations = append(violations, FieldViolation{Field: "expires_at", Code: "required"}) + } + if len(v.ExpiresAt) > 64 { + violations = append(violations, FieldViolation{Field: "expires_at", Code: "max_length"}) + } + if v.ExpiresAt != "" { + if parsed, err := time.Parse(time.RFC3339Nano, v.ExpiresAt); err != nil || parsed.UTC().Format(time.RFC3339Nano) != v.ExpiresAt { + violations = append(violations, FieldViolation{Field: "expires_at", Code: "invalid_time"}) + } + } + if len(v.RefreshExpiresAt) > 64 { + violations = append(violations, FieldViolation{Field: "refresh_expires_at", Code: "max_length"}) + } + if v.RefreshExpiresAt != "" { + if parsed, err := time.Parse(time.RFC3339Nano, v.RefreshExpiresAt); err != nil || parsed.UTC().Format(time.RFC3339Nano) != v.RefreshExpiresAt { + violations = append(violations, FieldViolation{Field: "refresh_expires_at", Code: "invalid_time"}) + } + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeNativeCredential(data []byte) (NativeCredential, error) { + var value NativeCredential + 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["access_token"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "access_token", Code: "required"}}} + } + if raw, ok := fields["expires_at"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "expires_at", Code: "required"}}} + } + if raw, ok := fields["family_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "family_id", Code: "required"}}} + } + if raw, ok := fields["refresh_token"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "refresh_token", 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 EncodeNativeCredential(value NativeCredential) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} + +func (v PageInfo) Validate() error { + var violations []FieldViolation + if v.Limit == 0 { + violations = append(violations, FieldViolation{Field: "limit", Code: "required"}) + } + if v.Limit != 0 && v.Limit < 1 { + violations = append(violations, FieldViolation{Field: "limit", Code: "minimum"}) + } + if v.Limit > 100 { + violations = append(violations, FieldViolation{Field: "limit", Code: "maximum"}) + } + if v.NextCursor == "" { + violations = append(violations, FieldViolation{Field: "next_cursor", Code: "required"}) + } + if len(v.NextCursor) > 512 { + violations = append(violations, FieldViolation{Field: "next_cursor", Code: "max_length"}) + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodePageInfo(data []byte) (PageInfo, error) { + var value PageInfo + 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["limit"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "limit", Code: "required"}}} + } + if raw, ok := fields["next_cursor"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "next_cursor", 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 EncodePageInfo(value PageInfo) ([]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 == "" { + violations = append(violations, FieldViolation{Field: "token", Code: "required"}) + } + if len(v.Token) < 1 && v.Token != "" { + violations = append(violations, FieldViolation{Field: "token", Code: "min_length"}) + } + if len(v.Token) > 256 { + violations = append(violations, FieldViolation{Field: "token", Code: "max_length"}) + } + if v.Purpose == "" { + violations = append(violations, FieldViolation{Field: "purpose", Code: "required"}) + } + if len(v.Purpose) < 1 && v.Purpose != "" { + violations = append(violations, FieldViolation{Field: "purpose", Code: "min_length"}) + } + if len(v.Purpose) > 64 { + violations = append(violations, FieldViolation{Field: "purpose", Code: "max_length"}) + } + if v.ExpiresAt == "" { + violations = append(violations, FieldViolation{Field: "expires_at", Code: "required"}) + } + if len(v.ExpiresAt) > 64 { + violations = append(violations, FieldViolation{Field: "expires_at", Code: "max_length"}) + } + if v.ExpiresAt != "" { + if parsed, err := time.Parse(time.RFC3339Nano, v.ExpiresAt); err != nil || parsed.UTC().Format(time.RFC3339Nano) != v.ExpiresAt { + violations = append(violations, FieldViolation{Field: "expires_at", Code: "invalid_time"}) + } + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeReauthGrant(data []byte) (ReauthGrant, error) { + var value ReauthGrant + 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["expires_at"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "expires_at", Code: "required"}}} + } + if raw, ok := fields["purpose"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "purpose", Code: "required"}}} + } + if raw, ok := fields["token"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "token", 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 EncodeReauthGrant(value ReauthGrant) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} + +func (v ReauthRequest) Validate() error { + var violations []FieldViolation + if v.Password == "" { + violations = append(violations, FieldViolation{Field: "password", Code: "required"}) + } + if len(v.Password) < 1 && v.Password != "" { + violations = append(violations, FieldViolation{Field: "password", Code: "min_length"}) + } + if len(v.Password) > 1024 { + violations = append(violations, FieldViolation{Field: "password", Code: "max_length"}) + } + if v.Purpose == "" { + violations = append(violations, FieldViolation{Field: "purpose", Code: "required"}) + } + if v.Purpose != "" && !(v.Purpose == "identity_change" || v.Purpose == "key_change" || v.Purpose == "backup_enable" || v.Purpose == "external_database_tls_disabled" || v.Purpose == "assignment_change") { + violations = append(violations, FieldViolation{Field: "purpose", Code: "invalid_value"}) + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeReauthRequest(data []byte) (ReauthRequest, error) { + var value ReauthRequest + 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["password"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "password", Code: "required"}}} + } + if raw, ok := fields["purpose"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "purpose", 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 EncodeReauthRequest(value ReauthRequest) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} + +func (v ReconnectRequest) Validate() error { + var violations []FieldViolation + if v.ClientDeviceID == "" { + violations = append(violations, FieldViolation{Field: "client_device_id", Code: "required"}) + } + if len(v.ClientDeviceID) < 1 && v.ClientDeviceID != "" { + violations = append(violations, FieldViolation{Field: "client_device_id", Code: "min_length"}) + } + if len(v.ClientDeviceID) > 128 { + violations = append(violations, FieldViolation{Field: "client_device_id", Code: "max_length"}) + } + if v.DeviceKeyID == "" { + violations = append(violations, FieldViolation{Field: "device_key_id", Code: "required"}) + } + if len(v.DeviceKeyID) < 1 && v.DeviceKeyID != "" { + violations = append(violations, FieldViolation{Field: "device_key_id", Code: "min_length"}) + } + if len(v.DeviceKeyID) > 128 { + violations = append(violations, FieldViolation{Field: "device_key_id", Code: "max_length"}) + } + if v.ExpectedVersion == 0 { + violations = append(violations, FieldViolation{Field: "expected_version", Code: "required"}) + } + if v.ExpectedVersion != 0 && v.ExpectedVersion < 1 { + violations = append(violations, FieldViolation{Field: "expected_version", Code: "minimum"}) + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeReconnectRequest(data []byte) (ReconnectRequest, error) { + var value ReconnectRequest + 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["client_device_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "client_device_id", Code: "required"}}} + } + if raw, ok := fields["device_key_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "device_key_id", Code: "required"}}} + } + if raw, ok := fields["expected_version"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "expected_version", 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 EncodeReconnectRequest(value ReconnectRequest) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} + +func (v RefreshRequest) Validate() error { + var violations []FieldViolation + if v.FamilyID == "" { + violations = append(violations, FieldViolation{Field: "family_id", Code: "required"}) + } + if len(v.FamilyID) < 1 && v.FamilyID != "" { + violations = append(violations, FieldViolation{Field: "family_id", Code: "min_length"}) + } + if len(v.FamilyID) > 128 { + violations = append(violations, FieldViolation{Field: "family_id", Code: "max_length"}) + } + if v.RefreshToken == "" { + violations = append(violations, FieldViolation{Field: "refresh_token", Code: "required"}) + } + if len(v.RefreshToken) < 1 && v.RefreshToken != "" { + violations = append(violations, FieldViolation{Field: "refresh_token", Code: "min_length"}) + } + if len(v.RefreshToken) > 256 { + violations = append(violations, FieldViolation{Field: "refresh_token", Code: "max_length"}) + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeRefreshRequest(data []byte) (RefreshRequest, error) { + var value RefreshRequest + 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["family_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "family_id", Code: "required"}}} + } + if raw, ok := fields["refresh_token"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "refresh_token", 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 EncodeRefreshRequest(value RefreshRequest) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} + +func (v Resource) Validate() error { + var violations []FieldViolation + if v.ID == "" { + violations = append(violations, FieldViolation{Field: "id", Code: "required"}) + } + if len(v.ID) < 1 && v.ID != "" { + violations = append(violations, FieldViolation{Field: "id", Code: "min_length"}) + } + if len(v.ID) > 128 { + violations = append(violations, FieldViolation{Field: "id", Code: "max_length"}) + } + if v.Kind == "" { + violations = append(violations, FieldViolation{Field: "kind", Code: "required"}) + } + if len(v.Kind) < 1 && v.Kind != "" { + violations = append(violations, FieldViolation{Field: "kind", Code: "min_length"}) + } + if len(v.Kind) > 64 { + violations = append(violations, FieldViolation{Field: "kind", Code: "max_length"}) + } + if v.Name == "" { + violations = append(violations, FieldViolation{Field: "name", Code: "required"}) + } + if len(v.Name) < 1 && v.Name != "" { + violations = append(violations, FieldViolation{Field: "name", Code: "min_length"}) + } + if len(v.Name) > 256 { + violations = append(violations, FieldViolation{Field: "name", Code: "max_length"}) + } + if v.State == "" { + violations = append(violations, FieldViolation{Field: "state", Code: "required"}) + } + if len(v.State) < 1 && v.State != "" { + violations = append(violations, FieldViolation{Field: "state", Code: "min_length"}) + } + if len(v.State) > 64 { + violations = append(violations, FieldViolation{Field: "state", Code: "max_length"}) + } + if len(v.AssignmentState) > 64 { + violations = append(violations, FieldViolation{Field: "assignment_state", Code: "max_length"}) + } + if v.Version == 0 { + violations = append(violations, FieldViolation{Field: "version", Code: "required"}) + } + if v.Version != 0 && v.Version < 1 { + violations = append(violations, FieldViolation{Field: "version", Code: "minimum"}) + } + if v.Links == nil { + violations = append(violations, FieldViolation{Field: "links", Code: "required"}) + } + if len(v.Links) > 16 { + violations = append(violations, FieldViolation{Field: "links", Code: "max_items"}) + } + for index := range v.Links { + if err := v.Links[index].Validate(); err != nil { + violations = append(violations, FieldViolation{Field: fmt.Sprintf("links[%d]", index), Code: "invalid_item"}) + } + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeResource(data []byte) (Resource, error) { + var value Resource + 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["id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "id", Code: "required"}}} + } + if raw, ok := fields["kind"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "kind", Code: "required"}}} + } + if raw, ok := fields["links"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "links", Code: "required"}}} + } + if raw, ok := fields["name"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "name", Code: "required"}}} + } + 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["version"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "version", 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 EncodeResource(value Resource) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} + +func (v ResourceLink) Validate() error { + var violations []FieldViolation + if v.Type == "" { + violations = append(violations, FieldViolation{Field: "type", Code: "required"}) + } + if len(v.Type) < 1 && v.Type != "" { + violations = append(violations, FieldViolation{Field: "type", Code: "min_length"}) + } + if len(v.Type) > 64 { + violations = append(violations, FieldViolation{Field: "type", Code: "max_length"}) + } + if v.ID == "" { + violations = append(violations, FieldViolation{Field: "id", Code: "required"}) + } + if len(v.ID) < 1 && v.ID != "" { + violations = append(violations, FieldViolation{Field: "id", Code: "min_length"}) + } + if len(v.ID) > 128 { + violations = append(violations, FieldViolation{Field: "id", Code: "max_length"}) + } + if v.Version == 0 { + violations = append(violations, FieldViolation{Field: "version", Code: "required"}) + } + if v.Version != 0 && v.Version < 1 { + violations = append(violations, FieldViolation{Field: "version", Code: "minimum"}) + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeResourceLink(data []byte) (ResourceLink, error) { + var value ResourceLink + 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["id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "id", Code: "required"}}} + } + if raw, ok := fields["type"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "type", Code: "required"}}} + } + if raw, ok := fields["version"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "version", 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 EncodeResourceLink(value ResourceLink) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} + +func (v ResourceList) Validate() error { + var violations []FieldViolation + if v.AssignedDesktops == nil { + violations = append(violations, FieldViolation{Field: "assigned_desktops", Code: "required"}) + } + if len(v.AssignedDesktops) > 100 { + violations = append(violations, FieldViolation{Field: "assigned_desktops", Code: "max_items"}) + } + for index := range v.AssignedDesktops { + if err := v.AssignedDesktops[index].Validate(); err != nil { + violations = append(violations, FieldViolation{Field: fmt.Sprintf("assigned_desktops[%d]", index), Code: "invalid_item"}) + } + } + if v.EntitledPools == nil { + violations = append(violations, FieldViolation{Field: "entitled_pools", Code: "required"}) + } + if len(v.EntitledPools) > 100 { + violations = append(violations, FieldViolation{Field: "entitled_pools", Code: "max_items"}) + } + for index := range v.EntitledPools { + if err := v.EntitledPools[index].Validate(); err != nil { + violations = append(violations, FieldViolation{Field: fmt.Sprintf("entitled_pools[%d]", index), Code: "invalid_item"}) + } + } + if reflect.DeepEqual(v.Page, PageInfo{}) { + violations = append(violations, FieldViolation{Field: "page", Code: "required"}) + } + if err := v.Page.Validate(); err != nil { + violations = append(violations, FieldViolation{Field: "page", Code: "invalid_object"}) + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeResourceList(data []byte) (ResourceList, error) { + var value ResourceList + 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["assigned_desktops"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "assigned_desktops", Code: "required"}}} + } + if raw, ok := fields["entitled_pools"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "entitled_pools", Code: "required"}}} + } + if raw, ok := fields["page"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "page", 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 EncodeResourceList(value ResourceList) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} + +func (v SessionRequest) Validate() error { + var violations []FieldViolation + if v.ClientDeviceID == "" { + violations = append(violations, FieldViolation{Field: "client_device_id", Code: "required"}) + } + if len(v.ClientDeviceID) < 1 && v.ClientDeviceID != "" { + violations = append(violations, FieldViolation{Field: "client_device_id", Code: "min_length"}) + } + if len(v.ClientDeviceID) > 128 { + violations = append(violations, FieldViolation{Field: "client_device_id", Code: "max_length"}) + } + if v.DeviceKeyID == "" { + violations = append(violations, FieldViolation{Field: "device_key_id", Code: "required"}) + } + if len(v.DeviceKeyID) < 1 && v.DeviceKeyID != "" { + violations = append(violations, FieldViolation{Field: "device_key_id", Code: "min_length"}) + } + if len(v.DeviceKeyID) > 128 { + violations = append(violations, FieldViolation{Field: "device_key_id", Code: "max_length"}) + } + if v.PoolID == "" { + violations = append(violations, FieldViolation{Field: "pool_id", Code: "required"}) + } + if len(v.PoolID) < 1 && v.PoolID != "" { + violations = append(violations, FieldViolation{Field: "pool_id", Code: "min_length"}) + } + if len(v.PoolID) > 128 { + violations = append(violations, FieldViolation{Field: "pool_id", Code: "max_length"}) + } + if v.IdempotencyKey == "" { + violations = append(violations, FieldViolation{Field: "idempotency_key", Code: "required"}) + } + if len(v.IdempotencyKey) < 1 && v.IdempotencyKey != "" { + violations = append(violations, FieldViolation{Field: "idempotency_key", Code: "min_length"}) + } + if len(v.IdempotencyKey) > 256 { + violations = append(violations, FieldViolation{Field: "idempotency_key", Code: "max_length"}) + } + if reflect.DeepEqual(v.PolicySnapshot, AllocationPolicy{}) { + violations = append(violations, FieldViolation{Field: "policy_snapshot", Code: "required"}) + } + if err := v.PolicySnapshot.Validate(); err != nil { + violations = append(violations, FieldViolation{Field: "policy_snapshot", Code: "invalid_object"}) + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeSessionRequest(data []byte) (SessionRequest, error) { + var value SessionRequest + 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["client_device_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "client_device_id", Code: "required"}}} + } + if raw, ok := fields["device_key_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "device_key_id", Code: "required"}}} + } + if raw, ok := fields["idempotency_key"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "idempotency_key", Code: "required"}}} + } + if raw, ok := fields["policy_snapshot"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "policy_snapshot", Code: "required"}}} + } + if raw, ok := fields["pool_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "pool_id", 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 EncodeSessionRequest(value SessionRequest) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} + +func (v VersionNegotiation) Validate() error { + var violations []FieldViolation + if v.SupportedVersions == nil { + violations = append(violations, FieldViolation{Field: "supported_versions", Code: "required"}) + } + if len(v.SupportedVersions) < 1 { + violations = append(violations, FieldViolation{Field: "supported_versions", Code: "min_items"}) + } + if len(v.SupportedVersions) > 3 { + violations = append(violations, FieldViolation{Field: "supported_versions", Code: "max_items"}) + } + if v.Features == nil { + violations = append(violations, FieldViolation{Field: "features", Code: "required"}) + } + if len(v.Features) > 64 { + violations = append(violations, FieldViolation{Field: "features", Code: "max_items"}) + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeVersionNegotiation(data []byte) (VersionNegotiation, error) { + var value VersionNegotiation + 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["features"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "features", Code: "required"}}} + } + if raw, ok := fields["supported_versions"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "supported_versions", 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 EncodeVersionNegotiation(value VersionNegotiation) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} diff --git a/gen/manifest.json b/gen/manifest.json new file mode 100644 index 0000000..6cda779 --- /dev/null +++ b/gen/manifest.json @@ -0,0 +1,18 @@ +{ + "compatibility": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "current": "1", + "datagram_registry": "registries/datagrams.json", + "feature_registry": "registries/features.json", + "n_minus_1": "0", + "n_minus_2": "-1", + "protocol": "versevdi-control", + "unsupported": [ + "-2", + "2" + ] + }, + "generator_sha256": "cb975bcd42bf77641b6a0f44d5ec7a6fdba1858d6b8a865e0f04e53bad82648c", + "protocol_version": "1.0.0", + "schema_sha256": "36e4c8bac2eae674c1eba551c6ca8c64bf80fcc092ca63ec89a2c71c2bec86e1" +} diff --git a/gen/protobuf/control-v1.pb b/gen/protobuf/control-v1.pb new file mode 100644 index 0000000..887b790 Binary files /dev/null and b/gen/protobuf/control-v1.pb differ diff --git a/gen/protobuf/tunnel-v1.pb b/gen/protobuf/tunnel-v1.pb new file mode 100644 index 0000000..f9ef9c2 Binary files /dev/null and b/gen/protobuf/tunnel-v1.pb differ diff --git a/gen/rust/protocol.rs b/gen/rust/protocol.rs new file mode 100644 index 0000000..9baf3c2 --- /dev/null +++ b/gen/rust/protocol.rs @@ -0,0 +1,256 @@ +// Code generated by tools/generate.py; DO NOT EDIT. +#![allow(non_snake_case)] +pub const SCHEMA_SHA256: &str = "36e4c8bac2eae674c1eba551c6ca8c64bf80fcc092ca63ec89a2c71c2bec86e1"; +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"; +pub type JsonObject = std::collections::BTreeMap; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AllocationPolicy { + pub minimumKbps: i64, + pub targetKbps: i64, + pub maximumKbps: i64, + pub tier: String, + pub audience: String, + pub protocol: String, + pub protocolVersion: i64, + pub grantTtlSeconds: i64, + pub reservationLeaseSeconds: i64, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AssignedDesktop { + pub assignmentId: String, + pub poolId: String, + pub name: String, + pub availability: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BrokerSession { + pub id: String, + pub principalId: String, + pub poolId: String, + pub assignmentId: Option, + pub state: String, + pub policySnapshot: AllocationPolicy, + pub reconnectDeadline: Option, + pub outcome: Option, + pub failureCode: Option, + pub cleanupState: String, + pub idempotencyKey: String, + pub correlationId: String, + pub requestedAt: String, + pub endedAt: Option, + pub version: i64, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ClipboardText { + pub text: String, + pub encoding: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ConnectionManifest { + pub version: String, + pub purpose: String, + pub sessionId: String, + pub reconnectSequence: i64, + pub gateway: ManifestGateway, + pub tunnel: ManifestTunnel, + pub profile: ManifestProfile, + pub grant: GrantReference, + pub correlationId: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeviceChallenge { + pub deviceId: String, + pub serverId: String, + pub principalId: String, + pub challenge: String, + pub expiresAt: String, + pub algorithm: String, + pub signatureFormat: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeviceProofRequest { + pub challenge: String, + pub signature: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeviceRegistrationRequest { + pub name: String, + pub platform: String, + pub deviceSubject: String, + pub algorithm: String, + pub publicKey: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EntitledPool { + pub poolId: String, + pub name: String, + pub assignmentState: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ErrorEnvelope { + pub status: bool, + pub error: String, + pub code: String, + pub message: String, + pub resolution: String, + pub requestId: String, + pub violations: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EventEnvelope { + pub eventId: String, + pub sequence: i64, + pub typeValue: String, + pub version: i64, + pub resource: ResourceLink, + pub occurredAt: String, + pub correlationId: String, + pub payload: JsonObject, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EventResume { + pub cursor: String, + pub lastSequence: i64, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FieldViolation { + pub field: String, + pub code: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GrantReference { + pub opaqueValue: String, + pub expiresAt: String, + pub audience: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LoginRequest { + pub provider: Option, + pub username: String, + pub password: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ManifestBounds { + pub minimumKbps: i64, + pub targetKbps: i64, + pub maximumKbps: i64, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ManifestGateway { + pub id: String, + pub addresses: Vec, + pub publicIdentity: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ManifestProfile { + pub id: String, + pub bounds: ManifestBounds, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ManifestTunnel { + pub versions: Vec, + pub features: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NativeCredential { + pub deviceId: Option, + pub familyId: String, + pub accessToken: String, + pub refreshToken: String, + pub expiresAt: String, + pub refreshExpiresAt: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PageInfo { + pub limit: i64, + pub nextCursor: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ReauthGrant { + pub token: String, + pub purpose: String, + pub expiresAt: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ReauthRequest { + pub password: String, + pub purpose: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ReconnectRequest { + pub clientDeviceId: String, + pub deviceKeyId: String, + pub expectedVersion: i64, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RefreshRequest { + pub familyId: String, + pub refreshToken: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Resource { + pub id: String, + pub kind: String, + pub name: String, + pub state: String, + pub assignmentState: Option, + pub version: i64, + pub links: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResourceLink { + pub typeValue: String, + pub id: String, + pub version: i64, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResourceList { + pub assignedDesktops: Vec, + pub entitledPools: Vec, + pub page: PageInfo, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SessionRequest { + pub clientDeviceId: String, + pub deviceKeyId: String, + pub poolId: String, + pub idempotencyKey: String, + pub policySnapshot: AllocationPolicy, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct VersionNegotiation { + pub supportedVersions: Vec, + pub features: Vec, +} diff --git a/gen/swift/Protocol.swift b/gen/swift/Protocol.swift new file mode 100644 index 0000000..843d3e8 --- /dev/null +++ b/gen/swift/Protocol.swift @@ -0,0 +1,664 @@ +// Code generated by tools/generate.py; DO NOT EDIT. +import Foundation +public typealias JSONObject = [String: String] +public let schemaSHA256 = "36e4c8bac2eae674c1eba551c6ca8c64bf80fcc092ca63ec89a2c71c2bec86e1" +public let currentWireVersion = "1" +public let nMinus1WireVersion = "0" +public let nMinus2WireVersion = "-1" + +public struct AllocationPolicy: Codable, Equatable { + public let minimumKbps: Int64 + public let targetKbps: Int64 + public let maximumKbps: Int64 + public let tier: String + public let audience: String + public let protocolValue: String + public let protocolVersion: Int64 + public let grantTtlSeconds: Int64 + public let reservationLeaseSeconds: Int64 + enum CodingKeys: String, CodingKey { + case minimumKbps = "minimum_kbps" + case targetKbps = "target_kbps" + case maximumKbps = "maximum_kbps" + case tier = "tier" + case audience = "audience" + case protocolValue = "protocol" + case protocolVersion = "protocol_version" + case grantTtlSeconds = "grant_ttl_seconds" + case reservationLeaseSeconds = "reservation_lease_seconds" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + minimumKbps = try c.decode(Int64.self, forKey: .minimumKbps) + targetKbps = try c.decode(Int64.self, forKey: .targetKbps) + maximumKbps = try c.decode(Int64.self, forKey: .maximumKbps) + tier = try c.decode(String.self, forKey: .tier) + audience = try c.decode(String.self, forKey: .audience) + protocolValue = try c.decode(String.self, forKey: .protocolValue) + protocolVersion = try c.decode(Int64.self, forKey: .protocolVersion) + grantTtlSeconds = try c.decode(Int64.self, forKey: .grantTtlSeconds) + reservationLeaseSeconds = try c.decode(Int64.self, forKey: .reservationLeaseSeconds) + } +} + +public struct AssignedDesktop: Codable, Equatable { + public let assignmentId: String + public let poolId: String + public let name: String + public let availability: String + enum CodingKeys: String, CodingKey { + case assignmentId = "assignment_id" + case poolId = "pool_id" + case name = "name" + case availability = "availability" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + assignmentId = try c.decode(String.self, forKey: .assignmentId) + poolId = try c.decode(String.self, forKey: .poolId) + name = try c.decode(String.self, forKey: .name) + availability = try c.decode(String.self, forKey: .availability) + } +} + +public struct BrokerSession: Codable, Equatable { + public let id: String + public let principalId: String + public let poolId: String + public let assignmentId: String? + public let state: String + public let policySnapshot: AllocationPolicy + public let reconnectDeadline: String? + public let outcome: String? + public let failureCode: String? + public let cleanupState: String + public let idempotencyKey: String + public let correlationId: String + public let requestedAt: String + public let endedAt: String? + public let version: Int64 + enum CodingKeys: String, CodingKey { + case id = "id" + case principalId = "principal_id" + case poolId = "pool_id" + case assignmentId = "assignment_id" + case state = "state" + case policySnapshot = "policy_snapshot" + case reconnectDeadline = "reconnect_deadline" + case outcome = "outcome" + case failureCode = "failure_code" + case cleanupState = "cleanup_state" + case idempotencyKey = "idempotency_key" + case correlationId = "correlation_id" + case requestedAt = "requested_at" + case endedAt = "ended_at" + case version = "version" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + id = try c.decode(String.self, forKey: .id) + principalId = try c.decode(String.self, forKey: .principalId) + poolId = try c.decode(String.self, forKey: .poolId) + assignmentId = try c.decodeIfPresent(String.self, forKey: .assignmentId) + state = try c.decode(String.self, forKey: .state) + policySnapshot = try c.decode(AllocationPolicy.self, forKey: .policySnapshot) + reconnectDeadline = try c.decodeIfPresent(String.self, forKey: .reconnectDeadline) + outcome = try c.decodeIfPresent(String.self, forKey: .outcome) + failureCode = try c.decodeIfPresent(String.self, forKey: .failureCode) + cleanupState = try c.decode(String.self, forKey: .cleanupState) + idempotencyKey = try c.decode(String.self, forKey: .idempotencyKey) + correlationId = try c.decode(String.self, forKey: .correlationId) + requestedAt = try c.decode(String.self, forKey: .requestedAt) + endedAt = try c.decodeIfPresent(String.self, forKey: .endedAt) + version = try c.decode(Int64.self, forKey: .version) + } +} + +public struct ClipboardText: Codable, Equatable { + public let text: String + public let encoding: String + enum CodingKeys: String, CodingKey { + case text = "text" + case encoding = "encoding" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + text = try c.decode(String.self, forKey: .text) + encoding = try c.decode(String.self, forKey: .encoding) + } +} + +public struct ConnectionManifest: Codable, Equatable { + public let version: String + public let purpose: String + public let sessionId: String + public let reconnectSequence: Int64 + public let gateway: ManifestGateway + public let tunnel: ManifestTunnel + public let profile: ManifestProfile + public let grant: GrantReference + public let correlationId: String + enum CodingKeys: String, CodingKey { + case version = "version" + case purpose = "purpose" + case sessionId = "session_id" + case reconnectSequence = "reconnect_sequence" + case gateway = "gateway" + case tunnel = "tunnel" + case profile = "profile" + case grant = "grant" + case correlationId = "correlation_id" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + version = try c.decode(String.self, forKey: .version) + purpose = try c.decode(String.self, forKey: .purpose) + sessionId = try c.decode(String.self, forKey: .sessionId) + reconnectSequence = try c.decode(Int64.self, forKey: .reconnectSequence) + gateway = try c.decode(ManifestGateway.self, forKey: .gateway) + tunnel = try c.decode(ManifestTunnel.self, forKey: .tunnel) + profile = try c.decode(ManifestProfile.self, forKey: .profile) + grant = try c.decode(GrantReference.self, forKey: .grant) + correlationId = try c.decode(String.self, forKey: .correlationId) + } +} + +public struct DeviceChallenge: Codable, Equatable { + public let deviceId: String + public let serverId: String + public let principalId: String + public let challenge: String + public let expiresAt: String + public let algorithm: String + public let signatureFormat: String + enum CodingKeys: String, CodingKey { + case deviceId = "device_id" + case serverId = "server_id" + case principalId = "principal_id" + case challenge = "challenge" + case expiresAt = "expires_at" + case algorithm = "algorithm" + case signatureFormat = "signature_format" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + deviceId = try c.decode(String.self, forKey: .deviceId) + serverId = try c.decode(String.self, forKey: .serverId) + principalId = try c.decode(String.self, forKey: .principalId) + challenge = try c.decode(String.self, forKey: .challenge) + expiresAt = try c.decode(String.self, forKey: .expiresAt) + algorithm = try c.decode(String.self, forKey: .algorithm) + signatureFormat = try c.decode(String.self, forKey: .signatureFormat) + } +} + +public struct DeviceProofRequest: Codable, Equatable { + public let challenge: String + public let signature: String + enum CodingKeys: String, CodingKey { + case challenge = "challenge" + case signature = "signature" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + challenge = try c.decode(String.self, forKey: .challenge) + signature = try c.decode(String.self, forKey: .signature) + } +} + +public struct DeviceRegistrationRequest: Codable, Equatable { + public let name: String + public let platform: String + public let deviceSubject: String + public let algorithm: String + public let publicKey: String + enum CodingKeys: String, CodingKey { + case name = "name" + case platform = "platform" + case deviceSubject = "device_subject" + case algorithm = "algorithm" + case publicKey = "public_key" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + name = try c.decode(String.self, forKey: .name) + platform = try c.decode(String.self, forKey: .platform) + deviceSubject = try c.decode(String.self, forKey: .deviceSubject) + algorithm = try c.decode(String.self, forKey: .algorithm) + publicKey = try c.decode(String.self, forKey: .publicKey) + } +} + +public struct EntitledPool: Codable, Equatable { + public let poolId: String + public let name: String + public let assignmentState: String + enum CodingKeys: String, CodingKey { + case poolId = "pool_id" + case name = "name" + case assignmentState = "assignment_state" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + poolId = try c.decode(String.self, forKey: .poolId) + name = try c.decode(String.self, forKey: .name) + assignmentState = try c.decode(String.self, forKey: .assignmentState) + } +} + +public struct ErrorEnvelope: Codable, Equatable { + public let status: Bool + public let error: String + public let code: String + public let message: String + public let resolution: String + public let requestId: String + public let violations: [FieldViolation] + enum CodingKeys: String, CodingKey { + case status = "status" + case error = "error" + case code = "code" + case message = "message" + case resolution = "resolution" + case requestId = "request_id" + case violations = "violations" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + status = try c.decode(Bool.self, forKey: .status) + error = try c.decode(String.self, forKey: .error) + code = try c.decode(String.self, forKey: .code) + message = try c.decode(String.self, forKey: .message) + resolution = try c.decode(String.self, forKey: .resolution) + requestId = try c.decode(String.self, forKey: .requestId) + violations = try c.decode([FieldViolation].self, forKey: .violations) + } +} + +public struct EventEnvelope: Codable, Equatable { + public let eventId: String + public let sequence: Int64 + public let type: String + public let version: Int64 + public let resource: ResourceLink + public let occurredAt: String + public let correlationId: String + public let payload: JSONObject + enum CodingKeys: String, CodingKey { + case eventId = "event_id" + case sequence = "sequence" + case type = "type" + case version = "version" + case resource = "resource" + case occurredAt = "occurred_at" + case correlationId = "correlation_id" + case payload = "payload" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + eventId = try c.decode(String.self, forKey: .eventId) + sequence = try c.decode(Int64.self, forKey: .sequence) + type = try c.decode(String.self, forKey: .type) + version = try c.decode(Int64.self, forKey: .version) + resource = try c.decode(ResourceLink.self, forKey: .resource) + occurredAt = try c.decode(String.self, forKey: .occurredAt) + correlationId = try c.decode(String.self, forKey: .correlationId) + payload = try c.decode(JSONObject.self, forKey: .payload) + } +} + +public struct EventResume: Codable, Equatable { + public let cursor: String + public let lastSequence: Int64 + enum CodingKeys: String, CodingKey { + case cursor = "cursor" + case lastSequence = "last_sequence" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + cursor = try c.decode(String.self, forKey: .cursor) + lastSequence = try c.decode(Int64.self, forKey: .lastSequence) + } +} + +public struct FieldViolation: Codable, Equatable { + public let field: String + public let code: String + enum CodingKeys: String, CodingKey { + case field = "field" + case code = "code" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + field = try c.decode(String.self, forKey: .field) + code = try c.decode(String.self, forKey: .code) + } +} + +public struct GrantReference: Codable, Equatable { + public let opaqueValue: String + public let expiresAt: String + public let audience: String + enum CodingKeys: String, CodingKey { + case opaqueValue = "opaque_value" + case expiresAt = "expires_at" + case audience = "audience" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + opaqueValue = try c.decode(String.self, forKey: .opaqueValue) + expiresAt = try c.decode(String.self, forKey: .expiresAt) + audience = try c.decode(String.self, forKey: .audience) + } +} + +public struct LoginRequest: Codable, Equatable { + public let provider: String? + public let username: String + public let password: String + enum CodingKeys: String, CodingKey { + case provider = "provider" + case username = "username" + case password = "password" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + provider = try c.decodeIfPresent(String.self, forKey: .provider) + username = try c.decode(String.self, forKey: .username) + password = try c.decode(String.self, forKey: .password) + } +} + +public struct ManifestBounds: Codable, Equatable { + public let minimumKbps: Int64 + public let targetKbps: Int64 + public let maximumKbps: Int64 + enum CodingKeys: String, CodingKey { + case minimumKbps = "minimum_kbps" + case targetKbps = "target_kbps" + case maximumKbps = "maximum_kbps" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + minimumKbps = try c.decode(Int64.self, forKey: .minimumKbps) + targetKbps = try c.decode(Int64.self, forKey: .targetKbps) + maximumKbps = try c.decode(Int64.self, forKey: .maximumKbps) + } +} + +public struct ManifestGateway: Codable, Equatable { + public let id: String + public let addresses: [String] + public let publicIdentity: String + enum CodingKeys: String, CodingKey { + case id = "id" + case addresses = "addresses" + case publicIdentity = "public_identity" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + id = try c.decode(String.self, forKey: .id) + addresses = try c.decode([String].self, forKey: .addresses) + publicIdentity = try c.decode(String.self, forKey: .publicIdentity) + } +} + +public struct ManifestProfile: Codable, Equatable { + public let id: String + public let bounds: ManifestBounds + enum CodingKeys: String, CodingKey { + case id = "id" + case bounds = "bounds" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + id = try c.decode(String.self, forKey: .id) + bounds = try c.decode(ManifestBounds.self, forKey: .bounds) + } +} + +public struct ManifestTunnel: Codable, Equatable { + public let versions: [String] + public let features: [String] + enum CodingKeys: String, CodingKey { + case versions = "versions" + case features = "features" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + versions = try c.decode([String].self, forKey: .versions) + features = try c.decode([String].self, forKey: .features) + } +} + +public struct NativeCredential: Codable, Equatable { + public let deviceId: String? + public let familyId: String + public let accessToken: String + public let refreshToken: String + public let expiresAt: String + public let refreshExpiresAt: String? + enum CodingKeys: String, CodingKey { + case deviceId = "device_id" + case familyId = "family_id" + case accessToken = "access_token" + case refreshToken = "refresh_token" + case expiresAt = "expires_at" + case refreshExpiresAt = "refresh_expires_at" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + deviceId = try c.decodeIfPresent(String.self, forKey: .deviceId) + familyId = try c.decode(String.self, forKey: .familyId) + accessToken = try c.decode(String.self, forKey: .accessToken) + refreshToken = try c.decode(String.self, forKey: .refreshToken) + expiresAt = try c.decode(String.self, forKey: .expiresAt) + refreshExpiresAt = try c.decodeIfPresent(String.self, forKey: .refreshExpiresAt) + } +} + +public struct PageInfo: Codable, Equatable { + public let limit: Int64 + public let nextCursor: String + enum CodingKeys: String, CodingKey { + case limit = "limit" + case nextCursor = "next_cursor" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + limit = try c.decode(Int64.self, forKey: .limit) + nextCursor = try c.decode(String.self, forKey: .nextCursor) + } +} + +public struct ReauthGrant: Codable, Equatable { + public let token: String + public let purpose: String + public let expiresAt: String + enum CodingKeys: String, CodingKey { + case token = "token" + case purpose = "purpose" + case expiresAt = "expires_at" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + token = try c.decode(String.self, forKey: .token) + purpose = try c.decode(String.self, forKey: .purpose) + expiresAt = try c.decode(String.self, forKey: .expiresAt) + } +} + +public struct ReauthRequest: Codable, Equatable { + public let password: String + public let purpose: String + enum CodingKeys: String, CodingKey { + case password = "password" + case purpose = "purpose" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + password = try c.decode(String.self, forKey: .password) + purpose = try c.decode(String.self, forKey: .purpose) + } +} + +public struct ReconnectRequest: Codable, Equatable { + public let clientDeviceId: String + public let deviceKeyId: String + public let expectedVersion: Int64 + enum CodingKeys: String, CodingKey { + case clientDeviceId = "client_device_id" + case deviceKeyId = "device_key_id" + case expectedVersion = "expected_version" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + clientDeviceId = try c.decode(String.self, forKey: .clientDeviceId) + deviceKeyId = try c.decode(String.self, forKey: .deviceKeyId) + expectedVersion = try c.decode(Int64.self, forKey: .expectedVersion) + } +} + +public struct RefreshRequest: Codable, Equatable { + public let familyId: String + public let refreshToken: String + enum CodingKeys: String, CodingKey { + case familyId = "family_id" + case refreshToken = "refresh_token" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + familyId = try c.decode(String.self, forKey: .familyId) + refreshToken = try c.decode(String.self, forKey: .refreshToken) + } +} + +public struct Resource: Codable, Equatable { + public let id: String + public let kind: String + public let name: String + public let state: String + public let assignmentState: String? + public let version: Int64 + public let links: [ResourceLink] + enum CodingKeys: String, CodingKey { + case id = "id" + case kind = "kind" + case name = "name" + case state = "state" + case assignmentState = "assignment_state" + case version = "version" + case links = "links" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + id = try c.decode(String.self, forKey: .id) + kind = try c.decode(String.self, forKey: .kind) + name = try c.decode(String.self, forKey: .name) + state = try c.decode(String.self, forKey: .state) + assignmentState = try c.decodeIfPresent(String.self, forKey: .assignmentState) + version = try c.decode(Int64.self, forKey: .version) + links = try c.decode([ResourceLink].self, forKey: .links) + } +} + +public struct ResourceLink: Codable, Equatable { + public let type: String + public let id: String + public let version: Int64 + enum CodingKeys: String, CodingKey { + case type = "type" + case id = "id" + case version = "version" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + type = try c.decode(String.self, forKey: .type) + id = try c.decode(String.self, forKey: .id) + version = try c.decode(Int64.self, forKey: .version) + } +} + +public struct ResourceList: Codable, Equatable { + public let assignedDesktops: [AssignedDesktop] + public let entitledPools: [EntitledPool] + public let page: PageInfo + enum CodingKeys: String, CodingKey { + case assignedDesktops = "assigned_desktops" + case entitledPools = "entitled_pools" + case page = "page" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + assignedDesktops = try c.decode([AssignedDesktop].self, forKey: .assignedDesktops) + entitledPools = try c.decode([EntitledPool].self, forKey: .entitledPools) + page = try c.decode(PageInfo.self, forKey: .page) + } +} + +public struct SessionRequest: Codable, Equatable { + public let clientDeviceId: String + public let deviceKeyId: String + public let poolId: String + public let idempotencyKey: String + public let policySnapshot: AllocationPolicy + enum CodingKeys: String, CodingKey { + case clientDeviceId = "client_device_id" + case deviceKeyId = "device_key_id" + case poolId = "pool_id" + case idempotencyKey = "idempotency_key" + case policySnapshot = "policy_snapshot" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + clientDeviceId = try c.decode(String.self, forKey: .clientDeviceId) + deviceKeyId = try c.decode(String.self, forKey: .deviceKeyId) + poolId = try c.decode(String.self, forKey: .poolId) + idempotencyKey = try c.decode(String.self, forKey: .idempotencyKey) + policySnapshot = try c.decode(AllocationPolicy.self, forKey: .policySnapshot) + } +} + +public struct VersionNegotiation: Codable, Equatable { + public let supportedVersions: [String] + public let features: [String] + enum CodingKeys: String, CodingKey { + case supportedVersions = "supported_versions" + case features = "features" + } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + supportedVersions = try c.decode([String].self, forKey: .supportedVersions) + features = try c.decode([String].self, forKey: .features) + } +} diff --git a/tests/go/protocol_test.go b/tests/go/protocol_test.go new file mode 100644 index 0000000..3977ec8 --- /dev/null +++ b/tests/go/protocol_test.go @@ -0,0 +1,37 @@ +package protocol_test + +import ( + "strings" + "testing" + + protocol "github.com/sechmachine/VerseVDI-Protocol/gen/go/protocol" +) + +func TestManifestRejectsForbiddenAndUnknownFields(t *testing.T) { + valid := `{"version":"1","purpose":"launch","session_id":"session-1","reconnect_sequence":0,"gateway":{"id":"gateway-1","addresses":["gateway.control.test:443"],"public_identity":"gateway-1"},"tunnel":{"versions":["verse-gateway-v1/1"],"features":["control.v1"]},"profile":{"id":"standard","bounds":{"minimum_kbps":1,"target_kbps":2,"maximum_kbps":3}},"grant":{"opaque_value":"opaque-one-time-grant-value-with-at-least-43-bytes","expires_at":"2099-01-01T00:00:00Z","audience":"versevdi-gateway"},"correlation_id":"correlation-1"}` + manifest, err := protocol.DecodeConnectionManifest([]byte(valid)) + if err != nil || manifest.Gateway.ID != "gateway-1" { + t.Fatalf("valid manifest = %+v, err = %v", manifest, err) + } + for _, field := range []string{"provider_url", "vm_address", "password"} { + payload := strings.Replace(valid, `"correlation_id":"correlation-1"`, `"correlation_id":"correlation-1","`+field+`":"forbidden"`, 1) + if _, err := protocol.DecodeConnectionManifest([]byte(payload)); err == nil { + t.Fatalf("DecodeConnectionManifest accepted forbidden field %q", field) + } + } +} + +func TestPageInfoRejectsOutOfBoundsLimit(t *testing.T) { + if _, err := protocol.DecodePageInfo([]byte(`{"limit":101,"next_cursor":""}`)); err == nil { + t.Fatal("DecodePageInfo accepted limit above the contract maximum") + } +} + +func TestGeneratedDecodersRejectMissingRequiredFieldsAndTrailingValues(t *testing.T) { + if _, err := protocol.DecodeErrorEnvelope([]byte(`{"status":false,"error":"safe","code":"invalid_request","message":"safe","resolution":"retry","violations":[]} {}`)); err == nil { + t.Fatal("DecodeErrorEnvelope accepted a trailing JSON value") + } + if _, err := protocol.DecodeErrorEnvelope([]byte(`{"error":"safe","code":"invalid_request","message":"safe","resolution":"retry","request_id":"req-1","violations":[]}`)); err == nil { + t.Fatal("DecodeErrorEnvelope accepted a missing required boolean") + } +} diff --git a/tools/fixture_digest.py b/tools/fixture_digest.py new file mode 100644 index 0000000..b4b9b97 --- /dev/null +++ b/tools/fixture_digest.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +"""Check the content-addressed Phase 3A conformance corpus.""" + +from __future__ import annotations + +import hashlib +import json +import pathlib +import sys + +ROOT = pathlib.Path(__file__).resolve().parents[1] +MANIFEST = ROOT / "fixtures/manifest.json" + + +def digest(paths: list[str]) -> str: + value = hashlib.sha256() + for relative in paths: + path = ROOT / relative + value.update(relative.encode("utf-8")) + value.update(b"\0") + value.update(path.read_bytes()) + value.update(b"\0") + return value.hexdigest() + + +def main() -> int: + manifest = json.loads(MANIFEST.read_text(encoding="utf-8")) + paths = sorted(path.relative_to(ROOT).as_posix() for path in (ROOT / "fixtures/conformance").glob("*.tsv")) + if paths != manifest.get("files"): + raise ValueError("fixture manifest file list is stale") + actual = digest(paths) + expected = manifest.get("corpus_sha256") + if not expected or actual != expected: + raise ValueError(f"fixture corpus hash mismatch: {actual}") + print(f"Fixture corpus SHA256 {actual}") + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except (OSError, ValueError, json.JSONDecodeError) as exc: + print(f"fixture_digest: {exc}", file=sys.stderr) + raise SystemExit(1) diff --git a/tools/generate.py b/tools/generate.py new file mode 100644 index 0000000..2b579b1 --- /dev/null +++ b/tools/generate.py @@ -0,0 +1,383 @@ +#!/usr/bin/env python3 +"""Deterministically generate the small language-neutral Phase 3A bindings.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import pathlib +import re +import subprocess +import sys +from typing import Any + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +SCHEMA_PATH = ROOT / "schemas/control-v1.schema.json" +VERSION_PATH = ROOT / "VERSION" +COMPATIBILITY_PATH = ROOT / "compatibility.json" + + +def load_schema() -> tuple[dict[str, Any], str, str, dict[str, Any]]: + raw = SCHEMA_PATH.read_bytes() + schema = json.loads(raw) + version = VERSION_PATH.read_text(encoding="utf-8").strip() + if not re.fullmatch(r"\d+\.\d+\.\d+", version): + raise ValueError("VERSION must be semantic version text") + compatibility = json.loads(COMPATIBILITY_PATH.read_text(encoding="utf-8")) + for key in ("current", "n_minus_1", "n_minus_2"): + if not isinstance(compatibility.get(key), str): + raise ValueError("compatibility declaration is incomplete") + return schema, hashlib.sha256(raw).hexdigest(), version, compatibility + + +def pascal(name: str) -> str: + return "".join(part[:1].upper() + part[1:] for part in re.split(r"[_-]", name)) + + +def go_field(name: str) -> str: + initialisms = {"api": "API", "http": "HTTP", "id": "ID", "ip": "IP", "sha": "SHA", "ttl": "TTL", "url": "URL", "uuid": "UUID"} + return "".join( + initialisms.get(part, part[:1].upper() + part[1:]) + for part in re.split(r"[_-]", name) + ) + + +def camel(name: str) -> str: + parts = re.split(r"[_-]", name) + return parts[0] + "".join(part[:1].upper() + part[1:] for part in parts[1:]) + + +def swift_field(name: str) -> str: + value = camel(name) + if value in {"protocol", "class", "struct", "enum", "extension", "private", "public", "internal", "fileprivate", "open", "func", "let", "var", "import", "switch", "case", "default", "operator", "where", "repeat", "return", "throw", "throws", "try", "catch", "defer", "in", "is", "as"}: + return value + "Value" + return value + + +RUST_KEYWORDS = { + "as", "break", "const", "continue", "crate", "else", "enum", "extern", + "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", + "move", "mut", "pub", "ref", "return", "self", "Self", "static", "struct", + "super", "trait", "true", "type", "unsafe", "use", "where", "while", "async", + "await", "dyn", "abstract", "become", "box", "do", "final", "macro", "override", + "priv", "typeof", "unsized", "virtual", "yield", "try", "union", +} + + +def rust_field(name: str) -> str: + value = camel(name) + return value + "Value" if value in RUST_KEYWORDS else value + + +def ref_name(value: Any) -> str | None: + if isinstance(value, dict) and isinstance(value.get("$ref"), str): + return value["$ref"].split("/")[-1] + return None + + +def prop_type(prop: dict[str, Any], language: str) -> str: + reference = ref_name(prop) + if reference: + return reference if language == "go" else (reference if language == "rust" else reference) + if prop.get("type") == "array": + item = prop.get("items", {}) + item_type = prop_type(item, language) + if language == "go": + return f"[]{item_type}" + if language == "rust": + return f"Vec<{item_type}>" + return f"[{item_type}]" + if prop.get("type") == "integer": + return "int64" if language == "go" else ("i64" if language == "rust" else "Int64") + if prop.get("type") == "boolean": + return "bool" if language != "swift" else "Bool" + if prop.get("type") == "object": + return "map[string]any" if language == "go" else ("JsonObject" if language == "rust" else "JSONObject") + return "string" if language == "go" else ("String" if language == "rust" else "String") + + +def go_zero(prop: dict[str, Any], field: str) -> str: + typ = prop_type(prop, "go") + if typ in {"string", "int64", "bool"}: + zero = {"string": '""', "int64": "0", "bool": "false"}[typ] + return f"v.{field} == {zero}" + if typ.startswith("[]"): + return f"v.{field} == nil" + if typ == "map[string]any": + return f"v.{field} == nil" + return f"reflect.DeepEqual(v.{field}, {typ}{{}})" + + +def go_validation(definition: dict[str, Any]) -> list[str]: + lines: list[str] = [] + name = definition["name"] + required = set(definition.get("required", [])) + for prop_name, prop in definition.get("properties", {}).items(): + field = go_field(prop_name) + required_zero_is_valid = prop.get("type") == "integer" and prop.get("minimum") == 0 + if prop_name in required and prop.get("type") != "boolean" and not required_zero_is_valid: + zero = go_zero(prop, field) + lines.append(f"\tif {zero} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"required\"}}) }}") + if prop.get("type") == "string": + if "minLength" in prop: + lines.append(f"\tif len(v.{field}) < {prop['minLength']} && v.{field} != \"\" {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"min_length\"}}) }}") + if "maxLength" in prop: + lines.append(f"\tif len(v.{field}) > {prop['maxLength']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"max_length\"}}) }}") + if "const" in prop: + lines.append(f"\tif v.{field} != \"{prop['const']}\" && v.{field} != \"\" {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_value\"}}) }}") + if "enum" in prop: + allowed = " || ".join(f'v.{field} == "{value}"' for value in prop["enum"]) + lines.append(f"\tif v.{field} != \"\" && !({allowed}) {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_value\"}}) }}") + if prop.get("format") == "date-time": + lines.append( + '\tif v.%s != "" { if parsed, err := time.Parse(time.RFC3339Nano, v.%s); err != nil || parsed.UTC().Format(time.RFC3339Nano) != v.%s { violations = append(violations, FieldViolation{Field: "%s", Code: "invalid_time"}) } }' + % (field, field, field, prop_name) + ) + if prop.get("type") == "integer": + if "minimum" in prop: + lines.append(f"\tif v.{field} != 0 && v.{field} < {prop['minimum']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"minimum\"}}) }}") + if "maximum" in prop: + lines.append(f"\tif v.{field} > {prop['maximum']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"maximum\"}}) }}") + if prop.get("type") == "array": + if "minItems" in prop: + 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", {})) + 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) + if reference: + lines.append(f"\tif err := v.{field}.Validate(); err != nil {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_object\"}}) }}") + return lines + + +def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str, compatibility: dict[str, Any]) -> str: + out = [ + "// Code generated by tools/generate.py; DO NOT EDIT.", + "package protocol", + "", + "import (", + "\"bytes\"", + "\"encoding/json\"", + "\"errors\"", + "\"fmt\"", + "\"reflect\"", + "\"time\"", + ")", + "", + f'const SchemaSHA256 = "{schema_hash}"', + f'const ProtocolVersion = "{version}"', + f'const CurrentWireVersion = "{compatibility["current"]}"', + f'const NMinus1WireVersion = "{compatibility["n_minus_1"]}"', + f'const NMinus2WireVersion = "{compatibility["n_minus_2"]}"', + "", + "type FieldViolation struct {", + "\tField string `json:\"field\"`", + "\tCode string `json:\"code\"`", + "}", + "", + "type ValidationError struct {", + "\tViolations []FieldViolation", + "}", + "", + "func (e ValidationError) Error() string { return \"protocol validation failed\" }", + "", + ] + for name in sorted(defs): + if name == "FieldViolation": + continue + definition = defs[name] + out.append(f"type {name} struct {{") + required = set(definition.get("required", [])) + for prop_name, prop in definition.get("properties", {}).items(): + tag = prop_name + (",omitempty" if prop_name not in required else "") + out.append(f"\t{go_field(prop_name)} {prop_type(prop, 'go')} `json:\"{tag}\"`") + out.extend(["}", ""]) + for name in sorted(defs): + out.append(f"func (v {name}) Validate() error {{") + out.append("\tvar violations []FieldViolation") + out.extend(go_validation(defs[name])) + out.append("\tif len(violations) > 0 { return ValidationError{Violations: violations} }") + out.append("\treturn nil") + out.append("}") + out.append("") + out.append(f"func Decode{name}(data []byte) ({name}, error) {{") + out.append(f"\tvar value {name}") + out.append("\tif len(data) > 1024*1024 { return value, errors.New(\"protocol payload exceeds limit\") }") + out.append("\tvar fields map[string]json.RawMessage") + out.append("\tif err := json.Unmarshal(data, &fields); err != nil { return value, err }") + required_fields = sorted(defs[name].get("required", [])) + for prop_name in required_fields: + out.append( + '\tif raw, ok := fields["%s"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { return value, ValidationError{Violations: []FieldViolation{{Field: "%s", Code: "required"}}} }' + % (prop_name, prop_name) + ) + for prop_name, prop in defs[name].get("properties", {}).items(): + if "x-max-bytes" in prop: + out.append( + '\tif raw, ok := fields["%s"]; ok && len(raw) > %d { return value, ValidationError{Violations: []FieldViolation{{Field: "%s", Code: "max_bytes"}}} }' + % (prop_name, prop["x-max-bytes"], prop_name) + ) + out.append("\tdecoder := json.NewDecoder(bytes.NewReader(data))") + out.append("\tdecoder.DisallowUnknownFields()") + out.append("\tif err := decoder.Decode(&value); err != nil { return value, err }") + out.append("\tvar trailing any") + out.append("\tif err := decoder.Decode(&trailing); err != io.EOF { if err == nil { return value, errors.New(\"trailing JSON value\") }; return value, err }") + out.append("\tif err := value.Validate(); err != nil { return value, err }") + out.append("\treturn value, nil") + out.append("}") + out.append("") + out.append(f"func Encode{name}(value {name}) ([]byte, error) {{") + out.append("\tif err := value.Validate(); err != nil { return nil, err }") + out.append("\treturn json.Marshal(value)") + out.append("}") + out.append("") + # Use io.EOF in generated code without making every generated decoder depend on + # error-string comparison; replace the deliberately compact placeholder. + text = "\n".join(out).replace('"errors"\n"fmt"', '"errors"\n"fmt"\n\"io"') + text = text.replace('!errors.Is(err, errors.New("EOF")) && err != nil', '!errors.Is(err, io.EOF)') + return text + "\n" + + +def rust_type(prop: dict[str, Any]) -> str: + reference = ref_name(prop) + if reference: + return reference + if prop.get("type") == "array": + return f"Vec<{rust_type(prop.get('items', {}))}>" + if prop.get("type") == "integer": + return "i64" + if prop.get("type") == "boolean": + return "bool" + if prop.get("type") == "object": + return "JsonObject" + return "String" + + +def swift_type(prop: dict[str, Any]) -> str: + reference = ref_name(prop) + if reference: + return reference + if prop.get("type") == "array": + return f"[{swift_type(prop.get('items', {}))}]" + if prop.get("type") == "integer": + return "Int64" + if prop.get("type") == "boolean": + return "Bool" + if prop.get("type") == "object": + return "JSONObject" + return "String" + + +def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibility: dict[str, Any]) -> str: + out = [ + "// Code generated by tools/generate.py; DO NOT EDIT.", + "#![allow(non_snake_case)]", + "pub const SCHEMA_SHA256: &str = \"" + schema_hash + "\";", + f'pub const CURRENT_WIRE_VERSION: &str = "{compatibility["current"]}";', + f'pub const N_MINUS_1_WIRE_VERSION: &str = "{compatibility["n_minus_1"]}";', + f'pub const N_MINUS_2_WIRE_VERSION: &str = "{compatibility["n_minus_2"]}";', + "pub type JsonObject = std::collections::BTreeMap;", + "", + ] + for name in sorted(defs): + definition = defs[name] + out.extend(["#[derive(Debug, Clone, PartialEq, Eq)]", f"pub struct {name} {{"]) + required = set(definition.get("required", [])) + for prop_name, prop in definition.get("properties", {}).items(): + field = rust_field(prop_name) + typ = rust_type(prop) + if prop_name not in required: + typ = f"Option<{typ}>" + out.append(f" pub {field}: {typ},") + out.extend(["}", ""]) + return "\n".join(out) + + +def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibility: dict[str, Any]) -> str: + out = [ + "// Code generated by tools/generate.py; DO NOT EDIT.", + "import Foundation", + "public typealias JSONObject = [String: String]", + f"public let schemaSHA256 = \"{schema_hash}\"", + f'public let currentWireVersion = "{compatibility["current"]}"', + f'public let nMinus1WireVersion = "{compatibility["n_minus_1"]}"', + f'public let nMinus2WireVersion = "{compatibility["n_minus_2"]}"', + "", + ] + for name in sorted(defs): + definition = defs[name] + required = set(definition.get("required", [])) + out.extend(["public struct " + name + ": Codable, Equatable {",]) + for prop_name, prop in definition.get("properties", {}).items(): + typ = swift_type(prop) + if prop_name not in required: + typ += "?" + out.append(f" public let {swift_field(prop_name)}: {typ}") + out.append(" enum CodingKeys: String, CodingKey {") + for prop_name in definition.get("properties", {}): + out.append(f" case {swift_field(prop_name)} = \"{prop_name}\"") + out.extend([" }", "", " public init(from decoder: Decoder) throws {",]) + out.append(" let c = try decoder.container(keyedBy: CodingKeys.self)") + for prop_name, prop in definition.get("properties", {}).items(): + field = swift_field(prop_name) + typ = swift_type(prop) + if prop_name in required: + out.append(f" {field} = try c.decode({typ}.self, forKey: .{field})") + else: + out.append(f" {field} = try c.decodeIfPresent({typ}.self, forKey: .{field})") + out.extend([" }", "}", ""]) + return "\n".join(out) + + +def write_or_check(path: pathlib.Path, content: str, check: bool) -> None: + if check: + if not path.exists() or path.read_text(encoding="utf-8") != content: + raise ValueError(f"generated output differs: {path.relative_to(ROOT)}") + return + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + +def format_go(content: str) -> str: + result = subprocess.run(["gofmt"], input=content, text=True, capture_output=True, check=False) + if result.returncode != 0: + raise ValueError(f"gofmt failed: {result.stderr.strip()}") + return result.stdout + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--check", action="store_true") + args = parser.parse_args() + schema, schema_hash, version, compatibility = load_schema() + generator_hash = hashlib.sha256(pathlib.Path(__file__).read_bytes()).hexdigest() + defs = schema.get("$defs") + if not isinstance(defs, dict) or not defs: + raise ValueError("schema must contain non-empty $defs") + normalized = {name: dict(value, name=name) for name, value in defs.items()} + outputs = { + ROOT / "gen/go/protocol/protocol.go": format_go(generate_go(normalized, schema_hash, version, compatibility)), + ROOT / "gen/rust/protocol.rs": generate_rust(normalized, schema_hash, compatibility).rstrip() + "\n", + ROOT / "gen/swift/Protocol.swift": generate_swift(normalized, schema_hash, compatibility).rstrip() + "\n", + ROOT / "gen/manifest.json": json.dumps({ + "generator_sha256": generator_hash, + "schema_sha256": schema_hash, + "protocol_version": version, + "compatibility": compatibility, + }, sort_keys=True, indent=2) + "\n", + } + for path, content in outputs.items(): + write_or_check(path, content, args.check) + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except (OSError, ValueError, json.JSONDecodeError) as exc: + print(f"generate: {exc}", file=sys.stderr) + raise SystemExit(1) diff --git a/tools/go-conformance/main.go b/tools/go-conformance/main.go new file mode 100644 index 0000000..5134f09 --- /dev/null +++ b/tools/go-conformance/main.go @@ -0,0 +1,209 @@ +package main + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + + protocol "github.com/sechmachine/VerseVDI-Protocol/gen/go/protocol" +) + +const ( + datagramHeaderBytes = 21 + maximumFrameBytes = 65536 +) + +func main() { + entries, err := os.ReadDir("fixtures/conformance") + if err != nil { + panic(err) + } + var results []string + for _, entry := range entries { + if entry.IsDir() || filepath.Ext(entry.Name()) != ".tsv" { + continue + } + path := filepath.Join("fixtures/conformance", entry.Name()) + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + lines := strings.Split(strings.TrimSuffix(string(data), "\n"), "\n") + if len(lines) == 0 || lines[0] != "id\tversion\tkind\tinput\texpected" { + panic("invalid fixture header") + } + for _, line := range lines[1:] { + fields := strings.Split(line, "\t") + if len(fields) != 5 { + panic("invalid fixture row") + } + actual := evaluate(fields[2], fields[3]) + if actual != fields[4] { + panic(fmt.Sprintf("%s: got %s want %s", fields[0], actual, fields[4])) + } + results = append(results, fields[0]+"\t"+actual) + } + } + fixtureHash := readFixtureHash() + fmt.Printf("Go conformance passed normalized=%s fixtures=%s\n", normalizedDigest(results), fixtureHash) +} + +func evaluate(kind, input string) string { + parts := map[string]string{} + for _, item := range strings.Split(input, ";") { + pair := strings.SplitN(item, "=", 2) + if len(pair) == 2 { + parts[pair[0]] = pair[1] + } + } + switch kind { + case "version": + if input == "1" || input == "0" || input == "-1" { + return "valid" + } + return "invalid:unsupported_version" + case "page": + limit, err := strconv.Atoi(parts["limit"]) + value := protocol.PageInfo{Limit: int64(limit), NextCursor: parts["cursor"]} + if err == nil && value.Validate() == nil { + return "valid" + } + return "invalid:invalid_limit" + case "manifest": + forbidden := []string{"provider_url", "vm_address", "password", "private_key"} + for _, key := range forbidden { + if _, ok := parts[key]; ok { + return "invalid:forbidden_field" + } + } + value := protocol.ConnectionManifest{ + Version: parts["version"], Purpose: parts["purpose"], SessionID: "session-1", + ReconnectSequence: 0, + Gateway: protocol.ManifestGateway{ + ID: parts["gateway_id"], Addresses: []string{"gateway.control.test:443"}, PublicIdentity: parts["gateway_id"], + }, + Tunnel: protocol.ManifestTunnel{Versions: []string{parts["protocol"] + "/1"}, Features: []string{"control.v1"}}, + Profile: protocol.ManifestProfile{ID: "standard", Bounds: protocol.ManifestBounds{MinimumKbps: 1, TargetKbps: 2, MaximumKbps: 3}}, + Grant: protocol.GrantReference{OpaqueValue: parts["grant"], ExpiresAt: parts["expires_at"], Audience: parts["audience"]}, + CorrelationID: "correlation-1", + } + if value.Validate() == nil { + return "valid" + } + return "invalid:invalid_manifest" + case "clipboard": + _, hasFile := parts["file"] + value := protocol.ClipboardText{Text: parts["text"], Encoding: parts["encoding"]} + if !hasFile && value.Validate() == nil { + return "valid" + } + return "invalid:unsupported_clipboard" + case "event": + sequence, sequenceErr := strconv.ParseInt(parts["sequence"], 10, 64) + payloadBytes, payloadErr := strconv.Atoi(parts["payload_bytes"]) + if parts["version"] != "1" { + return "invalid:unsupported_version" + } + if parts["after"] != "" && parts["earliest"] != "" { + after, afterErr := strconv.ParseInt(parts["after"], 10, 64) + earliest, earliestErr := strconv.ParseInt(parts["earliest"], 10, 64) + if afterErr == nil && earliestErr == nil && after > 0 && earliest > 0 && after < earliest-1 { + return "invalid:gap" + } + } + value := protocol.EventEnvelope{ + EventID: "event-1", Sequence: sequence, Type: "broker.session.changed", Version: 1, + Resource: protocol.ResourceLink{Type: "broker_session", ID: "session-1", Version: 1}, + OccurredAt: "2099-01-01T00:00:00Z", CorrelationID: parts["correlation_id"], Payload: map[string]any{}, + } + if payloadErr != nil || payloadBytes > 16384 { + return "invalid:payload_limit" + } + if sequenceErr != nil || value.Validate() != nil { + return "invalid:required" + } + return "valid" + case "tunnel": + if (parts["offered"] == "1" || parts["offered"] == "0" || parts["offered"] == "-1") && parts["feature"] == "control.v1" { + return "valid" + } + if parts["feature"] != "control.v1" { + return "invalid:unsupported_feature" + } + return "invalid:unsupported_version" + case "datagram": + return classifyDatagram(parts["hex"]) + default: + return "invalid:unknown_kind" + } +} + +func classifyDatagram(encoded string) string { + raw, err := hex.DecodeString(encoded) + if err != nil { + return "invalid:hex" + } + if len(raw) < datagramHeaderBytes { + return "invalid:truncated" + } + if string(raw[:2]) != "VD" { + return "invalid:magic" + } + if raw[2] != 1 { + return "invalid:unsupported_version" + } + limits := map[byte]int{1: 1024, 2: 2048, 3: 65515} + limit, ok := limits[raw[3]] + if !ok { + return "invalid:unknown_channel" + } + if raw[4] != 0 { + return "invalid:flags" + } + if raw[18] == 0 || raw[17] >= raw[18] { + return "invalid:fragment" + } + payloadLength := int(raw[19])<<8 | int(raw[20]) + if payloadLength > limit { + return "invalid:payload_limit" + } + if len(raw) != datagramHeaderBytes+payloadLength { + return "invalid:length_mismatch" + } + if len(raw) > maximumFrameBytes { + return "invalid:frame_limit" + } + return "valid" +} + +func normalizedDigest(results []string) string { + const offset = uint64(14695981039346656037) + const prime = uint64(1099511628211) + value := offset + for _, result := range results { + for _, byteValue := range []byte(result + "\n") { + value ^= uint64(byteValue) + value *= prime + } + } + return fmt.Sprintf("%016x", value) +} + +func readFixtureHash() string { + data, err := os.ReadFile("fixtures/manifest.json") + if err != nil { + panic(err) + } + var manifest struct { + CorpusSHA256 string `json:"corpus_sha256"` + } + if err := json.Unmarshal(data, &manifest); err != nil || len(manifest.CorpusSHA256) != sha256.Size*2 { + panic("invalid fixture manifest") + } + return manifest.CorpusSHA256 +} diff --git a/tools/native_conformance.rs b/tools/native_conformance.rs new file mode 100644 index 0000000..ac18f88 --- /dev/null +++ b/tools/native_conformance.rs @@ -0,0 +1,154 @@ +use std::fs; +use std::path::PathBuf; + +fn values(input: &str) -> std::collections::BTreeMap { + input + .split(';') + .filter_map(|item| item.split_once('=')) + .map(|(key, value)| (key.to_owned(), value.to_owned())) + .collect() +} + +fn evaluate(kind: &str, input: &str) -> &'static str { + let values = values(input); + match kind { + "version" if matches!(input, "1" | "0" | "-1") => "valid", + "version" => "invalid:unsupported_version", + "page" => match values.get("limit").and_then(|value| value.parse::().ok()) { + Some(limit) if (1..=100).contains(&limit) => "valid", + _ => "invalid:invalid_limit", + }, + "manifest" if ["provider_url", "vm_address", "password", "private_key"] + .iter() + .any(|key| values.contains_key(*key)) => "invalid:forbidden_field", + "manifest" + if values.get("version").map(String::as_str) == Some("1") + && values.contains_key("gateway_id") + && values.get("grant").map_or(false, |value| value.len() >= 43) + && values.get("purpose").map(String::as_str) == Some("launch") => "valid", + "manifest" => "invalid:invalid_manifest", + "clipboard" if values.get("encoding").map(String::as_str) == Some("utf-8") + && !values.contains_key("file") => "valid", + "clipboard" => "invalid:unsupported_clipboard", + "event" if values.get("version").map(String::as_str) != Some("1") => { + "invalid:unsupported_version" + } + "event" if values.get("after").and_then(|value| value.parse::().ok()).is_some() + && values.get("earliest").and_then(|value| value.parse::().ok()).is_some() + && values["after"].parse::().unwrap() > 0 + && values["earliest"].parse::().unwrap() > 0 + && values["after"].parse::().unwrap() < values["earliest"].parse::().unwrap() - 1 => { + "invalid:gap" + } + "event" if values.get("payload_bytes").and_then(|value| value.parse::().ok()).map_or(true, |size| size > 16384) => { + "invalid:payload_limit" + } + "event" if values.get("sequence").and_then(|value| value.parse::().ok()).map_or(true, |sequence| sequence < 1) + || !values.contains_key("correlation_id") => "invalid:required", + "event" => "valid", + "tunnel" if matches!(values.get("offered").map(String::as_str), Some("1") | Some("0") | Some("-1")) + && values.get("feature").map(String::as_str) == Some("control.v1") => "valid", + "tunnel" if values.get("feature").map(String::as_str) != Some("control.v1") => { + "invalid:unsupported_feature" + } + "tunnel" => "invalid:unsupported_version", + "datagram" => classify_datagram(values.get("hex").map(String::as_str).unwrap_or_default()), + _ => "invalid:unknown_kind", + } +} + +fn decode_hex(input: &str) -> Option> { + if input.len() % 2 != 0 { + return None; + } + (0..input.len()) + .step_by(2) + .map(|index| u8::from_str_radix(&input[index..index + 2], 16).ok()) + .collect() +} + +fn classify_datagram(encoded: &str) -> &'static str { + let raw = match decode_hex(encoded) { + Some(raw) => raw, + None => return "invalid:hex", + }; + if raw.len() < 21 { + return "invalid:truncated"; + } + if raw[0..2] != *b"VD" { + return "invalid:magic"; + } + if raw[2] != 1 { + return "invalid:unsupported_version"; + } + let limit = match raw[3] { + 1 => 1024, + 2 => 2048, + 3 => 65515, + _ => return "invalid:unknown_channel", + }; + if raw[4] != 0 { + return "invalid:flags"; + } + if raw[18] == 0 || raw[17] >= raw[18] { + return "invalid:fragment"; + } + let payload_length = ((raw[19] as usize) << 8) | raw[20] as usize; + if payload_length > limit { + return "invalid:payload_limit"; + } + if raw.len() != 21 + payload_length { + return "invalid:length_mismatch"; + } + if raw.len() > 65536 { + return "invalid:frame_limit"; + } + "valid" +} + +fn normalized_digest(results: &[String]) -> String { + let mut value: u64 = 14695981039346656037; + for result in results { + for byte in format!("{result}\n").bytes() { + value ^= u64::from(byte); + value = value.wrapping_mul(1099511628211); + } + } + format!("{value:016x}") +} + +fn fixture_hash() -> String { + let text = fs::read_to_string("fixtures/manifest.json").expect("fixture manifest"); + text.split("\"corpus_sha256\": \"") + .nth(1) + .and_then(|value| value.split('"').next()) + .expect("fixture hash") + .to_owned() +} + +fn main() { + let mut paths: Vec = fs::read_dir("fixtures/conformance") + .expect("fixture corpus") + .map(|entry| entry.expect("fixture entry").path()) + .filter(|path| path.extension().and_then(|value| value.to_str()) == Some("tsv")) + .collect(); + paths.sort(); + let mut results = Vec::new(); + for path in paths { + let text = fs::read_to_string(path).expect("fixture file"); + let mut lines = text.lines(); + assert_eq!(lines.next(), Some("id\tversion\tkind\tinput\texpected")); + for line in lines { + let fields: Vec<&str> = line.split('\t').collect(); + assert_eq!(fields.len(), 5); + let actual = evaluate(fields[2], fields[3]); + assert_eq!(actual, fields[4], "{}", fields[0]); + results.push(format!("{}\t{}", fields[0], actual)); + } + } + println!( + "Rust conformance passed normalized={} fixtures={}", + normalized_digest(&results), + fixture_hash() + ); +} diff --git a/tools/native_conformance.swift b/tools/native_conformance.swift new file mode 100644 index 0000000..b0b7999 --- /dev/null +++ b/tools/native_conformance.swift @@ -0,0 +1,103 @@ +import Foundation + +func values(_ input: String) -> [String: String] { + var result: [String: String] = [:] + for item in input.split(separator: ";") { + let pair = item.split(separator: "=", maxSplits: 1).map(String.init) + if pair.count == 2 { result[pair[0]] = pair[1] } + } + return result +} + +func evaluate(_ kind: String, _ input: String) -> String { + let values = values(input) + switch kind { + case "version": return ["1", "0", "-1"].contains(input) ? "valid" : "invalid:unsupported_version" + case "page": + guard let raw = values["limit"], let limit = Int(raw), (1...100).contains(limit) else { return "invalid:invalid_limit" } + return "valid" + case "manifest": + for key in ["provider_url", "vm_address", "password", "private_key"] where values[key] != nil { return "invalid:forbidden_field" } + return values["version"] == "1" && values["gateway_id"] != nil && (values["grant"]?.utf8.count ?? 0) >= 43 && values["purpose"] == "launch" ? "valid" : "invalid:invalid_manifest" + case "clipboard": return values["encoding"] == "utf-8" && values["file"] == nil ? "valid" : "invalid:unsupported_clipboard" + case "event": + guard values["version"] == "1" else { return "invalid:unsupported_version" } + if let after = Int(values["after"] ?? ""), let earliest = Int(values["earliest"] ?? ""), after > 0, earliest > 0, after < earliest - 1 { return "invalid:gap" } + if (Int(values["payload_bytes"] ?? "") ?? Int.max) > 16384 { return "invalid:payload_limit" } + guard let sequence = Int(values["sequence"] ?? ""), sequence > 0, values["correlation_id"] != nil else { return "invalid:required" } + return "valid" + case "tunnel": + if ["1", "0", "-1"].contains(values["offered"] ?? "") && values["feature"] == "control.v1" { return "valid" } + return values["feature"] == "control.v1" ? "invalid:unsupported_version" : "invalid:unsupported_feature" + case "datagram": return classifyDatagram(values["hex"] ?? "") + default: return "invalid:unknown_kind" + } +} + +func classifyDatagram(_ encoded: String) -> String { + let characters = Array(encoded) + guard characters.count % 2 == 0 else { return "invalid:hex" } + var raw: [UInt8] = [] + for index in stride(from: 0, to: characters.count, by: 2) { + guard let byte = UInt8(String(characters[index...index + 1]), radix: 16) else { return "invalid:hex" } + raw.append(byte) + } + guard raw.count >= 21 else { return "invalid:truncated" } + guard raw[0] == 0x56 && raw[1] == 0x44 else { return "invalid:magic" } + guard raw[2] == 1 else { return "invalid:unsupported_version" } + let limit: Int + switch raw[3] { + case 1: limit = 1024 + case 2: limit = 2048 + case 3: limit = 65515 + default: return "invalid:unknown_channel" + } + guard raw[4] == 0 else { return "invalid:flags" } + guard raw[18] > 0 && raw[17] < raw[18] else { return "invalid:fragment" } + let payloadLength = Int(raw[19]) * 256 + Int(raw[20]) + guard payloadLength <= limit else { return "invalid:payload_limit" } + guard raw.count == 21 + payloadLength else { return "invalid:length_mismatch" } + guard raw.count <= 65536 else { return "invalid:frame_limit" } + return "valid" +} + +func normalizedDigest(_ results: [String]) -> String { + var value: UInt64 = 14695981039346656037 + for result in results { + for byte in Array("\(result)\n".utf8) { + value ^= UInt64(byte) + value = value &* 1099511628211 + } + } + return String(format: "%016llx", value) +} + +func fixtureHash() -> String { + let text = try! String(contentsOfFile: "fixtures/manifest.json", encoding: .utf8) + let marker = "\"corpus_sha256\": \"" + guard let start = text.range(of: marker)?.upperBound else { fatalError("fixture hash") } + let suffix = text[start...] + guard let end = suffix.firstIndex(of: "\"") else { fatalError("fixture hash") } + return String(suffix[.. None: + result = subprocess.run(command, cwd=ROOT, text=True, capture_output=True) + if result.returncode != 0: + raise SystemExit(result.stdout + result.stderr) + print(result.stdout.strip()) + + +def main() -> int: + with tempfile.TemporaryDirectory(prefix="versevdi-protocol-conformance-") as directory: + temp = pathlib.Path(directory) + rust_bin = temp / "rust-conformance" + swift_bin = temp / "swift-conformance" + run(["rustc", "tools/native_conformance.rs", "-O", "-o", str(rust_bin)]) + run([str(rust_bin)]) + main_source = temp / "main.swift" + main_source.write_text((ROOT / "tools/native_conformance.swift").read_text(encoding="utf-8"), encoding="utf-8") + run(["swiftc", "-parse-as-library", "gen/swift/Protocol.swift", str(main_source), "-o", str(swift_bin)]) + run([str(swift_bin)]) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/validate.py b/tools/validate.py new file mode 100644 index 0000000..1255de5 --- /dev/null +++ b/tools/validate.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""Dependency-free structural and scope validation for the Protocol sources.""" + +from __future__ import annotations + +import json +import pathlib +import hashlib +import re +import sys + +ROOT = pathlib.Path(__file__).resolve().parents[1] + + +def main() -> int: + schema_path = ROOT / "schemas/control-v1.schema.json" + schema = json.loads(schema_path.read_text(encoding="utf-8")) + assert schema["$schema"].endswith("2020-12/schema") + defs = schema["$defs"] + assert len(defs) >= 16 + for name, definition in defs.items(): + assert definition["type"] == "object", name + assert definition["additionalProperties"] is False, name + assert set(definition["required"]).issubset(definition["properties"]), name + + compatibility = json.loads((ROOT / "compatibility.json").read_text(encoding="utf-8")) + assert set([compatibility["current"], compatibility["n_minus_1"], compatibility["n_minus_2"]]) == {"1", "0", "-1"} + assert len(set(compatibility["unsupported"])) == len(compatibility["unsupported"]) + + for registry in ("registries/features.json", "registries/datagrams.json"): + value = json.loads((ROOT / registry).read_text(encoding="utf-8")) + entries = value.get("features", value.get("datagrams")) + assert entries and len({entry["id"] for entry in entries}) == len(entries) + for entry in entries: + maximum = entry.get("max_frame_bytes", entry.get("max_payload_bytes")) + assert isinstance(maximum, int) and 1 <= maximum <= 65536 + + manifest = json.loads((ROOT / "fixtures/valid/manifest.json").read_text(encoding="utf-8")) + assert set(manifest).issubset(set(defs["ConnectionManifest"]["properties"])) + forbidden = json.loads((ROOT / "fixtures/invalid/manifest-provider-field.json").read_text(encoding="utf-8")) + assert "provider_url" not in defs["ConnectionManifest"]["properties"] and "provider_url" in forbidden + + expected_header = "id\tversion\tkind\tinput\texpected" + ids = set() + for fixture_path in sorted((ROOT / "fixtures/conformance").glob("*.tsv")): + lines = fixture_path.read_text(encoding="utf-8").splitlines() + assert lines and lines[0] == expected_header, fixture_path + for line in lines[1:]: + fields = line.split("\t") + assert len(fields) == 5, line + assert fields[0] not in ids, fields[0] + ids.add(fields[0]) + assert fields[4] == "valid" or fields[4].startswith("invalid:"), line + + fixture_manifest = json.loads((ROOT / "fixtures/manifest.json").read_text(encoding="utf-8")) + assert fixture_manifest["files"] == sorted( + path.relative_to(ROOT).as_posix() for path in (ROOT / "fixtures/conformance").glob("*.tsv") + ) + fixture_hash = hashlib.sha256() + for relative in fixture_manifest["files"]: + fixture_hash.update(relative.encode("utf-8")) + fixture_hash.update(b"\0") + fixture_hash.update((ROOT / relative).read_bytes()) + fixture_hash.update(b"\0") + assert fixture_manifest["corpus_sha256"] == fixture_hash.hexdigest() + + openapi = (ROOT / "openapi/control-v1.yaml").read_text(encoding="utf-8") + assert "openapi: 3.1.0" in openapi + assert "/api/v1/auth/refresh:" in openapi and "/api/v1/resources:" in openapi and "/api/v1/events:" in openapi + assert "provider_url" not in openapi and "vm_address" not in openapi + print("Protocol source validation passed") + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except (AssertionError, OSError, json.JSONDecodeError) as exc: + print(f"validate: {exc}", file=sys.stderr) + raise SystemExit(1) diff --git a/tools/validate_frames.py b/tools/validate_frames.py new file mode 100644 index 0000000..73a9096 --- /dev/null +++ b/tools/validate_frames.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""Validate the bounded Phase 3A datagram header and fixture corpus.""" + +from __future__ import annotations + +import binascii +import pathlib + +ROOT = pathlib.Path(__file__).resolve().parents[1] +HEADER_BYTES = 21 +MAX_FRAME_BYTES = 65536 +CHANNEL_LIMITS = {1: 1024, 2: 2048, 3: 65515} + + +def classify(raw: bytes) -> str: + if len(raw) < HEADER_BYTES: + return "invalid:truncated" + if raw[:2] != b"VD": + return "invalid:magic" + if raw[2] != 1: + return "invalid:unsupported_version" + if raw[3] not in CHANNEL_LIMITS: + return "invalid:unknown_channel" + if raw[4] != 0: + return "invalid:flags" + fragment_index, fragment_count = raw[17], raw[18] + if fragment_count == 0 or fragment_index >= fragment_count: + return "invalid:fragment" + payload_length = int.from_bytes(raw[19:21], "big") + if payload_length > CHANNEL_LIMITS[raw[3]]: + return "invalid:payload_limit" + if len(raw) != HEADER_BYTES + payload_length: + return "invalid:length_mismatch" + if len(raw) > MAX_FRAME_BYTES: + return "invalid:frame_limit" + return "valid" + + +def main() -> None: + lines = (ROOT / "fixtures/conformance/datagram-v1.tsv").read_text(encoding="utf-8").splitlines() + assert lines[0] == "id\tversion\tkind\tinput\texpected" + for line in lines[1:]: + identifier, version, kind, input_value, expected = line.split("\t") + assert kind == "datagram" and version == "1" + encoded = input_value.removeprefix("hex=") + try: + actual = classify(binascii.unhexlify(encoded)) + except binascii.Error: + actual = "invalid:hex" + assert actual == expected, f"{identifier}: {actual} != {expected}" + print("Datagram frame validation passed") + + +if __name__ == "__main__": + main()