feat(protocol): add gateway transport authority contracts
This commit is contained in:
@@ -4,3 +4,5 @@ invalid-short 1 datagram hex=564401 invalid:truncated
|
|||||||
invalid-version 1 datagram hex=564402010000000000000000000000000000010000 invalid:unsupported_version
|
invalid-version 1 datagram hex=564402010000000000000000000000000000010000 invalid:unsupported_version
|
||||||
invalid-channel 1 datagram hex=564401990000000000000000000000000000010000 invalid:unknown_channel
|
invalid-channel 1 datagram hex=564401990000000000000000000000000000010000 invalid:unknown_channel
|
||||||
invalid-length 1 datagram hex=564401010000000000000000000000000000010001 invalid:length_mismatch
|
invalid-length 1 datagram hex=564401010000000000000000000000000000010001 invalid:length_mismatch
|
||||||
|
valid-video-empty 1 datagram hex=5644010a0000000000000000000000000000010000 valid
|
||||||
|
invalid-media-channel 1 datagram hex=5644010d0000000000000000000000000000010000 invalid:unknown_channel
|
||||||
|
|||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"fixtures/conformance/events-v1.tsv",
|
"fixtures/conformance/events-v1.tsv",
|
||||||
"fixtures/conformance/tunnel-v1.tsv"
|
"fixtures/conformance/tunnel-v1.tsv"
|
||||||
],
|
],
|
||||||
"corpus_sha256": "c91a512dc67aa9912b31b21144be2adfeacf0dc80dd8515bd3b4a8f52977e761"
|
"corpus_sha256": "0874d39dd14c0107e602ea8909f8d53673f964c67dc9fd5fcadb8641e6ebf592"
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-5
@@ -1,7 +1,9 @@
|
|||||||
# VerseVDI control datagram v1
|
# VerseVDI control datagram v1
|
||||||
|
|
||||||
Phase 3A reserves a bounded control-datagram envelope. It does not forward video,
|
Phase 3C adds bounded encoded video/audio and sequenced-input channels to the same
|
||||||
audio, provider traffic, VM traffic, or arbitrary binary clipboard data.
|
transport envelope. The gateway forwards encoded bytes; it does not decode, encode,
|
||||||
|
transcode, render, or expose provider traffic. Arbitrary binary clipboard data remains
|
||||||
|
rejected.
|
||||||
|
|
||||||
All multi-byte integers are unsigned big-endian. The fixed header is 21 bytes:
|
All multi-byte integers are unsigned big-endian. The fixed header is 21 bytes:
|
||||||
|
|
||||||
@@ -22,6 +24,9 @@ Truncated, oversized, unknown-version, unknown-channel, invalid-fragment, and
|
|||||||
length-mismatch frames are rejected before allocation proportional to the claimed
|
length-mismatch frames are rejected before allocation proportional to the claimed
|
||||||
payload. Media/provider identifiers are not registered channels.
|
payload. Media/provider identifiers are not registered channels.
|
||||||
|
|
||||||
Registered Phase 3A channels are `control.ack.v1`, `control.cancel.v1`, and
|
Registered channels are `control.ack.v1`, `control.cancel.v1`, `clipboard.text.v1`,
|
||||||
`clipboard.text.v1`. Clipboard payloads are UTF-8 JSON text contracts and remain
|
`media.video.v1`, `media.audio.v1`, and `input.sequenced.v1`. Media/input frames use
|
||||||
subject to the 65,536-byte text limit and explicit authorization.
|
application flow IDs 10, 11, and 12 and a path-MTU-safe payload limit of 1,179 bytes;
|
||||||
|
larger encoded units use at most 16 validated fragments. Clipboard payloads are UTF-8
|
||||||
|
JSON text contracts and remain subject to the 65,536-byte text limit and explicit
|
||||||
|
authorization.
|
||||||
|
|||||||
@@ -6,7 +6,10 @@
|
|||||||
"channels": [
|
"channels": [
|
||||||
{"id": 1, "name": "control.ack.v1", "direction": "bidirectional", "max_payload_bytes": 1024},
|
{"id": 1, "name": "control.ack.v1", "direction": "bidirectional", "max_payload_bytes": 1024},
|
||||||
{"id": 2, "name": "control.cancel.v1", "direction": "client-to-server", "max_payload_bytes": 2048},
|
{"id": 2, "name": "control.cancel.v1", "direction": "client-to-server", "max_payload_bytes": 2048},
|
||||||
{"id": 3, "name": "clipboard.text.v1", "direction": "bidirectional", "max_payload_bytes": 65515}
|
{"id": 3, "name": "clipboard.text.v1", "direction": "bidirectional", "max_payload_bytes": 65515},
|
||||||
|
{"id": 10, "name": "media.video.v1", "direction": "server-to-client", "max_payload_bytes": 1179},
|
||||||
|
{"id": 11, "name": "media.audio.v1", "direction": "server-to-client", "max_payload_bytes": 1179},
|
||||||
|
{"id": 12, "name": "input.sequenced.v1", "direction": "client-to-server", "max_payload_bytes": 1179}
|
||||||
],
|
],
|
||||||
"reserved_rejected": ["video", "audio", "provider", "vm", "file-transfer", "clipboard.binary"]
|
"reserved_rejected": ["provider", "vm", "file-transfer", "clipboard.binary"]
|
||||||
}
|
}
|
||||||
|
|||||||
+1149
-1
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -14,5 +14,5 @@
|
|||||||
},
|
},
|
||||||
"generator_sha256": "cb975bcd42bf77641b6a0f44d5ec7a6fdba1858d6b8a865e0f04e53bad82648c",
|
"generator_sha256": "cb975bcd42bf77641b6a0f44d5ec7a6fdba1858d6b8a865e0f04e53bad82648c",
|
||||||
"protocol_version": "1.0.0",
|
"protocol_version": "1.0.0",
|
||||||
"schema_sha256": "36e4c8bac2eae674c1eba551c6ca8c64bf80fcc092ca63ec89a2c71c2bec86e1"
|
"schema_sha256": "b8a69785112bb94d45f47c2250ca59d0bde47e3667b8ad89c9b0e2c4cfb25aec"
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
+102
-1
@@ -1,6 +1,6 @@
|
|||||||
// Code generated by tools/generate.py; DO NOT EDIT.
|
// Code generated by tools/generate.py; DO NOT EDIT.
|
||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
pub const SCHEMA_SHA256: &str = "36e4c8bac2eae674c1eba551c6ca8c64bf80fcc092ca63ec89a2c71c2bec86e1";
|
pub const SCHEMA_SHA256: &str = "b8a69785112bb94d45f47c2250ca59d0bde47e3667b8ad89c9b0e2c4cfb25aec";
|
||||||
pub const CURRENT_WIRE_VERSION: &str = "1";
|
pub const CURRENT_WIRE_VERSION: &str = "1";
|
||||||
pub const N_MINUS_1_WIRE_VERSION: &str = "0";
|
pub const N_MINUS_1_WIRE_VERSION: &str = "0";
|
||||||
pub const N_MINUS_2_WIRE_VERSION: &str = "-1";
|
pub const N_MINUS_2_WIRE_VERSION: &str = "-1";
|
||||||
@@ -46,6 +46,28 @@ pub struct BrokerSession {
|
|||||||
pub version: i64,
|
pub version: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct CapabilityProfile {
|
||||||
|
pub transport: String,
|
||||||
|
pub framing: String,
|
||||||
|
pub media: String,
|
||||||
|
pub audio: String,
|
||||||
|
pub sourceRateControl: String,
|
||||||
|
pub clientDecode: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ChannelFrame {
|
||||||
|
pub version: String,
|
||||||
|
pub flowId: String,
|
||||||
|
pub sequence: i64,
|
||||||
|
pub flags: i64,
|
||||||
|
pub fragmentIndex: i64,
|
||||||
|
pub fragmentCount: i64,
|
||||||
|
pub timestampMs: i64,
|
||||||
|
pub payload: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct ClipboardText {
|
pub struct ClipboardText {
|
||||||
pub text: String,
|
pub text: String,
|
||||||
@@ -133,6 +155,43 @@ pub struct FieldViolation {
|
|||||||
pub code: String,
|
pub code: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct GatewayDrain {
|
||||||
|
pub version: String,
|
||||||
|
pub gatewayId: String,
|
||||||
|
pub sequence: i64,
|
||||||
|
pub reason: String,
|
||||||
|
pub deadline: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct GatewayHeartbeat {
|
||||||
|
pub version: String,
|
||||||
|
pub gatewayId: String,
|
||||||
|
pub sequence: i64,
|
||||||
|
pub observedAt: String,
|
||||||
|
pub activeConnections: i64,
|
||||||
|
pub egressKbps: i64,
|
||||||
|
pub state: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct GatewayRegistration {
|
||||||
|
pub version: String,
|
||||||
|
pub gatewayId: String,
|
||||||
|
pub instanceIdentity: String,
|
||||||
|
pub certificateIdentity: String,
|
||||||
|
pub publicIdentity: String,
|
||||||
|
pub address: String,
|
||||||
|
pub providerIdentity: String,
|
||||||
|
pub protocolMinVersion: i64,
|
||||||
|
pub protocolMaxVersion: i64,
|
||||||
|
pub connectionCapacity: i64,
|
||||||
|
pub bandwidthCapacityKbps: i64,
|
||||||
|
pub features: Vec<String>,
|
||||||
|
pub capabilities: CapabilityProfile,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct GrantReference {
|
pub struct GrantReference {
|
||||||
pub opaqueValue: String,
|
pub opaqueValue: String,
|
||||||
@@ -189,6 +248,15 @@ pub struct PageInfo {
|
|||||||
pub nextCursor: String,
|
pub nextCursor: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ProviderState {
|
||||||
|
pub version: String,
|
||||||
|
pub sessionId: String,
|
||||||
|
pub state: String,
|
||||||
|
pub cleanupPending: bool,
|
||||||
|
pub channels: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct ReauthGrant {
|
pub struct ReauthGrant {
|
||||||
pub token: String,
|
pub token: String,
|
||||||
@@ -240,6 +308,19 @@ pub struct ResourceList {
|
|||||||
pub page: PageInfo,
|
pub page: PageInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct SessionAuthority {
|
||||||
|
pub version: String,
|
||||||
|
pub sessionId: String,
|
||||||
|
pub gatewayId: String,
|
||||||
|
pub audience: String,
|
||||||
|
pub reconnectSequence: i64,
|
||||||
|
pub expiresAt: String,
|
||||||
|
pub capabilities: CapabilityProfile,
|
||||||
|
pub providerProfile: String,
|
||||||
|
pub providerIdentity: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct SessionRequest {
|
pub struct SessionRequest {
|
||||||
pub clientDeviceId: String,
|
pub clientDeviceId: String,
|
||||||
@@ -249,6 +330,26 @@ pub struct SessionRequest {
|
|||||||
pub policySnapshot: AllocationPolicy,
|
pub policySnapshot: AllocationPolicy,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct StableError {
|
||||||
|
pub version: String,
|
||||||
|
pub code: String,
|
||||||
|
pub message: String,
|
||||||
|
pub retryable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct TunnelAdmissionRequest {
|
||||||
|
pub version: String,
|
||||||
|
pub sessionId: String,
|
||||||
|
pub gatewayId: String,
|
||||||
|
pub audience: String,
|
||||||
|
pub grant: String,
|
||||||
|
pub reconnectSequence: i64,
|
||||||
|
pub clientNonce: String,
|
||||||
|
pub capabilities: CapabilityProfile,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct VersionNegotiation {
|
pub struct VersionNegotiation {
|
||||||
pub supportedVersions: Vec<String>,
|
pub supportedVersions: Vec<String>,
|
||||||
|
|||||||
+277
-1
@@ -1,7 +1,7 @@
|
|||||||
// Code generated by tools/generate.py; DO NOT EDIT.
|
// Code generated by tools/generate.py; DO NOT EDIT.
|
||||||
import Foundation
|
import Foundation
|
||||||
public typealias JSONObject = [String: String]
|
public typealias JSONObject = [String: String]
|
||||||
public let schemaSHA256 = "36e4c8bac2eae674c1eba551c6ca8c64bf80fcc092ca63ec89a2c71c2bec86e1"
|
public let schemaSHA256 = "b8a69785112bb94d45f47c2250ca59d0bde47e3667b8ad89c9b0e2c4cfb25aec"
|
||||||
public let currentWireVersion = "1"
|
public let currentWireVersion = "1"
|
||||||
public let nMinus1WireVersion = "0"
|
public let nMinus1WireVersion = "0"
|
||||||
public let nMinus2WireVersion = "-1"
|
public let nMinus2WireVersion = "-1"
|
||||||
@@ -117,6 +117,66 @@ public struct BrokerSession: Codable, Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct CapabilityProfile: Codable, Equatable {
|
||||||
|
public let transport: String
|
||||||
|
public let framing: String
|
||||||
|
public let media: String
|
||||||
|
public let audio: String
|
||||||
|
public let sourceRateControl: String
|
||||||
|
public let clientDecode: String
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case transport = "transport"
|
||||||
|
case framing = "framing"
|
||||||
|
case media = "media"
|
||||||
|
case audio = "audio"
|
||||||
|
case sourceRateControl = "source_rate_control"
|
||||||
|
case clientDecode = "client_decode"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
transport = try c.decode(String.self, forKey: .transport)
|
||||||
|
framing = try c.decode(String.self, forKey: .framing)
|
||||||
|
media = try c.decode(String.self, forKey: .media)
|
||||||
|
audio = try c.decode(String.self, forKey: .audio)
|
||||||
|
sourceRateControl = try c.decode(String.self, forKey: .sourceRateControl)
|
||||||
|
clientDecode = try c.decode(String.self, forKey: .clientDecode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ChannelFrame: Codable, Equatable {
|
||||||
|
public let version: String
|
||||||
|
public let flowId: String
|
||||||
|
public let sequence: Int64
|
||||||
|
public let flags: Int64
|
||||||
|
public let fragmentIndex: Int64
|
||||||
|
public let fragmentCount: Int64
|
||||||
|
public let timestampMs: Int64
|
||||||
|
public let payload: String
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case version = "version"
|
||||||
|
case flowId = "flow_id"
|
||||||
|
case sequence = "sequence"
|
||||||
|
case flags = "flags"
|
||||||
|
case fragmentIndex = "fragment_index"
|
||||||
|
case fragmentCount = "fragment_count"
|
||||||
|
case timestampMs = "timestamp_ms"
|
||||||
|
case payload = "payload"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
version = try c.decode(String.self, forKey: .version)
|
||||||
|
flowId = try c.decode(String.self, forKey: .flowId)
|
||||||
|
sequence = try c.decode(Int64.self, forKey: .sequence)
|
||||||
|
flags = try c.decode(Int64.self, forKey: .flags)
|
||||||
|
fragmentIndex = try c.decode(Int64.self, forKey: .fragmentIndex)
|
||||||
|
fragmentCount = try c.decode(Int64.self, forKey: .fragmentCount)
|
||||||
|
timestampMs = try c.decode(Int64.self, forKey: .timestampMs)
|
||||||
|
payload = try c.decode(String.self, forKey: .payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct ClipboardText: Codable, Equatable {
|
public struct ClipboardText: Codable, Equatable {
|
||||||
public let text: String
|
public let text: String
|
||||||
public let encoding: String
|
public let encoding: String
|
||||||
@@ -348,6 +408,108 @@ public struct FieldViolation: Codable, Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct GatewayDrain: Codable, Equatable {
|
||||||
|
public let version: String
|
||||||
|
public let gatewayId: String
|
||||||
|
public let sequence: Int64
|
||||||
|
public let reason: String
|
||||||
|
public let deadline: String
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case version = "version"
|
||||||
|
case gatewayId = "gateway_id"
|
||||||
|
case sequence = "sequence"
|
||||||
|
case reason = "reason"
|
||||||
|
case deadline = "deadline"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
version = try c.decode(String.self, forKey: .version)
|
||||||
|
gatewayId = try c.decode(String.self, forKey: .gatewayId)
|
||||||
|
sequence = try c.decode(Int64.self, forKey: .sequence)
|
||||||
|
reason = try c.decode(String.self, forKey: .reason)
|
||||||
|
deadline = try c.decode(String.self, forKey: .deadline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct GatewayHeartbeat: Codable, Equatable {
|
||||||
|
public let version: String
|
||||||
|
public let gatewayId: String
|
||||||
|
public let sequence: Int64
|
||||||
|
public let observedAt: String
|
||||||
|
public let activeConnections: Int64
|
||||||
|
public let egressKbps: Int64
|
||||||
|
public let state: String
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case version = "version"
|
||||||
|
case gatewayId = "gateway_id"
|
||||||
|
case sequence = "sequence"
|
||||||
|
case observedAt = "observed_at"
|
||||||
|
case activeConnections = "active_connections"
|
||||||
|
case egressKbps = "egress_kbps"
|
||||||
|
case state = "state"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
version = try c.decode(String.self, forKey: .version)
|
||||||
|
gatewayId = try c.decode(String.self, forKey: .gatewayId)
|
||||||
|
sequence = try c.decode(Int64.self, forKey: .sequence)
|
||||||
|
observedAt = try c.decode(String.self, forKey: .observedAt)
|
||||||
|
activeConnections = try c.decode(Int64.self, forKey: .activeConnections)
|
||||||
|
egressKbps = try c.decode(Int64.self, forKey: .egressKbps)
|
||||||
|
state = try c.decode(String.self, forKey: .state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct GatewayRegistration: Codable, Equatable {
|
||||||
|
public let version: String
|
||||||
|
public let gatewayId: String
|
||||||
|
public let instanceIdentity: String
|
||||||
|
public let certificateIdentity: String
|
||||||
|
public let publicIdentity: String
|
||||||
|
public let address: String
|
||||||
|
public let providerIdentity: String
|
||||||
|
public let protocolMinVersion: Int64
|
||||||
|
public let protocolMaxVersion: Int64
|
||||||
|
public let connectionCapacity: Int64
|
||||||
|
public let bandwidthCapacityKbps: Int64
|
||||||
|
public let features: [String]
|
||||||
|
public let capabilities: CapabilityProfile
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case version = "version"
|
||||||
|
case gatewayId = "gateway_id"
|
||||||
|
case instanceIdentity = "instance_identity"
|
||||||
|
case certificateIdentity = "certificate_identity"
|
||||||
|
case publicIdentity = "public_identity"
|
||||||
|
case address = "address"
|
||||||
|
case providerIdentity = "provider_identity"
|
||||||
|
case protocolMinVersion = "protocol_min_version"
|
||||||
|
case protocolMaxVersion = "protocol_max_version"
|
||||||
|
case connectionCapacity = "connection_capacity"
|
||||||
|
case bandwidthCapacityKbps = "bandwidth_capacity_kbps"
|
||||||
|
case features = "features"
|
||||||
|
case capabilities = "capabilities"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
version = try c.decode(String.self, forKey: .version)
|
||||||
|
gatewayId = try c.decode(String.self, forKey: .gatewayId)
|
||||||
|
instanceIdentity = try c.decode(String.self, forKey: .instanceIdentity)
|
||||||
|
certificateIdentity = try c.decode(String.self, forKey: .certificateIdentity)
|
||||||
|
publicIdentity = try c.decode(String.self, forKey: .publicIdentity)
|
||||||
|
address = try c.decode(String.self, forKey: .address)
|
||||||
|
providerIdentity = try c.decode(String.self, forKey: .providerIdentity)
|
||||||
|
protocolMinVersion = try c.decode(Int64.self, forKey: .protocolMinVersion)
|
||||||
|
protocolMaxVersion = try c.decode(Int64.self, forKey: .protocolMaxVersion)
|
||||||
|
connectionCapacity = try c.decode(Int64.self, forKey: .connectionCapacity)
|
||||||
|
bandwidthCapacityKbps = try c.decode(Int64.self, forKey: .bandwidthCapacityKbps)
|
||||||
|
features = try c.decode([String].self, forKey: .features)
|
||||||
|
capabilities = try c.decode(CapabilityProfile.self, forKey: .capabilities)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct GrantReference: Codable, Equatable {
|
public struct GrantReference: Codable, Equatable {
|
||||||
public let opaqueValue: String
|
public let opaqueValue: String
|
||||||
public let expiresAt: String
|
public let expiresAt: String
|
||||||
@@ -492,6 +654,30 @@ public struct PageInfo: Codable, Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct ProviderState: Codable, Equatable {
|
||||||
|
public let version: String
|
||||||
|
public let sessionId: String
|
||||||
|
public let state: String
|
||||||
|
public let cleanupPending: Bool
|
||||||
|
public let channels: [String]
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case version = "version"
|
||||||
|
case sessionId = "session_id"
|
||||||
|
case state = "state"
|
||||||
|
case cleanupPending = "cleanup_pending"
|
||||||
|
case channels = "channels"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
version = try c.decode(String.self, forKey: .version)
|
||||||
|
sessionId = try c.decode(String.self, forKey: .sessionId)
|
||||||
|
state = try c.decode(String.self, forKey: .state)
|
||||||
|
cleanupPending = try c.decode(Bool.self, forKey: .cleanupPending)
|
||||||
|
channels = try c.decode([String].self, forKey: .channels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct ReauthGrant: Codable, Equatable {
|
public struct ReauthGrant: Codable, Equatable {
|
||||||
public let token: String
|
public let token: String
|
||||||
public let purpose: String
|
public let purpose: String
|
||||||
@@ -624,6 +810,42 @@ public struct ResourceList: Codable, Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct SessionAuthority: Codable, Equatable {
|
||||||
|
public let version: String
|
||||||
|
public let sessionId: String
|
||||||
|
public let gatewayId: String
|
||||||
|
public let audience: String
|
||||||
|
public let reconnectSequence: Int64
|
||||||
|
public let expiresAt: String
|
||||||
|
public let capabilities: CapabilityProfile
|
||||||
|
public let providerProfile: String
|
||||||
|
public let providerIdentity: String
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case version = "version"
|
||||||
|
case sessionId = "session_id"
|
||||||
|
case gatewayId = "gateway_id"
|
||||||
|
case audience = "audience"
|
||||||
|
case reconnectSequence = "reconnect_sequence"
|
||||||
|
case expiresAt = "expires_at"
|
||||||
|
case capabilities = "capabilities"
|
||||||
|
case providerProfile = "provider_profile"
|
||||||
|
case providerIdentity = "provider_identity"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
version = try c.decode(String.self, forKey: .version)
|
||||||
|
sessionId = try c.decode(String.self, forKey: .sessionId)
|
||||||
|
gatewayId = try c.decode(String.self, forKey: .gatewayId)
|
||||||
|
audience = try c.decode(String.self, forKey: .audience)
|
||||||
|
reconnectSequence = try c.decode(Int64.self, forKey: .reconnectSequence)
|
||||||
|
expiresAt = try c.decode(String.self, forKey: .expiresAt)
|
||||||
|
capabilities = try c.decode(CapabilityProfile.self, forKey: .capabilities)
|
||||||
|
providerProfile = try c.decode(String.self, forKey: .providerProfile)
|
||||||
|
providerIdentity = try c.decode(String.self, forKey: .providerIdentity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct SessionRequest: Codable, Equatable {
|
public struct SessionRequest: Codable, Equatable {
|
||||||
public let clientDeviceId: String
|
public let clientDeviceId: String
|
||||||
public let deviceKeyId: String
|
public let deviceKeyId: String
|
||||||
@@ -648,6 +870,60 @@ public struct SessionRequest: Codable, Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct StableError: Codable, Equatable {
|
||||||
|
public let version: String
|
||||||
|
public let code: String
|
||||||
|
public let message: String
|
||||||
|
public let retryable: Bool
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case version = "version"
|
||||||
|
case code = "code"
|
||||||
|
case message = "message"
|
||||||
|
case retryable = "retryable"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
version = try c.decode(String.self, forKey: .version)
|
||||||
|
code = try c.decode(String.self, forKey: .code)
|
||||||
|
message = try c.decode(String.self, forKey: .message)
|
||||||
|
retryable = try c.decode(Bool.self, forKey: .retryable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct TunnelAdmissionRequest: Codable, Equatable {
|
||||||
|
public let version: String
|
||||||
|
public let sessionId: String
|
||||||
|
public let gatewayId: String
|
||||||
|
public let audience: String
|
||||||
|
public let grant: String
|
||||||
|
public let reconnectSequence: Int64
|
||||||
|
public let clientNonce: String
|
||||||
|
public let capabilities: CapabilityProfile
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case version = "version"
|
||||||
|
case sessionId = "session_id"
|
||||||
|
case gatewayId = "gateway_id"
|
||||||
|
case audience = "audience"
|
||||||
|
case grant = "grant"
|
||||||
|
case reconnectSequence = "reconnect_sequence"
|
||||||
|
case clientNonce = "client_nonce"
|
||||||
|
case capabilities = "capabilities"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
version = try c.decode(String.self, forKey: .version)
|
||||||
|
sessionId = try c.decode(String.self, forKey: .sessionId)
|
||||||
|
gatewayId = try c.decode(String.self, forKey: .gatewayId)
|
||||||
|
audience = try c.decode(String.self, forKey: .audience)
|
||||||
|
grant = try c.decode(String.self, forKey: .grant)
|
||||||
|
reconnectSequence = try c.decode(Int64.self, forKey: .reconnectSequence)
|
||||||
|
clientNonce = try c.decode(String.self, forKey: .clientNonce)
|
||||||
|
capabilities = try c.decode(CapabilityProfile.self, forKey: .capabilities)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct VersionNegotiation: Codable, Equatable {
|
public struct VersionNegotiation: Codable, Equatable {
|
||||||
public let supportedVersions: [String]
|
public let supportedVersions: [String]
|
||||||
public let features: [String]
|
public let features: [String]
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
## Decisions
|
||||||
|
|
||||||
|
- Use the existing generated JSON binding pipeline and add only object definitions that
|
||||||
|
consumers need now.
|
||||||
|
- Keep registration/authority messages distinct from `ConnectionManifest`; the latter is
|
||||||
|
client-facing and contains no provider route, certificate, identity, or credential.
|
||||||
|
- Encode capability domains as bounded registered strings (`transport`, `framing`, `media`,
|
||||||
|
`audio`, `source_rate_control`, `client_decode`) so unknown or empty required domains
|
||||||
|
cannot silently fall back.
|
||||||
|
- Use a fixed 21-byte big-endian datagram header with application flow IDs and a payload
|
||||||
|
limit below the path MTU; control channels 1-3 remain compatible with Phase 3A.
|
||||||
|
|
||||||
|
## Bounds and failure behavior
|
||||||
|
|
||||||
|
All arrays, strings, payloads, fragments, and timestamps are bounded by the JSON schema or
|
||||||
|
frame registry. Generated decoders reject unknown fields, trailing values, invalid versions,
|
||||||
|
and missing required fields. Provider address, RTSP, credential, and private-key names are
|
||||||
|
not added to client-facing definitions.
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
The current wire version remains `1`; current, N-1, and N-2 declarations remain unchanged.
|
||||||
|
New identifiers are additive. Consumers must reject an unknown major or no-overlap selection
|
||||||
|
before provider launch.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
Phase 3A defines gateway-only manifests and control datagrams but has no versioned
|
||||||
|
contracts for the separately deployed gateway's registration, admission authority,
|
||||||
|
capability intersection, lifecycle, or encoded relay framing. Phase 3C-G needs those
|
||||||
|
contracts frozen before Data Plane or Connection Server consumers change.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Add bounded JSON bindings for gateway registration, heartbeat/drain, tunnel admission,
|
||||||
|
session-scoped authority, capability selection, channel framing, provider state, and
|
||||||
|
stable errors.
|
||||||
|
- Extend the tunnel protobuf descriptor with the same versioned control messages.
|
||||||
|
- Register only gateway-owned media/input flow identifiers; keep provider endpoints and
|
||||||
|
credentials out of client-facing manifests.
|
||||||
|
|
||||||
|
## Ownership and provenance
|
||||||
|
|
||||||
|
Protocol owns the wire contract. The contract is original VerseVDI work derived from the
|
||||||
|
Phase 3A schemas and public Apollo/Moonlight behavior recorded in the Planning Hub. No
|
||||||
|
GPL source is copied into this repository.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Provider implementation, database policy, QUIC implementation, media decoding, or client
|
||||||
|
rendering.
|
||||||
|
- Direct client-to-Apollo routing, provider endpoint exposure, or a speculative plugin ABI.
|
||||||
|
|
||||||
|
## Stop conditions
|
||||||
|
|
||||||
|
Unknown versions, malformed bounds, no capability overlap, downgrade without explicit
|
||||||
|
acknowledgement, forbidden provider fields, and oversized frames fail closed.
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Versioned gateway authority contracts
|
||||||
|
Gateway registration, heartbeat, drain, admission, capability selection, channel framing,
|
||||||
|
provider state, and stable errors SHALL use bounded versioned objects with strict decoding.
|
||||||
|
|
||||||
|
#### Scenario: Unknown or malformed gateway message
|
||||||
|
- **WHEN** a consumer receives an unknown version, missing required field, unknown field,
|
||||||
|
invalid bound, or trailing JSON value
|
||||||
|
- **THEN** it rejects the message before allocating provider or media state with a stable
|
||||||
|
validation error.
|
||||||
|
|
||||||
|
### Requirement: Gateway-only client manifest
|
||||||
|
Client-facing manifests SHALL expose only the selected gateway, tunnel/profile identifiers,
|
||||||
|
policy bounds, opaque grant, audience, expiry, session, and correlation data.
|
||||||
|
|
||||||
|
#### Scenario: Provider route injection
|
||||||
|
- **WHEN** a manifest or client-facing authority contains a provider address, RTSP URL,
|
||||||
|
certificate, pairing identity, credential, or private key field
|
||||||
|
- **THEN** schema validation rejects it and no generated client binding accepts it.
|
||||||
|
|
||||||
|
### Requirement: Explicit capability intersection
|
||||||
|
Transport, framing, media, audio, source-rate-control, and client-decode capabilities SHALL
|
||||||
|
remain separate and no-overlap or unknown-required-profile results SHALL fail closed.
|
||||||
|
|
||||||
|
#### Scenario: No capability overlap
|
||||||
|
- **WHEN** policy, gateway, provider, and client capabilities have no allowed intersection
|
||||||
|
- **THEN** negotiation returns a stable no-overlap error before provider launch or media flow.
|
||||||
|
|
||||||
|
### Requirement: Bounded encoded datagrams
|
||||||
|
Encoded media and approved sequenced input SHALL use registered application flow identifiers,
|
||||||
|
validated fixed headers, bounded fragments, and payload bytes that are not codec-transformed.
|
||||||
|
|
||||||
|
#### Scenario: Malformed or oversized datagram
|
||||||
|
- **WHEN** a datagram is truncated, has an unknown flow, invalid fragment, length mismatch,
|
||||||
|
or exceeds its registered payload limit
|
||||||
|
- **THEN** the datagram is rejected without allocation proportional to the claimed payload.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
- [x] Add versioned gateway and authority definitions to the schema and tunnel descriptor.
|
||||||
|
- [x] Add registered media/audio/sequenced-input flow identifiers and positive/negative fixtures.
|
||||||
|
- [x] Regenerate Go/Rust/Swift bindings and descriptor outputs.
|
||||||
|
- [x] Run strict validation, cross-language conformance, and deterministic fixture hashing.
|
||||||
|
- [ ] Freeze the local Protocol candidate commit and record its hash for consumers.
|
||||||
@@ -49,3 +49,95 @@ message ChannelOpen {
|
|||||||
string direction = 2;
|
string direction = 2;
|
||||||
uint32 maximum_frame_bytes = 3;
|
uint32 maximum_frame_bytes = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message CapabilityProfile {
|
||||||
|
string transport = 1;
|
||||||
|
string framing = 2;
|
||||||
|
string media = 3;
|
||||||
|
string audio = 4;
|
||||||
|
string source_rate_control = 5;
|
||||||
|
string client_decode = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GatewayRegistration {
|
||||||
|
string version = 1;
|
||||||
|
string gateway_id = 2;
|
||||||
|
string instance_identity = 3;
|
||||||
|
string certificate_identity = 4;
|
||||||
|
string public_identity = 5;
|
||||||
|
string address = 6;
|
||||||
|
uint32 protocol_min_version = 7;
|
||||||
|
uint32 protocol_max_version = 8;
|
||||||
|
uint32 connection_capacity = 9;
|
||||||
|
uint64 bandwidth_capacity_kbps = 10;
|
||||||
|
repeated string features = 11;
|
||||||
|
CapabilityProfile capabilities = 12;
|
||||||
|
string provider_identity = 13;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GatewayHeartbeat {
|
||||||
|
string version = 1;
|
||||||
|
string gateway_id = 2;
|
||||||
|
uint64 sequence = 3;
|
||||||
|
google.protobuf.Timestamp observed_at = 4;
|
||||||
|
uint32 active_connections = 5;
|
||||||
|
uint64 egress_kbps = 6;
|
||||||
|
string state = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GatewayDrain {
|
||||||
|
string version = 1;
|
||||||
|
string gateway_id = 2;
|
||||||
|
uint64 sequence = 3;
|
||||||
|
string reason = 4;
|
||||||
|
google.protobuf.Timestamp deadline = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TunnelAdmissionRequest {
|
||||||
|
string version = 1;
|
||||||
|
string session_id = 2;
|
||||||
|
string gateway_id = 3;
|
||||||
|
string audience = 4;
|
||||||
|
string grant = 5;
|
||||||
|
uint64 reconnect_sequence = 6;
|
||||||
|
string client_nonce = 7;
|
||||||
|
CapabilityProfile capabilities = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SessionAuthority {
|
||||||
|
string version = 1;
|
||||||
|
string session_id = 2;
|
||||||
|
string gateway_id = 3;
|
||||||
|
string audience = 4;
|
||||||
|
uint64 reconnect_sequence = 5;
|
||||||
|
google.protobuf.Timestamp expires_at = 6;
|
||||||
|
CapabilityProfile capabilities = 7;
|
||||||
|
string provider_profile = 8;
|
||||||
|
string provider_identity = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ChannelFrame {
|
||||||
|
string version = 1;
|
||||||
|
string flow_id = 2;
|
||||||
|
uint64 sequence = 3;
|
||||||
|
uint32 flags = 4;
|
||||||
|
uint32 fragment_index = 5;
|
||||||
|
uint32 fragment_count = 6;
|
||||||
|
uint64 timestamp_ms = 7;
|
||||||
|
bytes payload = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ProviderState {
|
||||||
|
string version = 1;
|
||||||
|
string session_id = 2;
|
||||||
|
string state = 3;
|
||||||
|
bool cleanup_pending = 4;
|
||||||
|
repeated string channels = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message StableError {
|
||||||
|
string version = 1;
|
||||||
|
string code = 2;
|
||||||
|
string message = 3;
|
||||||
|
bool retryable = 4;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"datagrams": [
|
"datagrams": [
|
||||||
{"id": "control.ack.v1", "direction": "bidirectional", "max_payload_bytes": 1024},
|
{"id": "control.ack.v1", "direction": "bidirectional", "max_payload_bytes": 1024},
|
||||||
{"id": "control.cancel.v1", "direction": "client-to-server", "max_payload_bytes": 2048},
|
{"id": "control.cancel.v1", "direction": "client-to-server", "max_payload_bytes": 2048},
|
||||||
{"id": "clipboard.text.v1", "direction": "bidirectional", "max_payload_bytes": 65536}
|
{"id": "clipboard.text.v1", "direction": "bidirectional", "max_payload_bytes": 65536},
|
||||||
|
{"id": "media.video.v1", "direction": "server-to-client", "max_payload_bytes": 1200},
|
||||||
|
{"id": "media.audio.v1", "direction": "server-to-client", "max_payload_bytes": 1200},
|
||||||
|
{"id": "input.sequenced.v1", "direction": "client-to-server", "max_payload_bytes": 1200}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -343,6 +343,134 @@
|
|||||||
"supported_versions": {"type": "array", "minItems": 1, "maxItems": 3, "items": {"type": "string", "maxLength": 16}},
|
"supported_versions": {"type": "array", "minItems": 1, "maxItems": 3, "items": {"type": "string", "maxLength": 16}},
|
||||||
"features": {"type": "array", "maxItems": 64, "items": {"type": "string", "maxLength": 64}}
|
"features": {"type": "array", "maxItems": 64, "items": {"type": "string", "maxLength": 64}}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"CapabilityProfile": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["transport", "framing", "media", "audio", "source_rate_control", "client_decode"],
|
||||||
|
"properties": {
|
||||||
|
"transport": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||||
|
"framing": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||||
|
"media": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||||
|
"audio": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||||
|
"source_rate_control": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||||
|
"client_decode": {"type": "string", "minLength": 1, "maxLength": 64}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GatewayRegistration": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["version", "gateway_id", "instance_identity", "certificate_identity", "public_identity", "address", "provider_identity", "protocol_min_version", "protocol_max_version", "connection_capacity", "bandwidth_capacity_kbps", "features", "capabilities"],
|
||||||
|
"properties": {
|
||||||
|
"version": {"type": "string", "const": "1"},
|
||||||
|
"gateway_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||||
|
"instance_identity": {"type": "string", "minLength": 1, "maxLength": 512},
|
||||||
|
"certificate_identity": {"type": "string", "minLength": 1, "maxLength": 512},
|
||||||
|
"public_identity": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||||
|
"address": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||||
|
"provider_identity": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||||
|
"protocol_min_version": {"type": "integer", "minimum": 1, "maximum": 100},
|
||||||
|
"protocol_max_version": {"type": "integer", "minimum": 1, "maximum": 100},
|
||||||
|
"connection_capacity": {"type": "integer", "minimum": 1, "maximum": 1000000},
|
||||||
|
"bandwidth_capacity_kbps": {"type": "integer", "minimum": 1, "maximum": 1000000000},
|
||||||
|
"features": {"type": "array", "maxItems": 64, "items": {"type": "string", "minLength": 1, "maxLength": 64}},
|
||||||
|
"capabilities": {"$ref": "#/$defs/CapabilityProfile"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GatewayHeartbeat": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["version", "gateway_id", "sequence", "observed_at", "active_connections", "egress_kbps", "state"],
|
||||||
|
"properties": {
|
||||||
|
"version": {"type": "string", "const": "1"},
|
||||||
|
"gateway_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||||
|
"sequence": {"type": "integer", "minimum": 1},
|
||||||
|
"observed_at": {"type": "string", "format": "date-time", "maxLength": 64},
|
||||||
|
"active_connections": {"type": "integer", "minimum": 0, "maximum": 1000000},
|
||||||
|
"egress_kbps": {"type": "integer", "minimum": 0, "maximum": 1000000000},
|
||||||
|
"state": {"type": "string", "enum": ["ready", "draining", "offline"]}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GatewayDrain": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["version", "gateway_id", "sequence", "reason", "deadline"],
|
||||||
|
"properties": {
|
||||||
|
"version": {"type": "string", "const": "1"},
|
||||||
|
"gateway_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||||
|
"sequence": {"type": "integer", "minimum": 1},
|
||||||
|
"reason": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||||
|
"deadline": {"type": "string", "format": "date-time", "maxLength": 64}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TunnelAdmissionRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["version", "session_id", "gateway_id", "audience", "grant", "reconnect_sequence", "client_nonce", "capabilities"],
|
||||||
|
"properties": {
|
||||||
|
"version": {"type": "string", "const": "1"},
|
||||||
|
"session_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||||
|
"gateway_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||||
|
"audience": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||||
|
"grant": {"type": "string", "minLength": 43, "maxLength": 256},
|
||||||
|
"reconnect_sequence": {"type": "integer", "minimum": 0},
|
||||||
|
"client_nonce": {"type": "string", "minLength": 16, "maxLength": 128},
|
||||||
|
"capabilities": {"$ref": "#/$defs/CapabilityProfile"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SessionAuthority": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities", "provider_profile", "provider_identity"],
|
||||||
|
"properties": {
|
||||||
|
"version": {"type": "string", "const": "1"},
|
||||||
|
"session_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||||
|
"gateway_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||||
|
"audience": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||||
|
"reconnect_sequence": {"type": "integer", "minimum": 0},
|
||||||
|
"expires_at": {"type": "string", "format": "date-time", "maxLength": 64},
|
||||||
|
"capabilities": {"$ref": "#/$defs/CapabilityProfile"},
|
||||||
|
"provider_profile": {"type": "string", "enum": ["apollo"]},
|
||||||
|
"provider_identity": {"type": "string", "minLength": 1, "maxLength": 256}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ChannelFrame": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["version", "flow_id", "sequence", "flags", "fragment_index", "fragment_count", "timestamp_ms", "payload"],
|
||||||
|
"properties": {
|
||||||
|
"version": {"type": "string", "const": "1"},
|
||||||
|
"flow_id": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||||
|
"sequence": {"type": "integer", "minimum": 0},
|
||||||
|
"flags": {"type": "integer", "minimum": 0, "maximum": 255},
|
||||||
|
"fragment_index": {"type": "integer", "minimum": 0, "maximum": 15},
|
||||||
|
"fragment_count": {"type": "integer", "minimum": 1, "maximum": 16},
|
||||||
|
"timestamp_ms": {"type": "integer", "minimum": 0},
|
||||||
|
"payload": {"type": "string", "maxLength": 87384, "x-max-bytes": 65536}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ProviderState": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["version", "session_id", "state", "cleanup_pending", "channels"],
|
||||||
|
"properties": {
|
||||||
|
"version": {"type": "string", "const": "1"},
|
||||||
|
"session_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||||
|
"state": {"type": "string", "enum": ["starting", "ready", "disconnected", "terminating", "terminated", "cleanup_pending", "failed"]},
|
||||||
|
"cleanup_pending": {"type": "boolean"},
|
||||||
|
"channels": {"type": "array", "maxItems": 8, "items": {"type": "string", "minLength": 1, "maxLength": 64}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"StableError": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["version", "code", "message", "retryable"],
|
||||||
|
"properties": {
|
||||||
|
"version": {"type": "string", "const": "1"},
|
||||||
|
"code": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||||
|
"message": {"type": "string", "minLength": 1, "maxLength": 512},
|
||||||
|
"retryable": {"type": "boolean"}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,3 +35,28 @@ func TestGeneratedDecodersRejectMissingRequiredFieldsAndTrailingValues(t *testin
|
|||||||
t.Fatal("DecodeErrorEnvelope accepted a missing required boolean")
|
t.Fatal("DecodeErrorEnvelope accepted a missing required boolean")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGatewayContractsRejectUnknownVersionsAndFields(t *testing.T) {
|
||||||
|
registration := `{"version":"1","gateway_id":"gateway-1","instance_identity":"instance-1","certificate_identity":"cert-1","public_identity":"public-1","address":"gateway.test:443","provider_identity":"apollo-provider-1","protocol_min_version":1,"protocol_max_version":1,"connection_capacity":8,"bandwidth_capacity_kbps":100000,"features":["datagram.media"],"capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":"h264-opus"}}`
|
||||||
|
if _, err := protocol.DecodeGatewayRegistration([]byte(registration)); err != nil {
|
||||||
|
t.Fatalf("valid gateway registration rejected: %v", err)
|
||||||
|
}
|
||||||
|
for _, invalid := range []string{
|
||||||
|
strings.Replace(registration, `"version":"1"`, `"version":"2"`, 1),
|
||||||
|
strings.Replace(registration, `"features":["datagram.media"]`, `"features":["datagram.media"],"provider_url":"https://provider.invalid"`, 1),
|
||||||
|
} {
|
||||||
|
if _, err := protocol.DecodeGatewayRegistration([]byte(invalid)); err == nil {
|
||||||
|
t.Fatalf("invalid gateway registration accepted: %s", invalid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionAuthorityRejectsProviderRoute(t *testing.T) {
|
||||||
|
valid := `{"version":"1","session_id":"session-1","gateway_id":"gateway-1","audience":"versevdi-gateway","reconnect_sequence":0,"expires_at":"2099-01-01T00:00:00Z","capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":"h264-opus"},"provider_profile":"apollo","provider_identity":"provider-1"}`
|
||||||
|
if _, err := protocol.DecodeSessionAuthority([]byte(valid)); err != nil {
|
||||||
|
t.Fatalf("valid session authority rejected: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := protocol.DecodeSessionAuthority([]byte(strings.Replace(valid, `"provider_identity":"provider-1"`, `"provider_identity":"provider-1","rtsp_url":"rtsp://provider.invalid"`, 1))); err == nil {
|
||||||
|
t.Fatal("session authority accepted a provider route")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ func classifyDatagram(encoded string) string {
|
|||||||
if raw[2] != 1 {
|
if raw[2] != 1 {
|
||||||
return "invalid:unsupported_version"
|
return "invalid:unsupported_version"
|
||||||
}
|
}
|
||||||
limits := map[byte]int{1: 1024, 2: 2048, 3: 65515}
|
limits := map[byte]int{1: 1024, 2: 2048, 3: 65515, 10: 1179, 11: 1179, 12: 1179}
|
||||||
limit, ok := limits[raw[3]]
|
limit, ok := limits[raw[3]]
|
||||||
if !ok {
|
if !ok {
|
||||||
return "invalid:unknown_channel"
|
return "invalid:unknown_channel"
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ fn classify_datagram(encoded: &str) -> &'static str {
|
|||||||
1 => 1024,
|
1 => 1024,
|
||||||
2 => 2048,
|
2 => 2048,
|
||||||
3 => 65515,
|
3 => 65515,
|
||||||
|
10 | 11 | 12 => 1179,
|
||||||
_ => return "invalid:unknown_channel",
|
_ => return "invalid:unknown_channel",
|
||||||
};
|
};
|
||||||
if raw[4] != 0 {
|
if raw[4] != 0 {
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ func classifyDatagram(_ encoded: String) -> String {
|
|||||||
case 1: limit = 1024
|
case 1: limit = 1024
|
||||||
case 2: limit = 2048
|
case 2: limit = 2048
|
||||||
case 3: limit = 65515
|
case 3: limit = 65515
|
||||||
|
case 10, 11, 12: limit = 1179
|
||||||
default: return "invalid:unknown_channel"
|
default: return "invalid:unknown_channel"
|
||||||
}
|
}
|
||||||
guard raw[4] == 0 else { return "invalid:flags" }
|
guard raw[4] == 0 else { return "invalid:flags" }
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import pathlib
|
|||||||
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||||
HEADER_BYTES = 21
|
HEADER_BYTES = 21
|
||||||
MAX_FRAME_BYTES = 65536
|
MAX_FRAME_BYTES = 65536
|
||||||
CHANNEL_LIMITS = {1: 1024, 2: 2048, 3: 65515}
|
CHANNEL_LIMITS = {1: 1024, 2: 2048, 3: 65515, 10: 1179, 11: 1179, 12: 1179}
|
||||||
|
|
||||||
|
|
||||||
def classify(raw: bytes) -> str:
|
def classify(raw: bytes) -> str:
|
||||||
|
|||||||
Reference in New Issue
Block a user