Compare commits

...
Author SHA1 Message Date
sechmachine c3ea2f35c6 fix(protocol): align quality ack schema
Verify Protocol / module (push) Successful in 1m21s
Verify Protocol / verify (push) Successful in 1m2s
2026-08-13 07:53:35 +07:00
sechmachine 6e18bc9ee6 fix(protocol): fence gateway work recovery 2026-08-13 07:43:42 +07:00
sechmachine 8eacc4fda9 feat(protocol): freeze M4 native session RC6 contract 2026-08-13 07:18:03 +07:00
43 changed files with 5819 additions and 488 deletions
@@ -9,9 +9,12 @@ valid-controller 1 gateway_input hex=5647493105110200030004ffff00010002000300040
valid-controller-release 1 gateway_input hex=5647493105110200000000000000000000000000000000 valid valid-controller-release 1 gateway_input hex=5647493105110200000000000000000000000000000000 valid
valid-absolute-mouse 1 gateway_input hex=56474931060804d202370a0005a0 valid valid-absolute-mouse 1 gateway_input hex=56474931060804d202370a0005a0 valid
valid-scroll 1 gateway_input hex=564749310704ff880078 valid valid-scroll 1 gateway_input hex=564749310704ff880078 valid
valid-controller-arrival 1 gateway_input hex=564749310808020003010000001f valid
valid-idr 1 gateway_feedback hex=5647463100010000 valid valid-idr 1 gateway_feedback hex=5647463100010000 valid
valid-fec 1 gateway_feedback hex=56474631000200150000002a000500030002000a000200080002140001 valid valid-fec 1 gateway_feedback hex=56474631000200150000002a000500030002000a000200080002140001 valid
valid-terminal-receipt 1 gateway_feedback hex=5647463100030000 valid valid-terminal-receipt 1 gateway_feedback hex=5647463100030000 valid
valid-quality-prompt 1 gateway_feedback hex=564746310004001812345678123456781234567812345678000000000000002a valid
valid-stop-prompt 1 gateway_feedback hex=564746310005001012345678123456781234567812345678 valid
valid-termination 1 gateway_feedback hex=564746310110000400000001 valid valid-termination 1 gateway_feedback hex=564746310110000400000001 valid
valid-rumble 1 gateway_feedback hex=56474631011100050112345678 valid valid-rumble 1 gateway_feedback hex=56474631011100050112345678 valid
valid-hdr 1 gateway_feedback hex=564746310112000101 valid valid-hdr 1 gateway_feedback hex=564746310112000101 valid
@@ -30,5 +33,9 @@ invalid-terminal-receipt-direction 1 gateway_feedback hex=5647463101030000 inval
invalid-terminal-receipt-body 1 gateway_feedback hex=5647463100030001ff invalid:length invalid-terminal-receipt-body 1 gateway_feedback hex=5647463100030001ff invalid:length
invalid-terminal-receipt-truncated 1 gateway_feedback hex=56474631000300 invalid:truncated invalid-terminal-receipt-truncated 1 gateway_feedback hex=56474631000300 invalid:truncated
invalid-terminal-receipt-length 1 gateway_feedback hex=5647463100030001 invalid:length invalid-terminal-receipt-length 1 gateway_feedback hex=5647463100030001 invalid:length
invalid-feedback-type 1 gateway_feedback hex=5647463100040000 invalid:type invalid-feedback-type 1 gateway_feedback hex=5647463100060000 invalid:type
invalid-quality-prompt-trailing 1 gateway_feedback hex=564746310004001912345678123456781234567812345678000000000000002a00 invalid:length
invalid-quality-prompt-zero-uuid 1 gateway_feedback hex=564746310004001800000000000000000000000000000000000000000000002a invalid:field
invalid-stop-prompt-zero-uuid 1 gateway_feedback hex=564746310005001000000000000000000000000000000000 invalid:field
invalid-controller-arrival-family 1 gateway_input hex=564749310808020003040000001f invalid:field
invalid-feedback-length 1 gateway_feedback hex=5647463101100003000000 invalid:length invalid-feedback-length 1 gateway_feedback hex=5647463101100003000000 invalid:length
1 id version kind input expected
9 valid-controller-release 1 gateway_input hex=5647493105110200000000000000000000000000000000 valid
10 valid-absolute-mouse 1 gateway_input hex=56474931060804d202370a0005a0 valid
11 valid-scroll 1 gateway_input hex=564749310704ff880078 valid
12 valid-controller-arrival 1 gateway_input hex=564749310808020003010000001f valid
13 valid-idr 1 gateway_feedback hex=5647463100010000 valid
14 valid-fec 1 gateway_feedback hex=56474631000200150000002a000500030002000a000200080002140001 valid
15 valid-terminal-receipt 1 gateway_feedback hex=5647463100030000 valid
16 valid-quality-prompt 1 gateway_feedback hex=564746310004001812345678123456781234567812345678000000000000002a valid
17 valid-stop-prompt 1 gateway_feedback hex=564746310005001012345678123456781234567812345678 valid
18 valid-termination 1 gateway_feedback hex=564746310110000400000001 valid
19 valid-rumble 1 gateway_feedback hex=56474631011100050112345678 valid
20 valid-hdr 1 gateway_feedback hex=564746310112000101 valid
33 invalid-terminal-receipt-body 1 gateway_feedback hex=5647463100030001ff invalid:length
34 invalid-terminal-receipt-truncated 1 gateway_feedback hex=56474631000300 invalid:truncated
35 invalid-terminal-receipt-length 1 gateway_feedback hex=5647463100030001 invalid:length
36 invalid-feedback-type 1 gateway_feedback hex=5647463100040000 hex=5647463100060000 invalid:type
37 invalid-quality-prompt-trailing 1 gateway_feedback hex=564746310004001912345678123456781234567812345678000000000000002a00 invalid:length
38 invalid-quality-prompt-zero-uuid 1 gateway_feedback hex=564746310004001800000000000000000000000000000000000000000000002a invalid:field
39 invalid-stop-prompt-zero-uuid 1 gateway_feedback hex=564746310005001000000000000000000000000000000000 invalid:field
40 invalid-controller-arrival-family 1 gateway_input hex=564749310808020003040000001f invalid:field
41 invalid-feedback-length 1 gateway_feedback hex=5647463101100003000000 invalid:length
@@ -0,0 +1 @@
{"mode":"auto","target_kbps":1000}
@@ -0,0 +1 @@
{"transport":"quic-tls13","framing":"datagram-v2","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":["h264-opus"]}
@@ -0,0 +1 @@
{"version":"1","session_id":"session-1","gateway_id":"gateway-1","reconnect_sequence":2,"operation_id":"12345678-1234-1234-1234-123456789abc","revision":7,"lease_generation":3,"outcome":"applied","current_applied_revision":6}
@@ -0,0 +1 @@
{"version":"1","session_id":"session-1","gateway_id":"gateway-1","reconnect_sequence":2,"operation_id":"12345678-1234-1234-1234-123456789ABC","revision":7,"lease_generation":3,"outcome":"unknown"}
@@ -0,0 +1 @@
{"version":"1","session_id":"session-1","gateway_id":"gateway-1","reconnect_sequence":2,"operation_id":"00000000-0000-0000-0000-000000000000","revision":7,"lease_generation":3,"outcome":"unknown"}
@@ -0,0 +1 @@
{"version":"1","session_id":"session-1","gateway_id":"gateway-1","reconnect_sequence":2,"acquisition":"poll","operation_id":"12345678-1234-1234-1234-123456789abc","revision":7}
+10 -1
View File
@@ -26,5 +26,14 @@
"audience": "versevdi-gateway" "audience": "versevdi-gateway"
}, },
"correlation_id": "correlation-1", "correlation_id": "correlation-1",
"provider_url": "https://provider.invalid" "provider_url": "https://provider.invalid",
"selected_descriptor": {
"video_profile": {"codec": "h264", "bit_depth": 8, "chroma_subsampling": "4:2:0", "color_space": "bt709-limited", "transfer_function": "sdr"},
"audio_profile": {"codec": "opus", "sample_rate_hz": 48000, "channels": 2, "channel_layout": "stereo", "packet_duration_ms": 5},
"display_mode": {"resolution_width": 1920, "resolution_height": 1080, "fps": 60},
"bitrate_target_kbps": 20000,
"bitrate_maximum_kbps": 40000,
"adjustment": {"display_reason": "none", "bitrate_reason": "none"},
"media_timestamp_basis": "gateway-send-wall-clock-ms"
}
} }
@@ -0,0 +1 @@
{"resolution_width":1920,"resolution_height":1080,"fps":60,"codec":"H264","bitrate_kbps":20000,"audio_enabled":true}
@@ -0,0 +1 @@
{"operation_id":"12345678-1234-1234-1234-123456789abc","session_id":"session-1","revision":7,"state":"pending","requested_bitrate_preference":{"mode":"auto"},"effective_bitrate_kbps":20000,"governing_policy_version":"policy-1","session_version":2,"created_at":"2099-01-01T00:00:00.100Z","deadline_at":"2099-01-01T00:00:30Z","updated_at":"2099-01-01T00:00:00Z"}
@@ -0,0 +1 @@
{"operation_id":"12345678-1234-1234-1234-123456789abc","session_id":"session-1","revision":7,"state":"pending","requested_bitrate_preference":{"mode":"auto"},"effective_bitrate_kbps":20000,"governing_policy_version":"policy-1","session_version":2,"created_at":"2099-01-01T00:00:00+00:00","deadline_at":"2099-01-01T00:00:30Z","updated_at":"2099-01-01T00:00:00Z"}
@@ -0,0 +1 @@
{"video_profile":{"codec":"h264","bit_depth":8,"chroma_subsampling":"4:2:0","color_space":"bt709-limited","transfer_function":"sdr"},"audio_profile":{"codec":"opus","sample_rate_hz":48000,"channels":2,"channel_layout":"stereo","packet_duration_ms":5},"display_mode":{"resolution_width":1920,"resolution_height":1080,"fps":60},"bitrate_target_kbps":20000,"bitrate_maximum_kbps":40000,"adjustment":{"display_reason":"none","bitrate_reason":"none"},"media_timestamp_basis":"gateway-send-wall-clock-ms","provider_identity":"forbidden"}
@@ -0,0 +1 @@
{"client_device_id":"device-1","device_key_id":"key-1","pool_id":"pool-1","idempotency_key":"request-1"}
@@ -0,0 +1 @@
{"codec":"vp9","bit_depth":8,"chroma_subsampling":"4:2:0","color_space":"bt709-limited","transfer_function":"sdr"}
+30 -1
View File
@@ -11,5 +11,34 @@
"fixtures/conformance/gateway-input-feedback-v1.tsv", "fixtures/conformance/gateway-input-feedback-v1.tsv",
"fixtures/conformance/tunnel-v1.tsv" "fixtures/conformance/tunnel-v1.tsv"
], ],
"corpus_sha256": "6d2ce3a855b2fa45733a5f7b5b4c2e68448cceed5dfbca535ec81fe8cf230b30" "corpus_sha256": "10751a9192961aeb8ddcf3bbf720b026b07f92c871c813c301660dd3de930186",
"json_files": [
"fixtures/invalid/authenticated-session-partial-native-identity.json",
"fixtures/invalid/bitrate-preference-auto-target.json",
"fixtures/invalid/browser-session-native-identity.json",
"fixtures/invalid/capability-rc5-opaque.json",
"fixtures/invalid/gateway-quality-ack-contradictory.json",
"fixtures/invalid/gateway-quality-ack-uppercase-uuid.json",
"fixtures/invalid/gateway-quality-ack-zero-uuid.json",
"fixtures/invalid/gateway-quality-poll-coordinates.json",
"fixtures/invalid/manifest-provider-field.json",
"fixtures/invalid/native-session-missing-identity.json",
"fixtures/invalid/provider-stream-policy-rc5.json",
"fixtures/invalid/quality-operation-noncanonical-fraction.json",
"fixtures/invalid/quality-operation-offset-time.json",
"fixtures/invalid/selected-session-descriptor-provider-field.json",
"fixtures/invalid/session-request-policy-snapshot.json",
"fixtures/invalid/session-request-rc5.json",
"fixtures/invalid/video-profile-unknown.json",
"fixtures/valid/authenticated-browser-session.json",
"fixtures/valid/authenticated-native-session.json",
"fixtures/valid/gateway-quality-ack-applied.json",
"fixtures/valid/gateway-quality-poll.json",
"fixtures/valid/manifest.json",
"fixtures/valid/native-tunnel-credential.json",
"fixtures/valid/selected-session-descriptor.json",
"fixtures/valid/session-quality-limits.json",
"fixtures/valid/session-request.json"
],
"json_corpus_sha256": "08dea278526a523d21ea4ca6270ba1e33d44dc0d701681917d7ffc6fb13053d7"
} }
@@ -0,0 +1 @@
{"version":"1","session_id":"session-1","gateway_id":"gateway-1","reconnect_sequence":2,"operation_id":"12345678-1234-1234-1234-123456789abc","revision":7,"lease_generation":3,"outcome":"applied","current_applied_revision":7}
+1
View File
@@ -0,0 +1 @@
{"version":"1","session_id":"session-1","gateway_id":"gateway-1","reconnect_sequence":2,"acquisition":"poll"}
+10 -1
View File
@@ -25,5 +25,14 @@
"expires_at": "2099-01-01T00:00:00Z", "expires_at": "2099-01-01T00:00:00Z",
"audience": "versevdi-gateway" "audience": "versevdi-gateway"
}, },
"correlation_id": "correlation-1" "correlation_id": "correlation-1",
"selected_descriptor": {
"video_profile": {"codec": "h264", "bit_depth": 8, "chroma_subsampling": "4:2:0", "color_space": "bt709-limited", "transfer_function": "sdr"},
"audio_profile": {"codec": "opus", "sample_rate_hz": 48000, "channels": 2, "channel_layout": "stereo", "packet_duration_ms": 5},
"display_mode": {"resolution_width": 1920, "resolution_height": 1080, "fps": 60},
"bitrate_target_kbps": 20000,
"bitrate_maximum_kbps": 40000,
"adjustment": {"display_reason": "none", "bitrate_reason": "none"},
"media_timestamp_basis": "gateway-send-wall-clock-ms"
}
} }
@@ -0,0 +1,9 @@
{
"video_profile": {"codec": "hevc", "bit_depth": 8, "chroma_subsampling": "4:2:0", "color_space": "bt709-limited", "transfer_function": "sdr"},
"audio_profile": {"codec": "opus", "sample_rate_hz": 48000, "channels": 2, "channel_layout": "stereo", "packet_duration_ms": 5},
"display_mode": {"resolution_width": 2560, "resolution_height": 1440, "fps": 120},
"bitrate_target_kbps": 40000,
"bitrate_maximum_kbps": 50000,
"adjustment": {"display_reason": "none", "bitrate_reason": "policy_default"},
"media_timestamp_basis": "gateway-send-wall-clock-ms"
}
@@ -0,0 +1,9 @@
{
"policy_version_id": "policy-1",
"policy_display_limit": {"resolution_width": 3840, "resolution_height": 2160, "fps": 120},
"selectable_display_maximum": {"resolution_width": 3840, "resolution_height": 2160, "fps": 120},
"display_limit_override": false,
"bitrate_minimum_kbps": 1000,
"bitrate_target_kbps": 20000,
"bitrate_maximum_kbps": 50000
}
+5 -1
View File
@@ -2,5 +2,9 @@
"client_device_id": "device-1", "client_device_id": "device-1",
"device_key_id": "key-1", "device_key_id": "key-1",
"pool_id": "pool-1", "pool_id": "pool-1",
"idempotency_key": "request-1" "idempotency_key": "request-1",
"video_profiles": [
{"codec": "h264", "bit_depth": 8, "chroma_subsampling": "4:2:0", "color_space": "bt709-limited", "transfer_function": "sdr"}
],
"bitrate_preference": {"mode": "auto"}
} }
+7 -1
View File
@@ -36,6 +36,7 @@ during cleanup.
| `0x05` | controller state | `controller` (0 through 15), `active_mask` (u16), `button_flags` (u16), `left_trigger` (u8), `right_trigger` (u8), `left_x` (i16), `left_y` (i16), `right_x` (i16), `right_y` (i16), `extra_button_flags` (u16). A zero `active_mask` and zero state is release. | | `0x05` | controller state | `controller` (0 through 15), `active_mask` (u16), `button_flags` (u16), `left_trigger` (u8), `right_trigger` (u8), `left_x` (i16), `left_y` (i16), `right_x` (i16), `right_y` (i16), `extra_button_flags` (u16). A zero `active_mask` and zero state is release. |
| `0x06` | absolute mouse | `x` (u16), `y` (u16), `viewport_width` (u16), `viewport_height` (u16). Viewport dimensions must be nonzero and each coordinate must be strictly smaller than its corresponding dimension. | | `0x06` | absolute mouse | `x` (u16), `y` (u16), `viewport_width` (u16), `viewport_height` (u16). Viewport dimensions must be nonzero and each coordinate must be strictly smaller than its corresponding dimension. |
| `0x07` | high-resolution scroll | `vertical_delta` (i16), `horizontal_delta` (i16). Positive and negative values preserve the client scroll direction without provider-specific scaling. | | `0x07` | high-resolution scroll | `vertical_delta` (i16), `horizontal_delta` (i16). Positive and negative values preserve the client scroll direction without provider-specific scaling. |
| `0x08` | controller arrival | `controller` (u8, 0 through 15), `active_mask` (u16), `family` (u8: `0` unknown, `1` xbox, `2` playstation, `3` nintendo), `supported_button_flags` (u32). It must precede controller state for that controller. |
Keyboard, mouse button, UTF-8, controller, absolute mouse, and scroll messages Keyboard, mouse button, UTF-8, controller, absolute mouse, and scroll messages
are delivered over the gateway's reliable ordered input flow. Relative and are delivered over the gateway's reliable ordered input flow. Relative and
@@ -60,7 +61,12 @@ is the following exact envelope:
| 8 | N | payload | exact type-specific body | | 8 | N | payload | exact type-specific body |
The client-to-gateway types are `0x01` IDR request (empty), `0x02` FEC The client-to-gateway types are `0x01` IDR request (empty), `0x02` FEC
status, and `0x03` terminal receipt (empty). FEC status contains status, `0x03` terminal receipt (empty), `0x04` quality-work prompt, and
`0x05` Stop-work prompt. The quality prompt is exactly `operation_id` (16
canonical UUID bytes) followed by `revision` (nonzero u64). The Stop prompt is
exactly `operation_id` (16 canonical UUID bytes). Prompts never authorize or
complete work; the gateway acquires the matching durable mTLS work item.
FEC status contains
`frame_index` (u32), `highest_received_sequence` (u16), `frame_index` (u32), `highest_received_sequence` (u16),
`next_contiguous_sequence` (u16), `missing_before_highest` (u16), `next_contiguous_sequence` (u16), `missing_before_highest` (u16),
`total_data_packets` (u16), `total_parity_packets` (u16), `total_data_packets` (u16), `total_parity_packets` (u16),
+2416 -149
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -12,7 +12,7 @@
"3" "3"
] ]
}, },
"generator_sha256": "00c1905fc611ca9e226cd90da761b48b8e203734b10542befea397a30082d360", "generator_sha256": "c5756780f59a82d8f56762e52f58e22a4d6aca6648157f45e5bad6559b9deeec",
"protocol_version": "1.0.0", "protocol_version": "1.0.0",
"schema_sha256": "b2353c12269304289b4e872f27cc370ae61b958dea90d9fb7b6ab8afd7d37248" "schema_sha256": "b9e72838b2ae02b3a47e7e2dfa03620b8580f2c98668e6eaf551ea8fb642d297"
} }
Binary file not shown.
+759 -59
View File
File diff suppressed because it is too large Load Diff
+1103 -119
View File
File diff suppressed because it is too large Load Diff
+169
View File
@@ -10,6 +10,8 @@ tags:
- name: resources - name: resources
- name: broker - name: broker
- name: events - name: events
- name: session-quality
- name: gateway
paths: paths:
/api/v1/auth/login: /api/v1/auth/login:
post: post:
@@ -326,6 +328,151 @@ paths:
'401': {$ref: '#/components/responses/Unauthorized'} '401': {$ref: '#/components/responses/Unauthorized'}
'404': {$ref: '#/components/responses/NotFound'} '404': {$ref: '#/components/responses/NotFound'}
'409': {$ref: '#/components/responses/Conflict'} '409': {$ref: '#/components/responses/Conflict'}
/api/v1/session-quality-limits:
get:
tags: [session-quality]
operationId: getSessionQualityLimits
security: [{nativeBearer: []}]
responses:
'200': {description: 'Current global native session quality limits. Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/SessionQualityLimits}}}}
'401': {$ref: '#/components/responses/Unauthorized'}
/api/v1/session-quality-limits/assignments/{assignment_id}:
get:
tags: [session-quality]
operationId: getAssignmentSessionQualityLimits
security: [{nativeBearer: []}]
parameters: [{$ref: '#/components/parameters/AssignmentID'}]
responses:
'200': {description: 'Current assignment-specific native session quality limits. Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/SessionQualityLimits}}}}
'401': {$ref: '#/components/responses/Unauthorized'}
'404': {$ref: '#/components/responses/NotFound'}
/api/v1/session-quality-limits/pools/{pool_id}:
get:
tags: [session-quality]
operationId: getPoolSessionQualityLimits
security: [{nativeBearer: []}]
parameters: [{$ref: '#/components/parameters/PoolID'}]
responses:
'200': {description: 'Current pool-specific native session quality limits. Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/SessionQualityLimits}}}}
'401': {$ref: '#/components/responses/Unauthorized'}
'404': {$ref: '#/components/responses/NotFound'}
/api/v1/admin/entitlements/{entitlement_id}/display-limit-override:
patch:
tags: [session-quality]
operationId: updateEntitlementDisplayLimitOverride
security:
- browserSession: []
browserCsrfCookie: []
browserCsrfHeader: []
parameters: [{$ref: '#/components/parameters/EntitlementID'}]
requestBody: {required: true, description: 'Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/DisplayLimitOverrideRequest}}}}
responses:
'204': {description: Direct entitlement display override updated.}
'400': {$ref: '#/components/responses/InvalidRequest'}
'401': {$ref: '#/components/responses/Unauthorized'}
'403': {$ref: '#/components/responses/Forbidden'}
'404': {$ref: '#/components/responses/NotFound'}
'409': {$ref: '#/components/responses/Conflict'}
/api/v1/broker/sessions/{session_id}/quality-changes:
post:
tags: [session-quality]
operationId: createSessionQualityChange
description: The owning principal and active device/key MUST match the subject-scoped broker session. Maximum JSON body: 16384 bytes.
security: [{nativeBearer: []}]
parameters: [{$ref: '#/components/parameters/SessionID'}, {$ref: '#/components/parameters/IdempotencyKey'}]
requestBody: {required: true, description: 'Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/QualityChangeRequest}}}}
responses:
'201': {description: 'Durable quality change created. Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/QualityChangeOperation}}}}
'200': {description: 'Existing idempotent quality change returned. Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/QualityChangeOperation}}}}
'400': {$ref: '#/components/responses/InvalidRequest'}
'401': {$ref: '#/components/responses/Unauthorized'}
'404': {$ref: '#/components/responses/NotFound'}
'409': {$ref: '#/components/responses/Conflict'}
/api/v1/broker/sessions/{session_id}/quality-changes/{operation_id}:
get:
tags: [session-quality]
operationId: getSessionQualityChange
description: The owning principal and active device/key MUST match the subject-scoped broker session and operation. Maximum JSON body: 16384 bytes.
security: [{nativeBearer: []}]
parameters: [{$ref: '#/components/parameters/SessionID'}, {$ref: '#/components/parameters/OperationID'}]
responses:
'200': {description: 'Durable quality change state. Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/QualityChangeOperation}}}}
'401': {$ref: '#/components/responses/Unauthorized'}
'404': {$ref: '#/components/responses/NotFound'}
/api/v1/broker/sessions/{session_id}/stop-operations:
post:
tags: [session-quality]
operationId: createSessionStopOperation
description: The owning principal and active device/key MUST match the subject-scoped broker session. Maximum JSON body: 16384 bytes.
security: [{nativeBearer: []}]
parameters: [{$ref: '#/components/parameters/SessionID'}, {$ref: '#/components/parameters/IdempotencyKey'}]
responses:
'201': {description: 'Durable Stop operation created. Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/StopOperation}}}}
'200': {description: 'Existing idempotent Stop operation returned. Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/StopOperation}}}}
'401': {$ref: '#/components/responses/Unauthorized'}
'404': {$ref: '#/components/responses/NotFound'}
'409': {$ref: '#/components/responses/Conflict'}
/api/v1/broker/sessions/{session_id}/stop-operations/{operation_id}:
get:
tags: [session-quality]
operationId: getSessionStopOperation
description: The owning principal and active device/key MUST match the subject-scoped broker session and operation. Maximum JSON body: 16384 bytes.
security: [{nativeBearer: []}]
parameters: [{$ref: '#/components/parameters/SessionID'}, {$ref: '#/components/parameters/OperationID'}]
responses:
'200': {description: 'Durable Stop operation state. Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/StopOperation}}}}
'401': {$ref: '#/components/responses/Unauthorized'}
'404': {$ref: '#/components/responses/NotFound'}
/api/v1/gateway/quality-work:
post:
tags: [gateway]
operationId: acquireGatewayQualityWork
description: The authenticated gateway mTLS certificate identity MUST match `gateway_id`; Server authority MUST match the session, gateway, reconnect sequence, and supplied operation/revision/lease generation coordinates. A `poll` acquisition omits unknown operation coordinates and discovers pending session work after a lost prompt; `prompt` and `observation` coordinates MUST match exactly. Maximum JSON body: 16384 bytes.
security: [{gatewayMutualTLS: []}]
requestBody: {required: true, description: 'Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/GatewayQualityWorkRequest}}}}
responses:
'200': {description: 'Leased quality work or reconciliation observation. Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/GatewayQualityWork}}}}
'204': {description: No matching work is available.}
'400': {$ref: '#/components/responses/InvalidRequest'}
'401': {$ref: '#/components/responses/Unauthorized'}
'409': {$ref: '#/components/responses/Conflict'}
/api/v1/gateway/quality-ack:
post:
tags: [gateway]
operationId: acknowledgeGatewayQualityWork
description: The authenticated gateway mTLS certificate identity MUST match `gateway_id`; Server authority MUST exactly match the session, gateway, reconnect sequence, operation, revision, and monotonic lease generation tuple. Stale lease generations MUST be rejected. `applied` requires `current_applied_revision == revision`; `proven_prior` requires `current_applied_revision < revision`; `unknown` forbids `current_applied_revision` and makes no applied-revision assertion. Maximum JSON body: 16384 bytes.
security: [{gatewayMutualTLS: []}]
requestBody: {required: true, description: 'Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/GatewayQualityAck}}}}
responses:
'204': {description: Quality acknowledgement committed idempotently.}
'400': {$ref: '#/components/responses/InvalidRequest'}
'401': {$ref: '#/components/responses/Unauthorized'}
'409': {$ref: '#/components/responses/Conflict'}
/api/v1/gateway/stop-work:
post:
tags: [gateway]
operationId: acquireGatewayStopWork
description: The authenticated gateway mTLS certificate identity MUST match `gateway_id`; Server authority MUST match the session, gateway, reconnect sequence, and any supplied operation coordinate. A `poll` acquisition omits the unknown operation ID and discovers pending session work after a lost prompt; a `prompt` coordinate MUST match exactly. Maximum JSON body: 16384 bytes.
security: [{gatewayMutualTLS: []}]
requestBody: {required: true, description: 'Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/GatewayStopWorkRequest}}}}
responses:
'200': {description: 'One-way non-reissued Stop work claim. Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/GatewayStopWork}}}}
'204': {description: No matching Stop work is available.}
'400': {$ref: '#/components/responses/InvalidRequest'}
'401': {$ref: '#/components/responses/Unauthorized'}
'409': {$ref: '#/components/responses/Conflict'}
/api/v1/gateway/stop-ack:
post:
tags: [gateway]
operationId: acknowledgeGatewayStopWork
description: The authenticated gateway mTLS certificate identity MUST match `gateway_id`; Server authority MUST exactly match the session, gateway, reconnect sequence, and operation tuple. Maximum JSON body: 16384 bytes.
security: [{gatewayMutualTLS: []}]
requestBody: {required: true, description: 'Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/GatewayStopAck}}}}
responses:
'204': {description: Stop acknowledgement committed idempotently.}
'400': {$ref: '#/components/responses/InvalidRequest'}
'401': {$ref: '#/components/responses/Unauthorized'}
'409': {$ref: '#/components/responses/Conflict'}
/api/v1/events: /api/v1/events:
get: get:
tags: [events] tags: [events]
@@ -385,6 +532,8 @@ components:
type: http type: http
scheme: bearer scheme: bearer
bearerFormat: opaque bearerFormat: opaque
gatewayMutualTLS:
type: mutualTLS
parameters: parameters:
DeviceID: DeviceID:
name: device_id name: device_id
@@ -396,6 +545,26 @@ components:
in: path in: path
required: true required: true
schema: {type: string, minLength: 1, maxLength: 128} schema: {type: string, minLength: 1, maxLength: 128}
AssignmentID:
name: assignment_id
in: path
required: true
schema: {type: string, minLength: 1, maxLength: 128}
PoolID:
name: pool_id
in: path
required: true
schema: {type: string, minLength: 1, maxLength: 128}
EntitlementID:
name: entitlement_id
in: path
required: true
schema: {type: string, minLength: 1, maxLength: 128}
OperationID:
name: operation_id
in: path
required: true
schema: {type: string, format: uuid, pattern: '^(?!00000000-0000-0000-0000-000000000000$)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'}
Limit: Limit:
name: limit name: limit
in: query in: query
+118 -2
View File
@@ -54,9 +54,56 @@ message CapabilityProfile {
string transport = 1; string transport = 1;
string framing = 2; string framing = 2;
string media = 3; string media = 3;
string audio = 4; reserved 4;
string source_rate_control = 5; string source_rate_control = 5;
repeated string client_decode = 6; reserved 6;
repeated VideoProfile video_profiles = 7;
repeated AudioProfile audio_profiles = 8;
}
message VideoProfile {
string codec = 1;
uint32 bit_depth = 2;
string chroma_subsampling = 3;
string color_space = 4;
string transfer_function = 5;
}
message AudioProfile {
string codec = 1;
uint32 sample_rate_hz = 2;
uint32 channels = 3;
string channel_layout = 4;
uint32 packet_duration_ms = 5;
}
message DisplayMode {
uint32 resolution_width = 1;
uint32 resolution_height = 2;
uint32 fps = 3;
}
message SessionAdjustment {
string display_reason = 1;
string bitrate_reason = 2;
}
message SelectedSessionDescriptor {
VideoProfile video_profile = 1;
AudioProfile audio_profile = 2;
DisplayMode display_mode = 3;
uint64 bitrate_target_kbps = 4;
uint64 bitrate_maximum_kbps = 5;
SessionAdjustment adjustment = 6;
string media_timestamp_basis = 7;
}
message ProviderStreamPolicy {
VideoProfile video_profile = 1;
AudioProfile audio_profile = 2;
DisplayMode display_mode = 3;
uint64 bitrate_target_kbps = 4;
uint64 bitrate_maximum_kbps = 5;
} }
message GatewayRegistration { message GatewayRegistration {
@@ -115,6 +162,7 @@ message SessionAuthority {
CapabilityProfile capabilities = 7; CapabilityProfile capabilities = 7;
string provider_profile = 8; string provider_profile = 8;
string provider_identity = 9; string provider_identity = 9;
SelectedSessionDescriptor selected_descriptor = 10;
} }
message ClientSessionAuthority { message ClientSessionAuthority {
@@ -125,6 +173,7 @@ message ClientSessionAuthority {
uint64 reconnect_sequence = 5; uint64 reconnect_sequence = 5;
google.protobuf.Timestamp expires_at = 6; google.protobuf.Timestamp expires_at = 6;
CapabilityProfile capabilities = 7; CapabilityProfile capabilities = 7;
SelectedSessionDescriptor selected_descriptor = 8;
} }
message ProviderSessionWork { message ProviderSessionWork {
@@ -147,6 +196,73 @@ message ProviderSessionWork {
string client_id = 17; string client_id = 17;
ClipboardPolicy clipboard_policy = 18; ClipboardPolicy clipboard_policy = 18;
bool provider_application_termination_allowed = 19; bool provider_application_termination_allowed = 19;
ProviderStreamPolicy stream_policy = 20;
}
message GatewayQualityWorkRequest {
string version = 1;
string session_id = 2;
string gateway_id = 3;
uint64 reconnect_sequence = 4;
string operation_id = 5;
optional uint64 revision = 6;
optional uint64 current_applied_revision = 7;
string acquisition = 8;
optional uint64 lease_generation = 9;
}
message GatewayQualityWork {
string version = 1;
string session_id = 2;
string gateway_id = 3;
uint64 reconnect_sequence = 4;
string operation_id = 5;
uint64 revision = 6;
google.protobuf.Timestamp lease_expires_at = 7;
SelectedSessionDescriptor selected_descriptor = 8;
optional uint64 current_applied_revision = 9;
uint64 lease_generation = 10;
}
message GatewayQualityAck {
string version = 1;
string session_id = 2;
string gateway_id = 3;
uint64 reconnect_sequence = 4;
string operation_id = 5;
uint64 revision = 6;
string outcome = 7;
optional uint64 current_applied_revision = 8;
string failure_code = 9;
uint64 lease_generation = 10;
}
message GatewayStopWork {
string version = 1;
string session_id = 2;
string gateway_id = 3;
uint64 reconnect_sequence = 4;
string operation_id = 5;
uint32 attempt = 6;
}
message GatewayStopWorkRequest {
string version = 1;
string session_id = 2;
string gateway_id = 3;
uint64 reconnect_sequence = 4;
string operation_id = 5;
string acquisition = 6;
}
message GatewayStopAck {
string version = 1;
string session_id = 2;
string gateway_id = 3;
uint64 reconnect_sequence = 4;
string operation_id = 5;
string outcome = 6;
string failure_code = 7;
} }
message ClipboardPolicy { message ClipboardPolicy {
+3
View File
@@ -8,6 +8,9 @@
"datagrams": [ "datagrams": [
{"id": "control.ack.v1", "direction": "bidirectional", "max_payload_bytes": 1024, "payload_profile": "gateway-feedback-v1"}, {"id": "control.ack.v1", "direction": "bidirectional", "max_payload_bytes": 1024, "payload_profile": "gateway-feedback-v1"},
{"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": "session.quality.prompt.v1", "direction": "client-to-server", "max_payload_bytes": 32, "payload_profile": "gateway-feedback-v1"},
{"id": "session.stop.prompt.v1", "direction": "client-to-server", "max_payload_bytes": 24, "payload_profile": "gateway-feedback-v1"},
{"id": "controller.arrival.v1", "direction": "client-to-server", "max_payload_bytes": 14, "payload_profile": "gateway-input-v1"},
{"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.video.v1", "direction": "server-to-client", "max_payload_bytes": 1200},
{"id": "media.audio.v1", "direction": "server-to-client", "max_payload_bytes": 1200}, {"id": "media.audio.v1", "direction": "server-to-client", "max_payload_bytes": 1200},
+5 -1
View File
@@ -7,6 +7,10 @@
{"id": "clipboard.text.v1", "direction": "bidirectional", "max_frame_bytes": 65536}, {"id": "clipboard.text.v1", "direction": "bidirectional", "max_frame_bytes": 65536},
{"id": "display.request.v1", "direction": "client-to-server", "max_frame_bytes": 65536}, {"id": "display.request.v1", "direction": "client-to-server", "max_frame_bytes": 65536},
{"id": "input.absolute.v1", "direction": "client-to-server", "max_frame_bytes": 14}, {"id": "input.absolute.v1", "direction": "client-to-server", "max_frame_bytes": 14},
{"id": "input.scroll.v1", "direction": "client-to-server", "max_frame_bytes": 10} {"id": "input.scroll.v1", "direction": "client-to-server", "max_frame_bytes": 10},
{"id": "video.profile.v1", "direction": "bidirectional", "max_frame_bytes": 65536},
{"id": "session.quality.v1", "direction": "bidirectional", "max_frame_bytes": 32},
{"id": "session.stop.v1", "direction": "bidirectional", "max_frame_bytes": 24},
{"id": "controller.arrival.v1", "direction": "client-to-server", "max_frame_bytes": 14}
] ]
} }
+241 -31
View File
@@ -64,22 +64,24 @@
"AssignedDesktop": { "AssignedDesktop": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": ["assignment_id", "pool_id", "name", "availability"], "required": ["assignment_id", "pool_id", "name", "availability", "quality_limits"],
"properties": { "properties": {
"assignment_id": {"type": "string", "minLength": 1, "maxLength": 128}, "assignment_id": {"type": "string", "minLength": 1, "maxLength": 128},
"pool_id": {"type": "string", "minLength": 1, "maxLength": 128}, "pool_id": {"type": "string", "minLength": 1, "maxLength": 128},
"name": {"type": "string", "minLength": 1, "maxLength": 256}, "name": {"type": "string", "minLength": 1, "maxLength": 256},
"availability": {"type": "string", "minLength": 1, "maxLength": 64} "availability": {"type": "string", "minLength": 1, "maxLength": 64},
"quality_limits": {"$ref": "#/$defs/SessionQualityLimits"}
} }
}, },
"EntitledPool": { "EntitledPool": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": ["pool_id", "name", "assignment_state"], "required": ["pool_id", "name", "assignment_state", "quality_limits"],
"properties": { "properties": {
"pool_id": {"type": "string", "minLength": 1, "maxLength": 128}, "pool_id": {"type": "string", "minLength": 1, "maxLength": 128},
"name": {"type": "string", "minLength": 1, "maxLength": 256}, "name": {"type": "string", "minLength": 1, "maxLength": 256},
"assignment_state": {"type": "string", "minLength": 1, "maxLength": 64} "assignment_state": {"type": "string", "minLength": 1, "maxLength": 64},
"quality_limits": {"$ref": "#/$defs/SessionQualityLimits"}
} }
}, },
"ResourceList": { "ResourceList": {
@@ -232,32 +234,106 @@
"fps": {"type": "integer", "minimum": 1, "maximum": 240} "fps": {"type": "integer", "minimum": 1, "maximum": 240}
} }
}, },
"VideoProfile": {
"type": "object",
"additionalProperties": false,
"required": ["codec", "bit_depth", "chroma_subsampling", "color_space", "transfer_function"],
"properties": {
"codec": {"type": "string", "enum": ["h264", "hevc", "av1"]},
"bit_depth": {"type": "integer", "minimum": 8, "maximum": 8},
"chroma_subsampling": {"type": "string", "const": "4:2:0"},
"color_space": {"type": "string", "const": "bt709-limited"},
"transfer_function": {"type": "string", "const": "sdr"}
}
},
"AudioProfile": {
"type": "object",
"additionalProperties": false,
"required": ["codec", "sample_rate_hz", "channels", "channel_layout", "packet_duration_ms"],
"properties": {
"codec": {"type": "string", "const": "opus"},
"sample_rate_hz": {"type": "integer", "minimum": 48000, "maximum": 48000},
"channels": {"type": "integer", "minimum": 2, "maximum": 2},
"channel_layout": {"type": "string", "const": "stereo"},
"packet_duration_ms": {"type": "integer", "minimum": 5, "maximum": 5}
}
},
"BitratePreference": {
"type": "object",
"additionalProperties": false,
"required": ["mode"],
"properties": {
"mode": {"type": "string", "enum": ["auto", "explicit"]},
"target_kbps": {"type": "integer", "minimum": 100, "maximum": 1000000, "x-optional-pointer": true}
}
},
"SessionQualityLimits": {
"type": "object",
"x-max-bytes": 16384,
"additionalProperties": false,
"required": ["policy_version_id", "policy_display_limit", "selectable_display_maximum", "display_limit_override", "bitrate_minimum_kbps", "bitrate_target_kbps", "bitrate_maximum_kbps"],
"properties": {
"policy_version_id": {"type": "string", "minLength": 1, "maxLength": 128},
"policy_display_limit": {"$ref": "#/$defs/DisplayMode"},
"selectable_display_maximum": {"$ref": "#/$defs/DisplayMode"},
"display_limit_override": {"type": "boolean"},
"bitrate_minimum_kbps": {"type": "integer", "minimum": 100, "maximum": 1000000},
"bitrate_target_kbps": {"type": "integer", "minimum": 100, "maximum": 1000000},
"bitrate_maximum_kbps": {"type": "integer", "minimum": 100, "maximum": 1000000}
}
},
"SessionAdjustment": {
"type": "object",
"additionalProperties": false,
"required": ["display_reason", "bitrate_reason"],
"properties": {
"display_reason": {"type": "string", "enum": ["none", "policy_clamp", "direct_entitlement_override"]},
"bitrate_reason": {"type": "string", "enum": ["none", "policy_default", "session_ceiling_clamp", "aggregate_capacity_clamp"]}
}
},
"SelectedSessionDescriptor": {
"type": "object",
"additionalProperties": false,
"required": ["video_profile", "audio_profile", "display_mode", "bitrate_target_kbps", "bitrate_maximum_kbps", "adjustment", "media_timestamp_basis"],
"properties": {
"video_profile": {"$ref": "#/$defs/VideoProfile"},
"audio_profile": {"$ref": "#/$defs/AudioProfile"},
"display_mode": {"$ref": "#/$defs/DisplayMode"},
"bitrate_target_kbps": {"type": "integer", "minimum": 100, "maximum": 1000000},
"bitrate_maximum_kbps": {"type": "integer", "minimum": 100, "maximum": 1000000},
"adjustment": {"$ref": "#/$defs/SessionAdjustment"},
"media_timestamp_basis": {"type": "string", "const": "gateway-send-wall-clock-ms"}
}
},
"SessionRequest": { "SessionRequest": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": ["client_device_id", "device_key_id", "pool_id", "idempotency_key"], "required": ["client_device_id", "device_key_id", "pool_id", "idempotency_key", "video_profiles", "bitrate_preference"],
"properties": { "properties": {
"client_device_id": {"type": "string", "minLength": 1, "maxLength": 128}, "client_device_id": {"type": "string", "minLength": 1, "maxLength": 128},
"device_key_id": {"type": "string", "minLength": 1, "maxLength": 128}, "device_key_id": {"type": "string", "minLength": 1, "maxLength": 128},
"pool_id": {"type": "string", "minLength": 1, "maxLength": 128}, "pool_id": {"type": "string", "minLength": 1, "maxLength": 128},
"idempotency_key": {"type": "string", "minLength": 1, "maxLength": 256}, "idempotency_key": {"type": "string", "minLength": 1, "maxLength": 256},
"video_profiles": {"type": "array", "minItems": 1, "maxItems": 12, "uniqueItems": true, "items": {"$ref": "#/$defs/VideoProfile"}},
"bitrate_preference": {"$ref": "#/$defs/BitratePreference"},
"requested_display_mode": {"$ref": "#/$defs/DisplayMode"} "requested_display_mode": {"$ref": "#/$defs/DisplayMode"}
} }
}, },
"ReconnectRequest": { "ReconnectRequest": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": ["client_device_id", "device_key_id", "expected_version"], "required": ["client_device_id", "device_key_id", "expected_version", "display_relaunch_confirmed"],
"properties": { "properties": {
"client_device_id": {"type": "string", "minLength": 1, "maxLength": 128}, "client_device_id": {"type": "string", "minLength": 1, "maxLength": 128},
"device_key_id": {"type": "string", "minLength": 1, "maxLength": 128}, "device_key_id": {"type": "string", "minLength": 1, "maxLength": 128},
"expected_version": {"type": "integer", "minimum": 1} "expected_version": {"type": "integer", "minimum": 1},
"display_relaunch_confirmed": {"type": "boolean"}
} }
}, },
"BrokerSession": { "BrokerSession": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": ["id", "principal_id", "pool_id", "state", "policy_snapshot", "cleanup_state", "idempotency_key", "correlation_id", "requested_at", "version"], "required": ["id", "principal_id", "pool_id", "state", "policy_snapshot", "cleanup_state", "idempotency_key", "correlation_id", "requested_at", "version", "requested_video_profiles", "requested_bitrate_preference"],
"properties": { "properties": {
"id": {"type": "string", "minLength": 1, "maxLength": 128}, "id": {"type": "string", "minLength": 1, "maxLength": 128},
"principal_id": {"type": "string", "minLength": 1, "maxLength": 128}, "principal_id": {"type": "string", "minLength": 1, "maxLength": 128},
@@ -275,7 +351,10 @@
"ended_at": {"type": "string", "format": "date-time", "maxLength": 64}, "ended_at": {"type": "string", "format": "date-time", "maxLength": 64},
"version": {"type": "integer", "minimum": 1}, "version": {"type": "integer", "minimum": 1},
"requested_display_mode": {"$ref": "#/$defs/DisplayMode"}, "requested_display_mode": {"$ref": "#/$defs/DisplayMode"},
"effective_display_mode": {"$ref": "#/$defs/DisplayMode"} "effective_display_mode": {"$ref": "#/$defs/DisplayMode"},
"requested_video_profiles": {"type": "array", "minItems": 1, "maxItems": 12, "uniqueItems": true, "items": {"$ref": "#/$defs/VideoProfile"}},
"requested_bitrate_preference": {"$ref": "#/$defs/BitratePreference"},
"selected_descriptor": {"$ref": "#/$defs/SelectedSessionDescriptor"}
} }
}, },
"AllocationPolicy": { "AllocationPolicy": {
@@ -297,7 +376,7 @@
"ConnectionManifest": { "ConnectionManifest": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": ["version", "purpose", "session_id", "reconnect_sequence", "gateway", "tunnel", "profile", "grant", "correlation_id"], "required": ["version", "purpose", "session_id", "reconnect_sequence", "gateway", "tunnel", "profile", "grant", "correlation_id", "selected_descriptor"],
"properties": { "properties": {
"version": {"type": "string", "const": "1"}, "version": {"type": "string", "const": "1"},
"purpose": {"type": "string", "enum": ["launch", "reconnect"]}, "purpose": {"type": "string", "enum": ["launch", "reconnect"]},
@@ -307,7 +386,8 @@
"tunnel": {"$ref": "#/$defs/ManifestTunnel"}, "tunnel": {"$ref": "#/$defs/ManifestTunnel"},
"profile": {"$ref": "#/$defs/ManifestProfile"}, "profile": {"$ref": "#/$defs/ManifestProfile"},
"grant": {"$ref": "#/$defs/GrantReference"}, "grant": {"$ref": "#/$defs/GrantReference"},
"correlation_id": {"type": "string", "minLength": 1, "maxLength": 128} "correlation_id": {"type": "string", "minLength": 1, "maxLength": 128},
"selected_descriptor": {"$ref": "#/$defs/SelectedSessionDescriptor"}
} }
}, },
"ManifestGateway": { "ManifestGateway": {
@@ -431,20 +511,14 @@
"CapabilityProfile": { "CapabilityProfile": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": ["transport", "framing", "media", "audio", "source_rate_control", "client_decode"], "required": ["transport", "framing", "media", "source_rate_control", "video_profiles", "audio_profiles"],
"properties": { "properties": {
"transport": {"type": "string", "minLength": 1, "maxLength": 64}, "transport": {"type": "string", "minLength": 1, "maxLength": 64},
"framing": {"type": "string", "enum": ["datagram-v1", "datagram-v2"]}, "framing": {"type": "string", "enum": ["datagram-v1", "datagram-v2"]},
"media": {"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}, "source_rate_control": {"type": "string", "minLength": 1, "maxLength": 64},
"client_decode": { "video_profiles": {"type": "array", "minItems": 1, "maxItems": 12, "uniqueItems": true, "items": {"$ref": "#/$defs/VideoProfile"}},
"type": "array", "audio_profiles": {"type": "array", "minItems": 1, "maxItems": 1, "uniqueItems": true, "items": {"$ref": "#/$defs/AudioProfile"}}
"minItems": 1,
"maxItems": 2,
"uniqueItems": true,
"items": {"type": "string", "enum": ["h264-opus", "hevc-opus"]}
}
} }
}, },
"GatewayRegistration": { "GatewayRegistration": {
@@ -538,7 +612,7 @@
"SessionAuthority": { "SessionAuthority": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": ["version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities", "provider_profile", "provider_identity"], "required": ["version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities", "provider_profile", "provider_identity", "selected_descriptor"],
"properties": { "properties": {
"version": {"type": "string", "const": "1"}, "version": {"type": "string", "const": "1"},
"session_id": {"type": "string", "minLength": 1, "maxLength": 128}, "session_id": {"type": "string", "minLength": 1, "maxLength": 128},
@@ -548,13 +622,14 @@
"expires_at": {"type": "string", "format": "date-time", "maxLength": 64}, "expires_at": {"type": "string", "format": "date-time", "maxLength": 64},
"capabilities": {"$ref": "#/$defs/CapabilityProfile"}, "capabilities": {"$ref": "#/$defs/CapabilityProfile"},
"provider_profile": {"type": "string", "enum": ["apollo"]}, "provider_profile": {"type": "string", "enum": ["apollo"]},
"provider_identity": {"type": "string", "minLength": 1, "maxLength": 256} "provider_identity": {"type": "string", "minLength": 1, "maxLength": 256},
"selected_descriptor": {"$ref": "#/$defs/SelectedSessionDescriptor"}
} }
}, },
"ClientSessionAuthority": { "ClientSessionAuthority": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": ["version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities"], "required": ["version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities", "selected_descriptor"],
"properties": { "properties": {
"version": {"type": "string", "const": "1"}, "version": {"type": "string", "const": "1"},
"session_id": {"type": "string", "minLength": 1, "maxLength": 128}, "session_id": {"type": "string", "minLength": 1, "maxLength": 128},
@@ -562,20 +637,155 @@
"audience": {"type": "string", "minLength": 1, "maxLength": 256}, "audience": {"type": "string", "minLength": 1, "maxLength": 256},
"reconnect_sequence": {"type": "integer", "minimum": 0}, "reconnect_sequence": {"type": "integer", "minimum": 0},
"expires_at": {"type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$", "maxLength": 64}, "expires_at": {"type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$", "maxLength": 64},
"capabilities": {"$ref": "#/$defs/CapabilityProfile"} "capabilities": {"$ref": "#/$defs/CapabilityProfile"},
"selected_descriptor": {"$ref": "#/$defs/SelectedSessionDescriptor"}
} }
}, },
"ProviderStreamPolicy": { "ProviderStreamPolicy": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": ["resolution_width", "resolution_height", "fps", "codec", "bitrate_kbps", "audio_enabled"], "required": ["video_profile", "audio_profile", "display_mode", "bitrate_target_kbps", "bitrate_maximum_kbps"],
"properties": { "properties": {
"resolution_width": {"type": "integer", "minimum": 320, "maximum": 16384}, "video_profile": {"$ref": "#/$defs/VideoProfile"},
"resolution_height": {"type": "integer", "minimum": 200, "maximum": 8640}, "audio_profile": {"$ref": "#/$defs/AudioProfile"},
"fps": {"type": "integer", "minimum": 1, "maximum": 240}, "display_mode": {"$ref": "#/$defs/DisplayMode"},
"codec": {"type": "string", "enum": ["H264", "HEVC", "AV1"]}, "bitrate_target_kbps": {"type": "integer", "minimum": 100, "maximum": 1000000},
"bitrate_kbps": {"type": "integer", "minimum": 100, "maximum": 1000000}, "bitrate_maximum_kbps": {"type": "integer", "minimum": 100, "maximum": 1000000}
"audio_enabled": {"type": "boolean"} }
},
"DisplayLimitOverrideRequest": {
"type": "object",
"x-max-bytes": 16384,
"additionalProperties": false,
"required": ["allow_display_limit_override", "expected_version"],
"properties": {
"allow_display_limit_override": {"type": "boolean"},
"expected_version": {"type": "integer", "minimum": 1}
}
},
"QualityChangeRequest": {
"type": "object",
"x-max-bytes": 16384,
"additionalProperties": false,
"required": ["bitrate_preference", "expected_session_version", "expected_policy_version"],
"properties": {
"bitrate_preference": {"$ref": "#/$defs/BitratePreference"},
"expected_session_version": {"type": "integer", "minimum": 1},
"expected_policy_version": {"type": "string", "minLength": 1, "maxLength": 128}
}
},
"QualityChangeOperation": {
"type": "object",
"x-max-bytes": 16384,
"additionalProperties": false,
"required": ["operation_id", "session_id", "revision", "state", "requested_bitrate_preference", "effective_bitrate_kbps", "governing_policy_version", "session_version", "created_at", "deadline_at", "updated_at"],
"properties": {
"operation_id": {"type": "string", "format": "uuid", "pattern": "^(?!00000000-0000-0000-0000-000000000000$)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", "minLength": 36, "maxLength": 36},
"session_id": {"type": "string", "minLength": 1, "maxLength": 128},
"revision": {"type": "integer", "minimum": 1},
"state": {"type": "string", "enum": ["pending", "applying", "applied", "failed", "timed_out"]},
"requested_bitrate_preference": {"$ref": "#/$defs/BitratePreference"},
"effective_bitrate_kbps": {"type": "integer", "minimum": 100, "maximum": 1000000},
"governing_policy_version": {"type": "string", "minLength": 1, "maxLength": 128},
"session_version": {"type": "integer", "minimum": 1},
"created_at": {"type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]{0,8}[1-9])?Z$", "maxLength": 64},
"deadline_at": {"type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]{0,8}[1-9])?Z$", "maxLength": 64},
"updated_at": {"type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]{0,8}[1-9])?Z$", "maxLength": 64},
"failure_code": {"type": "string", "minLength": 1, "maxLength": 128}
}
},
"StopOperation": {
"type": "object",
"x-max-bytes": 16384,
"additionalProperties": false,
"required": ["operation_id", "session_id", "state", "session_version", "created_at", "deadline_at", "updated_at"],
"properties": {
"operation_id": {"type": "string", "format": "uuid", "pattern": "^(?!00000000-0000-0000-0000-000000000000$)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", "minLength": 36, "maxLength": 36},
"session_id": {"type": "string", "minLength": 1, "maxLength": 128},
"state": {"type": "string", "enum": ["pending", "attempting", "applied", "failed", "termination_unconfirmed"]},
"session_version": {"type": "integer", "minimum": 1},
"created_at": {"type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]{0,8}[1-9])?Z$", "maxLength": 64},
"deadline_at": {"type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]{0,8}[1-9])?Z$", "maxLength": 64},
"updated_at": {"type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]{0,8}[1-9])?Z$", "maxLength": 64},
"failure_code": {"type": "string", "minLength": 1, "maxLength": 128}
}
},
"GatewayQualityWorkRequest": {
"type": "object", "additionalProperties": false,
"x-max-bytes": 16384,
"required": ["version", "session_id", "gateway_id", "reconnect_sequence", "acquisition"],
"properties": {
"version": {"type": "string", "const": "1"}, "session_id": {"type": "string", "minLength": 1, "maxLength": 128},
"gateway_id": {"type": "string", "minLength": 1, "maxLength": 128}, "reconnect_sequence": {"type": "integer", "minimum": 0},
"acquisition": {"type": "string", "enum": ["poll", "prompt", "observation"]},
"operation_id": {"type": "string", "format": "uuid", "pattern": "^(?!00000000-0000-0000-0000-000000000000$)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", "minLength": 36, "maxLength": 36},
"revision": {"type": "integer", "minimum": 1, "x-optional-pointer": true},
"lease_generation": {"type": "integer", "minimum": 1, "x-optional-pointer": true},
"current_applied_revision": {"type": "integer", "minimum": 0, "x-optional-pointer": true}
}
},
"GatewayQualityWork": {
"type": "object", "additionalProperties": false,
"x-max-bytes": 16384,
"required": ["version", "session_id", "gateway_id", "reconnect_sequence", "operation_id", "revision", "lease_generation", "lease_expires_at", "selected_descriptor"],
"properties": {
"version": {"type": "string", "const": "1"}, "session_id": {"type": "string", "minLength": 1, "maxLength": 128},
"gateway_id": {"type": "string", "minLength": 1, "maxLength": 128}, "reconnect_sequence": {"type": "integer", "minimum": 0},
"operation_id": {"type": "string", "format": "uuid", "pattern": "^(?!00000000-0000-0000-0000-000000000000$)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", "minLength": 36, "maxLength": 36}, "revision": {"type": "integer", "minimum": 1},
"lease_generation": {"type": "integer", "minimum": 1},
"lease_expires_at": {"type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]{0,8}[1-9])?Z$", "maxLength": 64}, "selected_descriptor": {"$ref": "#/$defs/SelectedSessionDescriptor"},
"current_applied_revision": {"type": "integer", "minimum": 0, "x-optional-pointer": true}
}
},
"GatewayQualityAck": {
"type": "object", "additionalProperties": false,
"x-max-bytes": 16384,
"description": "Outcome invariants: applied requires current_applied_revision equal to revision; proven_prior requires current_applied_revision strictly less than revision; unknown forbids current_applied_revision and makes no applied-revision assertion.",
"required": ["version", "session_id", "gateway_id", "reconnect_sequence", "operation_id", "revision", "lease_generation", "outcome"],
"oneOf": [
{"properties": {"outcome": {"const": "applied"}}, "required": ["current_applied_revision"]},
{"properties": {"outcome": {"const": "proven_prior"}}, "required": ["current_applied_revision"]},
{"properties": {"outcome": {"const": "unknown"}}, "not": {"required": ["current_applied_revision"]}}
],
"properties": {
"version": {"type": "string", "const": "1"}, "session_id": {"type": "string", "minLength": 1, "maxLength": 128},
"gateway_id": {"type": "string", "minLength": 1, "maxLength": 128}, "reconnect_sequence": {"type": "integer", "minimum": 0},
"operation_id": {"type": "string", "format": "uuid", "pattern": "^(?!00000000-0000-0000-0000-000000000000$)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", "minLength": 36, "maxLength": 36}, "revision": {"type": "integer", "minimum": 1},
"lease_generation": {"type": "integer", "minimum": 1},
"outcome": {"type": "string", "enum": ["applied", "proven_prior", "unknown"]}, "current_applied_revision": {"type": "integer", "minimum": 0, "x-optional-pointer": true},
"failure_code": {"type": "string", "minLength": 1, "maxLength": 128}
}
},
"GatewayStopWorkRequest": {
"type": "object", "additionalProperties": false,
"x-max-bytes": 16384,
"required": ["version", "session_id", "gateway_id", "reconnect_sequence", "acquisition"],
"properties": {
"version": {"type": "string", "const": "1"}, "session_id": {"type": "string", "minLength": 1, "maxLength": 128},
"gateway_id": {"type": "string", "minLength": 1, "maxLength": 128}, "reconnect_sequence": {"type": "integer", "minimum": 0},
"acquisition": {"type": "string", "enum": ["poll", "prompt"]},
"operation_id": {"type": "string", "format": "uuid", "pattern": "^(?!00000000-0000-0000-0000-000000000000$)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", "minLength": 36, "maxLength": 36}
}
},
"GatewayStopWork": {
"type": "object", "additionalProperties": false,
"x-max-bytes": 16384,
"required": ["version", "session_id", "gateway_id", "reconnect_sequence", "operation_id", "attempt"],
"properties": {
"version": {"type": "string", "const": "1"}, "session_id": {"type": "string", "minLength": 1, "maxLength": 128},
"gateway_id": {"type": "string", "minLength": 1, "maxLength": 128}, "reconnect_sequence": {"type": "integer", "minimum": 0},
"operation_id": {"type": "string", "format": "uuid", "pattern": "^(?!00000000-0000-0000-0000-000000000000$)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", "minLength": 36, "maxLength": 36}, "attempt": {"type": "integer", "minimum": 1, "maximum": 1}
}
},
"GatewayStopAck": {
"type": "object", "additionalProperties": false,
"x-max-bytes": 16384,
"required": ["version", "session_id", "gateway_id", "reconnect_sequence", "operation_id", "outcome"],
"properties": {
"version": {"type": "string", "const": "1"}, "session_id": {"type": "string", "minLength": 1, "maxLength": 128},
"gateway_id": {"type": "string", "minLength": 1, "maxLength": 128}, "reconnect_sequence": {"type": "integer", "minimum": 0},
"operation_id": {"type": "string", "format": "uuid", "pattern": "^(?!00000000-0000-0000-0000-000000000000$)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", "minLength": 36, "maxLength": 36}, "outcome": {"type": "string", "enum": ["applied", "failed", "termination_unconfirmed"]},
"failure_code": {"type": "string", "minLength": 1, "maxLength": 128}
} }
}, },
"ProviderSessionWork": { "ProviderSessionWork": {
+51 -31
View File
@@ -11,6 +11,24 @@ import (
protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol" protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol"
) )
func rc6Video(codec string) protocol.VideoProfile {
return protocol.VideoProfile{Codec: codec, BitDepth: 8, ChromaSubsampling: "4:2:0", ColorSpace: "bt709-limited", TransferFunction: "sdr"}
}
func rc6Audio() protocol.AudioProfile {
return protocol.AudioProfile{Codec: "opus", SampleRateHz: 48000, Channels: 2, ChannelLayout: "stereo", PacketDurationMs: 5}
}
func rc6Capability(codecs ...string) protocol.CapabilityProfile {
videos := make([]protocol.VideoProfile, len(codecs))
for index, codec := range codecs { videos[index] = rc6Video(codec) }
return protocol.CapabilityProfile{Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", SourceRateControl: "server", VideoProfiles: videos, AudioProfiles: []protocol.AudioProfile{rc6Audio()}}
}
func rc6Descriptor(codec string) protocol.SelectedSessionDescriptor {
return protocol.SelectedSessionDescriptor{VideoProfile: rc6Video(codec), AudioProfile: rc6Audio(), DisplayMode: protocol.DisplayMode{ResolutionWidth: 1920, ResolutionHeight: 1080, Fps: 60}, BitrateTargetKbps: 20000, BitrateMaximumKbps: 40000, Adjustment: protocol.SessionAdjustment{DisplayReason: "none", BitrateReason: "none"}, MediaTimestampBasis: "gateway-send-wall-clock-ms"}
}
func TestDeviceRegistrationProofTranscriptIsCanonicalAndStrict(t *testing.T) { func TestDeviceRegistrationProofTranscriptIsCanonicalAndStrict(t *testing.T) {
serverID, _ := hex.DecodeString("00112233445566778899aabbccddeeff") serverID, _ := hex.DecodeString("00112233445566778899aabbccddeeff")
principalID, _ := hex.DecodeString("102132435465768798a9bacbdcedfe0f") principalID, _ := hex.DecodeString("102132435465768798a9bacbdcedfe0f")
@@ -51,7 +69,7 @@ func TestDeviceRegistrationProofTranscriptIsCanonicalAndStrict(t *testing.T) {
} }
func TestManifestRejectsForbiddenAndUnknownFields(t *testing.T) { 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.control.test"},"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"}` valid := `{"version":"1","purpose":"launch","session_id":"session-1","reconnect_sequence":0,"gateway":{"id":"gateway-1","addresses":["gateway.control.test:443"],"public_identity":"gateway.control.test"},"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","selected_descriptor":{"video_profile":{"codec":"h264","bit_depth":8,"chroma_subsampling":"4:2:0","color_space":"bt709-limited","transfer_function":"sdr"},"audio_profile":{"codec":"opus","sample_rate_hz":48000,"channels":2,"channel_layout":"stereo","packet_duration_ms":5},"display_mode":{"resolution_width":1920,"resolution_height":1080,"fps":60},"bitrate_target_kbps":20000,"bitrate_maximum_kbps":40000,"adjustment":{"display_reason":"none","bitrate_reason":"none"},"media_timestamp_basis":"gateway-send-wall-clock-ms"}}`
manifest, err := protocol.DecodeConnectionManifest([]byte(valid)) manifest, err := protocol.DecodeConnectionManifest([]byte(valid))
if err != nil || manifest.Gateway.ID != "gateway-1" { if err != nil || manifest.Gateway.ID != "gateway-1" {
t.Fatalf("valid manifest = %+v, err = %v", manifest, err) t.Fatalf("valid manifest = %+v, err = %v", manifest, err)
@@ -83,6 +101,7 @@ func TestDisplayModeIsOptionalStrictAndBounded(t *testing.T) {
request := protocol.SessionRequest{ request := protocol.SessionRequest{
ClientDeviceID: "device-1", DeviceKeyID: "key-1", PoolID: "pool-1", IdempotencyKey: "request-1", ClientDeviceID: "device-1", DeviceKeyID: "key-1", PoolID: "pool-1", IdempotencyKey: "request-1",
VideoProfiles: []protocol.VideoProfile{rc6Video("h264")}, BitratePreference: protocol.BitratePreference{Mode: "auto"},
} }
legacy, err := protocol.EncodeSessionRequest(request) legacy, err := protocol.EncodeSessionRequest(request)
if err != nil { if err != nil {
@@ -140,7 +159,7 @@ func TestSessionRequestRejectsClientPolicySnapshot(t *testing.T) {
if protocol.CurrentWireVersion != "2" || protocol.NMinus1WireVersion != "1" || protocol.NMinus2WireVersion != "0" { if protocol.CurrentWireVersion != "2" || protocol.NMinus1WireVersion != "1" || protocol.NMinus2WireVersion != "0" {
t.Fatalf("wire compatibility = current %q, n-1 %q, n-2 %q", protocol.CurrentWireVersion, protocol.NMinus1WireVersion, protocol.NMinus2WireVersion) t.Fatalf("wire compatibility = current %q, n-1 %q, n-2 %q", protocol.CurrentWireVersion, protocol.NMinus1WireVersion, protocol.NMinus2WireVersion)
} }
request := `{"client_device_id":"device-1","device_key_id":"key-1","pool_id":"pool-1","idempotency_key":"request-1"}` request := `{"client_device_id":"device-1","device_key_id":"key-1","pool_id":"pool-1","idempotency_key":"request-1","video_profiles":[{"codec":"h264","bit_depth":8,"chroma_subsampling":"4:2:0","color_space":"bt709-limited","transfer_function":"sdr"}],"bitrate_preference":{"mode":"auto"}}`
if _, err := protocol.DecodeSessionRequest([]byte(request)); err != nil { if _, err := protocol.DecodeSessionRequest([]byte(request)); err != nil {
t.Fatalf("policy-free SessionRequest rejected: %v", err) t.Fatalf("policy-free SessionRequest rejected: %v", err)
} }
@@ -225,7 +244,7 @@ func TestGeneratedDecodersRejectMissingRequiredFieldsAndTrailingValues(t *testin
} }
func TestGatewayContractsRejectUnknownVersionsAndFields(t *testing.T) { func TestGatewayContractsRejectUnknownVersionsAndFields(t *testing.T) {
registration := `{"version":"1","gateway_id":"gateway-1","instance_identity":"instance-1","certificate_identity":"cert-1","public_identity":"public-1","address":"gateway.test:443","provider_identity":"apollo-provider-1","protocol_min_version":1,"protocol_max_version":1,"connection_capacity":8,"bandwidth_capacity_kbps":100000,"features":["datagram.media"],"capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":["h264-opus"]}}` registration := `{"version":"1","gateway_id":"gateway-1","instance_identity":"instance-1","certificate_identity":"cert-1","public_identity":"public-1","address":"gateway.test:443","provider_identity":"apollo-provider-1","protocol_min_version":1,"protocol_max_version":1,"connection_capacity":8,"bandwidth_capacity_kbps":100000,"features":["datagram.media"],"capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","source_rate_control":"server","video_profiles":[{"codec":"h264","bit_depth":8,"chroma_subsampling":"4:2:0","color_space":"bt709-limited","transfer_function":"sdr"}],"audio_profiles":[{"codec":"opus","sample_rate_hz":48000,"channels":2,"channel_layout":"stereo","packet_duration_ms":5}]}}`
if _, err := protocol.DecodeGatewayRegistration([]byte(registration)); err != nil { if _, err := protocol.DecodeGatewayRegistration([]byte(registration)); err != nil {
t.Fatalf("valid gateway registration rejected: %v", err) t.Fatalf("valid gateway registration rejected: %v", err)
} }
@@ -244,7 +263,7 @@ func TestGatewayContractsRejectUnknownVersionsAndFields(t *testing.T) {
} }
func TestGatewayRegistrationRejectsInvertedProtocolBounds(t *testing.T) { func TestGatewayRegistrationRejectsInvertedProtocolBounds(t *testing.T) {
registration := `{"version":"1","gateway_id":"gateway-1","instance_identity":"instance-1","certificate_identity":"cert-1","public_identity":"public-1","address":"gateway.test:443","provider_identity":"apollo-provider-1","protocol_min_version":2,"protocol_max_version":1,"connection_capacity":8,"bandwidth_capacity_kbps":100000,"features":["datagram.media"],"capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":["h264-opus"]}}` registration := `{"version":"1","gateway_id":"gateway-1","instance_identity":"instance-1","certificate_identity":"cert-1","public_identity":"public-1","address":"gateway.test:443","provider_identity":"apollo-provider-1","protocol_min_version":2,"protocol_max_version":1,"connection_capacity":8,"bandwidth_capacity_kbps":100000,"features":["datagram.media"],"capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","source_rate_control":"server","video_profiles":[{"codec":"h264","bit_depth":8,"chroma_subsampling":"4:2:0","color_space":"bt709-limited","transfer_function":"sdr"}],"audio_profiles":[{"codec":"opus","sample_rate_hz":48000,"channels":2,"channel_layout":"stereo","packet_duration_ms":5}]}}`
if _, err := protocol.DecodeGatewayRegistration([]byte(registration)); err == nil { if _, err := protocol.DecodeGatewayRegistration([]byte(registration)); err == nil {
t.Fatal("DecodeGatewayRegistration accepted inverted protocol bounds") t.Fatal("DecodeGatewayRegistration accepted inverted protocol bounds")
} }
@@ -267,12 +286,12 @@ func TestGatewayHeartbeatCarriesBoundedObservedTelemetry(t *testing.T) {
} }
func TestCapabilityIntersectionRejectsNoOverlap(t *testing.T) { func TestCapabilityIntersectionRejectsNoOverlap(t *testing.T) {
first := protocol.CapabilityProfile{Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded", SourceRateControl: "server", ClientDecode: []string{"h264-opus"}} first := rc6Capability("h264")
if got, err := protocol.IntersectCapabilityProfiles(first, first); err != nil || !reflect.DeepEqual(got, first) { if got, err := protocol.IntersectCapabilityProfiles(first, first); err != nil || !reflect.DeepEqual(got, first) {
t.Fatalf("IntersectCapabilityProfiles matching profiles = %+v, %v", got, err) t.Fatalf("IntersectCapabilityProfiles matching profiles = %+v, %v", got, err)
} }
second := first second := first
second.ClientDecode = []string{"hevc-opus"} second.VideoProfiles = []protocol.VideoProfile{rc6Video("hevc")}
if _, err := protocol.IntersectCapabilityProfiles(first, second); err == nil { if _, err := protocol.IntersectCapabilityProfiles(first, second); err == nil {
t.Fatal("IntersectCapabilityProfiles accepted profiles without a common codec profile") t.Fatal("IntersectCapabilityProfiles accepted profiles without a common codec profile")
} }
@@ -280,31 +299,31 @@ func TestCapabilityIntersectionRejectsNoOverlap(t *testing.T) {
func TestCapabilityIntersectionSelectsRegisteredOrderedProfiles(t *testing.T) { func TestCapabilityIntersectionSelectsRegisteredOrderedProfiles(t *testing.T) {
gateway := protocol.CapabilityProfile{ gateway := protocol.CapabilityProfile{
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded", Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded",
SourceRateControl: "server", ClientDecode: []string{"hevc-opus", "h264-opus"}, SourceRateControl: "server", VideoProfiles: []protocol.VideoProfile{rc6Video("hevc"), rc6Video("h264")}, AudioProfiles: []protocol.AudioProfile{rc6Audio()},
} }
h264Client := gateway h264Client := gateway
h264Client.ClientDecode = []string{"h264-opus"} h264Client.VideoProfiles = []protocol.VideoProfile{rc6Video("h264")}
selected, err := protocol.IntersectCapabilityProfiles(gateway, h264Client) selected, err := protocol.IntersectCapabilityProfiles(gateway, h264Client)
if err != nil || !reflect.DeepEqual(selected.ClientDecode, []string{"h264-opus"}) { if err != nil || !reflect.DeepEqual(selected.VideoProfiles, []protocol.VideoProfile{rc6Video("h264")}) {
t.Fatalf("H.264 profile intersection = %+v, %v", selected, err) t.Fatalf("H.264 profile intersection = %+v, %v", selected, err)
} }
hevcClient := gateway hevcClient := gateway
hevcClient.ClientDecode = []string{"hevc-opus"} hevcClient.VideoProfiles = []protocol.VideoProfile{rc6Video("hevc")}
selected, err = protocol.IntersectCapabilityProfiles(gateway, hevcClient) selected, err = protocol.IntersectCapabilityProfiles(gateway, hevcClient)
if err != nil || !reflect.DeepEqual(selected.ClientDecode, []string{"hevc-opus"}) { if err != nil || !reflect.DeepEqual(selected.VideoProfiles, []protocol.VideoProfile{rc6Video("hevc")}) {
t.Fatalf("HEVC profile intersection = %+v, %v", selected, err) t.Fatalf("HEVC profile intersection = %+v, %v", selected, err)
} }
noOverlap := gateway noOverlap := gateway
noOverlap.ClientDecode = []string{"h264-opus"} noOverlap.VideoProfiles = []protocol.VideoProfile{rc6Video("h264")}
if _, err := protocol.IntersectCapabilityProfiles(noOverlap, hevcClient); err == nil { if _, err := protocol.IntersectCapabilityProfiles(noOverlap, hevcClient); err == nil {
t.Fatal("intersection accepted registered profiles without overlap") t.Fatal("intersection accepted registered profiles without overlap")
} }
for _, invalid := range [][]string{{"h264-hevc-opus"}, {"h264-opus", "h264-opus"}} { for _, invalid := range [][]protocol.VideoProfile{{{Codec: "vp9", BitDepth: 8, ChromaSubsampling: "4:2:0", ColorSpace: "bt709-limited", TransferFunction: "sdr"}}, {rc6Video("h264"), rc6Video("h264")}} {
profile := gateway profile := gateway
profile.ClientDecode = invalid profile.VideoProfiles = invalid
if err := profile.Validate(); err == nil { if err := profile.Validate(); err == nil {
t.Fatalf("CapabilityProfile accepted invalid registered profile set %q", invalid) t.Fatalf("CapabilityProfile accepted invalid registered profile set %+v", invalid)
} }
} }
} }
@@ -312,7 +331,7 @@ func TestCapabilityIntersectionSelectsRegisteredOrderedProfiles(t *testing.T) {
func TestCapabilityProfileRequiresRegisteredFraming(t *testing.T) { func TestCapabilityProfileRequiresRegisteredFraming(t *testing.T) {
valid := protocol.CapabilityProfile{ valid := protocol.CapabilityProfile{
Transport: "quic-tls13", Framing: "datagram-v2", Media: "encoded", Transport: "quic-tls13", Framing: "datagram-v2", Media: "encoded",
Audio: "encoded", SourceRateControl: "server", ClientDecode: []string{"h264-opus"}, SourceRateControl: "server", VideoProfiles: []protocol.VideoProfile{rc6Video("h264")}, AudioProfiles: []protocol.AudioProfile{rc6Audio()},
} }
if err := valid.Validate(); err != nil { if err := valid.Validate(); err != nil {
t.Fatalf("registered datagram-v2 rejected: %v", err) t.Fatalf("registered datagram-v2 rejected: %v", err)
@@ -329,8 +348,8 @@ func TestTunnelAdmissionRequiresDeviceSignature(t *testing.T) {
Version: "1", SessionID: "session-1", GatewayID: "gateway-1", Audience: "versevdi-gateway", Version: "1", SessionID: "session-1", GatewayID: "gateway-1", Audience: "versevdi-gateway",
Grant: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_", ReconnectSequence: 0, Grant: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_", ReconnectSequence: 0,
ClientNonce: "0123456789abcdef", Capabilities: protocol.CapabilityProfile{ ClientNonce: "0123456789abcdef", Capabilities: protocol.CapabilityProfile{
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded", Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded",
SourceRateControl: "server", ClientDecode: []string{"h264-opus"}, SourceRateControl: "server", VideoProfiles: []protocol.VideoProfile{rc6Video("h264")}, AudioProfiles: []protocol.AudioProfile{rc6Audio()},
}, },
} }
if _, err := protocol.EncodeTunnelAdmissionRequest(request); err == nil { if _, err := protocol.EncodeTunnelAdmissionRequest(request); err == nil {
@@ -343,18 +362,18 @@ func TestTunnelAdmissionTranscriptIsDomainSeparatedAndLengthDelimited(t *testing
Version: "1", SessionID: "session", GatewayID: "gateway", Audience: "audience", Version: "1", SessionID: "session", GatewayID: "gateway", Audience: "audience",
Grant: strings.Repeat("g", 43), ReconnectSequence: 0, ClientNonce: strings.Repeat("n", 16), Grant: strings.Repeat("g", 43), ReconnectSequence: 0, ClientNonce: strings.Repeat("n", 16),
DeviceSignature: strings.Repeat("s", 86), Capabilities: protocol.CapabilityProfile{ DeviceSignature: strings.Repeat("s", 86), Capabilities: protocol.CapabilityProfile{
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded", Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded",
SourceRateControl: "server", ClientDecode: []string{"h264-opus"}, SourceRateControl: "server", VideoProfiles: []protocol.VideoProfile{rc6Video("h264")}, AudioProfiles: []protocol.AudioProfile{rc6Audio()},
}, },
} }
want := "versevdi/tunnel-admission/v17:session7:gateway8:audience43:" + strings.Repeat("g", 43) + "1:016:" + strings.Repeat("n", 16) + "10:quic-tls1311:datagram-v17:encoded7:encoded6:server1:19:h264-opus" want := "versevdi/tunnel-admission/v17:session7:gateway8:audience43:" + strings.Repeat("g", 43) + "1:016:" + strings.Repeat("n", 16) + "10:quic-tls1311:datagram-v17:encoded6:server1:11:14:h2641:85:4:2:013:bt709-limited3:sdr4:opus5:480001:26:stereo1:5"
if got := string(request.DeviceAdmissionTranscript()); got != want { if got := string(request.DeviceAdmissionTranscript()); got != want {
t.Fatalf("DeviceAdmissionTranscript() = %q, want %q", got, want) t.Fatalf("DeviceAdmissionTranscript() = %q, want %q", got, want)
} }
} }
func TestSessionAuthorityRejectsProviderRoute(t *testing.T) { func TestSessionAuthorityRejectsProviderRoute(t *testing.T) {
valid := `{"version":"1","session_id":"session-1","gateway_id":"gateway-1","audience":"versevdi-gateway","reconnect_sequence":0,"expires_at":"2099-01-01T00:00:00Z","capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":["h264-opus"]},"provider_profile":"apollo","provider_identity":"provider-1"}` valid := `{"version":"1","session_id":"session-1","gateway_id":"gateway-1","audience":"versevdi-gateway","reconnect_sequence":0,"expires_at":"2099-01-01T00:00:00Z","capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","source_rate_control":"server","video_profiles":[{"codec":"h264","bit_depth":8,"chroma_subsampling":"4:2:0","color_space":"bt709-limited","transfer_function":"sdr"}],"audio_profiles":[{"codec":"opus","sample_rate_hz":48000,"channels":2,"channel_layout":"stereo","packet_duration_ms":5}]},"provider_profile":"apollo","provider_identity":"provider-1","selected_descriptor":{"video_profile":{"codec":"h264","bit_depth":8,"chroma_subsampling":"4:2:0","color_space":"bt709-limited","transfer_function":"sdr"},"audio_profile":{"codec":"opus","sample_rate_hz":48000,"channels":2,"channel_layout":"stereo","packet_duration_ms":5},"display_mode":{"resolution_width":1920,"resolution_height":1080,"fps":60},"bitrate_target_kbps":20000,"bitrate_maximum_kbps":40000,"adjustment":{"display_reason":"none","bitrate_reason":"none"},"media_timestamp_basis":"gateway-send-wall-clock-ms"}}`
if _, err := protocol.DecodeSessionAuthority([]byte(valid)); err != nil { if _, err := protocol.DecodeSessionAuthority([]byte(valid)); err != nil {
t.Fatalf("valid session authority rejected: %v", err) t.Fatalf("valid session authority rejected: %v", err)
} }
@@ -367,9 +386,10 @@ func TestClientSessionAuthorityIsStrictAndProviderFree(t *testing.T) {
authority := protocol.ClientSessionAuthority{ authority := protocol.ClientSessionAuthority{
Version: "1", SessionID: "session-1", GatewayID: "gateway-1", Audience: "versevdi-gateway", Version: "1", SessionID: "session-1", GatewayID: "gateway-1", Audience: "versevdi-gateway",
ReconnectSequence: 2, ExpiresAt: "2099-01-01T00:00:00Z", Capabilities: protocol.CapabilityProfile{ ReconnectSequence: 2, ExpiresAt: "2099-01-01T00:00:00Z", Capabilities: protocol.CapabilityProfile{
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded", Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded",
SourceRateControl: "server", ClientDecode: []string{"h264-opus"}, SourceRateControl: "server", VideoProfiles: []protocol.VideoProfile{rc6Video("h264")}, AudioProfiles: []protocol.AudioProfile{rc6Audio()},
}, },
SelectedDescriptor: rc6Descriptor("h264"),
} }
encoded, err := protocol.EncodeClientSessionAuthority(authority) encoded, err := protocol.EncodeClientSessionAuthority(authority)
if err != nil { if err != nil {
@@ -381,7 +401,7 @@ func TestClientSessionAuthorityIsStrictAndProviderFree(t *testing.T) {
} }
wantFields := map[string]bool{ wantFields := map[string]bool{
"version": true, "session_id": true, "gateway_id": true, "audience": true, "version": true, "session_id": true, "gateway_id": true, "audience": true,
"reconnect_sequence": true, "expires_at": true, "capabilities": true, "reconnect_sequence": true, "expires_at": true, "capabilities": true, "selected_descriptor": true,
} }
if len(fields) != len(wantFields) { if len(fields) != len(wantFields) {
t.Fatalf("encoded client authority fields = %v; want exactly %v", fields, wantFields) t.Fatalf("encoded client authority fields = %v; want exactly %v", fields, wantFields)
@@ -399,7 +419,7 @@ func TestClientSessionAuthorityIsStrictAndProviderFree(t *testing.T) {
t.Fatalf("DecodeClientSessionAuthority() = %+v, %v; want %+v", decoded, err, authority) t.Fatalf("DecodeClientSessionAuthority() = %+v, %v; want %+v", decoded, err, authority)
} }
for _, field := range []string{"version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities"} { for _, field := range []string{"version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities", "selected_descriptor"} {
missing := make(map[string]json.RawMessage, len(fields)-1) missing := make(map[string]json.RawMessage, len(fields)-1)
for key, value := range fields { for key, value := range fields {
if key != field { if key != field {
@@ -439,7 +459,7 @@ func TestClientSessionAuthorityIsStrictAndProviderFree(t *testing.T) {
} }
func TestProviderSessionWorkIsStrictAndSessionBound(t *testing.T) { func TestProviderSessionWorkIsStrictAndSessionBound(t *testing.T) {
valid := `{"version":"1","session_id":"session-1","gateway_id":"gateway-1","reconnect_sequence":0,"expires_at":"2099-01-01T00:00:00Z","provider_profile":"apollo","provider_identity":"provider-1","policy_version_id":"policy-1","stream_policy":{"resolution_width":2560,"resolution_height":1440,"fps":120,"codec":"HEVC","bitrate_kbps":40000,"audio_enabled":true},"application_id":"42","client_id":"paired-client-1","management_host":"apollo.test","management_port":47990,"stream_host":"apollo.test","stream_port":47984,"client_certificate_pem":"certificate","client_private_key_pem":"private-key","server_certificate_pem":"server-certificate","clipboard_policy":{"client_to_provider_enabled":false,"provider_to_client_enabled":false,"max_text_bytes":65536,"max_updates_per_minute":30},"provider_application_termination_allowed":false}` valid := `{"version":"1","session_id":"session-1","gateway_id":"gateway-1","reconnect_sequence":0,"expires_at":"2099-01-01T00:00:00Z","provider_profile":"apollo","provider_identity":"provider-1","policy_version_id":"policy-1","stream_policy":{"video_profile":{"codec":"hevc","bit_depth":8,"chroma_subsampling":"4:2:0","color_space":"bt709-limited","transfer_function":"sdr"},"audio_profile":{"codec":"opus","sample_rate_hz":48000,"channels":2,"channel_layout":"stereo","packet_duration_ms":5},"display_mode":{"resolution_width":2560,"resolution_height":1440,"fps":120},"bitrate_target_kbps":40000,"bitrate_maximum_kbps":50000},"application_id":"42","client_id":"paired-client-1","management_host":"apollo.test","management_port":47990,"stream_host":"apollo.test","stream_port":47984,"client_certificate_pem":"certificate","client_private_key_pem":"private-key","server_certificate_pem":"server-certificate","clipboard_policy":{"client_to_provider_enabled":false,"provider_to_client_enabled":false,"max_text_bytes":65536,"max_updates_per_minute":30},"provider_application_termination_allowed":false}`
if _, err := protocol.DecodeProviderSessionWork([]byte(valid)); err != nil { if _, err := protocol.DecodeProviderSessionWork([]byte(valid)); err != nil {
t.Fatalf("valid provider work rejected: %v", err) t.Fatalf("valid provider work rejected: %v", err)
} }
@@ -450,10 +470,10 @@ func TestProviderSessionWorkIsStrictAndSessionBound(t *testing.T) {
t.Fatal("provider work accepted missing clipboard policy") t.Fatal("provider work accepted missing clipboard policy")
} }
for _, invalid := range []string{ for _, invalid := range []string{
strings.Replace(valid, `,"stream_policy":{"resolution_width":2560,"resolution_height":1440,"fps":120,"codec":"HEVC","bitrate_kbps":40000,"audio_enabled":true}`, "", 1), strings.Replace(valid, `,"stream_policy":{"video_profile":{"codec":"hevc","bit_depth":8,"chroma_subsampling":"4:2:0","color_space":"bt709-limited","transfer_function":"sdr"},"audio_profile":{"codec":"opus","sample_rate_hz":48000,"channels":2,"channel_layout":"stereo","packet_duration_ms":5},"display_mode":{"resolution_width":2560,"resolution_height":1440,"fps":120},"bitrate_target_kbps":40000,"bitrate_maximum_kbps":50000}`, "", 1),
strings.Replace(valid, `"fps":120`, `"fps":241`, 1), strings.Replace(valid, `"fps":120`, `"fps":241`, 1),
strings.Replace(valid, `"codec":"HEVC"`, `"codec":"VP9"`, 1), strings.Replace(valid, `"codec":"hevc"`, `"codec":"vp9"`, 1),
strings.Replace(valid, `"audio_enabled":true`, `"audio_enabled":true,"unknown":false`, 1), strings.Replace(valid, `"packet_duration_ms":5`, `"packet_duration_ms":5,"unknown":false`, 1),
} { } {
if _, err := protocol.DecodeProviderSessionWork([]byte(invalid)); err == nil { if _, err := protocol.DecodeProviderSessionWork([]byte(invalid)); err == nil {
t.Fatalf("provider work accepted invalid stream policy: %s", invalid) t.Fatalf("provider work accepted invalid stream policy: %s", invalid)
+260
View File
@@ -0,0 +1,260 @@
package protocol_test
import (
"os"
"reflect"
"strings"
"testing"
protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol"
)
func TestRC6GeneratedContractsHaveFrozenExactShapes(t *testing.T) {
tests := []struct {
value any
fields []string
}{
{protocol.VideoProfile{}, []string{"Codec", "BitDepth", "ChromaSubsampling", "ColorSpace", "TransferFunction"}},
{protocol.AudioProfile{}, []string{"Codec", "SampleRateHz", "Channels", "ChannelLayout", "PacketDurationMs"}},
{protocol.BitratePreference{}, []string{"Mode", "TargetKbps"}},
{protocol.SessionQualityLimits{}, []string{"PolicyVersionID", "PolicyDisplayLimit", "SelectableDisplayMaximum", "DisplayLimitOverride", "BitrateMinimumKbps", "BitrateTargetKbps", "BitrateMaximumKbps"}},
{protocol.SessionAdjustment{}, []string{"DisplayReason", "BitrateReason"}},
{protocol.SelectedSessionDescriptor{}, []string{"VideoProfile", "AudioProfile", "DisplayMode", "BitrateTargetKbps", "BitrateMaximumKbps", "Adjustment", "MediaTimestampBasis"}},
}
for _, test := range tests {
typeOf := reflect.TypeOf(test.value)
if typeOf.NumField() != len(test.fields) {
t.Fatalf("%s field count = %d, want %d", typeOf.Name(), typeOf.NumField(), len(test.fields))
}
for index, field := range test.fields {
if typeOf.Field(index).Name != field {
t.Fatalf("%s field %d = %s, want %s", typeOf.Name(), index, typeOf.Field(index).Name, field)
}
}
}
}
func TestRC6ProfileValidationAndOrderedIntersection(t *testing.T) {
h264 := protocol.VideoProfile{Codec: "h264", BitDepth: 8, ChromaSubsampling: "4:2:0", ColorSpace: "bt709-limited", TransferFunction: "sdr"}
hevc := h264
hevc.Codec = "hevc"
audio := protocol.AudioProfile{Codec: "opus", SampleRateHz: 48000, Channels: 2, ChannelLayout: "stereo", PacketDurationMs: 5}
first := protocol.CapabilityProfile{Transport: "quic-tls13", Framing: "datagram-v2", Media: "encoded", SourceRateControl: "server", VideoProfiles: []protocol.VideoProfile{h264, hevc}, AudioProfiles: []protocol.AudioProfile{audio}}
second := first
second.VideoProfiles = []protocol.VideoProfile{hevc, h264}
selected, err := protocol.IntersectCapabilityProfiles(first, second)
if err != nil || !reflect.DeepEqual(selected.VideoProfiles, []protocol.VideoProfile{h264, hevc}) {
t.Fatalf("ordered intersection = %+v, %v", selected.VideoProfiles, err)
}
duplicate := first
duplicate.VideoProfiles = []protocol.VideoProfile{h264, h264}
if duplicate.Validate() == nil {
t.Fatal("CapabilityProfile accepted duplicate video profiles")
}
invalid := h264
invalid.BitDepth = 10
if invalid.Validate() == nil {
t.Fatal("VideoProfile accepted a non-RC6 bit depth")
}
}
func TestRC6StrictDecodeRejectsLegacyAndMalformedBodies(t *testing.T) {
for name, test := range map[string]struct {
payload []byte
decode func([]byte) error
}{
"opaque capability": {[]byte(`{"transport":"quic-tls13","framing":"datagram-v2","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":["h264-opus"]}`), func(raw []byte) error { _, err := protocol.DecodeCapabilityProfile(raw); return err }},
"old session request": {[]byte(`{"client_device_id":"device-1","device_key_id":"key-1","pool_id":"pool-1","idempotency_key":"request-1"}`), func(raw []byte) error { _, err := protocol.DecodeSessionRequest(raw); return err }},
"old provider policy": {[]byte(`{"resolution_width":1920,"resolution_height":1080,"fps":60,"codec":"H264","bitrate_kbps":20000,"audio_enabled":true}`), func(raw []byte) error { _, err := protocol.DecodeProviderStreamPolicy(raw); return err }},
"unknown field": {[]byte(`{"mode":"explicit","target_kbps":1000,"unknown":true}`), func(raw []byte) error { _, err := protocol.DecodeBitratePreference(raw); return err }},
"duplicate key": {[]byte(`{"mode":"explicit","mode":"auto","target_kbps":1000}`), func(raw []byte) error { _, err := protocol.DecodeBitratePreference(raw); return err }},
"trailing JSON": {[]byte(`{"mode":"explicit","target_kbps":1000} {}`), func(raw []byte) error { _, err := protocol.DecodeBitratePreference(raw); return err }},
} {
if err := test.decode(test.payload); err == nil {
t.Fatalf("RC6 accepted %s", name)
}
}
}
func TestRC6BitratePreferenceTaggedBounds(t *testing.T) {
for _, valid := range []protocol.BitratePreference{{Mode: "auto"}, {Mode: "explicit", TargetKbps: int64Pointer(100)}, {Mode: "explicit", TargetKbps: int64Pointer(1_000_000)}} {
if err := valid.Validate(); err != nil {
t.Fatalf("valid preference %+v rejected: %v", valid, err)
}
}
for _, invalid := range []protocol.BitratePreference{{Mode: "auto", TargetKbps: int64Pointer(100)}, {Mode: "explicit"}, {Mode: "explicit", TargetKbps: int64Pointer(99)}, {Mode: "explicit", TargetKbps: int64Pointer(1_000_001)}} {
if invalid.Validate() == nil {
t.Fatalf("invalid preference %+v accepted", invalid)
}
}
}
func TestRC6GatewayQualityDiscoveryAndLeaseFencing(t *testing.T) {
operationID := "12345678-1234-1234-1234-123456789abc"
revision := int64(7)
leaseGeneration := int64(3)
currentRevision := int64(6)
poll := protocol.GatewayQualityWorkRequest{Version: "1", SessionID: "session-1", GatewayID: "gateway-1", ReconnectSequence: 2, Acquisition: "poll"}
if err := poll.Validate(); err != nil {
t.Fatalf("session-bound lost-prompt poll rejected: %v", err)
}
prompt := poll
prompt.Acquisition = "prompt"
prompt.OperationID = operationID
prompt.Revision = &revision
if err := prompt.Validate(); err != nil {
t.Fatalf("prompt-bound acquisition rejected: %v", err)
}
observation := prompt
observation.Acquisition = "observation"
observation.LeaseGeneration = &leaseGeneration
observation.CurrentAppliedRevision = &currentRevision
if err := observation.Validate(); err != nil {
t.Fatalf("lease-bound applied-revision observation rejected: %v", err)
}
for name, invalid := range map[string]protocol.GatewayQualityWorkRequest{
"poll with unknown coordinates": prompt,
"prompt missing revision": {Version: "1", SessionID: "session-1", GatewayID: "gateway-1", Acquisition: "prompt", OperationID: operationID},
"observation missing lease": {Version: "1", SessionID: "session-1", GatewayID: "gateway-1", Acquisition: "observation", OperationID: operationID, Revision: &revision, CurrentAppliedRevision: &currentRevision},
} {
if name == "poll with unknown coordinates" {
invalid.Acquisition = "poll"
}
if invalid.Validate() == nil {
t.Fatalf("accepted invalid %s: %+v", name, invalid)
}
}
stopPoll := protocol.GatewayStopWorkRequest{Version: "1", SessionID: "session-1", GatewayID: "gateway-1", ReconnectSequence: 2, Acquisition: "poll"}
if err := stopPoll.Validate(); err != nil {
t.Fatalf("session-bound Stop poll rejected: %v", err)
}
stopPrompt := stopPoll
stopPrompt.Acquisition = "prompt"
stopPrompt.OperationID = operationID
if err := stopPrompt.Validate(); err != nil {
t.Fatalf("prompt-bound Stop acquisition rejected: %v", err)
}
stopPoll.OperationID = operationID
if stopPoll.Validate() == nil {
t.Fatal("Stop poll accepted unknown operation coordinates")
}
}
func TestRC6GatewayQualityAckHasUnambiguousObservation(t *testing.T) {
revision := int64(7)
prior := int64(6)
base := protocol.GatewayQualityAck{
Version: "1", SessionID: "session-1", GatewayID: "gateway-1", ReconnectSequence: 2,
OperationID: "12345678-1234-1234-1234-123456789abc", Revision: revision, LeaseGeneration: 3,
}
for _, valid := range []protocol.GatewayQualityAck{
func() protocol.GatewayQualityAck {
value := base
value.Outcome = "applied"
value.CurrentAppliedRevision = &revision
return value
}(),
func() protocol.GatewayQualityAck {
value := base
value.Outcome = "proven_prior"
value.CurrentAppliedRevision = &prior
return value
}(),
func() protocol.GatewayQualityAck { value := base; value.Outcome = "unknown"; return value }(),
} {
if err := valid.Validate(); err != nil {
t.Fatalf("valid quality acknowledgement rejected: %+v: %v", valid, err)
}
}
for _, invalid := range []protocol.GatewayQualityAck{
func() protocol.GatewayQualityAck {
value := base
value.Outcome = "applied"
value.CurrentAppliedRevision = &prior
return value
}(),
func() protocol.GatewayQualityAck {
value := base
value.Outcome = "proven_prior"
value.CurrentAppliedRevision = &revision
return value
}(),
func() protocol.GatewayQualityAck {
value := base
value.Outcome = "unknown"
value.CurrentAppliedRevision = &prior
return value
}(),
func() protocol.GatewayQualityAck {
value := base
value.Outcome = "unknown"
value.LeaseGeneration = 0
return value
}(),
} {
if invalid.Validate() == nil {
t.Fatalf("contradictory or unfenced quality acknowledgement accepted: %+v", invalid)
}
}
}
func TestRC6JSONFixturesAndCanonicalOperationFields(t *testing.T) {
validFixtures := map[string]func([]byte) error{
"../../fixtures/valid/session-request.json": func(raw []byte) error { _, err := protocol.DecodeSessionRequest(raw); return err },
"../../fixtures/valid/selected-session-descriptor.json": func(raw []byte) error { _, err := protocol.DecodeSelectedSessionDescriptor(raw); return err },
"../../fixtures/valid/session-quality-limits.json": func(raw []byte) error { _, err := protocol.DecodeSessionQualityLimits(raw); return err },
"../../fixtures/valid/gateway-quality-poll.json": func(raw []byte) error { _, err := protocol.DecodeGatewayQualityWorkRequest(raw); return err },
"../../fixtures/valid/gateway-quality-ack-applied.json": func(raw []byte) error { _, err := protocol.DecodeGatewayQualityAck(raw); return err },
}
for path, decode := range validFixtures {
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read valid fixture %s: %v", path, err)
}
if err := decode(raw); err != nil {
t.Fatalf("valid fixture %s rejected: %v", path, err)
}
}
invalidFixtures := map[string]func([]byte) error{
"../../fixtures/invalid/capability-rc5-opaque.json": func(raw []byte) error { _, err := protocol.DecodeCapabilityProfile(raw); return err },
"../../fixtures/invalid/session-request-rc5.json": func(raw []byte) error { _, err := protocol.DecodeSessionRequest(raw); return err },
"../../fixtures/invalid/provider-stream-policy-rc5.json": func(raw []byte) error { _, err := protocol.DecodeProviderStreamPolicy(raw); return err },
"../../fixtures/invalid/video-profile-unknown.json": func(raw []byte) error { _, err := protocol.DecodeVideoProfile(raw); return err },
"../../fixtures/invalid/bitrate-preference-auto-target.json": func(raw []byte) error { _, err := protocol.DecodeBitratePreference(raw); return err },
"../../fixtures/invalid/selected-session-descriptor-provider-field.json": func(raw []byte) error { _, err := protocol.DecodeSelectedSessionDescriptor(raw); return err },
"../../fixtures/invalid/gateway-quality-poll-coordinates.json": func(raw []byte) error { _, err := protocol.DecodeGatewayQualityWorkRequest(raw); return err },
"../../fixtures/invalid/gateway-quality-ack-contradictory.json": func(raw []byte) error { _, err := protocol.DecodeGatewayQualityAck(raw); return err },
"../../fixtures/invalid/gateway-quality-ack-uppercase-uuid.json": func(raw []byte) error { _, err := protocol.DecodeGatewayQualityAck(raw); return err },
"../../fixtures/invalid/gateway-quality-ack-zero-uuid.json": func(raw []byte) error { _, err := protocol.DecodeGatewayQualityAck(raw); return err },
"../../fixtures/invalid/quality-operation-offset-time.json": func(raw []byte) error { _, err := protocol.DecodeQualityChangeOperation(raw); return err },
"../../fixtures/invalid/quality-operation-noncanonical-fraction.json": func(raw []byte) error { _, err := protocol.DecodeQualityChangeOperation(raw); return err },
}
for path, decode := range invalidFixtures {
raw, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if decode(raw) == nil {
t.Fatalf("invalid fixture %s accepted", path)
}
}
validOperation := `{"operation_id":"12345678-1234-1234-1234-123456789abc","session_id":"session-1","revision":1,"state":"pending","requested_bitrate_preference":{"mode":"auto"},"effective_bitrate_kbps":20000,"governing_policy_version":"policy-1","session_version":1,"created_at":"2099-01-01T00:00:00Z","deadline_at":"2099-01-01T00:00:30Z","updated_at":"2099-01-01T00:00:00Z"}`
if _, err := protocol.DecodeQualityChangeOperation([]byte(validOperation)); err != nil {
t.Fatalf("valid quality operation rejected: %v", err)
}
for _, invalid := range []string{
strings.Replace(validOperation, "123456789abc", "123456789ABC", 1),
strings.Replace(validOperation, "12345678-1234-1234-1234-123456789abc", "00000000-0000-0000-0000-000000000000", 1),
strings.Replace(validOperation, "2099-01-01T00:00:00Z", "2099-01-01T00:00:00+00:00", 1),
strings.Replace(validOperation, `,"revision":1`, "", 1),
} {
if _, err := protocol.DecodeQualityChangeOperation([]byte(invalid)); err == nil {
t.Fatalf("invalid operation accepted: %s", invalid)
}
}
}
func int64Pointer(value int64) *int64 { return &value }
+11 -1
View File
@@ -32,7 +32,17 @@ def main() -> int:
expected = manifest.get("corpus_sha256") expected = manifest.get("corpus_sha256")
if not expected or actual != expected: if not expected or actual != expected:
raise ValueError(f"fixture corpus hash mismatch: {actual}") raise ValueError(f"fixture corpus hash mismatch: {actual}")
print(f"Fixture corpus SHA256 {actual}") json_paths = sorted(
path.relative_to(ROOT).as_posix()
for directory in (ROOT / "fixtures/valid", ROOT / "fixtures/invalid")
for path in directory.glob("*.json")
)
if json_paths != manifest.get("json_files"):
raise ValueError("JSON fixture manifest file list is stale")
json_actual = digest(json_paths)
if json_actual != manifest.get("json_corpus_sha256"):
raise ValueError(f"JSON fixture corpus hash mismatch: {json_actual}")
print(f"Fixture corpus SHA256 {actual}; JSON SHA256 {json_actual}")
return 0 return 0
+200 -33
View File
@@ -139,11 +139,15 @@ def go_validation(definition: dict[str, Any]) -> list[str]:
) )
if prop.get("format") == "base64url": if prop.get("format") == "base64url":
lines.append(f"\tif v.{field} != \"\" {{ if _, err := base64.RawURLEncoding.Strict().DecodeString(v.{field}); err != nil {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_format\"}}) }} }}") lines.append(f"\tif v.{field} != \"\" {{ if _, err := base64.RawURLEncoding.Strict().DecodeString(v.{field}); err != nil {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_format\"}}) }} }}")
if prop.get("format") == "uuid":
lines.append(f"\tif v.{field} != \"\" && !validCanonicalUUID(v.{field}) {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_uuid\"}}) }}")
if prop.get("type") == "integer": if prop.get("type") == "integer":
value = f"*v.{field}" if prop.get("x-optional-pointer") else f"v.{field}"
guard = f"v.{field} != nil && " if prop.get("x-optional-pointer") else ""
if "minimum" in prop: if "minimum" in prop:
lines.append(f"\tif v.{field} != 0 && v.{field} < {prop['minimum']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"minimum\"}}) }}") lines.append(f"\tif {guard}{value} != 0 && {value} < {prop['minimum']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"minimum\"}}) }}")
if "maximum" in prop: if "maximum" in prop:
lines.append(f"\tif v.{field} > {prop['maximum']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"maximum\"}}) }}") lines.append(f"\tif {guard}{value} > {prop['maximum']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"maximum\"}}) }}")
if prop.get("type") == "array": if prop.get("type") == "array":
if "minItems" in prop: if "minItems" in prop:
lines.append(f"\tif len(v.{field}) < {prop['minItems']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"min_items\"}}) }}") lines.append(f"\tif len(v.{field}) < {prop['minItems']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"min_items\"}}) }}")
@@ -159,8 +163,8 @@ def go_validation(definition: dict[str, Any]) -> list[str]:
if "enum" in items: if "enum" in items:
allowed = " || ".join(f'item == "{value}"' for value in items["enum"]) allowed = " || ".join(f'item == "{value}"' for value in items["enum"])
lines.append(f"\tfor _, item := range v.{field} {{ if !({allowed}) {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_item\"}}) }} }}") lines.append(f"\tfor _, item := range v.{field} {{ if !({allowed}) {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_item\"}}) }} }}")
if prop.get("uniqueItems") and items.get("type") == "string": if prop.get("uniqueItems"):
lines.append(f"\tfor index, item := range v.{field} {{ for prior := 0; prior < index; prior++ {{ if item == v.{field}[prior] {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"duplicate_item\"}}) }} }} }}") lines.append(f"\tfor index, item := range v.{field} {{ for prior := 0; prior < index; prior++ {{ if reflect.DeepEqual(item, v.{field}[prior]) {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"duplicate_item\"}}) }} }} }}")
item_ref = ref_name(items) item_ref = ref_name(items)
if item_ref: 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\"}}) }} }}") 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\"}}) }} }}")
@@ -173,6 +177,22 @@ def go_validation(definition: dict[str, Any]) -> list[str]:
lines.append(f"\t{validation}") lines.append(f"\t{validation}")
if name in {"AllocationPolicy", "ManifestBounds"}: if name in {"AllocationPolicy", "ManifestBounds"}:
lines.append("\tif v.MinimumKbps > v.TargetKbps || v.TargetKbps > v.MaximumKbps { violations = append(violations, FieldViolation{Field: \"bounds\", Code: \"invalid_order\"}) }") lines.append("\tif v.MinimumKbps > v.TargetKbps || v.TargetKbps > v.MaximumKbps { violations = append(violations, FieldViolation{Field: \"bounds\", Code: \"invalid_order\"}) }")
if name == "SessionQualityLimits":
lines.append("\tif v.BitrateMinimumKbps > v.BitrateTargetKbps || v.BitrateTargetKbps > v.BitrateMaximumKbps { violations = append(violations, FieldViolation{Field: \"bitrate_bounds\", Code: \"invalid_order\"}) }")
if name in {"SelectedSessionDescriptor", "ProviderStreamPolicy"}:
lines.append("\tif v.BitrateTargetKbps > v.BitrateMaximumKbps { violations = append(violations, FieldViolation{Field: \"bitrate_bounds\", Code: \"invalid_order\"}) }")
if name == "BitratePreference":
lines.append("\tif v.Mode == \"auto\" && v.TargetKbps != nil || v.Mode == \"explicit\" && v.TargetKbps == nil { violations = append(violations, FieldViolation{Field: \"target_kbps\", Code: \"invalid_tagged_value\"}) }")
if name == "GatewayQualityWorkRequest":
lines.append("\tif v.Acquisition == \"poll\" && (v.OperationID != \"\" || v.Revision != nil || v.LeaseGeneration != nil || v.CurrentAppliedRevision != nil) { violations = append(violations, FieldViolation{Field: \"acquisition\", Code: \"invalid_tagged_value\"}) }")
lines.append("\tif v.Acquisition == \"prompt\" && (v.OperationID == \"\" || v.Revision == nil || v.LeaseGeneration != nil || v.CurrentAppliedRevision != nil) { violations = append(violations, FieldViolation{Field: \"acquisition\", Code: \"invalid_tagged_value\"}) }")
lines.append("\tif v.Acquisition == \"observation\" && (v.OperationID == \"\" || v.Revision == nil || v.LeaseGeneration == nil || v.CurrentAppliedRevision == nil) { violations = append(violations, FieldViolation{Field: \"acquisition\", Code: \"invalid_tagged_value\"}) }")
if name == "GatewayStopWorkRequest":
lines.append("\tif v.Acquisition == \"poll\" && v.OperationID != \"\" || v.Acquisition == \"prompt\" && v.OperationID == \"\" { violations = append(violations, FieldViolation{Field: \"acquisition\", Code: \"invalid_tagged_value\"}) }")
if name == "GatewayQualityAck":
lines.append("\tif v.Outcome == \"applied\" && (v.CurrentAppliedRevision == nil || *v.CurrentAppliedRevision != v.Revision) { violations = append(violations, FieldViolation{Field: \"current_applied_revision\", Code: \"invalid_tagged_value\"}) }")
lines.append("\tif v.Outcome == \"proven_prior\" && (v.CurrentAppliedRevision == nil || *v.CurrentAppliedRevision >= v.Revision) { violations = append(violations, FieldViolation{Field: \"current_applied_revision\", Code: \"invalid_tagged_value\"}) }")
lines.append("\tif v.Outcome == \"unknown\" && v.CurrentAppliedRevision != nil { violations = append(violations, FieldViolation{Field: \"current_applied_revision\", Code: \"invalid_tagged_value\"}) }")
if name == "GatewayRegistration": if name == "GatewayRegistration":
lines.append("\tif v.ProtocolMinVersion > v.ProtocolMaxVersion { violations = append(violations, FieldViolation{Field: \"protocol_version\", Code: \"invalid_order\"}) }") lines.append("\tif v.ProtocolMinVersion > v.ProtocolMaxVersion { violations = append(violations, FieldViolation{Field: \"protocol_version\", Code: \"invalid_order\"}) }")
if name == "ChannelFrame": if name == "ChannelFrame":
@@ -214,6 +234,30 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
"", "",
"func (e ValidationError) Error() string { return \"protocol validation failed\" }", "func (e ValidationError) Error() string { return \"protocol validation failed\" }",
"", "",
"func validCanonicalUUID(value string) bool {",
"\tif len(value) != 36 || value[8] != '-' || value[13] != '-' || value[18] != '-' || value[23] != '-' { return false }",
"\tfor index, char := range []byte(value) { if index == 8 || index == 13 || index == 18 || index == 23 { continue }; if !((char >= '0' && char <= '9') || (char >= 'a' && char <= 'f')) { return false } }",
"\treturn value != \"00000000-0000-0000-0000-000000000000\"",
"}",
"",
"func rejectDuplicateJSONKeys(data []byte) error {",
"\tdecoder := json.NewDecoder(bytes.NewReader(data))",
"\tvar scan func(json.Token) error",
"\tscan = func(token json.Token) error {",
"\t\tdelim, ok := token.(json.Delim); if !ok { return nil }",
"\t\tswitch delim {",
"\t\tcase '{':",
"\t\t\tseen := map[string]struct{}{}",
"\t\t\tfor decoder.More() { keyToken, err := decoder.Token(); if err != nil { return err }; key, ok := keyToken.(string); if !ok { return errors.New(\"invalid JSON object key\") }; if _, exists := seen[key]; exists { return errors.New(\"duplicate JSON object key\") }; seen[key] = struct{}{}; value, err := decoder.Token(); if err != nil { return err }; if err := scan(value); err != nil { return err } }",
"\t\t\t_, err := decoder.Token(); return err",
"\t\tcase '[':",
"\t\t\tfor decoder.More() { value, err := decoder.Token(); if err != nil { return err }; if err := scan(value); err != nil { return err } }; _, err := decoder.Token(); return err",
"\t\t}",
"\t\treturn nil",
"\t}",
"\ttoken, err := decoder.Token(); if err != nil { return err }; return scan(token)",
"}",
"",
] ]
for name in sorted(defs): for name in sorted(defs):
if name == "FieldViolation": if name == "FieldViolation":
@@ -224,7 +268,9 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
for prop_name, prop in definition.get("properties", {}).items(): for prop_name, prop in definition.get("properties", {}).items():
tag = prop_name + (",omitempty" if prop_name not in required else "") tag = prop_name + (",omitempty" if prop_name not in required else "")
typ = prop_type(prop, "go") typ = prop_type(prop, "go")
if prop_name not in required and ref_name(prop): if prop.get("x-optional-pointer"):
typ = "*" + typ
elif prop_name not in required and ref_name(prop):
typ = "*" + typ typ = "*" + typ
out.append(f"\t{go_field(prop_name)} {typ} `json:\"{tag}\"`") out.append(f"\t{go_field(prop_name)} {typ} `json:\"{tag}\"`")
out.extend(["}", ""]) out.extend(["}", ""])
@@ -239,6 +285,7 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
out.append(f"func Decode{name}(data []byte) ({name}, error) {{") out.append(f"func Decode{name}(data []byte) ({name}, error) {{")
out.append(f"\tvar value {name}") out.append(f"\tvar value {name}")
out.append("\tif len(data) > 1024*1024 { return value, errors.New(\"protocol payload exceeds limit\") }") out.append("\tif len(data) > 1024*1024 { return value, errors.New(\"protocol payload exceeds limit\") }")
out.append("\tif err := rejectDuplicateJSONKeys(data); err != nil { return value, err }")
out.append("\tvar fields map[string]json.RawMessage") out.append("\tvar fields map[string]json.RawMessage")
out.append("\tif err := json.Unmarshal(data, &fields); err != nil { return value, err }") out.append("\tif err := json.Unmarshal(data, &fields); err != nil { return value, err }")
required_fields = sorted(defs[name].get("required", [])) required_fields = sorted(defs[name].get("required", []))
@@ -295,23 +342,29 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
"\tif len(profiles) == 0 { return CapabilityProfile{}, ErrNoCapabilityOverlap }", "\tif len(profiles) == 0 { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
"\tselected := profiles[0]", "\tselected := profiles[0]",
"\tif err := selected.Validate(); err != nil { return CapabilityProfile{}, ErrNoCapabilityOverlap }", "\tif err := selected.Validate(); err != nil { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
"\tcommon := append([]string(nil), selected.ClientDecode...)", "\tcommonVideo := append([]VideoProfile(nil), selected.VideoProfiles...)",
"\tcommonAudio := append([]AudioProfile(nil), selected.AudioProfiles...)",
"\tfor _, profile := range profiles[1:] {", "\tfor _, profile := range profiles[1:] {",
"\t\tif err := profile.Validate(); err != nil || profile.Transport != selected.Transport || profile.Framing != selected.Framing || profile.Media != selected.Media || profile.Audio != selected.Audio || profile.SourceRateControl != selected.SourceRateControl { return CapabilityProfile{}, ErrNoCapabilityOverlap }", "\t\tif err := profile.Validate(); err != nil || profile.Transport != selected.Transport || profile.Framing != selected.Framing || profile.Media != selected.Media || profile.SourceRateControl != selected.SourceRateControl { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
"\t\tnext := common[:0]", "\t\tnextVideo := commonVideo[:0]",
"\t\tfor _, candidate := range common { for _, offered := range profile.ClientDecode { if candidate == offered { next = append(next, candidate); break } } }", "\t\tfor _, candidate := range commonVideo { for _, offered := range profile.VideoProfiles { if candidate == offered { nextVideo = append(nextVideo, candidate); break } } }",
"\t\tcommon = next", "\t\tcommonVideo = nextVideo",
"\t\tif len(common) == 0 { return CapabilityProfile{}, ErrNoCapabilityOverlap }", "\t\tnextAudio := commonAudio[:0]",
"\t\tfor _, candidate := range commonAudio { for _, offered := range profile.AudioProfiles { if candidate == offered { nextAudio = append(nextAudio, candidate); break } } }",
"\t\tcommonAudio = nextAudio",
"\t\tif len(commonVideo) == 0 || len(commonAudio) == 0 { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
"\t}", "\t}",
"\tselected.ClientDecode = common", "\tselected.VideoProfiles = commonVideo",
"\tselected.AudioProfiles = commonAudio",
"\treturn selected, nil", "\treturn selected, nil",
"}", "}",
"", "",
]) ])
out.extend([ out.extend([
"func (v TunnelAdmissionRequest) DeviceAdmissionTranscript() []byte {", "func (v TunnelAdmissionRequest) DeviceAdmissionTranscript() []byte {",
"\tfields := []string{v.SessionID, v.GatewayID, v.Audience, v.Grant, fmt.Sprintf(\"%d\", v.ReconnectSequence), v.ClientNonce, v.Capabilities.Transport, v.Capabilities.Framing, v.Capabilities.Media, v.Capabilities.Audio, v.Capabilities.SourceRateControl, fmt.Sprintf(\"%d\", len(v.Capabilities.ClientDecode))}", "\tfields := []string{v.SessionID, v.GatewayID, v.Audience, v.Grant, fmt.Sprintf(\"%d\", v.ReconnectSequence), v.ClientNonce, v.Capabilities.Transport, v.Capabilities.Framing, v.Capabilities.Media, v.Capabilities.SourceRateControl, fmt.Sprintf(\"%d\", len(v.Capabilities.VideoProfiles)), fmt.Sprintf(\"%d\", len(v.Capabilities.AudioProfiles))}",
"\tfields = append(fields, v.Capabilities.ClientDecode...)", "\tfor _, profile := range v.Capabilities.VideoProfiles { fields = append(fields, profile.Codec, fmt.Sprintf(\"%d\", profile.BitDepth), profile.ChromaSubsampling, profile.ColorSpace, profile.TransferFunction) }",
"\tfor _, profile := range v.Capabilities.AudioProfiles { fields = append(fields, profile.Codec, fmt.Sprintf(\"%d\", profile.SampleRateHz), fmt.Sprintf(\"%d\", profile.Channels), profile.ChannelLayout, fmt.Sprintf(\"%d\", profile.PacketDurationMs)) }",
"\tvar transcript strings.Builder", "\tvar transcript strings.Builder",
"\ttranscript.WriteString(\"versevdi/tunnel-admission/v1\")", "\ttranscript.WriteString(\"versevdi/tunnel-admission/v1\")",
"\tfor _, field := range fields { fmt.Fprintf(&transcript, \"%d:%s\", len(field), field) }", "\tfor _, field := range fields { fmt.Fprintf(&transcript, \"%d:%s\", len(field), field) }",
@@ -386,11 +439,14 @@ def rust_validation(definition: dict[str, Any]) -> list[str]:
lines.append(f" {prefix}if !valid_rfc3339_utc({value}.as_str()) {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_time\")); }}") lines.append(f" {prefix}if !valid_rfc3339_utc({value}.as_str()) {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_time\")); }}")
if prop.get("format") == "base64url": if prop.get("format") == "base64url":
lines.append(f" {prefix}if !valid_base64_url({value}.as_str()) {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_format\")); }}") lines.append(f" {prefix}if !valid_base64_url({value}.as_str()) {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_format\")); }}")
if prop.get("format") == "uuid":
lines.append(f" {prefix}if !valid_canonical_uuid({value}.as_str()) {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_uuid\")); }}")
if prop.get("type") == "integer": if prop.get("type") == "integer":
numeric = f"*{value}" if prop_name not in required else value
if "minimum" in prop: if "minimum" in prop:
lines.append(f" {prefix}if {value} < {prop['minimum']} {{ return Err(ValidationError::new(\"{prop_name}\", \"minimum\")); }}") lines.append(f" {prefix}if {numeric} < {prop['minimum']} {{ return Err(ValidationError::new(\"{prop_name}\", \"minimum\")); }}")
if "maximum" in prop: if "maximum" in prop:
lines.append(f" {prefix}if {value} > {prop['maximum']} {{ return Err(ValidationError::new(\"{prop_name}\", \"maximum\")); }}") lines.append(f" {prefix}if {numeric} > {prop['maximum']} {{ return Err(ValidationError::new(\"{prop_name}\", \"maximum\")); }}")
if prop.get("type") == "array": if prop.get("type") == "array":
if "minItems" in prop: if "minItems" in prop:
lines.append(f" {prefix}if {value}.len() < {prop['minItems']} {{ return Err(ValidationError::new(\"{prop_name}\", \"min_items\")); }}") lines.append(f" {prefix}if {value}.len() < {prop['minItems']} {{ return Err(ValidationError::new(\"{prop_name}\", \"min_items\")); }}")
@@ -406,7 +462,7 @@ def rust_validation(definition: dict[str, Any]) -> list[str]:
if "enum" in items: if "enum" in items:
allowed = " && ".join(f'item != \"{item}\"' for item in items["enum"]) allowed = " && ".join(f'item != \"{item}\"' for item in items["enum"])
lines.append(f" {prefix}for item in {value}.iter() {{ if {allowed} {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_item\")); }} }}") lines.append(f" {prefix}for item in {value}.iter() {{ if {allowed} {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_item\")); }} }}")
if prop.get("uniqueItems") and items.get("type") == "string": if prop.get("uniqueItems"):
lines.append(f" {prefix}for (index, item) in {value}.iter().enumerate() {{ if {value}[..index].contains(item) {{ return Err(ValidationError::new(\"{prop_name}\", \"duplicate_item\")); }} }}") lines.append(f" {prefix}for (index, item) in {value}.iter().enumerate() {{ if {value}[..index].contains(item) {{ return Err(ValidationError::new(\"{prop_name}\", \"duplicate_item\")); }} }}")
item_ref = ref_name(items) item_ref = ref_name(items)
if item_ref: if item_ref:
@@ -419,6 +475,22 @@ def rust_validation(definition: dict[str, Any]) -> list[str]:
name = definition["name"] name = definition["name"]
if name in {"AllocationPolicy", "ManifestBounds"}: if name in {"AllocationPolicy", "ManifestBounds"}:
lines.append(" if self.minimumKbps > self.targetKbps || self.targetKbps > self.maximumKbps { return Err(ValidationError::new(\"bounds\", \"invalid_order\")); }") lines.append(" if self.minimumKbps > self.targetKbps || self.targetKbps > self.maximumKbps { return Err(ValidationError::new(\"bounds\", \"invalid_order\")); }")
if name == "SessionQualityLimits":
lines.append(" if self.bitrateMinimumKbps > self.bitrateTargetKbps || self.bitrateTargetKbps > self.bitrateMaximumKbps { return Err(ValidationError::new(\"bitrate_bounds\", \"invalid_order\")); }")
if name in {"SelectedSessionDescriptor", "ProviderStreamPolicy"}:
lines.append(" if self.bitrateTargetKbps > self.bitrateMaximumKbps { return Err(ValidationError::new(\"bitrate_bounds\", \"invalid_order\")); }")
if name == "BitratePreference":
lines.append(" if self.mode == \"auto\" && self.targetKbps.is_some() || self.mode == \"explicit\" && self.targetKbps.is_none() { return Err(ValidationError::new(\"target_kbps\", \"invalid_tagged_value\")); }")
if name == "GatewayQualityWorkRequest":
lines.append(" if self.acquisition == \"poll\" && (self.operationId.is_some() || self.revision.is_some() || self.leaseGeneration.is_some() || self.currentAppliedRevision.is_some()) { return Err(ValidationError::new(\"acquisition\", \"invalid_tagged_value\")); }")
lines.append(" if self.acquisition == \"prompt\" && (self.operationId.is_none() || self.revision.is_none() || self.leaseGeneration.is_some() || self.currentAppliedRevision.is_some()) { return Err(ValidationError::new(\"acquisition\", \"invalid_tagged_value\")); }")
lines.append(" if self.acquisition == \"observation\" && (self.operationId.is_none() || self.revision.is_none() || self.leaseGeneration.is_none() || self.currentAppliedRevision.is_none()) { return Err(ValidationError::new(\"acquisition\", \"invalid_tagged_value\")); }")
if name == "GatewayStopWorkRequest":
lines.append(" if self.acquisition == \"poll\" && self.operationId.is_some() || self.acquisition == \"prompt\" && self.operationId.is_none() { return Err(ValidationError::new(\"acquisition\", \"invalid_tagged_value\")); }")
if name == "GatewayQualityAck":
lines.append(" if self.outcome == \"applied\" && self.currentAppliedRevision != Some(self.revision) { return Err(ValidationError::new(\"current_applied_revision\", \"invalid_tagged_value\")); }")
lines.append(" if self.outcome == \"proven_prior\" && self.currentAppliedRevision.map_or(true, |current| current >= self.revision) { return Err(ValidationError::new(\"current_applied_revision\", \"invalid_tagged_value\")); }")
lines.append(" if self.outcome == \"unknown\" && self.currentAppliedRevision.is_some() { return Err(ValidationError::new(\"current_applied_revision\", \"invalid_tagged_value\")); }")
if name == "GatewayRegistration": if name == "GatewayRegistration":
lines.append(" if self.protocolMinVersion > self.protocolMaxVersion { return Err(ValidationError::new(\"protocol_version\", \"invalid_order\")); }") lines.append(" if self.protocolMinVersion > self.protocolMaxVersion { return Err(ValidationError::new(\"protocol_version\", \"invalid_order\")); }")
if name == "ChannelFrame": if name == "ChannelFrame":
@@ -473,6 +545,10 @@ def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibili
" let fraction = &bytes[20..bytes.len() - 1];", " let fraction = &bytes[20..bytes.len() - 1];",
" bytes[19] == b'.' && !fraction.is_empty() && fraction.len() <= 9 && fraction.iter().all(u8::is_ascii_digit) && *fraction.last().unwrap() != b'0'", " bytes[19] == b'.' && !fraction.is_empty() && fraction.len() <= 9 && fraction.iter().all(u8::is_ascii_digit) && *fraction.last().unwrap() != b'0'",
"}", "}",
"fn valid_canonical_uuid(value: &str) -> bool {",
" let bytes = value.as_bytes();",
" bytes.len() == 36 && [8, 13, 18, 23].iter().all(|index| bytes[*index] == b'-') && bytes.iter().enumerate().all(|(index, byte)| [8, 13, 18, 23].contains(&index) || byte.is_ascii_digit() || (b'a'..=b'f').contains(byte)) && value != \"00000000-0000-0000-0000-000000000000\"",
"}",
"", "",
] ]
for name in sorted(defs): for name in sorted(defs):
@@ -515,9 +591,12 @@ def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibili
out.extend([ out.extend([
" pub fn device_admission_transcript(&self) -> Vec<u8> {", " pub fn device_admission_transcript(&self) -> Vec<u8> {",
" let reconnect_sequence = self.reconnectSequence.to_string();", " let reconnect_sequence = self.reconnectSequence.to_string();",
" let client_decode_count = self.capabilities.clientDecode.len().to_string();", " let video_count = self.capabilities.videoProfiles.len().to_string();",
" let mut fields = vec![self.sessionId.as_str(), self.gatewayId.as_str(), self.audience.as_str(), self.grant.as_str(), reconnect_sequence.as_str(), self.clientNonce.as_str(), self.capabilities.transport.as_str(), self.capabilities.framing.as_str(), self.capabilities.media.as_str(), self.capabilities.audio.as_str(), self.capabilities.sourceRateControl.as_str(), client_decode_count.as_str()];", " let audio_count = self.capabilities.audioProfiles.len().to_string();",
" fields.extend(self.capabilities.clientDecode.iter().map(String::as_str));", " let mut owned = vec![self.sessionId.clone(), self.gatewayId.clone(), self.audience.clone(), self.grant.clone(), reconnect_sequence, self.clientNonce.clone(), self.capabilities.transport.clone(), self.capabilities.framing.clone(), self.capabilities.media.clone(), self.capabilities.sourceRateControl.clone(), video_count, audio_count];",
" for profile in &self.capabilities.videoProfiles { owned.extend([profile.codec.clone(), profile.bitDepth.to_string(), profile.chromaSubsampling.clone(), profile.colorSpace.clone(), profile.transferFunction.clone()]); }",
" for profile in &self.capabilities.audioProfiles { owned.extend([profile.codec.clone(), profile.sampleRateHz.to_string(), profile.channels.to_string(), profile.channelLayout.clone(), profile.packetDurationMs.to_string()]); }",
" let fields: Vec<&str> = owned.iter().map(String::as_str).collect();",
" let mut transcript = String::from(\"versevdi/tunnel-admission/v1\");", " let mut transcript = String::from(\"versevdi/tunnel-admission/v1\");",
" for field in fields { transcript.push_str(&format!(\"{}:{}\", field.as_bytes().len(), field)); }", " for field in fields { transcript.push_str(&format!(\"{}:{}\", field.as_bytes().len(), field)); }",
" transcript.into_bytes()", " transcript.into_bytes()",
@@ -545,9 +624,10 @@ def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibili
" selected.validate().map_err(|_| ValidationError::new(\"capabilities\", \"no_overlap\"))?;", " selected.validate().map_err(|_| ValidationError::new(\"capabilities\", \"no_overlap\"))?;",
" for profile in &profiles[1..] {", " for profile in &profiles[1..] {",
" profile.validate().map_err(|_| ValidationError::new(\"capabilities\", \"no_overlap\"))?;", " profile.validate().map_err(|_| ValidationError::new(\"capabilities\", \"no_overlap\"))?;",
" if profile.transport != selected.transport || profile.framing != selected.framing || profile.media != selected.media || profile.audio != selected.audio || profile.sourceRateControl != selected.sourceRateControl { return Err(ValidationError::new(\"capabilities\", \"no_overlap\")); }", " if profile.transport != selected.transport || profile.framing != selected.framing || profile.media != selected.media || profile.sourceRateControl != selected.sourceRateControl { return Err(ValidationError::new(\"capabilities\", \"no_overlap\")); }",
" selected.clientDecode.retain(|candidate| profile.clientDecode.contains(candidate));", " selected.videoProfiles.retain(|candidate| profile.videoProfiles.contains(candidate));",
" if selected.clientDecode.is_empty() { return Err(ValidationError::new(\"capabilities\", \"no_overlap\")); }", " selected.audioProfiles.retain(|candidate| profile.audioProfiles.contains(candidate));",
" if selected.videoProfiles.is_empty() || selected.audioProfiles.is_empty() { return Err(ValidationError::new(\"capabilities\", \"no_overlap\")); }",
" }", " }",
" Ok(selected)", " Ok(selected)",
"}", "}",
@@ -586,6 +666,8 @@ def swift_validation(definition: dict[str, Any]) -> list[str]:
lines.append(f" {prefix}if !validRFC3339UTC({value}) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_time\") }}") lines.append(f" {prefix}if !validRFC3339UTC({value}) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_time\") }}")
if prop.get("format") == "base64url": if prop.get("format") == "base64url":
lines.append(f" {prefix}if !validBase64URL({value}) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_format\") }}") lines.append(f" {prefix}if !validBase64URL({value}) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_format\") }}")
if prop.get("format") == "uuid":
lines.append(f" {prefix}if !validCanonicalUUID({value}) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_uuid\") }}")
if prop.get("type") == "integer": if prop.get("type") == "integer":
if "minimum" in prop: if "minimum" in prop:
lines.append(f" {prefix}if {value} < {prop['minimum']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"minimum\") }}") lines.append(f" {prefix}if {value} < {prop['minimum']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"minimum\") }}")
@@ -606,8 +688,8 @@ def swift_validation(definition: dict[str, Any]) -> list[str]:
if "enum" in items: if "enum" in items:
allowed = ", ".join(f'\"{item}\"' for item in items["enum"]) allowed = ", ".join(f'\"{item}\"' for item in items["enum"])
lines.append(f" {prefix}for item in {value} where ![{allowed}].contains(item) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_item\") }}") lines.append(f" {prefix}for item in {value} where ![{allowed}].contains(item) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_item\") }}")
if prop.get("uniqueItems") and items.get("type") == "string": if prop.get("uniqueItems"):
lines.append(f" {prefix}if Set({value}).count != {value}.count {{ throw ContractValidationError(field: \"{prop_name}\", code: \"duplicate_item\") }}") lines.append(f" {prefix}for (index, item) in {value}.enumerated() where {value}[..<index].contains(item) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"duplicate_item\") }}")
item_ref = ref_name(items) item_ref = ref_name(items)
if item_ref: if item_ref:
lines.append(f" {prefix}for item in {value} {{ try item.validate() }}") lines.append(f" {prefix}for item in {value} {{ try item.validate() }}")
@@ -619,6 +701,22 @@ def swift_validation(definition: dict[str, Any]) -> list[str]:
name = definition["name"] name = definition["name"]
if name in {"AllocationPolicy", "ManifestBounds"}: if name in {"AllocationPolicy", "ManifestBounds"}:
lines.append(" if minimumKbps > targetKbps || targetKbps > maximumKbps { throw ContractValidationError(field: \"bounds\", code: \"invalid_order\") }") lines.append(" if minimumKbps > targetKbps || targetKbps > maximumKbps { throw ContractValidationError(field: \"bounds\", code: \"invalid_order\") }")
if name == "SessionQualityLimits":
lines.append(" if bitrateMinimumKbps > bitrateTargetKbps || bitrateTargetKbps > bitrateMaximumKbps { throw ContractValidationError(field: \"bitrate_bounds\", code: \"invalid_order\") }")
if name in {"SelectedSessionDescriptor", "ProviderStreamPolicy"}:
lines.append(" if bitrateTargetKbps > bitrateMaximumKbps { throw ContractValidationError(field: \"bitrate_bounds\", code: \"invalid_order\") }")
if name == "BitratePreference":
lines.append(" if mode == \"auto\" && targetKbps != nil || mode == \"explicit\" && targetKbps == nil { throw ContractValidationError(field: \"target_kbps\", code: \"invalid_tagged_value\") }")
if name == "GatewayQualityWorkRequest":
lines.append(" if acquisition == \"poll\" && (operationId != nil || revision != nil || leaseGeneration != nil || currentAppliedRevision != nil) { throw ContractValidationError(field: \"acquisition\", code: \"invalid_tagged_value\") }")
lines.append(" if acquisition == \"prompt\" && (operationId == nil || revision == nil || leaseGeneration != nil || currentAppliedRevision != nil) { throw ContractValidationError(field: \"acquisition\", code: \"invalid_tagged_value\") }")
lines.append(" if acquisition == \"observation\" && (operationId == nil || revision == nil || leaseGeneration == nil || currentAppliedRevision == nil) { throw ContractValidationError(field: \"acquisition\", code: \"invalid_tagged_value\") }")
if name == "GatewayStopWorkRequest":
lines.append(" if acquisition == \"poll\" && operationId != nil || acquisition == \"prompt\" && operationId == nil { throw ContractValidationError(field: \"acquisition\", code: \"invalid_tagged_value\") }")
if name == "GatewayQualityAck":
lines.append(" if outcome == \"applied\" && currentAppliedRevision != revision { throw ContractValidationError(field: \"current_applied_revision\", code: \"invalid_tagged_value\") }")
lines.append(" if outcome == \"proven_prior\" && (currentAppliedRevision == nil || currentAppliedRevision! >= revision) { throw ContractValidationError(field: \"current_applied_revision\", code: \"invalid_tagged_value\") }")
lines.append(" if outcome == \"unknown\" && currentAppliedRevision != nil { throw ContractValidationError(field: \"current_applied_revision\", code: \"invalid_tagged_value\") }")
if name == "GatewayRegistration": if name == "GatewayRegistration":
lines.append(" if protocolMinVersion > protocolMaxVersion { throw ContractValidationError(field: \"protocol_version\", code: \"invalid_order\") }") lines.append(" if protocolMinVersion > protocolMaxVersion { throw ContractValidationError(field: \"protocol_version\", code: \"invalid_order\") }")
if name == "ChannelFrame": if name == "ChannelFrame":
@@ -637,6 +735,67 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
f'public let nMinus2WireVersion = "{compatibility["n_minus_2"]}"', f'public let nMinus2WireVersion = "{compatibility["n_minus_2"]}"',
"public struct ContractValidationError: Error, Equatable { public let field: String; public let code: String }", "public struct ContractValidationError: Error, Equatable { public let field: String; public let code: String }",
"private struct AnyCodingKey: CodingKey { let stringValue: String; let intValue: Int?; init?(stringValue: String) { self.stringValue = stringValue; self.intValue = nil }; init?(intValue: Int) { self.stringValue = String(intValue); self.intValue = intValue } }", "private struct AnyCodingKey: CodingKey { let stringValue: String; let intValue: Int?; init?(stringValue: String) { self.stringValue = stringValue; self.intValue = nil }; init?(intValue: Int) { self.stringValue = String(intValue); self.intValue = intValue } }",
"private func rejectDuplicateJSONKeys(_ data: Data) throws {",
" guard data.count <= 1_048_576 else { throw ContractValidationError(field: \"json\", code: \"payload_too_large\") }",
" var index = 0",
" func skipWhitespace() { while index < data.count && [9, 10, 13, 32].contains(data[index]) { index += 1 } }",
" func parseString() throws -> String {",
" guard index < data.count, data[index] == 34 else { throw ContractValidationError(field: \"json\", code: \"invalid_json\") }",
" let start = index",
" index += 1",
" while index < data.count {",
" if data[index] == 92 { index += 2; continue }",
" if data[index] == 34 { index += 1; return try JSONDecoder().decode(String.self, from: data[start..<index]) }",
" index += 1",
" }",
" throw ContractValidationError(field: \"json\", code: \"invalid_json\")",
" }",
" func parseValue(_ depth: Int) throws {",
" guard depth <= 64 else { throw ContractValidationError(field: \"json\", code: \"nesting_too_deep\") }",
" skipWhitespace()",
" guard index < data.count else { throw ContractValidationError(field: \"json\", code: \"invalid_json\") }",
" if data[index] == 123 {",
" index += 1",
" var keys = Set<String>()",
" skipWhitespace()",
" if index < data.count, data[index] == 125 { index += 1; return }",
" while true {",
" skipWhitespace()",
" let key = try parseString()",
" guard keys.insert(key).inserted else { throw ContractValidationError(field: key, code: \"duplicate_field\") }",
" skipWhitespace()",
" guard index < data.count, data[index] == 58 else { throw ContractValidationError(field: \"json\", code: \"invalid_json\") }",
" index += 1",
" try parseValue(depth + 1)",
" skipWhitespace()",
" guard index < data.count else { throw ContractValidationError(field: \"json\", code: \"invalid_json\") }",
" if data[index] == 125 { index += 1; return }",
" guard data[index] == 44 else { throw ContractValidationError(field: \"json\", code: \"invalid_json\") }",
" index += 1",
" }",
" }",
" if data[index] == 91 {",
" index += 1",
" skipWhitespace()",
" if index < data.count, data[index] == 93 { index += 1; return }",
" while true {",
" try parseValue(depth + 1)",
" skipWhitespace()",
" guard index < data.count else { throw ContractValidationError(field: \"json\", code: \"invalid_json\") }",
" if data[index] == 93 { index += 1; return }",
" guard data[index] == 44 else { throw ContractValidationError(field: \"json\", code: \"invalid_json\") }",
" index += 1",
" }",
" }",
" if data[index] == 34 { _ = try parseString(); return }",
" let start = index",
" while index < data.count && ![9, 10, 13, 32, 44, 93, 125].contains(data[index]) { index += 1 }",
" guard index > start else { throw ContractValidationError(field: \"json\", code: \"invalid_json\") }",
" }",
" try parseValue(0)",
" skipWhitespace()",
" guard index == data.count else { throw ContractValidationError(field: \"json\", code: \"trailing_json\") }",
"}",
"private func validBase64URL(_ value: String) -> Bool {", "private func validBase64URL(_ value: String) -> Bool {",
" guard !value.isEmpty, value.utf8.allSatisfy({ byte in", " guard !value.isEmpty, value.utf8.allSatisfy({ byte in",
" (byte >= 65 && byte <= 90) || (byte >= 97 && byte <= 122) || (byte >= 48 && byte <= 57) || byte == 45 || byte == 95", " (byte >= 65 && byte <= 90) || (byte >= 97 && byte <= 122) || (byte >= 48 && byte <= 57) || byte == 45 || byte == 95",
@@ -663,6 +822,11 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
" let fraction = bytes[20..<(bytes.count - 1)]", " let fraction = bytes[20..<(bytes.count - 1)]",
" return bytes[19] == 46 && !fraction.isEmpty && fraction.count <= 9 && fraction.allSatisfy { $0 >= 48 && $0 <= 57 } && fraction.last != 48", " return bytes[19] == 46 && !fraction.isEmpty && fraction.count <= 9 && fraction.allSatisfy { $0 >= 48 && $0 <= 57 } && fraction.last != 48",
"}", "}",
"private func validCanonicalUUID(_ value: String) -> Bool {",
" let bytes = Array(value.utf8)",
" guard bytes.count == 36, bytes[8] == 45, bytes[13] == 45, bytes[18] == 45, bytes[23] == 45, value != \"00000000-0000-0000-0000-000000000000\" else { return false }",
" return bytes.enumerated().allSatisfy { index, byte in [8, 13, 18, 23].contains(index) || (byte >= 48 && byte <= 57) || (byte >= 97 && byte <= 102) }",
"}",
"", "",
] ]
for name in sorted(defs): for name in sorted(defs):
@@ -704,7 +868,7 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
out.append(f" try self.init({', '.join(decoded)})") out.append(f" try self.init({', '.join(decoded)})")
out.extend([" }", "", " public func validate() throws {"]) out.extend([" }", "", " public func validate() throws {"])
out.extend(swift_validation(definition)) out.extend(swift_validation(definition))
out.extend([" }", "", " public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }", " public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) }", "}", ""]) out.extend([" }", "", " public static func decodeJSON(_ data: Data) throws -> Self { try rejectDuplicateJSONKeys(data); return try JSONDecoder().decode(Self.self, from: data) }", " public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) }", "}", ""])
out.extend([ out.extend([
"public func deviceRegistrationProofTranscript(serverID: Data, principalID: Data, deviceID: Data, challenge: Data, expiryUnixMilliseconds: Int64) throws -> Data {", "public func deviceRegistrationProofTranscript(serverID: Data, principalID: Data, deviceID: Data, challenge: Data, expiryUnixMilliseconds: Int64) throws -> Data {",
" for (field, value, length) in [(\"server_id\", serverID, 16), (\"principal_id\", principalID, 16), (\"device_id\", deviceID, 16), (\"challenge\", challenge, 32)] {", " for (field, value, length) in [(\"server_id\", serverID, 16), (\"principal_id\", principalID, 16), (\"device_id\", deviceID, 16), (\"challenge\", challenge, 32)] {",
@@ -723,8 +887,9 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
"", "",
"public extension TunnelAdmissionRequest {", "public extension TunnelAdmissionRequest {",
" func deviceAdmissionTranscript() -> Data {", " func deviceAdmissionTranscript() -> Data {",
" var fields = [sessionId, gatewayId, audience, grant, String(reconnectSequence), clientNonce, capabilities.transport, capabilities.framing, capabilities.media, capabilities.audio, capabilities.sourceRateControl, String(capabilities.clientDecode.count)]", " var fields = [sessionId, gatewayId, audience, grant, String(reconnectSequence), clientNonce, capabilities.transport, capabilities.framing, capabilities.media, capabilities.sourceRateControl, String(capabilities.videoProfiles.count), String(capabilities.audioProfiles.count)]",
" fields.append(contentsOf: capabilities.clientDecode)", " for profile in capabilities.videoProfiles { fields.append(contentsOf: [profile.codec, String(profile.bitDepth), profile.chromaSubsampling, profile.colorSpace, profile.transferFunction]) }",
" for profile in capabilities.audioProfiles { fields.append(contentsOf: [profile.codec, String(profile.sampleRateHz), String(profile.channels), profile.channelLayout, String(profile.packetDurationMs)]) }",
" var transcript = \"versevdi/tunnel-admission/v1\"", " var transcript = \"versevdi/tunnel-admission/v1\"",
" for field in fields { transcript += \"\\(field.utf8.count):\\(field)\" }", " for field in fields { transcript += \"\\(field.utf8.count):\\(field)\" }",
" return Data(transcript.utf8)", " return Data(transcript.utf8)",
@@ -735,14 +900,16 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
" static func intersection(_ profiles: [CapabilityProfile]) throws -> CapabilityProfile {", " static func intersection(_ profiles: [CapabilityProfile]) throws -> CapabilityProfile {",
" guard let selected = profiles.first else { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }", " guard let selected = profiles.first else { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
" try selected.validate()", " try selected.validate()",
" var common = selected.clientDecode", " var commonVideo = selected.videoProfiles",
" var commonAudio = selected.audioProfiles",
" for profile in profiles.dropFirst() {", " for profile in profiles.dropFirst() {",
" try profile.validate()", " try profile.validate()",
" if profile.transport != selected.transport || profile.framing != selected.framing || profile.media != selected.media || profile.audio != selected.audio || profile.sourceRateControl != selected.sourceRateControl { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }", " if profile.transport != selected.transport || profile.framing != selected.framing || profile.media != selected.media || profile.sourceRateControl != selected.sourceRateControl { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
" common = common.filter { profile.clientDecode.contains($0) }", " commonVideo = commonVideo.filter { profile.videoProfiles.contains($0) }",
" if common.isEmpty { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }", " commonAudio = commonAudio.filter { profile.audioProfiles.contains($0) }",
" if commonVideo.isEmpty || commonAudio.isEmpty { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
" }", " }",
" return try CapabilityProfile(transport: selected.transport, framing: selected.framing, media: selected.media, audio: selected.audio, sourceRateControl: selected.sourceRateControl, clientDecode: common)", " return try CapabilityProfile(transport: selected.transport, framing: selected.framing, media: selected.media, sourceRateControl: selected.sourceRateControl, videoProfiles: commonVideo, audioProfiles: commonAudio)",
" }", " }",
"}", "}",
"", "",
+44
View File
@@ -2,6 +2,7 @@ package main
import ( import (
"crypto/sha256" "crypto/sha256"
"encoding/binary"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -101,6 +102,15 @@ func evaluate(version, kind, input string) string {
Profile: protocol.ManifestProfile{ID: "standard", Bounds: protocol.ManifestBounds{MinimumKbps: 1, TargetKbps: 2, MaximumKbps: 3}}, 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"]}, Grant: protocol.GrantReference{OpaqueValue: parts["grant"], ExpiresAt: parts["expires_at"], Audience: parts["audience"]},
CorrelationID: "correlation-1", CorrelationID: "correlation-1",
SelectedDescriptor: protocol.SelectedSessionDescriptor{
VideoProfile: protocol.VideoProfile{Codec: "h264", BitDepth: 8, ChromaSubsampling: "4:2:0", ColorSpace: "bt709-limited", TransferFunction: "sdr"},
AudioProfile: protocol.AudioProfile{Codec: "opus", SampleRateHz: 48000, Channels: 2, ChannelLayout: "stereo", PacketDurationMs: 5},
DisplayMode: protocol.DisplayMode{ResolutionWidth: 1920, ResolutionHeight: 1080, Fps: 60},
BitrateTargetKbps: 12000,
BitrateMaximumKbps: 20000,
Adjustment: protocol.SessionAdjustment{DisplayReason: "none", BitrateReason: "none"},
MediaTimestampBasis: "gateway-send-wall-clock-ms",
},
} }
if value.Validate() == nil { if value.Validate() == nil {
return "valid" return "valid"
@@ -123,6 +133,8 @@ func evaluate(version, kind, input string) string {
value := protocol.SessionRequest{ value := protocol.SessionRequest{
ClientDeviceID: parts["client_device_id"], DeviceKeyID: parts["device_key_id"], ClientDeviceID: parts["client_device_id"], DeviceKeyID: parts["device_key_id"],
PoolID: parts["pool_id"], IdempotencyKey: parts["idempotency_key"], PoolID: parts["pool_id"], IdempotencyKey: parts["idempotency_key"],
VideoProfiles: []protocol.VideoProfile{{Codec: "h264", BitDepth: 8, ChromaSubsampling: "4:2:0", ColorSpace: "bt709-limited", TransferFunction: "sdr"}},
BitratePreference: protocol.BitratePreference{Mode: "auto"},
} }
if value.Validate() == nil { if value.Validate() == nil {
return "valid" return "valid"
@@ -309,6 +321,13 @@ func classifyGatewayInput(encoded string) string {
if len(body) != 4 { if len(body) != 4 {
return "invalid:length" return "invalid:length"
} }
case 8:
if len(body) != 8 {
return "invalid:length"
}
if body[0] > 15 || body[3] > 3 {
return "invalid:field"
}
default: default:
return "invalid:kind" return "invalid:kind"
} }
@@ -352,6 +371,22 @@ func classifyGatewayFeedback(encoded string) string {
if len(body) == 0 { if len(body) == 0 {
return "valid" return "valid"
} }
case 4:
if len(body) != 24 {
return "invalid:length"
}
if binary.BigEndian.Uint64(body[16:]) == 0 || allZero(body[:16]) {
return "invalid:field"
}
return "valid"
case 5:
if len(body) != 16 {
return "invalid:length"
}
if allZero(body) {
return "invalid:field"
}
return "valid"
default: default:
return "invalid:type" return "invalid:type"
} }
@@ -386,6 +421,15 @@ func classifyGatewayFeedback(encoded string) string {
return "invalid:field" return "invalid:field"
} }
func allZero(value []byte) bool {
for _, item := range value {
if item != 0 {
return false
}
}
return true
}
func validFECStatus(body []byte) bool { func validFECStatus(body []byte) bool {
if len(body) != 21 || int(body[10])<<8|int(body[11]) == 0 || int(body[14])<<8|int(body[15]) > int(body[10])<<8|int(body[11]) || int(body[16])<<8|int(body[17]) > int(body[12])<<8|int(body[13]) || body[18] > 100 || body[20] == 0 || body[19] >= body[20] { if len(body) != 21 || int(body[10])<<8|int(body[11]) == 0 || int(body[14])<<8|int(body[15]) > int(body[10])<<8|int(body[11]) || int(body[16])<<8|int(body[17]) > int(body[12])<<8|int(body[13]) || body[18] > 100 || body[20] == 0 || body[19] >= body[20] {
return false return false
+11
View File
@@ -38,6 +38,8 @@ fn evaluate(version: &str, kind: &str, input: &str) -> &'static str {
values.get("device_key_id").cloned().unwrap_or_default(), values.get("device_key_id").cloned().unwrap_or_default(),
values.get("pool_id").cloned().unwrap_or_default(), values.get("pool_id").cloned().unwrap_or_default(),
values.get("idempotency_key").cloned().unwrap_or_default(), values.get("idempotency_key").cloned().unwrap_or_default(),
vec![VideoProfile::new("h264".into(), 8, "4:2:0".into(), "bt709-limited".into(), "sdr".into()).unwrap()],
BitratePreference::new("auto".into(), None).unwrap(),
None, None,
) { ) {
Ok(_) => "valid", Ok(_) => "valid",
@@ -184,6 +186,9 @@ fn classify_gateway_input(encoded: &str) -> &'static str {
} }
7 if body.len() == 4 => "valid", 7 if body.len() == 4 => "valid",
7 => "invalid:length", 7 => "invalid:length",
8 if body.len() != 8 => "invalid:length",
8 if body[0] > 15 || body[3] > 3 => "invalid:field",
8 => "valid",
_ => "invalid:kind", _ => "invalid:kind",
} }
} }
@@ -219,6 +224,12 @@ fn classify_gateway_feedback(encoded: &str) -> &'static str {
2 => "invalid:field", 2 => "invalid:field",
3 if body.is_empty() => "valid", 3 if body.is_empty() => "valid",
3 => "invalid:length", 3 => "invalid:length",
4 if body.len() == 24 && body[..16].iter().any(|value| *value != 0) && body[16..24].iter().any(|value| *value != 0) => "valid",
4 if body.len() != 24 => "invalid:length",
4 => "invalid:field",
5 if body.len() == 16 && body.iter().any(|value| *value != 0) => "valid",
5 if body.len() != 16 => "invalid:length",
5 => "invalid:field",
_ => "invalid:type", _ => "invalid:type",
}; };
} }
+11
View File
@@ -26,6 +26,8 @@ func evaluate(_ version: String, _ kind: String, _ input: String) -> String {
guard (try? SessionRequest( guard (try? SessionRequest(
clientDeviceId: values["client_device_id"] ?? "", deviceKeyId: values["device_key_id"] ?? "", clientDeviceId: values["client_device_id"] ?? "", deviceKeyId: values["device_key_id"] ?? "",
poolId: values["pool_id"] ?? "", idempotencyKey: values["idempotency_key"] ?? "", poolId: values["pool_id"] ?? "", idempotencyKey: values["idempotency_key"] ?? "",
videoProfiles: [try! VideoProfile(codec: "h264", bitDepth: 8, chromaSubsampling: "4:2:0", colorSpace: "bt709-limited", transferFunction: "sdr")],
bitratePreference: try! BitratePreference(mode: "auto", targetKbps: nil),
requestedDisplayMode: nil requestedDisplayMode: nil
)) != nil else { return "invalid:required" } )) != nil else { return "invalid:required" }
return "valid" return "valid"
@@ -136,6 +138,9 @@ func classifyGatewayInput(_ encoded: String) -> String {
let height = Int(body[6]) * 256 + Int(body[7]) let height = Int(body[6]) * 256 + Int(body[7])
return width > 0 && height > 0 && x < width && y < height ? "valid" : "invalid:field" return width > 0 && height > 0 && x < width && y < height ? "valid" : "invalid:field"
case 7: return body.count == 4 ? "valid" : "invalid:length" case 7: return body.count == 4 ? "valid" : "invalid:length"
case 8:
guard body.count == 8 else { return "invalid:length" }
return body[0] <= 15 && body[3] <= 3 ? "valid" : "invalid:field"
default: return "invalid:kind" default: return "invalid:kind"
} }
} }
@@ -156,6 +161,12 @@ func classifyGatewayFeedback(_ encoded: String) -> String {
case 2: case 2:
return validFECStatus(body) ? "valid" : "invalid:field" return validFECStatus(body) ? "valid" : "invalid:field"
case 3: return body.isEmpty ? "valid" : "invalid:length" case 3: return body.isEmpty ? "valid" : "invalid:length"
case 4:
guard body.count == 24 else { return "invalid:length" }
return body[0...15].contains(where: { $0 != 0 }) && body[16...23].contains(where: { $0 != 0 }) ? "valid" : "invalid:field"
case 5:
guard body.count == 16 else { return "invalid:length" }
return body.contains(where: { $0 != 0 }) ? "valid" : "invalid:field"
default: return "invalid:type" default: return "invalid:type"
} }
} }
+6 -1
View File
@@ -4,12 +4,17 @@ from __future__ import annotations
import pathlib import pathlib
import subprocess import subprocess
import tempfile import tempfile
import os
ROOT = pathlib.Path(__file__).resolve().parents[1] ROOT = pathlib.Path(__file__).resolve().parents[1]
def run(command: list[str]) -> None: def run(command: list[str]) -> None:
result = subprocess.run(command, cwd=ROOT, text=True, capture_output=True) environment = os.environ.copy()
cache_root = pathlib.Path(tempfile.gettempdir()) / "versevdi-protocol-module-cache"
environment.setdefault("CLANG_MODULE_CACHE_PATH", str(cache_root / "clang"))
environment.setdefault("SWIFT_MODULECACHE_PATH", str(cache_root / "swift"))
result = subprocess.run(command, cwd=ROOT, env=environment, text=True, capture_output=True)
if result.returncode != 0: if result.returncode != 0:
raise SystemExit(result.stdout + result.stderr) raise SystemExit(result.stdout + result.stderr)
print(result.stdout.strip()) print(result.stdout.strip())
+157 -53
View File
@@ -8,13 +8,18 @@ import re
import shutil import shutil
import subprocess import subprocess
import tempfile import tempfile
import os
ROOT = pathlib.Path(__file__).resolve().parents[1] ROOT = pathlib.Path(__file__).resolve().parents[1]
def run(command: list[str], directory: pathlib.Path) -> None: def run(command: list[str], directory: pathlib.Path) -> None:
result = subprocess.run(command, cwd=directory, text=True, capture_output=True, check=False) environment = os.environ.copy()
cache_root = pathlib.Path(tempfile.gettempdir()) / "versevdi-protocol-module-cache"
environment.setdefault("CLANG_MODULE_CACHE_PATH", str(cache_root / "clang"))
environment.setdefault("SWIFT_MODULECACHE_PATH", str(cache_root / "swift"))
result = subprocess.run(command, cwd=directory, env=environment, text=True, capture_output=True, check=False)
if result.returncode != 0: if result.returncode != 0:
raise RuntimeError("%s\n%s%s" % (" ".join(command), result.stdout, result.stderr)) raise RuntimeError("%s\n%s%s" % (" ".join(command), result.stdout, result.stderr))
@@ -58,21 +63,42 @@ def main() -> int:
swift.write_text( swift.write_text(
"""import Foundation """import Foundation
let video = try VideoProfile(codec: "h264", bitDepth: 8, chromaSubsampling: "4:2:0", colorSpace: "bt709-limited", transferFunction: "sdr")
let hevc = try VideoProfile(codec: "hevc", bitDepth: 8, chromaSubsampling: "4:2:0", colorSpace: "bt709-limited", transferFunction: "sdr")
let audio = try AudioProfile(codec: "opus", sampleRateHz: 48000, channels: 2, channelLayout: "stereo", packetDurationMs: 5)
let display = try DisplayMode(resolutionWidth: 2560, resolutionHeight: 1440, fps: 120)
let adjustment = try SessionAdjustment(displayReason: "none", bitrateReason: "none")
let descriptor = try SelectedSessionDescriptor(videoProfile: video, audioProfile: audio, displayMode: display, bitrateTargetKbps: 40000, bitrateMaximumKbps: 50000, adjustment: adjustment, mediaTimestampBasis: "gateway-send-wall-clock-ms")
let operationId = "12345678-1234-1234-1234-123456789abc"
_ = try GatewayQualityWorkRequest(version: "1", sessionId: "session", gatewayId: "gateway", reconnectSequence: 2, acquisition: "poll", operationId: nil, revision: nil, leaseGeneration: nil, currentAppliedRevision: nil)
_ = try GatewayQualityWorkRequest(version: "1", sessionId: "session", gatewayId: "gateway", reconnectSequence: 2, acquisition: "prompt", operationId: operationId, revision: 7, leaseGeneration: nil, currentAppliedRevision: nil)
_ = try GatewayQualityWorkRequest(version: "1", sessionId: "session", gatewayId: "gateway", reconnectSequence: 2, acquisition: "observation", operationId: operationId, revision: 7, leaseGeneration: 3, currentAppliedRevision: 6)
do {
_ = try GatewayQualityWorkRequest(version: "1", sessionId: "session", gatewayId: "gateway", reconnectSequence: 2, acquisition: "poll", operationId: operationId, revision: 7, leaseGeneration: nil, currentAppliedRevision: nil)
fatalError("poll accepted unknown operation coordinates")
} catch { }
_ = try GatewayQualityAck(version: "1", sessionId: "session", gatewayId: "gateway", reconnectSequence: 2, operationId: operationId, revision: 7, leaseGeneration: 3, outcome: "applied", currentAppliedRevision: 7, failureCode: nil)
_ = try GatewayQualityAck(version: "1", sessionId: "session", gatewayId: "gateway", reconnectSequence: 2, operationId: operationId, revision: 7, leaseGeneration: 3, outcome: "proven_prior", currentAppliedRevision: 6, failureCode: nil)
_ = try GatewayQualityAck(version: "1", sessionId: "session", gatewayId: "gateway", reconnectSequence: 2, operationId: operationId, revision: 7, leaseGeneration: 3, outcome: "unknown", currentAppliedRevision: nil, failureCode: nil)
do {
_ = try GatewayQualityAck(version: "1", sessionId: "session", gatewayId: "gateway", reconnectSequence: 2, operationId: operationId, revision: 7, leaseGeneration: 3, outcome: "applied", currentAppliedRevision: 6, failureCode: nil)
fatalError("applied ack accepted a contradictory revision")
} catch { }
let capability = try CapabilityProfile( let capability = try CapabilityProfile(
transport: "quic-tls13", framing: "datagram-v1", media: "encoded", transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"] sourceRateControl: "server", videoProfiles: [video], audioProfiles: [audio]
) )
guard currentWireVersion == "2", nMinus1WireVersion == "1", nMinus2WireVersion == "0" else { guard currentWireVersion == "2", nMinus1WireVersion == "1", nMinus2WireVersion == "0" else {
fatalError("unexpected control wire compatibility declaration") fatalError("unexpected control wire compatibility declaration")
} }
_ = try CapabilityProfile( _ = try CapabilityProfile(
transport: "quic-tls13", framing: "datagram-v2", media: "encoded", transport: "quic-tls13", framing: "datagram-v2", media: "encoded",
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"] sourceRateControl: "server", videoProfiles: [video], audioProfiles: [audio]
) )
do { do {
_ = try CapabilityProfile( _ = try CapabilityProfile(
transport: "quic-tls13", framing: "datagram-v3", media: "encoded", transport: "quic-tls13", framing: "datagram-v3", media: "encoded",
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"] sourceRateControl: "server", videoProfiles: [video], audioProfiles: [audio]
) )
fatalError("unregistered framing was accepted") fatalError("unregistered framing was accepted")
} catch { } } catch { }
@@ -82,7 +108,7 @@ let request = try TunnelAdmissionRequest(
clientNonce: String(repeating: "n", count: 16), deviceSignature: String(repeating: "s", count: 86), capabilities: capability clientNonce: String(repeating: "n", count: 16), deviceSignature: String(repeating: "s", count: 86), capabilities: capability
) )
_ = request _ = request
let transcript = "versevdi/tunnel-admission/v17:session7:gateway8:audience43:" + String(repeating: "g", count: 43) + "1:016:" + String(repeating: "n", count: 16) + "10:quic-tls1311:datagram-v17:encoded7:encoded6:server1:19:h264-opus" let transcript = "versevdi/tunnel-admission/v17:session7:gateway8:audience43:" + String(repeating: "g", count: 43) + "1:016:" + String(repeating: "n", count: 16) + "10:quic-tls1311:datagram-v17:encoded6:server1:11:14:h2641:85:4:2:013:bt709-limited3:sdr4:opus5:480001:26:stereo1:5"
guard String(data: request.deviceAdmissionTranscript(), encoding: .utf8) == transcript else { guard String(data: request.deviceAdmissionTranscript(), encoding: .utf8) == transcript else {
fatalError("unexpected device admission transcript") fatalError("unexpected device admission transcript")
} }
@@ -126,18 +152,18 @@ for (name, field, serverID, principalID, deviceID, challenge, expiry) in invalid
} }
let incompatible = try CapabilityProfile( let incompatible = try CapabilityProfile(
transport: "quic-tls13", framing: "datagram-v1", media: "encoded", transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
audio: "encoded", sourceRateControl: "server", clientDecode: ["hevc-opus"] sourceRateControl: "server", videoProfiles: [hevc], audioProfiles: [audio]
) )
let gatewayCapability = try CapabilityProfile( let gatewayCapability = try CapabilityProfile(
transport: "quic-tls13", framing: "datagram-v1", media: "encoded", transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
audio: "encoded", sourceRateControl: "server", clientDecode: ["hevc-opus", "h264-opus"] sourceRateControl: "server", videoProfiles: [hevc, video], audioProfiles: [audio]
) )
do { do {
guard try CapabilityProfile.intersection([capability, capability]) == capability else { guard try CapabilityProfile.intersection([capability, capability]) == capability else {
fatalError("matching capability profiles did not intersect") fatalError("matching capability profiles did not intersect")
} }
} catch { fatalError("matching capability profiles did not intersect") } } catch { fatalError("matching capability profiles did not intersect") }
guard try CapabilityProfile.intersection([gatewayCapability, capability]).clientDecode == ["h264-opus"] else { guard try CapabilityProfile.intersection([gatewayCapability, capability]).videoProfiles == [video] else {
fatalError("ordered registered profile intersection changed") fatalError("ordered registered profile intersection changed")
} }
do { do {
@@ -165,17 +191,35 @@ for invalid in [
} }
let clientAuthority = try ClientSessionAuthority( let clientAuthority = try ClientSessionAuthority(
version: "1", sessionId: "session", gatewayId: "gateway", audience: "audience", version: "1", sessionId: "session", gatewayId: "gateway", audience: "audience",
reconnectSequence: 2, expiresAt: "2099-01-01T00:00:00Z", capabilities: capability reconnectSequence: 2, expiresAt: "2099-01-01T00:00:00Z", capabilities: capability, selectedDescriptor: descriptor
) )
let clientAuthorityJSON = try clientAuthority.encodeJSON() let clientAuthorityJSON = try clientAuthority.encodeJSON()
let clientAuthorityObject = try JSONSerialization.jsonObject(with: clientAuthorityJSON) as! [String: Any] let clientAuthorityObject = try JSONSerialization.jsonObject(with: clientAuthorityJSON) as! [String: Any]
guard Set(clientAuthorityObject.keys) == Set([ guard Set(clientAuthorityObject.keys) == Set([
"version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities" "version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities", "selected_descriptor"
]), !String(data: clientAuthorityJSON, encoding: .utf8)!.contains("provider_") else { ]), !String(data: clientAuthorityJSON, encoding: .utf8)!.contains("provider_") else {
fatalError("client authority was not exactly provider-free") fatalError("client authority was not exactly provider-free")
} }
_ = try ClientSessionAuthority.decodeJSON(clientAuthorityJSON) _ = try ClientSessionAuthority.decodeJSON(clientAuthorityJSON)
for field in ["version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities"] { let duplicateCapability = Data(#"{"transport":"quic-tls13","transport":"quic-tls13","framing":"datagram-v1","media":"encoded","source_rate_control":"server","video_profiles":[{"codec":"h264","bit_depth":8,"chroma_subsampling":"4:2:0","color_space":"bt709-limited","transfer_function":"sdr"}],"audio_profiles":[{"codec":"opus","sample_rate_hz":48000,"channels":2,"channel_layout":"stereo","packet_duration_ms":5}]}"#.utf8)
do {
_ = try CapabilityProfile.decodeJSON(duplicateCapability)
fatalError("capability accepted duplicate JSON keys")
} catch { }
do {
_ = try CapabilityProfile.decodeJSON(Data(repeating: 32, count: 1_048_577))
fatalError("capability accepted oversized JSON")
} catch let error as ContractValidationError {
guard error.code == "payload_too_large" else { fatalError("oversized JSON was not rejected before parsing") }
}
let deeplyNested = Data((String(repeating: "[", count: 65) + "null" + String(repeating: "]", count: 65)).utf8)
do {
_ = try CapabilityProfile.decodeJSON(deeplyNested)
fatalError("capability accepted over-deep JSON")
} catch let error as ContractValidationError {
guard error.code == "nesting_too_deep" else { fatalError("over-deep JSON was not rejected before decoding") }
}
for field in ["version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities", "selected_descriptor"] {
var missing = clientAuthorityObject var missing = clientAuthorityObject
missing.removeValue(forKey: field) missing.removeValue(forKey: field)
do { do {
@@ -217,7 +261,7 @@ do {
) )
fatalError("invalid allocation bounds were accepted") fatalError("invalid allocation bounds were accepted")
} catch { } } catch { }
let displayMode = try DisplayMode(resolutionWidth: 2560, resolutionHeight: 1440, fps: 120) let displayMode = display
for invalid in [ for invalid in [
{ try DisplayMode(resolutionWidth: 319, resolutionHeight: 1440, fps: 120) }, { try DisplayMode(resolutionWidth: 319, resolutionHeight: 1440, fps: 120) },
{ try DisplayMode(resolutionWidth: 2560, resolutionHeight: 199, fps: 120) }, { try DisplayMode(resolutionWidth: 2560, resolutionHeight: 199, fps: 120) },
@@ -230,14 +274,14 @@ for invalid in [
} }
let policyFreeV2Request = try SessionRequest( let policyFreeV2Request = try SessionRequest(
clientDeviceId: "device-1", deviceKeyId: "key-1", poolId: "pool-1", clientDeviceId: "device-1", deviceKeyId: "key-1", poolId: "pool-1",
idempotencyKey: "request-1", requestedDisplayMode: nil idempotencyKey: "request-1", videoProfiles: [video], bitratePreference: try BitratePreference(mode: "auto", targetKbps: nil), requestedDisplayMode: nil
).encodeJSON() ).encodeJSON()
guard !String(data: policyFreeV2Request, encoding: .utf8)!.contains("requested_display_mode") else { guard !String(data: policyFreeV2Request, encoding: .utf8)!.contains("requested_display_mode") else {
fatalError("wire-v2 request encoded an absent display mode") fatalError("wire-v2 request encoded an absent display mode")
} }
let displayRequest = try SessionRequest( let displayRequest = try SessionRequest(
clientDeviceId: "device-1", deviceKeyId: "key-1", poolId: "pool-1", clientDeviceId: "device-1", deviceKeyId: "key-1", poolId: "pool-1",
idempotencyKey: "request-1", requestedDisplayMode: displayMode idempotencyKey: "request-1", videoProfiles: [video], bitratePreference: try BitratePreference(mode: "explicit", targetKbps: 40000), requestedDisplayMode: displayMode
) )
guard try SessionRequest.decodeJSON(displayRequest.encodeJSON()).requestedDisplayMode == displayMode else { guard try SessionRequest.decodeJSON(displayRequest.encodeJSON()).requestedDisplayMode == displayMode else {
fatalError("display mode did not round-trip") fatalError("display mode did not round-trip")
@@ -295,14 +339,14 @@ for expiresAt in ["2099-01-01T00:00:00+00:00", "2099-01-01T00:00:00.100Z"] {
} catch { } } catch { }
} }
let streamPolicy = try ProviderStreamPolicy( let streamPolicy = try ProviderStreamPolicy(
resolutionWidth: 2560, resolutionHeight: 1440, fps: 120, videoProfile: hevc, audioProfile: audio, displayMode: displayMode,
codec: "HEVC", bitrateKbps: 40000, audioEnabled: true bitrateTargetKbps: 40000, bitrateMaximumKbps: 50000
) )
guard streamPolicy.codec == "HEVC" else { fatalError("stream policy changed") } guard streamPolicy.videoProfile == hevc else { fatalError("stream policy changed") }
for invalid in [ for invalid in [
{ try ProviderStreamPolicy(resolutionWidth: 319, resolutionHeight: 1440, fps: 120, codec: "HEVC", bitrateKbps: 40000, audioEnabled: true) }, { try ProviderStreamPolicy(videoProfile: video, audioProfile: audio, displayMode: try DisplayMode(resolutionWidth: 319, resolutionHeight: 1440, fps: 120), bitrateTargetKbps: 40000, bitrateMaximumKbps: 50000) },
{ try ProviderStreamPolicy(resolutionWidth: 2560, resolutionHeight: 1440, fps: 241, codec: "HEVC", bitrateKbps: 40000, audioEnabled: true) }, { try ProviderStreamPolicy(videoProfile: video, audioProfile: audio, displayMode: displayMode, bitrateTargetKbps: 50001, bitrateMaximumKbps: 50000) },
{ try ProviderStreamPolicy(resolutionWidth: 2560, resolutionHeight: 1440, fps: 120, codec: "VP9", bitrateKbps: 40000, audioEnabled: true) }, { try ProviderStreamPolicy(videoProfile: try VideoProfile(codec: "vp9", bitDepth: 8, chromaSubsampling: "4:2:0", colorSpace: "bt709-limited", transferFunction: "sdr"), audioProfile: audio, displayMode: displayMode, bitrateTargetKbps: 40000, bitrateMaximumKbps: 50000) },
] { ] {
do { do {
_ = try invalid() _ = try invalid()
@@ -363,17 +407,32 @@ fn main() {
assert_eq!(CURRENT_WIRE_VERSION, "2"); assert_eq!(CURRENT_WIRE_VERSION, "2");
assert_eq!(N_MINUS_1_WIRE_VERSION, "1"); assert_eq!(N_MINUS_1_WIRE_VERSION, "1");
assert_eq!(N_MINUS_2_WIRE_VERSION, "0"); assert_eq!(N_MINUS_2_WIRE_VERSION, "0");
let video = VideoProfile::new("h264".into(), 8, "4:2:0".into(), "bt709-limited".into(), "sdr".into()).unwrap();
let hevc = VideoProfile::new("hevc".into(), 8, "4:2:0".into(), "bt709-limited".into(), "sdr".into()).unwrap();
let audio = AudioProfile::new("opus".into(), 48000, 2, "stereo".into(), 5).unwrap();
let display = DisplayMode::new(2560, 1440, 120).unwrap();
let adjustment = SessionAdjustment::new("none".into(), "none".into()).unwrap();
let descriptor = SelectedSessionDescriptor::new(video.clone(), audio.clone(), display.clone(), 40000, 50000, adjustment, "gateway-send-wall-clock-ms".into()).unwrap();
let operation_id = "12345678-1234-1234-1234-123456789abc".to_string();
assert!(GatewayQualityWorkRequest::new("1".into(), "session".into(), "gateway".into(), 2, "poll".into(), None, None, None, None).is_ok());
assert!(GatewayQualityWorkRequest::new("1".into(), "session".into(), "gateway".into(), 2, "prompt".into(), Some(operation_id.clone()), Some(7), None, None).is_ok());
assert!(GatewayQualityWorkRequest::new("1".into(), "session".into(), "gateway".into(), 2, "observation".into(), Some(operation_id.clone()), Some(7), Some(3), Some(6)).is_ok());
assert!(GatewayQualityWorkRequest::new("1".into(), "session".into(), "gateway".into(), 2, "poll".into(), Some(operation_id.clone()), Some(7), None, None).is_err());
assert!(GatewayQualityAck::new("1".into(), "session".into(), "gateway".into(), 2, operation_id.clone(), 7, 3, "applied".into(), Some(7), None).is_ok());
assert!(GatewayQualityAck::new("1".into(), "session".into(), "gateway".into(), 2, operation_id.clone(), 7, 3, "proven_prior".into(), Some(6), None).is_ok());
assert!(GatewayQualityAck::new("1".into(), "session".into(), "gateway".into(), 2, operation_id.clone(), 7, 3, "unknown".into(), None, None).is_ok());
assert!(GatewayQualityAck::new("1".into(), "session".into(), "gateway".into(), 2, operation_id, 7, 3, "applied".into(), Some(6), None).is_err());
let capabilities = CapabilityProfile::new( let capabilities = CapabilityProfile::new(
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(), "quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
"encoded".into(), "server".into(), vec!["h264-opus".into()], "server".into(), vec![video.clone()], vec![audio.clone()],
).unwrap(); ).unwrap();
assert!(CapabilityProfile::new( assert!(CapabilityProfile::new(
"quic-tls13".into(), "datagram-v2".into(), "encoded".into(), "quic-tls13".into(), "datagram-v2".into(), "encoded".into(),
"encoded".into(), "server".into(), vec!["h264-opus".into()], "server".into(), vec![video.clone()], vec![audio.clone()],
).is_ok()); ).is_ok());
assert!(CapabilityProfile::new( assert!(CapabilityProfile::new(
"quic-tls13".into(), "datagram-v3".into(), "encoded".into(), "quic-tls13".into(), "datagram-v3".into(), "encoded".into(),
"encoded".into(), "server".into(), vec!["h264-opus".into()], "server".into(), vec![video.clone()], vec![audio.clone()],
).is_err()); ).is_err());
let request = TunnelAdmissionRequest::new( let request = TunnelAdmissionRequest::new(
"1".into(), "session".into(), "gateway".into(), "audience".into(), "1".into(), "session".into(), "gateway".into(), "audience".into(),
@@ -381,7 +440,7 @@ fn main() {
).unwrap(); ).unwrap();
let transcript = "versevdi/tunnel-admission/v17:session7:gateway8:audience43:".to_string() let transcript = "versevdi/tunnel-admission/v17:session7:gateway8:audience43:".to_string()
+ &"g".repeat(43) + "1:016:" + &"n".repeat(16) + &"g".repeat(43) + "1:016:" + &"n".repeat(16)
+ "10:quic-tls1311:datagram-v17:encoded7:encoded6:server1:19:h264-opus"; + "10:quic-tls1311:datagram-v17:encoded6:server1:11:14:h2641:85:4:2:013:bt709-limited3:sdr4:opus5:480001:26:stereo1:5";
assert_eq!(request.device_admission_transcript(), transcript.into_bytes()); assert_eq!(request.device_admission_transcript(), transcript.into_bytes());
let proof_server_id = vec![1u8; 16]; let proof_server_id = vec![1u8; 16];
let proof_principal_id = vec![2u8; 16]; let proof_principal_id = vec![2u8; 16];
@@ -423,26 +482,26 @@ fn main() {
).is_err()); ).is_err());
let client_authority = ClientSessionAuthority::new( let client_authority = ClientSessionAuthority::new(
"1".into(), "session".into(), "gateway".into(), "audience".into(), 2, "1".into(), "session".into(), "gateway".into(), "audience".into(), 2,
"2099-01-01T00:00:00Z".into(), capabilities.clone(), "2099-01-01T00:00:00Z".into(), capabilities.clone(), descriptor.clone(),
).unwrap(); ).unwrap();
assert_eq!(client_authority.sessionId(), "session"); assert_eq!(client_authority.sessionId(), "session");
assert_eq!(client_authority.capabilities(), &capabilities); assert_eq!(client_authority.capabilities(), &capabilities);
assert!(ClientSessionAuthority::new( assert!(ClientSessionAuthority::new(
"1".into(), "session".into(), "gateway".into(), "audience".into(), 2, "1".into(), "session".into(), "gateway".into(), "audience".into(), 2,
"not-a-time".into(), capabilities.clone(), "not-a-time".into(), capabilities.clone(), descriptor.clone(),
).is_err()); ).is_err());
assert!(intersect_capability_profiles(&[capabilities.clone(), capabilities.clone()]).is_ok()); assert!(intersect_capability_profiles(&[capabilities.clone(), capabilities.clone()]).is_ok());
let incompatible = CapabilityProfile::new( let incompatible = CapabilityProfile::new(
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(), "quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
"encoded".into(), "server".into(), vec!["hevc-opus".into()], "server".into(), vec![hevc.clone()], vec![audio.clone()],
).unwrap(); ).unwrap();
let gateway_capability = CapabilityProfile::new( let gateway_capability = CapabilityProfile::new(
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(), "quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
"encoded".into(), "server".into(), vec!["hevc-opus".into(), "h264-opus".into()], "server".into(), vec![hevc.clone(), video.clone()], vec![audio.clone()],
).unwrap(); ).unwrap();
assert_eq!( assert_eq!(
intersect_capability_profiles(&[gateway_capability, capabilities.clone()]).unwrap().clientDecode(), intersect_capability_profiles(&[gateway_capability, capabilities.clone()]).unwrap().videoProfiles(),
&vec!["h264-opus".to_string()], &vec![video.clone()],
); );
assert!(intersect_capability_profiles(&[capabilities, incompatible]).is_err()); assert!(intersect_capability_profiles(&[capabilities, incompatible]).is_err());
assert!(AllocationPolicy::new( assert!(AllocationPolicy::new(
@@ -454,11 +513,13 @@ fn main() {
assert!(DisplayMode::new(2560, 1440, 241).is_err()); assert!(DisplayMode::new(2560, 1440, 241).is_err());
let policy_free_v2_request = SessionRequest::new( let policy_free_v2_request = SessionRequest::new(
"device-1".into(), "key-1".into(), "pool-1".into(), "request-1".into(), "device-1".into(), "key-1".into(), "pool-1".into(), "request-1".into(),
vec![video.clone()], BitratePreference::new("auto".into(), None).unwrap(),
None, None,
).unwrap(); ).unwrap();
assert!(policy_free_v2_request.requestedDisplayMode().is_none()); assert!(policy_free_v2_request.requestedDisplayMode().is_none());
let display_request = SessionRequest::new( let display_request = SessionRequest::new(
"device-1".into(), "key-1".into(), "pool-1".into(), "request-1".into(), "device-1".into(), "key-1".into(), "pool-1".into(), "request-1".into(),
vec![video.clone()], BitratePreference::new("explicit".into(), Some(40000)).unwrap(),
Some(display_mode.clone()), Some(display_mode.clone()),
).unwrap(); ).unwrap();
assert_eq!(display_request.requestedDisplayMode(), &Some(display_mode)); assert_eq!(display_request.requestedDisplayMode(), &Some(display_mode));
@@ -486,17 +547,13 @@ fn main() {
).is_err()); ).is_err());
} }
assert!(ProviderStreamPolicy::new( assert!(ProviderStreamPolicy::new(
2560, 1440, 120, "HEVC".into(), 40000, true, hevc.clone(), audio.clone(), display.clone(), 40000, 50000,
).is_ok()); ).is_ok());
assert!(DisplayMode::new(319, 1440, 120).is_err());
assert!(ProviderStreamPolicy::new( assert!(ProviderStreamPolicy::new(
319, 1440, 120, "HEVC".into(), 40000, true, video.clone(), audio.clone(), display.clone(), 50001, 50000,
).is_err());
assert!(ProviderStreamPolicy::new(
2560, 1440, 241, "HEVC".into(), 40000, true,
).is_err());
assert!(ProviderStreamPolicy::new(
2560, 1440, 120, "VP9".into(), 40000, true,
).is_err()); ).is_err());
assert!(VideoProfile::new("vp9".into(), 8, "4:2:0".into(), "bt709-limited".into(), "sdr".into()).is_err());
assert!(GatewayTelemetry::new( assert!(GatewayTelemetry::new(
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, "ready".into(), 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, "ready".into(),
).is_ok()); ).is_ok());
@@ -524,21 +581,68 @@ fn main() {
) )
run(["rustc", str(rust), "-o", str(workspace / "rust-contracts")], ROOT) run(["rustc", str(rust), "-o", str(workspace / "rust-contracts")], ROOT)
run([str(workspace / "rust-contracts")], ROOT) run([str(workspace / "rust-contracts")], ROOT)
expected_protobuf_fields = [ expected_protobuf_messages = {
("version", 1), "ClientSessionAuthority": [
("session_id", 2), ("version", 1),
("gateway_id", 3), ("session_id", 2),
("audience", 4), ("gateway_id", 3),
("reconnect_sequence", 5), ("audience", 4),
("expires_at", 6), ("reconnect_sequence", 5),
("capabilities", 7), ("expires_at", 6),
] ("capabilities", 7),
actual_protobuf_fields = protobuf_message_fields("ClientSessionAuthority") ("selected_descriptor", 8),
if actual_protobuf_fields != expected_protobuf_fields: ],
raise RuntimeError( "GatewayQualityWorkRequest": [
f"ClientSessionAuthority protobuf fields = {actual_protobuf_fields}; " ("version", 1),
f"want {expected_protobuf_fields}" ("session_id", 2),
) ("gateway_id", 3),
("reconnect_sequence", 4),
("operation_id", 5),
("revision", 6),
("current_applied_revision", 7),
("acquisition", 8),
("lease_generation", 9),
],
"GatewayQualityWork": [
("version", 1),
("session_id", 2),
("gateway_id", 3),
("reconnect_sequence", 4),
("operation_id", 5),
("revision", 6),
("lease_expires_at", 7),
("selected_descriptor", 8),
("current_applied_revision", 9),
("lease_generation", 10),
],
"GatewayQualityAck": [
("version", 1),
("session_id", 2),
("gateway_id", 3),
("reconnect_sequence", 4),
("operation_id", 5),
("revision", 6),
("outcome", 7),
("current_applied_revision", 8),
("failure_code", 9),
("lease_generation", 10),
],
"GatewayStopWorkRequest": [
("version", 1),
("session_id", 2),
("gateway_id", 3),
("reconnect_sequence", 4),
("operation_id", 5),
("acquisition", 6),
],
}
for message, expected_protobuf_fields in expected_protobuf_messages.items():
actual_protobuf_fields = protobuf_message_fields(message)
if actual_protobuf_fields != expected_protobuf_fields:
raise RuntimeError(
f"{message} protobuf fields = {actual_protobuf_fields}; "
f"want {expected_protobuf_fields}"
)
rust_unknown = workspace / "unknown.rs" rust_unknown = workspace / "unknown.rs"
shutil.copyfile(ROOT / "gen/rust/protocol.rs", rust_unknown) shutil.copyfile(ROOT / "gen/rust/protocol.rs", rust_unknown)
with rust_unknown.open("a", encoding="utf-8") as output: with rust_unknown.open("a", encoding="utf-8") as output:
+138
View File
@@ -12,6 +12,17 @@ import sys
ROOT = pathlib.Path(__file__).resolve().parents[1] ROOT = pathlib.Path(__file__).resolve().parents[1]
def matches_outcome_branch(instance: dict[str, object], branch: dict[str, object]) -> bool:
required = branch.get("required", [])
if not all(field in instance for field in required):
return False
outcome = branch.get("properties", {}).get("outcome", {}).get("const")
if instance.get("outcome") != outcome:
return False
forbidden = branch.get("not", {}).get("required", [])
return not all(field in instance for field in forbidden) if forbidden else True
def main() -> int: def main() -> int:
schema_path = ROOT / "schemas/control-v1.schema.json" schema_path = ROOT / "schemas/control-v1.schema.json"
schema = json.loads(schema_path.read_text(encoding="utf-8")) schema = json.loads(schema_path.read_text(encoding="utf-8"))
@@ -39,6 +50,65 @@ def main() -> int:
feature_registry = json.loads((ROOT / "registries/features.json").read_text(encoding="utf-8")) feature_registry = json.loads((ROOT / "registries/features.json").read_text(encoding="utf-8"))
registered_features = {entry["id"] for entry in feature_registry["features"]} registered_features = {entry["id"] for entry in feature_registry["features"]}
assert {"control.v1", "control.v2", "display.request.v1", "input.absolute.v1", "input.scroll.v1"}.issubset(registered_features) assert {"control.v1", "control.v2", "display.request.v1", "input.absolute.v1", "input.scroll.v1"}.issubset(registered_features)
assert {"video.profile.v1", "session.quality.v1", "session.stop.v1", "controller.arrival.v1"}.issubset(registered_features)
assert defs["VideoProfile"]["required"] == ["codec", "bit_depth", "chroma_subsampling", "color_space", "transfer_function"]
assert defs["AudioProfile"]["required"] == ["codec", "sample_rate_hz", "channels", "channel_layout", "packet_duration_ms"]
assert defs["CapabilityProfile"]["required"] == ["transport", "framing", "media", "source_rate_control", "video_profiles", "audio_profiles"]
assert defs["CapabilityProfile"]["properties"]["video_profiles"] == {"type": "array", "minItems": 1, "maxItems": 12, "uniqueItems": True, "items": {"$ref": "#/$defs/VideoProfile"}}
assert defs["CapabilityProfile"]["properties"]["audio_profiles"] == {"type": "array", "minItems": 1, "maxItems": 1, "uniqueItems": True, "items": {"$ref": "#/$defs/AudioProfile"}}
assert defs["SessionRequest"]["required"][-2:] == ["video_profiles", "bitrate_preference"]
assert defs["ReconnectRequest"]["required"][-1] == "display_relaunch_confirmed"
assert defs["AssignedDesktop"]["required"][-1] == "quality_limits"
assert defs["EntitledPool"]["required"][-1] == "quality_limits"
for owner in ("SessionAuthority", "ClientSessionAuthority"):
assert defs[owner]["required"][-1] == "selected_descriptor"
assert defs["ConnectionManifest"]["required"][-1] == "selected_descriptor"
quality_request = defs["GatewayQualityWorkRequest"]
assert quality_request["required"] == ["version", "session_id", "gateway_id", "reconnect_sequence", "acquisition"]
assert quality_request["properties"]["acquisition"]["enum"] == ["poll", "prompt", "observation"]
assert quality_request["properties"]["lease_generation"]["minimum"] == 1
assert defs["GatewayStopWorkRequest"]["required"] == ["version", "session_id", "gateway_id", "reconnect_sequence", "acquisition"]
assert defs["GatewayQualityWork"]["required"][-3:] == ["lease_generation", "lease_expires_at", "selected_descriptor"]
quality_ack = defs["GatewayQualityAck"]
assert quality_ack["required"][-3:] == ["revision", "lease_generation", "outcome"]
assert quality_ack["properties"]["outcome"]["enum"] == ["applied", "proven_prior", "unknown"]
assert quality_ack["description"] == (
"Outcome invariants: applied requires current_applied_revision equal to revision; "
"proven_prior requires current_applied_revision strictly less than revision; "
"unknown forbids current_applied_revision and makes no applied-revision assertion."
)
assert quality_ack["oneOf"] == [
{"properties": {"outcome": {"const": "applied"}}, "required": ["current_applied_revision"]},
{"properties": {"outcome": {"const": "proven_prior"}}, "required": ["current_applied_revision"]},
{"properties": {"outcome": {"const": "unknown"}}, "not": {"required": ["current_applied_revision"]}},
]
ack_branches = quality_ack["oneOf"]
for valid_ack in (
{"outcome": "applied", "current_applied_revision": 7},
{"outcome": "proven_prior", "current_applied_revision": 6},
{"outcome": "unknown"},
):
assert sum(matches_outcome_branch(valid_ack, branch) for branch in ack_branches) == 1, valid_ack
for invalid_ack in (
{"outcome": "applied"},
{"outcome": "proven_prior"},
{"outcome": "unknown", "current_applied_revision": 6},
):
assert not any(matches_outcome_branch(invalid_ack, branch) for branch in ack_branches), invalid_ack
operation_id_pattern = r"^(?!00000000-0000-0000-0000-000000000000$)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
canonical_time_pattern = r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]{0,8}[1-9])?Z$"
for definition in ("QualityChangeOperation", "StopOperation", "GatewayQualityWorkRequest", "GatewayQualityWork", "GatewayQualityAck", "GatewayStopWorkRequest", "GatewayStopWork", "GatewayStopAck"):
assert defs[definition]["properties"]["operation_id"]["pattern"] == operation_id_pattern, definition
assert defs[definition]["x-max-bytes"] == 16384, definition
for definition, fields in {
"QualityChangeOperation": ("created_at", "deadline_at", "updated_at"),
"StopOperation": ("created_at", "deadline_at", "updated_at"),
"GatewayQualityWork": ("lease_expires_at",),
}.items():
for field in fields:
assert defs[definition]["properties"][field]["pattern"] == canonical_time_pattern, (definition, field)
display_mode = defs["DisplayMode"] display_mode = defs["DisplayMode"]
assert display_mode["required"] == ["resolution_width", "resolution_height", "fps"] assert display_mode["required"] == ["resolution_width", "resolution_height", "fps"]
@@ -113,6 +183,17 @@ def main() -> int:
assert "native_identity" not in native_missing_fixture assert "native_identity" not in native_missing_fixture
tunnel_credential_fixture = json.loads((ROOT / "fixtures/valid/native-tunnel-credential.json").read_text(encoding="utf-8")) tunnel_credential_fixture = json.loads((ROOT / "fixtures/valid/native-tunnel-credential.json").read_text(encoding="utf-8"))
assert set(tunnel_credential_fixture) == set(tunnel_credential["required"]) assert set(tunnel_credential_fixture) == set(tunnel_credential["required"])
valid_fixture_contracts = {
"fixtures/valid/session-request.json": "SessionRequest",
"fixtures/valid/selected-session-descriptor.json": "SelectedSessionDescriptor",
"fixtures/valid/session-quality-limits.json": "SessionQualityLimits",
}
for relative, definition in valid_fixture_contracts.items():
fixture = json.loads((ROOT / relative).read_text(encoding="utf-8"))
assert set(fixture) == set(defs[definition]["required"]), (relative, definition)
assert set(json.loads((ROOT / "fixtures/invalid/capability-rc5-opaque.json").read_text())) & {"audio", "client_decode"} == {"audio", "client_decode"}
assert "video_profiles" not in json.loads((ROOT / "fixtures/invalid/session-request-rc5.json").read_text())
assert set(json.loads((ROOT / "fixtures/invalid/provider-stream-policy-rc5.json").read_text())) == {"resolution_width", "resolution_height", "fps", "codec", "bitrate_kbps", "audio_enabled"}
expected_header = "id\tversion\tkind\tinput\texpected" expected_header = "id\tversion\tkind\tinput\texpected"
ids = set() ids = set()
@@ -139,10 +220,64 @@ def main() -> int:
fixture_hash.update((ROOT / relative).read_bytes()) fixture_hash.update((ROOT / relative).read_bytes())
fixture_hash.update(b"\0") fixture_hash.update(b"\0")
assert fixture_manifest["corpus_sha256"] == fixture_hash.hexdigest() assert fixture_manifest["corpus_sha256"] == fixture_hash.hexdigest()
json_fixture_paths = sorted(
path.relative_to(ROOT).as_posix()
for directory in (ROOT / "fixtures/valid", ROOT / "fixtures/invalid")
for path in directory.glob("*.json")
)
assert fixture_manifest["json_files"] == json_fixture_paths
json_fixture_hash = hashlib.sha256()
for relative in json_fixture_paths:
json_fixture_hash.update(relative.encode("utf-8"))
json_fixture_hash.update(b"\0")
json_fixture_hash.update((ROOT / relative).read_bytes())
json_fixture_hash.update(b"\0")
assert fixture_manifest["json_corpus_sha256"] == json_fixture_hash.hexdigest()
openapi = (ROOT / "openapi/control-v1.yaml").read_text(encoding="utf-8") openapi = (ROOT / "openapi/control-v1.yaml").read_text(encoding="utf-8")
assert "openapi: 3.1.0" in openapi 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 "/api/v1/auth/refresh:" in openapi and "/api/v1/resources:" in openapi and "/api/v1/events:" in openapi
for route in (
"/api/v1/session-quality-limits:",
"/api/v1/session-quality-limits/assignments/{assignment_id}:",
"/api/v1/session-quality-limits/pools/{pool_id}:",
"/api/v1/admin/entitlements/{entitlement_id}/display-limit-override:",
"/api/v1/broker/sessions/{session_id}/quality-changes:",
"/api/v1/broker/sessions/{session_id}/quality-changes/{operation_id}:",
"/api/v1/broker/sessions/{session_id}/stop-operations:",
"/api/v1/broker/sessions/{session_id}/stop-operations/{operation_id}:",
"/api/v1/gateway/quality-work:",
"/api/v1/gateway/quality-ack:",
"/api/v1/gateway/stop-work:",
"/api/v1/gateway/stop-ack:",
):
assert route in openapi, route
for operation_id in (
"getSessionQualityLimits", "getAssignmentSessionQualityLimits", "getPoolSessionQualityLimits",
"createSessionQualityChange", "getSessionQualityChange", "createSessionStopOperation", "getSessionStopOperation",
):
operation = openapi.split(f" operationId: {operation_id}\n", 1)[1].split(" responses:\n", 1)[0]
assert "nativeBearer: []" in operation and "browserSession" not in operation, operation_id
for operation_id in (
"acquireGatewayQualityWork", "acknowledgeGatewayQualityWork", "acquireGatewayStopWork", "acknowledgeGatewayStopWork",
):
operation = openapi.split(f" operationId: {operation_id}\n", 1)[1].split(" responses:\n", 1)[0]
assert "gatewayMutualTLS: []" in operation and "nativeBearer" not in operation and "browserSession" not in operation, operation_id
assert "certificate identity MUST match" in operation, operation_id
assert "Maximum JSON body: 16384 bytes." in operation, operation_id
quality_acquisition = openapi.split(" operationId: acquireGatewayQualityWork\n", 1)[1].split(" responses:\n", 1)[0]
assert "`poll` acquisition omits unknown operation coordinates" in quality_acquisition
assert "coordinates MUST match exactly" in quality_acquisition
quality_acknowledgement = openapi.split(" operationId: acknowledgeGatewayQualityWork\n", 1)[1].split(" responses:\n", 1)[0]
assert "Stale lease generations MUST be rejected" in quality_acknowledgement
assert "`applied` requires `current_applied_revision == revision`" in quality_acknowledgement
assert "`proven_prior` requires `current_applied_revision < revision`" in quality_acknowledgement
assert "`unknown` forbids `current_applied_revision`" in quality_acknowledgement
for operation_id in ("createSessionQualityChange", "getSessionQualityChange", "createSessionStopOperation", "getSessionStopOperation"):
operation = openapi.split(f" operationId: {operation_id}\n", 1)[1].split(" responses:\n", 1)[0]
assert "owning principal and active device/key" in operation, operation_id
assert openapi.count("Maximum JSON body: 16384 bytes.") >= 8
assert operation_id_pattern in openapi
assert "provider_url" not in openapi and "vm_address" not in openapi assert "provider_url" not in openapi and "vm_address" not in openapi
session_endpoint = openapi.split(" /api/v1/auth/session:", 1)[1].split("\n /api/", 1)[0] session_endpoint = openapi.split(" /api/v1/auth/session:", 1)[1].split("\n /api/", 1)[0]
assert "$defs/BrowserAuthenticatedSession" in session_endpoint assert "$defs/BrowserAuthenticatedSession" in session_endpoint
@@ -172,6 +307,9 @@ def main() -> int:
browserCsrfCookie: [] browserCsrfCookie: []
browserCsrfHeader: [] browserCsrfHeader: []
""" """
admin_override = openapi.split(" operationId: updateEntitlementDisplayLimitOverride\n", 1)[1].split(" responses:\n", 1)[0]
assert browser_requirement.removeprefix(" ") in admin_override
assert "Maximum JSON body: 16384 bytes." in admin_override
for operation_id in ( for operation_id in (
"issueReauthenticationGrant", "logoutSession", "registerDevice", "proveDevice", "revokeDevice", "issueReauthenticationGrant", "logoutSession", "registerDevice", "proveDevice", "revokeDevice",
"requestBrokerSession", "allocateBrokerSession", "reconnectBrokerSession", "cancelBrokerSession", "requestBrokerSession", "allocateBrokerSession", "reconnectBrokerSession", "cancelBrokerSession",
+12
View File
@@ -44,6 +44,10 @@ def classify_input(raw: bytes) -> str:
return "valid" if width and height and x < width and y < height else "invalid:field" return "valid" if width and height and x < width and y < height else "invalid:field"
if kind == 7: if kind == 7:
return "valid" if len(body) == 4 else "invalid:length" return "valid" if len(body) == 4 else "invalid:length"
if kind == 8:
if len(body) != 8:
return "invalid:length"
return "valid" if body[0] <= 15 and body[3] <= 3 else "invalid:field"
return "invalid:kind" return "invalid:kind"
@@ -68,6 +72,14 @@ def classify_feedback(raw: bytes) -> str:
return "valid" if valid_fec_status(body) else "invalid:field" return "valid" if valid_fec_status(body) else "invalid:field"
if kind == 3: if kind == 3:
return "valid" if not body else "invalid:length" return "valid" if not body else "invalid:length"
if kind == 4:
if len(body) != 24:
return "invalid:length"
return "valid" if any(body[:16]) and int.from_bytes(body[16:24], "big") > 0 else "invalid:field"
if kind == 5:
if len(body) != 16:
return "invalid:length"
return "valid" if any(body) else "invalid:field"
return "invalid:type" return "invalid:type"
if kind in (1, 2, 3): if kind in (1, 2, 3):
return "invalid:direction" return "invalid:direction"