Compare commits

...
Author SHA1 Message Date
sechmachine a19023995e fix(protocol): bind relaunch termination authority
Verify Protocol / module (push) Successful in 1m24s
Verify Protocol / verify (push) Canceled after 0s
2026-08-13 13:43:07 +07:00
sechmachine 878588d364 feat(protocol): add relaunch operation response 2026-08-13 13:33:00 +07:00
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
sechmachine 4693102b3c Protocol: require canonical client authority expiry
Verify Protocol / verify (push) Successful in 1m2s
Verify Protocol / module (push) Successful in 1m41s
2026-08-12 12:03:36 +07:00
sechmachine afbcea62f9 Protocol: split client session authority
Verify Protocol / verify (push) Successful in 1m2s
Verify Protocol / module (push) Successful in 1m45s
2026-08-12 11:50:41 +07:00
sechmachine b6a4f773e4 Protocol: freeze device proof and browser CSRF contracts
Verify Protocol / module (push) Successful in 1m12s
Verify Protocol / verify (push) Successful in 22s
2026-08-11 21:21:28 +07:00
sechmachine afcd5d99db feat(protocol): define native session credentials
Verify Protocol / module (push) Successful in 1m13s
Verify Protocol / verify (push) Successful in 35s
2026-08-11 18:38:19 +07:00
sechmachine 79d9e49497 docs(openspec): archive phase 3d display input 2026-08-10 23:09:58 +07:00
sechmachine 408d4f9cc3 feat(protocol): negotiate display and native input
Verify Protocol / module (push) Successful in 1m17s
Verify Protocol / verify (push) Successful in 55s
2026-08-10 23:07:04 +07:00
sechmachine 346bf5fe4d docs(openspec): archive complete frame transport 2026-08-10 08:54:36 +07:00
sechmachine 7c89ef5bf5 docs(openspec): record RC10 distribution
Verify Protocol / module (push) Successful in 2m10s
Verify Protocol / verify (push) Canceled after 0s
2026-08-09 13:58:00 +07:00
77 changed files with 8970 additions and 587 deletions
+1 -1
View File
@@ -21,10 +21,10 @@
# Go workspace file
go.work
go.work.sum
/.build
# env file
.env
# IDE files
/.idea
+1
View File
@@ -32,6 +32,7 @@ go-test:
binding-compile:
rustc --crate-type lib gen/rust/protocol.rs -o /tmp/versevdi-protocol-generated.rlib
swiftc -typecheck gen/swift/Protocol.swift
swift build
strict-contracts:
$(PYTHON) -B tools/test_generated_contracts.py
+13
View File
@@ -0,0 +1,13 @@
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "VerseVDIProtocol",
platforms: [.macOS(.v14)],
products: [
.library(name: "VerseVDIProtocol", targets: ["VerseVDIProtocol"]),
],
targets: [
.target(name: "VerseVDIProtocol", path: "gen/swift", sources: ["Protocol.swift"]),
]
)
+4 -4
View File
@@ -1,10 +1,10 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"protocol": "versevdi-control",
"current": "1",
"n_minus_1": "0",
"n_minus_2": "-1",
"unsupported": ["-2", "2"],
"current": "2",
"n_minus_1": "1",
"n_minus_2": "0",
"unsupported": ["-1", "3"],
"feature_registry": "registries/features.json",
"datagram_registry": "registries/datagrams.json"
}
+26 -6
View File
@@ -1,11 +1,31 @@
id version kind input expected
version-current 1 version 1 valid
version-n-minus-1 0 version 0 valid
version-n-minus-2 -1 version -1 valid
version-unsupported 2 version 2 invalid:unsupported_version
version-current 2 version 2 valid
version-n-minus-1 1 version 1 valid
version-n-minus-2 0 version 0 valid
version-unsupported 3 version 3 invalid:unsupported_version
page-valid 1 page limit=20;cursor=opaque valid
page-limit-high 1 page limit=101 invalid:invalid_limit
manifest-valid 1 manifest version=1;gateway_id=g-1;grant=opaque-one-time-grant-value-with-at-least-43-bytes;audience=versevdi-gateway;purpose=launch;protocol=verse-gateway-v1;expires_at=2099-01-01T00:00:00Z valid
manifest-provider-field 1 manifest gateway_id=g-1;grant=g-1;audience=versevdi-gateway;purpose=launch;provider_url=https://provider.invalid invalid:forbidden_field
manifest-valid 1 manifest version=1;gateway_id=g-1;public_identity=gateway.control.test;grant=opaque-one-time-grant-value-with-at-least-43-bytes;audience=versevdi-gateway;purpose=launch;protocol=verse-gateway-v1;expires_at=2099-01-01T00:00:00Z valid
manifest-missing-public-identity 1 manifest version=1;gateway_id=g-1;grant=opaque-one-time-grant-value-with-at-least-43-bytes;audience=versevdi-gateway;purpose=launch;protocol=verse-gateway-v1;expires_at=2099-01-01T00:00:00Z invalid:invalid_manifest
manifest-provider-field 1 manifest gateway_id=g-1;public_identity=gateway.control.test;grant=g-1;audience=versevdi-gateway;purpose=launch;provider_url=https://provider.invalid invalid:forbidden_field
clipboard-text-valid 1 clipboard encoding=utf-8;text=hello%20world valid
clipboard-file 1 clipboard encoding=octet-stream;file=/tmp/a invalid:unsupported_clipboard
session-request-policy-free 2 session_request client_device_id=device-1;device_key_id=key-1;pool_id=pool-1;idempotency_key=request-1 valid
session-request-wire-v1-shape 1 session_request client_device_id=device-1;device_key_id=key-1;pool_id=pool-1;idempotency_key=request-1;policy_snapshot=client-owned invalid:unsupported_version
session-request-client-policy 2 session_request client_device_id=device-1;device_key_id=key-1;pool_id=pool-1;idempotency_key=request-1;policy_snapshot=client-owned invalid:forbidden_field
authenticated-browser-session 2 browser_authenticated_session username=alice;provider=local;roles=user;role=user valid
authenticated-browser-native-identity 2 browser_authenticated_session username=alice;provider=local;roles=user;role=user;client_device_id=device-1;device_key_id=key-1 invalid:forbidden_field
authenticated-browser-empty-role 2 browser_authenticated_session username=alice;provider=local;roles=;role=user invalid:invalid_session
authenticated-browser-long-role 2 browser_authenticated_session username=alice;provider=local;roles=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;role=user invalid:invalid_session
authenticated-browser-role-64-utf8-bytes 2 browser_authenticated_session username=alice;provider=local;roles=éééééééééééééééééééééééééééééééé;role=user valid
authenticated-browser-role-66-utf8-bytes 2 browser_authenticated_session username=alice;provider=local;roles=ééééééééééééééééééééééééééééééééé;role=user invalid:invalid_session
authenticated-native-session 2 native_authenticated_session username=alice;provider=local;roles=user;role=user;client_device_id=device-1;device_key_id=key-1 valid
authenticated-native-missing-identity 2 native_authenticated_session username=alice;provider=local;roles=user;role=user invalid:required
authenticated-partial-native-session 2 native_authenticated_session username=alice;provider=local;roles=user;role=user;client_device_id=device-1 invalid:required
authenticated-native-empty-role 2 native_authenticated_session username=alice;provider=local;roles=;role=user;client_device_id=device-1;device_key_id=key-1 invalid:invalid_session
authenticated-native-long-role 2 native_authenticated_session username=alice;provider=local;roles=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;role=user;client_device_id=device-1;device_key_id=key-1 invalid:invalid_session
authenticated-native-role-64-utf8-bytes 2 native_authenticated_session username=alice;provider=local;roles=éééééééééééééééééééééééééééééééé;role=user;client_device_id=device-1;device_key_id=key-1 valid
authenticated-native-role-66-utf8-bytes 2 native_authenticated_session username=alice;provider=local;roles=ééééééééééééééééééééééééééééééééé;role=user;client_device_id=device-1;device_key_id=key-1 invalid:invalid_session
native-tunnel-credential 2 native_tunnel_credential client_device_id=device-1;device_key_id=key-1;certificate_chain_pem=certificate;trust_bundle_pem=trust;expires_at=2099-01-01T00:00:00Z valid
native-tunnel-credential-offset-time 2 native_tunnel_credential client_device_id=device-1;device_key_id=key-1;certificate_chain_pem=certificate;trust_bundle_pem=trust;expires_at=2099-01-01T00:00:00+00:00 invalid:invalid_credential
native-tunnel-credential-noncanonical-fraction 2 native_tunnel_credential client_device_id=device-1;device_key_id=key-1;certificate_chain_pem=certificate;trust_bundle_pem=trust;expires_at=2099-01-01T00:00:00.100Z invalid:invalid_credential
1 id version kind input expected
2 version-current 1 2 version 1 2 valid
3 version-n-minus-1 0 1 version 0 1 valid
4 version-n-minus-2 -1 0 version -1 0 valid
5 version-unsupported 2 3 version 2 3 invalid:unsupported_version
6 page-valid 1 page limit=20;cursor=opaque valid
7 page-limit-high 1 page limit=101 invalid:invalid_limit
8 manifest-valid 1 manifest version=1;gateway_id=g-1;grant=opaque-one-time-grant-value-with-at-least-43-bytes;audience=versevdi-gateway;purpose=launch;protocol=verse-gateway-v1;expires_at=2099-01-01T00:00:00Z version=1;gateway_id=g-1;public_identity=gateway.control.test;grant=opaque-one-time-grant-value-with-at-least-43-bytes;audience=versevdi-gateway;purpose=launch;protocol=verse-gateway-v1;expires_at=2099-01-01T00:00:00Z valid
9 manifest-provider-field manifest-missing-public-identity 1 manifest gateway_id=g-1;grant=g-1;audience=versevdi-gateway;purpose=launch;provider_url=https://provider.invalid version=1;gateway_id=g-1;grant=opaque-one-time-grant-value-with-at-least-43-bytes;audience=versevdi-gateway;purpose=launch;protocol=verse-gateway-v1;expires_at=2099-01-01T00:00:00Z invalid:forbidden_field invalid:invalid_manifest
10 manifest-provider-field 1 manifest gateway_id=g-1;public_identity=gateway.control.test;grant=g-1;audience=versevdi-gateway;purpose=launch;provider_url=https://provider.invalid invalid:forbidden_field
11 clipboard-text-valid 1 clipboard encoding=utf-8;text=hello%20world valid
12 clipboard-file 1 clipboard encoding=octet-stream;file=/tmp/a invalid:unsupported_clipboard
13 session-request-policy-free 2 session_request client_device_id=device-1;device_key_id=key-1;pool_id=pool-1;idempotency_key=request-1 valid
14 session-request-wire-v1-shape 1 session_request client_device_id=device-1;device_key_id=key-1;pool_id=pool-1;idempotency_key=request-1;policy_snapshot=client-owned invalid:unsupported_version
15 session-request-client-policy 2 session_request client_device_id=device-1;device_key_id=key-1;pool_id=pool-1;idempotency_key=request-1;policy_snapshot=client-owned invalid:forbidden_field
16 authenticated-browser-session 2 browser_authenticated_session username=alice;provider=local;roles=user;role=user valid
17 authenticated-browser-native-identity 2 browser_authenticated_session username=alice;provider=local;roles=user;role=user;client_device_id=device-1;device_key_id=key-1 invalid:forbidden_field
18 authenticated-browser-empty-role 2 browser_authenticated_session username=alice;provider=local;roles=;role=user invalid:invalid_session
19 authenticated-browser-long-role 2 browser_authenticated_session username=alice;provider=local;roles=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;role=user invalid:invalid_session
20 authenticated-browser-role-64-utf8-bytes 2 browser_authenticated_session username=alice;provider=local;roles=éééééééééééééééééééééééééééééééé;role=user valid
21 authenticated-browser-role-66-utf8-bytes 2 browser_authenticated_session username=alice;provider=local;roles=ééééééééééééééééééééééééééééééééé;role=user invalid:invalid_session
22 authenticated-native-session 2 native_authenticated_session username=alice;provider=local;roles=user;role=user;client_device_id=device-1;device_key_id=key-1 valid
23 authenticated-native-missing-identity 2 native_authenticated_session username=alice;provider=local;roles=user;role=user invalid:required
24 authenticated-partial-native-session 2 native_authenticated_session username=alice;provider=local;roles=user;role=user;client_device_id=device-1 invalid:required
25 authenticated-native-empty-role 2 native_authenticated_session username=alice;provider=local;roles=;role=user;client_device_id=device-1;device_key_id=key-1 invalid:invalid_session
26 authenticated-native-long-role 2 native_authenticated_session username=alice;provider=local;roles=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;role=user;client_device_id=device-1;device_key_id=key-1 invalid:invalid_session
27 authenticated-native-role-64-utf8-bytes 2 native_authenticated_session username=alice;provider=local;roles=éééééééééééééééééééééééééééééééé;role=user;client_device_id=device-1;device_key_id=key-1 valid
28 authenticated-native-role-66-utf8-bytes 2 native_authenticated_session username=alice;provider=local;roles=ééééééééééééééééééééééééééééééééé;role=user;client_device_id=device-1;device_key_id=key-1 invalid:invalid_session
29 native-tunnel-credential 2 native_tunnel_credential client_device_id=device-1;device_key_id=key-1;certificate_chain_pem=certificate;trust_bundle_pem=trust;expires_at=2099-01-01T00:00:00Z valid
30 native-tunnel-credential-offset-time 2 native_tunnel_credential client_device_id=device-1;device_key_id=key-1;certificate_chain_pem=certificate;trust_bundle_pem=trust;expires_at=2099-01-01T00:00:00+00:00 invalid:invalid_credential
31 native-tunnel-credential-noncanonical-fraction 2 native_tunnel_credential client_device_id=device-1;device_key_id=key-1;certificate_chain_pem=certificate;trust_bundle_pem=trust;expires_at=2099-01-01T00:00:00.100Z invalid:invalid_credential
+2
View File
@@ -0,0 +1,2 @@
id version kind input expected
device-proof-canonical 1 device_proof_transcript server_id=00112233445566778899aabbccddeeff;principal_id=102132435465768798a9bacbdcedfe0f;device_id=ffeeddccbbaa99887766554433221100;challenge=000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f;expiry_unix_ms=1700000000123 76657273657664692d6465766963652d70726f6f662d763100112233445566778899aabbccddeeff102132435465768798a9bacbdcedfe0fffeeddccbbaa99887766554433221100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f0000018bcfe5687b
1 id version kind input expected
2 device-proof-canonical 1 device_proof_transcript server_id=00112233445566778899aabbccddeeff;principal_id=102132435465768798a9bacbdcedfe0f;device_id=ffeeddccbbaa99887766554433221100;challenge=000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f;expiry_unix_ms=1700000000123 76657273657664692d6465766963652d70726f6f662d763100112233445566778899aabbccddeeff102132435465768798a9bacbdcedfe0fffeeddccbbaa99887766554433221100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f0000018bcfe5687b
@@ -7,9 +7,14 @@ valid-relative-mouse 1 gateway_input hex=564749310304fffe0003 valid
valid-utf8-scalar 1 gateway_input hex=564749310403e29883 valid
valid-controller 1 gateway_input hex=5647493105110200030004ffff00010002000300040005 valid
valid-controller-release 1 gateway_input hex=5647493105110200000000000000000000000000000000 valid
valid-absolute-mouse 1 gateway_input hex=56474931060804d202370a0005a0 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-fec 1 gateway_feedback hex=56474631000200150000002a000500030002000a000200080002140001 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-rumble 1 gateway_feedback hex=56474631011100050112345678 valid
valid-hdr 1 gateway_feedback hex=564746310112000101 valid
@@ -18,10 +23,19 @@ invalid-input-kind 1 gateway_input hex=564749317f00 invalid:kind
invalid-input-reserved 1 gateway_input hex=564749310203010101 invalid:reserved
invalid-input-utf8 1 gateway_input hex=564749310402c328 invalid:utf8
invalid-input-length 1 gateway_input hex=564749310104010200 invalid:length
invalid-absolute-zero-viewport 1 gateway_input hex=56474931060800000000000005a0 invalid:field
invalid-absolute-x-out-of-range 1 gateway_input hex=5647493106080a0000000a0005a0 invalid:field
invalid-absolute-y-out-of-range 1 gateway_input hex=564749310608000005a00a0005a0 invalid:field
invalid-absolute-length 1 gateway_input hex=56474931060700000000010001 invalid:length
invalid-scroll-length 1 gateway_input hex=5647493107020000 invalid:length
invalid-feedback-direction 1 gateway_feedback hex=5647463101020000 invalid:direction
invalid-terminal-receipt-direction 1 gateway_feedback hex=5647463101030000 invalid:direction
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-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
1 id version kind input expected
7 valid-utf8-scalar 1 gateway_input hex=564749310403e29883 valid
8 valid-controller 1 gateway_input hex=5647493105110200030004ffff00010002000300040005 valid
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
23 invalid-input-reserved 1 gateway_input hex=564749310203010101 invalid:reserved
24 invalid-input-utf8 1 gateway_input hex=564749310402c328 invalid:utf8
25 invalid-input-length 1 gateway_input hex=564749310104010200 invalid:length
26 invalid-absolute-zero-viewport 1 gateway_input hex=56474931060800000000000005a0 invalid:field
27 invalid-absolute-x-out-of-range 1 gateway_input hex=5647493106080a0000000a0005a0 invalid:field
28 invalid-absolute-y-out-of-range 1 gateway_input hex=564749310608000005a00a0005a0 invalid:field
29 invalid-absolute-length 1 gateway_input hex=56474931060700000000010001 invalid:length
30 invalid-scroll-length 1 gateway_input hex=5647493107020000 invalid:length
31 invalid-feedback-direction 1 gateway_feedback hex=5647463101020000 invalid:direction
32 invalid-terminal-receipt-direction 1 gateway_feedback hex=5647463101030000 invalid:direction
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
+8 -5
View File
@@ -1,6 +1,9 @@
id version kind input expected
tunnel-current 1 tunnel offered=1;feature=control.v1 valid
tunnel-n-minus-1 0 tunnel offered=0;feature=control.v1 valid
tunnel-n-minus-2 -1 tunnel offered=-1;feature=control.v1 valid
tunnel-unsupported 1 tunnel offered=2;feature=control.v1 invalid:unsupported_version
tunnel-no-control 1 tunnel offered=1;feature=media.video invalid:unsupported_feature
tunnel-current 2 tunnel offered=2;feature=control.v2 valid
tunnel-n-minus-1 1 tunnel offered=1;feature=control.v1 valid
tunnel-n-minus-2 0 tunnel offered=0;feature=control.v1 valid
tunnel-display-request 2 tunnel offered=2;feature=display.request.v1 valid
tunnel-absolute-input 2 tunnel offered=2;feature=input.absolute.v1 valid
tunnel-scroll-input 2 tunnel offered=2;feature=input.scroll.v1 valid
tunnel-unsupported 2 tunnel offered=3;feature=control.v2 invalid:unsupported_version
tunnel-no-control 2 tunnel offered=2;feature=media.video invalid:unsupported_feature
1 id version kind input expected
2 tunnel-current 1 2 tunnel offered=1;feature=control.v1 offered=2;feature=control.v2 valid
3 tunnel-n-minus-1 0 1 tunnel offered=0;feature=control.v1 offered=1;feature=control.v1 valid
4 tunnel-n-minus-2 -1 0 tunnel offered=-1;feature=control.v1 offered=0;feature=control.v1 valid
5 tunnel-unsupported tunnel-display-request 1 2 tunnel offered=2;feature=control.v1 offered=2;feature=display.request.v1 invalid:unsupported_version valid
6 tunnel-no-control tunnel-absolute-input 1 2 tunnel offered=1;feature=media.video offered=2;feature=input.absolute.v1 invalid:unsupported_feature valid
7 tunnel-scroll-input 2 tunnel offered=2;feature=input.scroll.v1 valid
8 tunnel-unsupported 2 tunnel offered=3;feature=control.v2 invalid:unsupported_version
9 tunnel-no-control 2 tunnel offered=2;feature=media.video invalid:unsupported_feature
@@ -0,0 +1,9 @@
{
"username": "alice",
"provider": "local",
"roles": ["user"],
"role": "user",
"native_identity": {
"client_device_id": "device-1"
}
}
@@ -0,0 +1 @@
{"mode":"auto","target_kbps":1000}
@@ -0,0 +1,10 @@
{
"username": "alice",
"provider": "local",
"roles": ["user"],
"role": "user",
"native_identity": {
"client_device_id": "device-1",
"device_key_id": "key-1"
}
}
@@ -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}
+11 -2
View File
@@ -6,7 +6,7 @@
"gateway": {
"id": "gateway-1",
"addresses": ["gateway.control.test:443"],
"public_identity": "gateway-1"
"public_identity": "gateway.control.test"
},
"tunnel": {
"versions": ["verse-gateway-v1/1"],
@@ -26,5 +26,14 @@
"audience": "versevdi-gateway"
},
"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,6 @@
{
"username": "alice",
"provider": "local",
"roles": ["user"],
"role": "user"
}
@@ -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,7 @@
{
"client_device_id": "device-1",
"device_key_id": "key-1",
"pool_id": "pool-1",
"idempotency_key": "request-1",
"policy_snapshot": {}
}
@@ -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"}
+31 -1
View File
@@ -4,11 +4,41 @@
"fixtures/conformance/control-v1.tsv",
"fixtures/conformance/datagram-v1.tsv",
"fixtures/conformance/datagram-v2.tsv",
"fixtures/conformance/device-proof-v1.tsv",
"fixtures/conformance/events-v1.tsv",
"fixtures/conformance/gateway-clipboard-audit-v1.tsv",
"fixtures/conformance/gateway-clipboard-v1.tsv",
"fixtures/conformance/gateway-input-feedback-v1.tsv",
"fixtures/conformance/tunnel-v1.tsv"
],
"corpus_sha256": "544ba8765b4ddcfbe807d1d6e6788fe03d06b8e50d1424b82e4e512312e51248"
"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,6 @@
{
"username": "alice",
"provider": "local",
"roles": ["user"],
"role": "user"
}
@@ -0,0 +1,10 @@
{
"username": "alice",
"provider": "local",
"roles": ["user"],
"role": "user",
"native_identity": {
"client_device_id": "device-1",
"device_key_id": "key-1"
}
}
@@ -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"}
+11 -2
View File
@@ -6,7 +6,7 @@
"gateway": {
"id": "gateway-1",
"addresses": ["gateway.control.test:443"],
"public_identity": "gateway-1"
"public_identity": "gateway.control.test"
},
"tunnel": {
"versions": ["verse-gateway-v1/1"],
@@ -25,5 +25,14 @@
"expires_at": "2099-01-01T00:00:00Z",
"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,7 @@
{
"client_device_id": "device-1",
"device_key_id": "key-1",
"certificate_chain_pem": "-----BEGIN CERTIFICATE-----\nfixture\n-----END CERTIFICATE-----",
"trust_bundle_pem": "-----BEGIN CERTIFICATE-----\nfixture-ca\n-----END CERTIFICATE-----",
"expires_at": "2099-01-01T00:00:00Z"
}
@@ -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
}
+10
View File
@@ -0,0 +1,10 @@
{
"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"}
}
+21 -9
View File
@@ -4,12 +4,14 @@ This grammar is carried only in an authenticated Phase 3C gateway session. It
is deliberately provider-neutral: it never carries provider routes,
certificates, credentials, opaque provider packets, clipboard bytes, files, or
client-folder data. It does not change the v1 datagram header or any existing
release candidate.
kind encoding; the new kinds require explicit feature negotiation and a new
immutable Protocol release candidate.
## `input.sequenced.v1` payload (`VGI1`)
All multibyte fields are unsigned big-endian. The payload has exactly six bytes
of header followed by the declared body:
All multibyte fields are big-endian and unsigned unless a field is explicitly
marked signed. The payload has exactly six bytes of header followed by the
declared body:
| Offset | Size | Field | Rule |
|---:|---:|---|---|
@@ -32,12 +34,17 @@ during cleanup.
| `0x03` | relative mouse | `delta_x` (i16), `delta_y` (i16). |
| `0x04` | UTF-8 scalar | exactly one valid UTF-8 Unicode scalar, one through four bytes. |
| `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. |
| `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, and controller messages are delivered over the
gateway's reliable ordered input flow. Relative mouse is a state change, not a
pressed-state entry. The gateway maps the validated values to the provider's
separate keyboard, mouse, UTF-8, and controller control messages; it does not
forward this envelope to the provider.
Keyboard, mouse button, UTF-8, controller, absolute mouse, and scroll messages
are delivered over the gateway's reliable ordered input flow. Relative and
absolute mouse movement and scroll are state changes, not pressed-state
entries. Kinds `0x06` and `0x07` are accepted only when the session advertises
`input.absolute.v1` and `input.scroll.v1`, respectively. The gateway maps the
validated values to the provider's separate input messages; it does not forward
this envelope to the provider.
## Reliable control payload (`VGF1`)
@@ -54,7 +61,12 @@ is the following exact envelope:
| 8 | N | payload | exact type-specific body |
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),
`next_contiguous_sequence` (u16), `missing_before_highest` (u16),
`total_data_packets` (u16), `total_parity_packets` (u16),
+3145 -152
View File
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -1,18 +1,18 @@
{
"compatibility": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"current": "1",
"current": "2",
"datagram_registry": "registries/datagrams.json",
"feature_registry": "registries/features.json",
"n_minus_1": "0",
"n_minus_2": "-1",
"n_minus_1": "1",
"n_minus_2": "0",
"protocol": "versevdi-control",
"unsupported": [
"-2",
"2"
"-1",
"3"
]
},
"generator_sha256": "00fdba050eb924a54dd3d63aac0a38560341b675f0de4e3e9631ee895057a9b6",
"generator_sha256": "c5756780f59a82d8f56762e52f58e22a4d6aca6648157f45e5bad6559b9deeec",
"protocol_version": "1.0.0",
"schema_sha256": "a86cbdaf2cfb884e3d98467968007e731ca55c6f6eb6dbd5cd6b95e062a9b058"
"schema_sha256": "b9e72838b2ae02b3a47e7e2dfa03620b8580f2c98668e6eaf551ea8fb642d297"
}
Binary file not shown.
+1040 -65
View File
File diff suppressed because it is too large Load Diff
+1477 -156
View File
File diff suppressed because it is too large Load Diff
+242 -19
View File
@@ -2,7 +2,7 @@ openapi: 3.1.0
info:
title: VerseVDI control contract
version: 1.0.0
description: Bounded Phase 3A control-plane wire contract; no provider or media endpoints.
description: Bounded control-plane wire contract. The current SessionRequest shape is control wire version 2; no provider or media endpoints.
servers:
- url: /
tags:
@@ -10,6 +10,8 @@ tags:
- name: resources
- name: broker
- name: events
- name: session-quality
- name: gateway
paths:
/api/v1/auth/login:
post:
@@ -23,18 +25,11 @@ paths:
$ref: ../schemas/control-v1.schema.json#/$defs/LoginRequest
responses:
'200':
description: Browser session established through secure cookies.
description: Browser session established through secure cookies; native identity is structurally forbidden.
content:
application/json:
schema:
type: object
additionalProperties: false
required: [username, provider, roles, role]
properties:
username: {type: string, minLength: 1, maxLength: 256}
provider: {type: string, minLength: 1, maxLength: 64}
roles: {type: array, maxItems: 16, items: {type: string, maxLength: 64}}
role: {type: string, enum: [user, admin]}
$ref: ../schemas/control-v1.schema.json#/$defs/BrowserAuthenticatedSession
'400': {$ref: '#/components/responses/InvalidRequest'}
'401': {$ref: '#/components/responses/Unauthorized'}
'503': {$ref: '#/components/responses/Unavailable'}
@@ -66,18 +61,32 @@ paths:
- nativeBearer: []
responses:
'200':
description: Authenticated subject summary.
description: Route-context-specific authenticated subject summary. Browser responses forbid native identity; native bearer responses require it.
content:
application/json:
schema:
type: object
additionalProperties: false
required: [username, provider, roles, role]
properties:
username: {type: string, minLength: 1, maxLength: 256}
provider: {type: string, minLength: 1, maxLength: 64}
roles: {type: array, maxItems: 16, items: {type: string, maxLength: 64}}
role: {type: string, enum: [user, admin]}
oneOf:
- $ref: ../schemas/control-v1.schema.json#/$defs/BrowserAuthenticatedSession
- $ref: ../schemas/control-v1.schema.json#/$defs/NativeAuthenticatedSession
'401': {$ref: '#/components/responses/Unauthorized'}
/api/v1/auth/tunnel-credentials:
post:
tags: [authentication]
operationId: issueNativeTunnelCredential
security:
- nativeBearer: []
responses:
'200':
description: Short-lived native tunnel credential. The response must not be stored.
headers:
Cache-Control:
description: Prevent storage of the credential response.
required: true
schema: {type: string, const: no-store}
content:
application/json:
schema:
$ref: ../schemas/control-v1.schema.json#/$defs/NativeTunnelCredential
'401': {$ref: '#/components/responses/Unauthorized'}
/api/v1/auth/reauth:
post:
@@ -85,6 +94,8 @@ paths:
operationId: issueReauthenticationGrant
security:
- browserSession: []
browserCsrfCookie: []
browserCsrfHeader: []
requestBody:
required: true
content:
@@ -106,6 +117,8 @@ paths:
operationId: logoutSession
security:
- browserSession: []
browserCsrfCookie: []
browserCsrfHeader: []
- nativeBearer: []
responses:
'204': {description: Session revoked and browser cookies cleared.}
@@ -116,6 +129,8 @@ paths:
operationId: registerDevice
security:
- browserSession: []
browserCsrfCookie: []
browserCsrfHeader: []
requestBody:
required: true
content:
@@ -137,6 +152,8 @@ paths:
operationId: proveDevice
security:
- browserSession: []
browserCsrfCookie: []
browserCsrfHeader: []
parameters:
- $ref: '#/components/parameters/DeviceID'
requestBody:
@@ -160,6 +177,8 @@ paths:
operationId: revokeDevice
security:
- browserSession: []
browserCsrfCookie: []
browserCsrfHeader: []
parameters:
- $ref: '#/components/parameters/DeviceID'
responses:
@@ -189,8 +208,11 @@ paths:
post:
tags: [broker]
operationId: requestBrokerSession
description: Control wire version 2 endpoint. Legacy version-1 SessionRequest payloads containing client-supplied policy_snapshot are rejected.
security:
- browserSession: []
browserCsrfCookie: []
browserCsrfHeader: []
- nativeBearer: []
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
@@ -241,6 +263,8 @@ paths:
operationId: allocateBrokerSession
security:
- browserSession: []
browserCsrfCookie: []
browserCsrfHeader: []
- nativeBearer: []
parameters:
- $ref: '#/components/parameters/SessionID'
@@ -266,6 +290,8 @@ paths:
operationId: reconnectBrokerSession
security:
- browserSession: []
browserCsrfCookie: []
browserCsrfHeader: []
- nativeBearer: []
parameters:
- $ref: '#/components/parameters/SessionID'
@@ -282,6 +308,24 @@ paths:
application/json:
schema:
$ref: ../schemas/control-v1.schema.json#/$defs/ConnectionManifest
'202':
description: >-
Confirmed over-cap reconnect creates or returns a durable `session.display_relaunch` termination operation.
Before initial operation creation and on every replay, the authenticated principal and active client device/key MUST
match the broker session. Under the session and unique Stop lock, the Server MUST create at most one termination
operation total per broker session. A same-owner/device lost-response retry MUST return that same operation.
If a user Stop wins first, reconnect MUST return a stable non-202 result. If display relaunch wins first, a later user
Stop MUST converge on that same existing `StopOperation` without creating a second operation or issuing a second Terminate.
The operation reuses the existing one-way non-reissued Stop work/ack path. This response is not a manifest, does not
assert termination completion, and does not authorize a replacement session before `applied`.
After `applied`, the client submits a fresh `SessionRequest` with a new idempotency key.
Failed or `termination_unconfirmed` outcomes never auto-relaunch. Client local Stop or teardown MUST invalidate
relaunch generation so a later `applied` state cannot cause a fresh launch.
Maximum JSON body: 16384 bytes.
content:
application/json:
schema:
$ref: ../schemas/control-v1.schema.json#/$defs/StopOperation
'400': {$ref: '#/components/responses/InvalidRequest'}
'401': {$ref: '#/components/responses/Unauthorized'}
'404': {$ref: '#/components/responses/NotFound'}
@@ -292,6 +336,8 @@ paths:
operationId: cancelBrokerSession
security:
- browserSession: []
browserCsrfCookie: []
browserCsrfHeader: []
- nativeBearer: []
parameters:
- $ref: '#/components/parameters/SessionID'
@@ -300,6 +346,151 @@ paths:
'401': {$ref: '#/components/responses/Unauthorized'}
'404': {$ref: '#/components/responses/NotFound'}
'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:
get:
tags: [events]
@@ -345,10 +536,22 @@ components:
type: apiKey
in: cookie
name: versevdi_session
browserCsrfCookie:
type: apiKey
in: cookie
name: versevdi_csrf
description: Must be identical to X-CSRF-Token and is checked against Server session state.
browserCsrfHeader:
type: apiKey
in: header
name: X-CSRF-Token
description: Must be identical to the versevdi_csrf cookie and is checked against Server session state.
nativeBearer:
type: http
scheme: bearer
bearerFormat: opaque
gatewayMutualTLS:
type: mutualTLS
parameters:
DeviceID:
name: device_id
@@ -360,6 +563,26 @@ components:
in: path
required: true
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:
name: limit
in: query
@@ -15,4 +15,6 @@
## 4. Immutable boundary
- [ ] 4.1 Publish one new never-reused immutable Protocol version and resolve it from empty consumer caches
- [x] 4.1 Publish one new never-reused immutable Protocol version, resolve it
from separate empty consumer caches, and pin the fetched checksums in both
consumers
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-10
@@ -0,0 +1,54 @@
## Context
The JSON control schema is the generated-binding authority for broker requests
and manifests, while VGI1 is the provider-neutral input payload. Both contracts
are strict: old decoders reject unknown response fields and old gateways reject
unknown VGI kinds. Phase 3D therefore needs optional fields plus explicit
feature negotiation rather than a wire-version or protobuf change.
## Goals / Non-Goals
**Goals:**
- Preserve legacy JSON shapes when display negotiation is absent.
- Generate optional referenced objects correctly in Go, Rust, and Swift.
- Define exact, bounded, cross-language absolute-pointer and scroll bytes.
**Non-Goals:**
- Server clamp policy, database persistence, or provider translation.
- Live display renegotiation, provider packet exposure, or protobuf changes.
## Decisions
- Keep control wire version 1 and gate additions with exact feature IDs. This
avoids changing every legacy request while allowing strict clients to demand
the accepted display field.
- Use one reusable `DisplayMode` with the existing provider-policy dimension
bounds. Optional referenced objects become Go pointers so `omitempty` is real;
Rust and Swift retain their existing optional generation.
- Encode absolute pointer as four big-endian u16 values and scroll as two
big-endian i16 values in VGI1. Viewport coordinates are self-contained and
provider-neutral; provider scaling stays outside Protocol.
- Leave protobuf unchanged because the observed broker and VGI consumers use
JSON and fixed byte frames, not generated protobuf messages.
## Risks / Trade-offs
- [Old strict consumers reject new response fields] → emit them only after
`display.request.v1` negotiation.
- [New VGI kinds reach an old or unadvertised gateway] → reject before provider
translation unless the matching input feature is active.
- [Generated Go optional values serialize as zero objects] → require pointers,
omission tests, and clean second generation.
## Migration Plan
Freeze a new never-reused Protocol RC after full verification. Server and Data
Plane then pin that exact tag together. Legacy sessions omit all new fields and
continue using existing input kinds.
## Open Questions
None at the Protocol boundary; policy clamp and Apollo translation remain
consumer-owned work.
@@ -0,0 +1,38 @@
## Why
The Phase 3D native client must request a bounded display mode and send
provider-neutral absolute-pointer and high-resolution scroll input. The current
wire contract exposes only policy-selected stream dimensions and rejects both
new input kinds.
## What Changes
- Add one bounded `DisplayMode` object and optional display fields to session
request/session/manifest contracts while keeping control wire version 1.
- Register `display.request.v1`, `input.absolute.v1`, and `input.scroll.v1` so
every new behavior is explicitly negotiated.
- Extend VGI1 with exact absolute-pointer and signed scroll bodies while
preserving all existing kind encodings.
- Require a new immutable Protocol release candidate after deterministic
Go/Rust/Swift generation and conformance pass; existing tags remain unchanged.
## Capabilities
### New Capabilities
- `session-display-mode`: Optional feature-gated requested and effective display
mode disclosure for native session creation and manifests.
### Modified Capabilities
- `gateway-input-feedback`: Add negotiated provider-neutral absolute-pointer and
high-resolution scroll kinds to the existing sequenced input grammar.
## Impact
- JSON Schema, feature registry, VGI frame documentation, fixed conformance
fixtures, generated Go/Rust/Swift bindings, and strict validators.
- Connection Server session persistence/allocation and Data Plane input
translation after consumers pin the immutable Protocol candidate.
- Protobuf remains unchanged because no observed consumer uses it for these
JSON control or VGI payload paths.
@@ -0,0 +1,33 @@
## MODIFIED Requirements
### Requirement: Typed sequenced input envelope
The `input.sequenced.v1` payload SHALL begin with ASCII `VGI1`, a one-byte
event kind, and one-byte payload length. It SHALL contain exactly one bounded
keyboard, mouse-button, relative-mouse, UTF-8 scalar, controller-state,
absolute-mouse, or high-resolution-scroll event. Absolute mouse kind `0x06`
SHALL contain big-endian u16 x, y, viewport width, and viewport height, reject
zero viewports and coordinates outside the viewport, and require negotiated
`input.absolute.v1`. Scroll kind `0x07` SHALL contain big-endian signed i16
vertical and horizontal deltas and require negotiated `input.scroll.v1`. False
keyboard/mouse state and zeroed controller state are explicit releases.
Unknown or unadvertised kinds, length mismatches, malformed UTF-8, unsupported
controller indices, and reserved fields SHALL be rejected before provider
translation.
#### Scenario: Keyboard state change
- **WHEN** a client sends a valid keyboard press or release envelope
- **THEN** the gateway forwards the corresponding typed provider input on its
reliable keyboard channel and records the pressed state for cleanup.
#### Scenario: Absolute pointer and scroll
- **WHEN** an advertised client sends an in-viewport absolute coordinate or
signed scroll delta with the exact body length
- **THEN** the gateway accepts the provider-neutral event without adding it to
pressed-state cleanup.
#### Scenario: Invalid input envelope
- **WHEN** a client sends an unknown or unadvertised kind, invalid length,
malformed UTF-8 scalar, zero viewport, out-of-range coordinate, or nonzero
reserved field
- **THEN** the gateway rejects it without sending provider input or changing
pressed state.
@@ -0,0 +1,35 @@
## ADDED Requirements
### Requirement: Bounded display mode contract
The Protocol SHALL define `DisplayMode` with required integer
`resolution_width` from 320 through 16384, `resolution_height` from 200 through
8640, and `fps` from 1 through 240, rejecting missing, unknown, or out-of-range
fields.
#### Scenario: Valid display mode
- **WHEN** a client encodes a 2560 by 1440 display mode at 120 FPS
- **THEN** generated Go, Rust, and Swift bindings accept the same exact values.
#### Scenario: Invalid display mode
- **WHEN** any dimension or FPS is outside its bound or an unknown field exists
- **THEN** strict Protocol validation rejects the object.
### Requirement: Feature-gated optional display disclosure
`SessionRequest.requested_display_mode`,
`BrokerSession.requested_display_mode`,
`BrokerSession.effective_display_mode`, and `ManifestProfile.display_mode`
SHALL be optional references to `DisplayMode`. Go bindings SHALL use pointers
with JSON omission and Rust/Swift bindings SHALL use optionals. A client MUST
send the request only after `display.request.v1` negotiation, and a
display-aware manifest MUST contain the accepted mode. Legacy requests SHALL
produce legacy response shapes without these fields.
#### Scenario: Legacy request remains unchanged
- **WHEN** a client omits `requested_display_mode`
- **THEN** encoding omits the field and compatible Server responses omit all
display-mode fields.
#### Scenario: Display-aware request discloses acceptance
- **WHEN** a negotiated client sends a valid requested mode
- **THEN** session responses preserve the requested mode and the allocated
manifest includes the Server-accepted mode.
@@ -0,0 +1,17 @@
## 1. Contract sources
- [x] 1.1 Add failing Go omission/bounds tests and fixed absolute/scroll vectors.
- [x] 1.2 Add the bounded display schema, optional generated references, and feature registrations.
- [x] 1.3 Extend the VGI frame grammar and Python/Go/Rust/Swift classifiers without changing existing kinds.
## 2. Deterministic verification
- [x] 2.1 Regenerate Go/Rust/Swift bindings and pass focused Go, frame, source, binding, and strict-contract checks.
- [x] 2.2 Run the complete Protocol verifier with isolated compiler caches.
- [x] 2.3 Regenerate a second time, confirm zero drift, and inspect the exact source/generated diff.
## 3. Freeze
- [x] 3.1 Commit the verified source and generated bindings as one unsigned capability-sized change.
- [x] 3.2 Verify the next Phase 3D Protocol RC name is unused locally and remotely, then create one local immutable candidate tag without publishing it.
- [x] 3.3 Reconcile and archive this OpenSpec change after the immutable candidate is frozen.
@@ -0,0 +1,4 @@
schema: spec-driven
created: 2026-08-12
goal: Split client-facing session authority from the provider-bearing
Server-to-gateway authority for the coordinated RC4 hard cut.
@@ -0,0 +1,41 @@
## Context
RC3 uses one provider-bearing `SessionAuthority` for both the authenticated Server-to-gateway control plane and the gateway-to-client acknowledgement. Provider profile and identity are valid inputs to gateway provider work and release, but they are forbidden at the client boundary. Existing strict RC3 clients require those fields, so changing the client shape is intentionally incompatible.
## Goals / Non-Goals
**Goals:**
- Make provider disclosure structurally impossible in the client-facing authority type.
- Preserve the provider-bound Server-to-gateway admission, work, release, and cleanup contract.
- Produce strict, matching JSON Schema, Protobuf, Go, Rust, and Swift contracts.
**Non-Goals:**
- Supporting mixed RC3/RC4 gateway and client pairings.
- Changing `SessionAuthority`, `ProviderSessionWork`, `VERSION`, or global compatibility history.
- Adding response negotiation, optional provider fields, or permissive decoding.
## Decisions
1. Add `ClientSessionAuthority` with exactly `version`, `session_id`, `gateway_id`, `audience`, `reconnect_sequence`, `expires_at`, and `capabilities`. Reusing the common validation bounds keeps the new acknowledgement session-bound without representing provider data.
2. Keep the existing provider-bearing `SessionAuthority` unchanged for Server-to-gateway operations. Deleting its provider fields would broaden the security-sensitive change into Server admission and provider-work validation.
3. Treat RC4 as a coordinated hard cut. A dual decoder would still accept the forbidden RC3 shape and is unnecessary for an unreleased candidate.
4. Use the existing generator unchanged. The JSON Schema definition is sufficient to generate strict Go, Rust, and Swift types; the matching Protobuf message uses fields 1 through 7.
## Risks / Trade-offs
- [RC3 and RC4 clients are not wire-compatible] → Pin and qualify Server, gateway, and client as one exact RC4 set; retain RC3 as an immutable rollback set.
- [A future gateway could serialize the wrong authority type] → Consumer gateway tests must capture the raw acknowledgement and require `ClientSessionAuthority` with no provider-bearing keys.
- [Strict decoding rejects future additive fields] → Version a future client authority explicitly instead of weakening this v1 decoder.
## Migration Plan
1. Publish the verified immutable Protocol RC4 tag.
2. Repin Data, Server, and macOS to the exact RC4 commit.
3. Change gateway egress and client decoders together, then qualify the exact all-RC4 set.
4. Roll back only as the complete immutable RC3 set; do not retag or mix candidates.
## Open Questions
None for this pre-release hard cut. Evidence of deployed RC3 coexistence would require a separate negotiated-version design and blocks this migration model.
@@ -0,0 +1,23 @@
## Why
The gateway currently serializes the provider-bearing Server-to-gateway `SessionAuthority` to clients, crossing provider identity into a client trust boundary that forbids it. RC4 must make that boundary structural before the pre-release client set is qualified.
## What Changes
- Add a strict provider-free `ClientSessionAuthority` with the seven session, gateway, audience, reconnect, expiry, and capability fields shared with `SessionAuthority`.
- Keep `SessionAuthority` and `ProviderSessionWork` unchanged for the authenticated Server-to-gateway control plane.
- **BREAKING** Replace the gateway-to-client RC3 response shape with `ClientSessionAuthority` as a coordinated RC4 hard cut; no mixed RC3/RC4 compatibility is claimed.
## Capabilities
### New Capabilities
- `gateway-transport-and-admission`: Defines the distinct client-facing authority and its provider-free gateway admission boundary.
### Modified Capabilities
None.
## Impact
Protocol JSON Schema, tunnel Protobuf, generated Go/Rust/Swift bindings, and consumer Protocol pins advance together to `v1.0.0-phase3d-macos-rc.4`. `VERSION`, global compatibility history, and the Server-to-gateway provider authority remain unchanged.
@@ -0,0 +1,30 @@
## ADDED Requirements
### Requirement: Client-facing authority is provider-free
The gateway-to-client acknowledgement SHALL use `ClientSessionAuthority` version `"1"` containing exactly `version`, `session_id`, `gateway_id`, `audience`, `reconnect_sequence`, `expires_at`, and `capabilities`. The contract SHALL reject missing required fields, unknown fields including provider identities and routes, invalid or noncanonical expiry timestamps, and trailing JSON values.
#### Scenario: Gateway acknowledges an admitted client
- **WHEN** provider work succeeds and gateway and client capabilities intersect
- **THEN** the gateway returns a valid `ClientSessionAuthority` containing the selected capabilities and no provider-bearing field
#### Scenario: Client receives provider-bearing authority
- **WHEN** a client authority payload contains `provider_profile`, `provider_identity`, a provider route, or any unknown key
- **THEN** the strict client authority decoder rejects the payload
#### Scenario: Client receives incomplete or malformed authority
- **WHEN** a client authority omits any required binding, has an invalid expiry, or is followed by another JSON value
- **THEN** the strict client authority decoder rejects the payload
### Requirement: Server-to-gateway authority remains provider-bound
The authenticated Server-to-gateway control plane SHALL continue to use the existing provider-bearing `SessionAuthority` for admission, provider work, release, and cleanup. `SessionAuthority` and `ProviderSessionWork` fields and semantics MUST remain unchanged by this change.
#### Scenario: Gateway performs provider work
- **WHEN** the Server admits a gateway session and the gateway requests provider work
- **THEN** the original provider-bearing `SessionAuthority` continues to bind provider work and subsequent release or cleanup
### Requirement: RC4 is a coordinated hard cut
The RC4 gateway and client SHALL use `ClientSessionAuthority`; mixed RC3/RC4 gateway-client compatibility SHALL NOT be claimed. RC4 SHALL NOT add optional provider fields, a dual decoder, or response negotiation for RC3.
#### Scenario: RC4 candidate is qualified
- **WHEN** the Protocol RC4 tag is pinned by Server, gateway, and client
- **THEN** qualification uses only that exact coordinated set
@@ -0,0 +1,10 @@
## 1. Contract and regressions
- [x] 1.1 Add RED-first Go, Swift, Rust, and Protobuf regressions for the strict provider-free authority.
- [x] 1.2 Add the exact seven-field JSON Schema and Protobuf `ClientSessionAuthority` without changing existing authority contracts.
- [x] 1.3 Regenerate Go, Rust, Swift, Protobuf, and manifest outputs using repository tooling.
## 2. Verification
- [x] 2.1 Pass focused Go and generated-contract regressions.
- [x] 2.2 Pass strict OpenSpec validation, full `make verify`, second-generation cleanliness, and diff checks.
+19 -7
View File
@@ -7,20 +7,32 @@ used across the authenticated Verse gateway boundary.
### Requirement: Typed sequenced input envelope
The `input.sequenced.v1` payload SHALL begin with ASCII `VGI1`, a one-byte
event kind, and one-byte payload length. It SHALL contain exactly one bounded
keyboard, mouse-button, relative-mouse, UTF-8 scalar, or controller-state
event. False keyboard/mouse state and zeroed controller state are explicit
releases. Multibyte integer fields SHALL be big-endian. Unknown kinds,
length mismatches, malformed UTF-8, unsupported controller indices, and
reserved fields SHALL be rejected before provider translation.
keyboard, mouse-button, relative-mouse, UTF-8 scalar, controller-state,
absolute-mouse, or high-resolution-scroll event. Absolute mouse kind `0x06`
SHALL contain big-endian u16 x, y, viewport width, and viewport height, reject
zero viewports and coordinates outside the viewport, and require negotiated
`input.absolute.v1`. Scroll kind `0x07` SHALL contain big-endian signed i16
vertical and horizontal deltas and require negotiated `input.scroll.v1`. False
keyboard/mouse state and zeroed controller state are explicit releases.
Unknown or unadvertised kinds, length mismatches, malformed UTF-8, unsupported
controller indices, and reserved fields SHALL be rejected before provider
translation.
#### Scenario: Keyboard state change
- **WHEN** a client sends a valid keyboard press or release envelope
- **THEN** the gateway forwards the corresponding typed provider input on its
reliable keyboard channel and records the pressed state for cleanup.
#### Scenario: Absolute pointer and scroll
- **WHEN** an advertised client sends an in-viewport absolute coordinate or
signed scroll delta with the exact body length
- **THEN** the gateway accepts the provider-neutral event without adding it to
pressed-state cleanup.
#### Scenario: Invalid input envelope
- **WHEN** a client sends an envelope with an unknown event kind, invalid
length, malformed UTF-8 scalar, or nonzero reserved field
- **WHEN** a client sends an unknown or unadvertised kind, invalid length,
malformed UTF-8 scalar, zero viewport, out-of-range coordinate, or nonzero
reserved field
- **THEN** the gateway rejects it without sending provider input or changing
pressed state.
@@ -0,0 +1,37 @@
# gateway-media-framing Specification
## Purpose
Define the versioned datagram framing, bounded reassembly, and explicit negotiation required to carry complete encoded media units.
## Requirements
### Requirement: Versioned complete encoded-frame datagrams
The Protocol SHALL register `datagram-v2` with a 23-byte network-order header containing magic, version, channel, flags, sequence, timestamp, unsigned 16-bit fragment index, unsigned 16-bit fragment count, and unsigned 16-bit payload length. Media datagrams MUST remain at most 1,200 bytes with at most 1,177 payload bytes, and one sequence MUST represent exactly one complete encoded unit of at most 1,048,576 bytes and 891 fragments.
#### Scenario: Bounded large encoded frame
- **WHEN** an encoded media frame is larger than the datagram-v1 18,864-byte limit but no larger than 1,048,576 bytes
- **THEN** datagram-v2 carries it under one sequence in at most 891 independently bounded fragments
#### Scenario: Oversized encoded frame
- **WHEN** a sender attempts more than 1,048,576 complete bytes or 891 fragments
- **THEN** framing rejects the unit before sending or allocating proportional state
### Requirement: Bounded complete-frame reassembly
Datagram-v2 reassembly SHALL retain at most four incomplete media units and only the bytes actually received, permit bounded fragment reorder and exact duplicate fragments, reject conflicting duplicates or inconsistent metadata, expire incomplete state after 250 milliseconds, and emit a payload only after every fragment is present exactly once.
#### Scenario: Reordered frame completes
- **WHEN** every valid fragment for one unit arrives out of order within the reassembly bounds
- **THEN** the receiver emits exactly one byte-identical complete encoded frame with its original boundary
#### Scenario: Malformed or stale reassembly
- **WHEN** fragments conflict, metadata changes, an index or count is invalid, state exceeds four incomplete units, or a unit exceeds its timeout
- **THEN** the receiver rejects or evicts that incomplete unit without proportional allocation or payload emission
### Requirement: Explicit framing negotiation
`CapabilityProfile.framing` SHALL accept only registered `datagram-v1` or `datagram-v2` values. A peer MUST use datagram-v2 only after exact capability intersection and MUST NOT reinterpret datagram-v1 bytes as datagram-v2.
#### Scenario: Independent datagram-v2 peers overlap
- **WHEN** both peers advertise the registered datagram-v2 framing profile
- **THEN** generated Go, Rust, and Swift validation and fixed conformance accept the profile
#### Scenario: Unknown or downgraded framing
- **WHEN** a peer advertises an unknown framing value or the peers advertise different versions
- **THEN** capability validation or intersection rejects the session before media forwarding
@@ -0,0 +1,56 @@
# native-session-tunnel-credentials Specification
## Purpose
Define server-owned native session identity and short-lived tunnel credential disclosure without accepting client-owned policy.
## Requirements
### Requirement: Client-owned allocation policy is removed by a coordinated wire-version cutover
Control wire version `2` SHALL be current, wire version `1` SHALL be n-1, wire version `0` SHALL be n-2, and `control.v2` SHALL identify the current control feature. The wire-version-2 `SessionRequest` MUST reject `policy_snapshot`; `BrokerSession` SHALL retain the Server-owned policy snapshot disclosure. This removal is intentionally incompatible with the wire-version-1 request shape and SHALL require coordinated Protocol, Server, Data Plane, and client pin updates. Declaring wire version `1` as n-1 does not permit a wire-version-1 request body to be decoded as wire version `2`.
#### Scenario: Client attempts to supply policy
- **WHEN** a session request contains `policy_snapshot`
- **THEN** strict Protocol decoding rejects the unknown field
#### Scenario: A legacy client presents a wire-version-1 request shape
- **WHEN** a client uses the removed client-owned policy field or does not negotiate `control.v2`
- **THEN** the wire-version-2 broker request path rejects the request instead of silently interpreting it as compatible
### Requirement: Authentication response shapes are route-specific
Browser login and browser session responses SHALL use `BrowserAuthenticatedSession`, which forbids `native_identity`. Native bearer session responses SHALL use `NativeAuthenticatedSession`, which requires a `NativeSessionIdentity` containing both `client_device_id` and `device_key_id`. Both response types SHALL require every `roles` item to contain 1 through 64 UTF-8 bytes.
#### Scenario: Native identity is incomplete
- **WHEN** a native session response omits either identity field
- **THEN** strict Protocol decoding rejects the nested object
#### Scenario: Browser response leaks native identity
- **WHEN** a browser login or browser session response contains `native_identity`
- **THEN** strict Protocol decoding rejects the response
#### Scenario: Authentication response has a malformed role
- **WHEN** any `roles` item is empty or exceeds 64 UTF-8 bytes
- **THEN** every generated binding rejects the response
### Requirement: Native tunnel credentials are non-cacheable
`POST /api/v1/auth/tunnel-credentials` SHALL accept only native bearer authentication, no request body, and return a bounded `NativeTunnelCredential` with `Cache-Control: no-store`. `NativeTunnelCredential.expires_at` SHALL be canonical RFC 3339 UTC in every generated binding: an uppercase `T`, a `Z` suffix, and either no fractional seconds or a 1-through-9-digit non-zero-terminated fractional component.
#### Scenario: Native client requests tunnel credentials
- **WHEN** an authenticated native client calls the endpoint
- **THEN** the response identifies the client device and key, supplies certificate and trust PEM, gives an expiry, and cannot be stored
#### Scenario: Native tunnel credential uses a noncanonical expiry
- **WHEN** `expires_at` uses a numeric offset or a noncanonical fractional representation
- **THEN** every generated binding rejects the credential
### Requirement: Swift bindings support the macOS client deployment floor
The generated Swift package SHALL declare macOS 14 as its minimum supported macOS platform.
#### Scenario: Native client consumes the Swift package
- **WHEN** the package manifest is resolved for the Phase 3D macOS client
- **THEN** its platform declaration permits macOS 14 and later
### Requirement: Manifest public identity is the TLS server name
`ConnectionManifest.gateway.public_identity` SHALL be the exact TLS server name and SHALL remain distinct from dial addresses, gateway UUIDs, certificate fingerprints, and provider identities.
#### Scenario: Client connects to a gateway address
- **WHEN** a client dials any advertised gateway address
- **THEN** it verifies TLS against `gateway.public_identity`
@@ -0,0 +1,38 @@
# session-display-mode Specification
## Purpose
TBD - created by archiving change phase-3d-client-display-input. Update Purpose after archive.
## Requirements
### Requirement: Bounded display mode contract
The Protocol SHALL define `DisplayMode` with required integer
`resolution_width` from 320 through 16384, `resolution_height` from 200 through
8640, and `fps` from 1 through 240, rejecting missing, unknown, or out-of-range
fields.
#### Scenario: Valid display mode
- **WHEN** a client encodes a 2560 by 1440 display mode at 120 FPS
- **THEN** generated Go, Rust, and Swift bindings accept the same exact values.
#### Scenario: Invalid display mode
- **WHEN** any dimension or FPS is outside its bound or an unknown field exists
- **THEN** strict Protocol validation rejects the object.
### Requirement: Feature-gated optional display disclosure
`SessionRequest.requested_display_mode`,
`BrokerSession.requested_display_mode`,
`BrokerSession.effective_display_mode`, and `ManifestProfile.display_mode`
SHALL be optional references to `DisplayMode`. Go bindings SHALL use pointers
with JSON omission and Rust/Swift bindings SHALL use optionals. A client MUST
send the request only after `display.request.v1` negotiation, and a
display-aware manifest MUST contain the accepted mode. Legacy requests SHALL
produce legacy response shapes without these fields.
#### Scenario: Legacy request remains unchanged
- **WHEN** a client omits `requested_display_mode`
- **THEN** encoding omits the field and compatible Server responses omit all
display-mode fields.
#### Scenario: Display-aware request discloses acceptance
- **WHEN** a negotiated client sends a valid requested mode
- **THEN** session responses preserve the requested mode and the allocated
manifest includes the Server-accepted mode.
+128 -2
View File
@@ -54,9 +54,56 @@ message CapabilityProfile {
string transport = 1;
string framing = 2;
string media = 3;
string audio = 4;
reserved 4;
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 {
@@ -115,6 +162,18 @@ message SessionAuthority {
CapabilityProfile capabilities = 7;
string provider_profile = 8;
string provider_identity = 9;
SelectedSessionDescriptor selected_descriptor = 10;
}
message ClientSessionAuthority {
string version = 1;
string session_id = 2;
string gateway_id = 3;
string audience = 4;
uint64 reconnect_sequence = 5;
google.protobuf.Timestamp expires_at = 6;
CapabilityProfile capabilities = 7;
SelectedSessionDescriptor selected_descriptor = 8;
}
message ProviderSessionWork {
@@ -137,6 +196,73 @@ message ProviderSessionWork {
string client_id = 17;
ClipboardPolicy clipboard_policy = 18;
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 {
+3
View File
@@ -8,6 +8,9 @@
"datagrams": [
{"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": "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": "media.video.v1", "direction": "server-to-client", "max_payload_bytes": 1200},
{"id": "media.audio.v1", "direction": "server-to-client", "max_payload_bytes": 1200},
+9 -1
View File
@@ -2,7 +2,15 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"features": [
{"id": "control.v1", "direction": "bidirectional", "max_frame_bytes": 65536},
{"id": "control.v2", "direction": "bidirectional", "max_frame_bytes": 65536},
{"id": "events.resume.v1", "direction": "server-to-client", "max_frame_bytes": 65536},
{"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": "input.absolute.v1", "direction": "client-to-server", "max_frame_bytes": 14},
{"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}
]
}
+318 -32
View File
@@ -64,22 +64,24 @@
"AssignedDesktop": {
"type": "object",
"additionalProperties": false,
"required": ["assignment_id", "pool_id", "name", "availability"],
"required": ["assignment_id", "pool_id", "name", "availability", "quality_limits"],
"properties": {
"assignment_id": {"type": "string", "minLength": 1, "maxLength": 128},
"pool_id": {"type": "string", "minLength": 1, "maxLength": 128},
"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": {
"type": "object",
"additionalProperties": false,
"required": ["pool_id", "name", "assignment_state"],
"required": ["pool_id", "name", "assignment_state", "quality_limits"],
"properties": {
"pool_id": {"type": "string", "minLength": 1, "maxLength": 128},
"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": {
@@ -178,32 +180,160 @@
"refresh_expires_at": {"type": "string", "format": "date-time", "maxLength": 64}
}
},
"NativeSessionIdentity": {
"type": "object",
"additionalProperties": false,
"required": ["client_device_id", "device_key_id"],
"properties": {
"client_device_id": {"type": "string", "minLength": 1, "maxLength": 128},
"device_key_id": {"type": "string", "minLength": 1, "maxLength": 128}
}
},
"BrowserAuthenticatedSession": {
"type": "object",
"additionalProperties": false,
"required": ["username", "provider", "roles", "role"],
"properties": {
"username": {"type": "string", "minLength": 1, "maxLength": 256},
"provider": {"type": "string", "minLength": 1, "maxLength": 64},
"roles": {"type": "array", "maxItems": 16, "items": {"type": "string", "minLength": 1, "maxLength": 64, "x-max-bytes": 64}},
"role": {"type": "string", "enum": ["user", "admin"]}
}
},
"NativeAuthenticatedSession": {
"type": "object",
"additionalProperties": false,
"required": ["username", "provider", "roles", "role", "native_identity"],
"properties": {
"username": {"type": "string", "minLength": 1, "maxLength": 256},
"provider": {"type": "string", "minLength": 1, "maxLength": 64},
"roles": {"type": "array", "maxItems": 16, "items": {"type": "string", "minLength": 1, "maxLength": 64, "x-max-bytes": 64}},
"role": {"type": "string", "enum": ["user", "admin"]},
"native_identity": {"$ref": "#/$defs/NativeSessionIdentity"}
}
},
"NativeTunnelCredential": {
"type": "object",
"additionalProperties": false,
"required": ["client_device_id", "device_key_id", "certificate_chain_pem", "trust_bundle_pem", "expires_at"],
"properties": {
"client_device_id": {"type": "string", "minLength": 1, "maxLength": 128},
"device_key_id": {"type": "string", "minLength": 1, "maxLength": 128},
"certificate_chain_pem": {"type": "string", "minLength": 1, "maxLength": 65536},
"trust_bundle_pem": {"type": "string", "minLength": 1, "maxLength": 65536},
"expires_at": {"type": "string", "format": "date-time", "maxLength": 64}
}
},
"DisplayMode": {
"type": "object",
"additionalProperties": false,
"required": ["resolution_width", "resolution_height", "fps"],
"properties": {
"resolution_width": {"type": "integer", "minimum": 320, "maximum": 16384},
"resolution_height": {"type": "integer", "minimum": 200, "maximum": 8640},
"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": {
"type": "object",
"additionalProperties": false,
"required": ["client_device_id", "device_key_id", "pool_id", "idempotency_key", "policy_snapshot"],
"required": ["client_device_id", "device_key_id", "pool_id", "idempotency_key", "video_profiles", "bitrate_preference"],
"properties": {
"client_device_id": {"type": "string", "minLength": 1, "maxLength": 128},
"device_key_id": {"type": "string", "minLength": 1, "maxLength": 128},
"pool_id": {"type": "string", "minLength": 1, "maxLength": 128},
"idempotency_key": {"type": "string", "minLength": 1, "maxLength": 256},
"policy_snapshot": {"$ref": "#/$defs/AllocationPolicy"}
"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"}
}
},
"ReconnectRequest": {
"type": "object",
"additionalProperties": false,
"required": ["client_device_id", "device_key_id", "expected_version"],
"required": ["client_device_id", "device_key_id", "expected_version", "display_relaunch_confirmed"],
"properties": {
"client_device_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": {
"type": "object",
"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": {
"id": {"type": "string", "minLength": 1, "maxLength": 128},
"principal_id": {"type": "string", "minLength": 1, "maxLength": 128},
@@ -219,7 +349,12 @@
"correlation_id": {"type": "string", "minLength": 1, "maxLength": 128},
"requested_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"},
"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": {
@@ -241,7 +376,7 @@
"ConnectionManifest": {
"type": "object",
"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": {
"version": {"type": "string", "const": "1"},
"purpose": {"type": "string", "enum": ["launch", "reconnect"]},
@@ -251,7 +386,8 @@
"tunnel": {"$ref": "#/$defs/ManifestTunnel"},
"profile": {"$ref": "#/$defs/ManifestProfile"},
"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": {
@@ -261,7 +397,12 @@
"properties": {
"id": {"type": "string", "minLength": 1, "maxLength": 128},
"addresses": {"type": "array", "minItems": 1, "maxItems": 4, "items": {"type": "string", "minLength": 1, "maxLength": 256}},
"public_identity": {"type": "string", "minLength": 1, "maxLength": 256}
"public_identity": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"description": "Exact TLS server name; distinct from dial addresses, gateway UUIDs, certificate fingerprints, and provider identities."
}
}
},
"ManifestTunnel": {
@@ -279,7 +420,8 @@
"required": ["id", "bounds"],
"properties": {
"id": {"type": "string", "minLength": 1, "maxLength": 128},
"bounds": {"$ref": "#/$defs/ManifestBounds"}
"bounds": {"$ref": "#/$defs/ManifestBounds"},
"display_mode": {"$ref": "#/$defs/DisplayMode"}
}
},
"ManifestBounds": {
@@ -369,20 +511,14 @@
"CapabilityProfile": {
"type": "object",
"additionalProperties": false,
"required": ["transport", "framing", "media", "audio", "source_rate_control", "client_decode"],
"required": ["transport", "framing", "media", "source_rate_control", "video_profiles", "audio_profiles"],
"properties": {
"transport": {"type": "string", "minLength": 1, "maxLength": 64},
"framing": {"type": "string", "enum": ["datagram-v1", "datagram-v2"]},
"media": {"type": "string", "minLength": 1, "maxLength": 64},
"audio": {"type": "string", "minLength": 1, "maxLength": 64},
"source_rate_control": {"type": "string", "minLength": 1, "maxLength": 64},
"client_decode": {
"type": "array",
"minItems": 1,
"maxItems": 2,
"uniqueItems": true,
"items": {"type": "string", "enum": ["h264-opus", "hevc-opus"]}
}
"video_profiles": {"type": "array", "minItems": 1, "maxItems": 12, "uniqueItems": true, "items": {"$ref": "#/$defs/VideoProfile"}},
"audio_profiles": {"type": "array", "minItems": 1, "maxItems": 1, "uniqueItems": true, "items": {"$ref": "#/$defs/AudioProfile"}}
}
},
"GatewayRegistration": {
@@ -476,7 +612,7 @@
"SessionAuthority": {
"type": "object",
"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": {
"version": {"type": "string", "const": "1"},
"session_id": {"type": "string", "minLength": 1, "maxLength": 128},
@@ -486,20 +622,170 @@
"expires_at": {"type": "string", "format": "date-time", "maxLength": 64},
"capabilities": {"$ref": "#/$defs/CapabilityProfile"},
"provider_profile": {"type": "string", "enum": ["apollo"]},
"provider_identity": {"type": "string", "minLength": 1, "maxLength": 256}
"provider_identity": {"type": "string", "minLength": 1, "maxLength": 256},
"selected_descriptor": {"$ref": "#/$defs/SelectedSessionDescriptor"}
}
},
"ClientSessionAuthority": {
"type": "object",
"additionalProperties": false,
"required": ["version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities", "selected_descriptor"],
"properties": {
"version": {"type": "string", "const": "1"},
"session_id": {"type": "string", "minLength": 1, "maxLength": 128},
"gateway_id": {"type": "string", "minLength": 1, "maxLength": 128},
"audience": {"type": "string", "minLength": 1, "maxLength": 256},
"reconnect_sequence": {"type": "integer", "minimum": 0},
"expires_at": {"type": "string", "format": "date-time", "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"},
"selected_descriptor": {"$ref": "#/$defs/SelectedSessionDescriptor"}
}
},
"ProviderStreamPolicy": {
"type": "object",
"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": {
"resolution_width": {"type": "integer", "minimum": 320, "maximum": 16384},
"resolution_height": {"type": "integer", "minimum": 200, "maximum": 8640},
"fps": {"type": "integer", "minimum": 1, "maximum": 240},
"codec": {"type": "string", "enum": ["H264", "HEVC", "AV1"]},
"bitrate_kbps": {"type": "integer", "minimum": 100, "maximum": 1000000},
"audio_enabled": {"type": "boolean"}
"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}
}
},
"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": {
+308 -26
View File
@@ -1,6 +1,9 @@
package protocol_test
import (
"bytes"
"encoding/hex"
"encoding/json"
"reflect"
"strings"
"testing"
@@ -8,8 +11,65 @@ import (
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) {
serverID, _ := hex.DecodeString("00112233445566778899aabbccddeeff")
principalID, _ := hex.DecodeString("102132435465768798a9bacbdcedfe0f")
deviceID, _ := hex.DecodeString("ffeeddccbbaa99887766554433221100")
challenge, _ := hex.DecodeString("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")
want, _ := hex.DecodeString("76657273657664692d6465766963652d70726f6f662d763100112233445566778899aabbccddeeff102132435465768798a9bacbdcedfe0fffeeddccbbaa99887766554433221100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f0000018bcfe5687b")
got, err := protocol.DeviceRegistrationProofTranscript(serverID, principalID, deviceID, challenge, 1700000000123)
if err != nil || !bytes.Equal(got, want) {
t.Fatalf("DeviceRegistrationProofTranscript() = %x, %v; want %x", got, err, want)
}
tests := []struct {
name string
serverID, principalID, deviceID, challenge []byte
expiry int64
field, code string
}{
{"server-short", serverID[:15], principalID, deviceID, challenge, 0, "server_id", "invalid_length"},
{"server-long", append(append([]byte(nil), serverID...), 0), principalID, deviceID, challenge, 0, "server_id", "invalid_length"},
{"principal-short", serverID, principalID[:15], deviceID, challenge, 0, "principal_id", "invalid_length"},
{"principal-long", serverID, append(append([]byte(nil), principalID...), 0), deviceID, challenge, 0, "principal_id", "invalid_length"},
{"device-short", serverID, principalID, deviceID[:15], challenge, 0, "device_id", "invalid_length"},
{"device-long", serverID, principalID, append(append([]byte(nil), deviceID...), 0), challenge, 0, "device_id", "invalid_length"},
{"challenge-short", serverID, principalID, deviceID, challenge[:31], 0, "challenge", "invalid_length"},
{"challenge-long", serverID, principalID, deviceID, append(append([]byte(nil), challenge...), 0), 0, "challenge", "invalid_length"},
{"negative-expiry", serverID, principalID, deviceID, challenge, -1, "expiry_unix_milliseconds", "minimum"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := protocol.DeviceRegistrationProofTranscript(test.serverID, test.principalID, test.deviceID, test.challenge, test.expiry)
validation, ok := err.(protocol.ValidationError)
if !ok || len(validation.Violations) != 1 || validation.Violations[0] != (protocol.FieldViolation{Field: test.field, Code: test.code}) {
t.Fatalf("error = %#v; want %s/%s validation error", err, test.field, test.code)
}
})
}
}
func TestManifestRejectsForbiddenAndUnknownFields(t *testing.T) {
valid := `{"version":"1","purpose":"launch","session_id":"session-1","reconnect_sequence":0,"gateway":{"id":"gateway-1","addresses":["gateway.control.test:443"],"public_identity":"gateway-1"},"tunnel":{"versions":["verse-gateway-v1/1"],"features":["control.v1"]},"profile":{"id":"standard","bounds":{"minimum_kbps":1,"target_kbps":2,"maximum_kbps":3}},"grant":{"opaque_value":"opaque-one-time-grant-value-with-at-least-43-bytes","expires_at":"2099-01-01T00:00:00Z","audience":"versevdi-gateway"},"correlation_id":"correlation-1"}`
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))
if err != nil || manifest.Gateway.ID != "gateway-1" {
t.Fatalf("valid manifest = %+v, err = %v", manifest, err)
@@ -22,6 +82,152 @@ func TestManifestRejectsForbiddenAndUnknownFields(t *testing.T) {
}
}
func TestDisplayModeIsOptionalStrictAndBounded(t *testing.T) {
displayType := reflect.TypeOf(protocol.DisplayMode{})
for _, field := range []struct {
owner reflect.Type
name string
}{
{reflect.TypeOf(protocol.SessionRequest{}), "RequestedDisplayMode"},
{reflect.TypeOf(protocol.BrokerSession{}), "RequestedDisplayMode"},
{reflect.TypeOf(protocol.BrokerSession{}), "EffectiveDisplayMode"},
{reflect.TypeOf(protocol.ManifestProfile{}), "DisplayMode"},
} {
actual, ok := field.owner.FieldByName(field.name)
if !ok || actual.Type.Kind() != reflect.Pointer || actual.Type.Elem() != displayType {
t.Fatalf("%s.%s must be *DisplayMode, got %v", field.owner, field.name, actual.Type)
}
}
request := protocol.SessionRequest{
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)
if err != nil {
t.Fatalf("EncodeSessionRequest legacy request: %v", err)
}
if bytes.Contains(legacy, []byte("requested_display_mode")) {
t.Fatalf("legacy request disclosed an absent display mode: %s", legacy)
}
mode := protocol.DisplayMode{ResolutionWidth: 2560, ResolutionHeight: 1440, Fps: 120}
request.RequestedDisplayMode = &mode
aware, err := protocol.EncodeSessionRequest(request)
if err != nil || !bytes.Contains(aware, []byte(`"requested_display_mode":{"resolution_width":2560,"resolution_height":1440,"fps":120}`)) {
t.Fatalf("display-aware request = %s, err = %v", aware, err)
}
decoded, err := protocol.DecodeSessionRequest(aware)
if err != nil || decoded.RequestedDisplayMode == nil || *decoded.RequestedDisplayMode != mode {
t.Fatalf("DecodeSessionRequest display mode = %+v, err = %v", decoded.RequestedDisplayMode, err)
}
nullMode := bytes.Replace(aware, []byte(`{"resolution_width":2560,"resolution_height":1440,"fps":120}`), []byte("null"), 1)
if _, err := protocol.DecodeSessionRequest(nullMode); err == nil {
t.Fatalf("DecodeSessionRequest accepted explicit null display mode: %s", nullMode)
}
profile := protocol.ManifestProfile{
ID: "standard",
Bounds: protocol.ManifestBounds{MinimumKbps: 1000, TargetKbps: 2000, MaximumKbps: 3000},
}
legacyProfile, err := protocol.EncodeManifestProfile(profile)
if err != nil || bytes.Contains(legacyProfile, []byte("display_mode")) {
t.Fatalf("legacy profile = %s, err = %v", legacyProfile, err)
}
profile.DisplayMode = &mode
awareProfile, err := protocol.EncodeManifestProfile(profile)
if err != nil || !bytes.Contains(awareProfile, []byte(`"display_mode":{"resolution_width":2560,"resolution_height":1440,"fps":120}`)) {
t.Fatalf("display-aware profile = %s, err = %v", awareProfile, err)
}
for _, invalid := range []string{
`{"resolution_width":319,"resolution_height":1440,"fps":120}`,
`{"resolution_width":16385,"resolution_height":1440,"fps":120}`,
`{"resolution_width":2560,"resolution_height":199,"fps":120}`,
`{"resolution_width":2560,"resolution_height":8641,"fps":120}`,
`{"resolution_width":2560,"resolution_height":1440,"fps":0}`,
`{"resolution_width":2560,"resolution_height":1440,"fps":241}`,
`{"resolution_width":2560,"resolution_height":1440,"fps":120,"unknown":true}`,
} {
if _, err := protocol.DecodeDisplayMode([]byte(invalid)); err == nil {
t.Fatalf("DecodeDisplayMode accepted %s", invalid)
}
}
}
func TestSessionRequestRejectsClientPolicySnapshot(t *testing.T) {
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)
}
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 {
t.Fatalf("policy-free SessionRequest rejected: %v", err)
}
withPolicy := strings.Replace(request, `"idempotency_key":"request-1"`, `"idempotency_key":"request-1","policy_snapshot":{}`, 1)
if _, err := protocol.DecodeSessionRequest([]byte(withPolicy)); err == nil {
t.Fatal("SessionRequest accepted client-supplied policy_snapshot")
}
if _, ok := reflect.TypeOf(protocol.SessionRequest{}).FieldByName("PolicySnapshot"); ok {
t.Fatal("SessionRequest generated a client-owned PolicySnapshot field")
}
if _, ok := reflect.TypeOf(protocol.BrokerSession{}).FieldByName("PolicySnapshot"); !ok {
t.Fatal("BrokerSession lost its server-owned PolicySnapshot disclosure")
}
}
func TestAuthenticatedSessionShapesAreRouteSpecific(t *testing.T) {
browser := `{"username":"alice","provider":"local","roles":["user"],"role":"user"}`
decodedBrowser, err := protocol.DecodeBrowserAuthenticatedSession([]byte(browser))
if err != nil || decodedBrowser.Username != "alice" {
t.Fatalf("browser session = %+v, err = %v", decodedBrowser, err)
}
native := `{"username":"alice","provider":"local","roles":["user"],"role":"user","native_identity":{"client_device_id":"device-1","device_key_id":"key-1"}}`
decodedNative, err := protocol.DecodeNativeAuthenticatedSession([]byte(native))
if err != nil || decodedNative.NativeIdentity.DeviceKeyID != "key-1" {
t.Fatalf("native session = %+v, err = %v", decodedNative, err)
}
if _, err := protocol.DecodeBrowserAuthenticatedSession([]byte(native)); err == nil {
t.Fatal("browser response accepted native identity")
}
if _, err := protocol.DecodeNativeAuthenticatedSession([]byte(browser)); err == nil {
t.Fatal("native response accepted missing native identity")
}
for _, invalid := range []string{
strings.Replace(native, `,"device_key_id":"key-1"`, "", 1),
strings.Replace(native, `"native_identity":{"client_device_id":"device-1","device_key_id":"key-1"}`, `"native_identity":null`, 1),
strings.Replace(native, `"device_key_id":"key-1"`, `"device_key_id":"key-1","unknown":true`, 1),
} {
if _, err := protocol.DecodeNativeAuthenticatedSession([]byte(invalid)); err == nil {
t.Fatalf("NativeAuthenticatedSession accepted non-atomic or unknown native identity: %s", invalid)
}
}
for _, invalidRoles := range []string{`[""]`, `["` + strings.Repeat("r", 65) + `"]`} {
invalid := strings.Replace(browser, `["user"]`, invalidRoles, 1)
if _, err := protocol.DecodeBrowserAuthenticatedSession([]byte(invalid)); err == nil {
t.Fatalf("BrowserAuthenticatedSession accepted invalid roles: %s", invalid)
}
}
}
func TestNativeTunnelCredentialIsStrictAndBounded(t *testing.T) {
valid := `{"client_device_id":"device-1","device_key_id":"key-1","certificate_chain_pem":"certificate","trust_bundle_pem":"trust","expires_at":"2099-01-01T00:00:00Z"}`
credential, err := protocol.DecodeNativeTunnelCredential([]byte(valid))
if err != nil || credential.ClientDeviceID != "device-1" {
t.Fatalf("native tunnel credential = %+v, err = %v", credential, err)
}
for _, invalid := range []string{
strings.Replace(valid, `"device_key_id":"key-1",`, "", 1),
strings.Replace(valid, `"expires_at":"2099-01-01T00:00:00Z"`, `"expires_at":"not-a-time"`, 1),
strings.Replace(valid, `"expires_at":"2099-01-01T00:00:00Z"`, `"expires_at":"2099-01-01T00:00:00+00:00"`, 1),
strings.Replace(valid, `"expires_at":"2099-01-01T00:00:00Z"`, `"expires_at":"2099-01-01T00:00:00.100Z"`, 1),
strings.Replace(valid, `"expires_at":"2099-01-01T00:00:00Z"`, `"expires_at":"2099-01-01T00:00:00Z","unknown":true`, 1),
} {
if _, err := protocol.DecodeNativeTunnelCredential([]byte(invalid)); err == nil {
t.Fatalf("NativeTunnelCredential accepted invalid payload: %s", invalid)
}
}
}
func TestPageInfoRejectsOutOfBoundsLimit(t *testing.T) {
if _, err := protocol.DecodePageInfo([]byte(`{"limit":101,"next_cursor":""}`)); err == nil {
t.Fatal("DecodePageInfo accepted limit above the contract maximum")
@@ -38,7 +244,7 @@ func TestGeneratedDecodersRejectMissingRequiredFieldsAndTrailingValues(t *testin
}
func TestGatewayContractsRejectUnknownVersionsAndFields(t *testing.T) {
registration := `{"version":"1","gateway_id":"gateway-1","instance_identity":"instance-1","certificate_identity":"cert-1","public_identity":"public-1","address":"gateway.test:443","provider_identity":"apollo-provider-1","protocol_min_version":1,"protocol_max_version":1,"connection_capacity":8,"bandwidth_capacity_kbps":100000,"features":["datagram.media"],"capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":["h264-opus"]}}`
registration := `{"version":"1","gateway_id":"gateway-1","instance_identity":"instance-1","certificate_identity":"cert-1","public_identity":"public-1","address":"gateway.test:443","provider_identity":"apollo-provider-1","protocol_min_version":1,"protocol_max_version":1,"connection_capacity":8,"bandwidth_capacity_kbps":100000,"features":["datagram.media"],"capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","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 {
t.Fatalf("valid gateway registration rejected: %v", err)
}
@@ -57,7 +263,7 @@ func TestGatewayContractsRejectUnknownVersionsAndFields(t *testing.T) {
}
func TestGatewayRegistrationRejectsInvertedProtocolBounds(t *testing.T) {
registration := `{"version":"1","gateway_id":"gateway-1","instance_identity":"instance-1","certificate_identity":"cert-1","public_identity":"public-1","address":"gateway.test:443","provider_identity":"apollo-provider-1","protocol_min_version":2,"protocol_max_version":1,"connection_capacity":8,"bandwidth_capacity_kbps":100000,"features":["datagram.media"],"capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":["h264-opus"]}}`
registration := `{"version":"1","gateway_id":"gateway-1","instance_identity":"instance-1","certificate_identity":"cert-1","public_identity":"public-1","address":"gateway.test:443","provider_identity":"apollo-provider-1","protocol_min_version":2,"protocol_max_version":1,"connection_capacity":8,"bandwidth_capacity_kbps":100000,"features":["datagram.media"],"capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","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 {
t.Fatal("DecodeGatewayRegistration accepted inverted protocol bounds")
}
@@ -80,12 +286,12 @@ func TestGatewayHeartbeatCarriesBoundedObservedTelemetry(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) {
t.Fatalf("IntersectCapabilityProfiles matching profiles = %+v, %v", got, err)
}
second := first
second.ClientDecode = []string{"hevc-opus"}
second.VideoProfiles = []protocol.VideoProfile{rc6Video("hevc")}
if _, err := protocol.IntersectCapabilityProfiles(first, second); err == nil {
t.Fatal("IntersectCapabilityProfiles accepted profiles without a common codec profile")
}
@@ -93,31 +299,31 @@ func TestCapabilityIntersectionRejectsNoOverlap(t *testing.T) {
func TestCapabilityIntersectionSelectsRegisteredOrderedProfiles(t *testing.T) {
gateway := protocol.CapabilityProfile{
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded",
SourceRateControl: "server", ClientDecode: []string{"hevc-opus", "h264-opus"},
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded",
SourceRateControl: "server", VideoProfiles: []protocol.VideoProfile{rc6Video("hevc"), rc6Video("h264")}, AudioProfiles: []protocol.AudioProfile{rc6Audio()},
}
h264Client := gateway
h264Client.ClientDecode = []string{"h264-opus"}
h264Client.VideoProfiles = []protocol.VideoProfile{rc6Video("h264")}
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)
}
hevcClient := gateway
hevcClient.ClientDecode = []string{"hevc-opus"}
hevcClient.VideoProfiles = []protocol.VideoProfile{rc6Video("hevc")}
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)
}
noOverlap := gateway
noOverlap.ClientDecode = []string{"h264-opus"}
noOverlap.VideoProfiles = []protocol.VideoProfile{rc6Video("h264")}
if _, err := protocol.IntersectCapabilityProfiles(noOverlap, hevcClient); err == nil {
t.Fatal("intersection accepted registered profiles without overlap")
}
for _, invalid := range [][]string{{"h264-hevc-opus"}, {"h264-opus", "h264-opus"}} {
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.ClientDecode = invalid
profile.VideoProfiles = invalid
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)
}
}
}
@@ -125,7 +331,7 @@ func TestCapabilityIntersectionSelectsRegisteredOrderedProfiles(t *testing.T) {
func TestCapabilityProfileRequiresRegisteredFraming(t *testing.T) {
valid := protocol.CapabilityProfile{
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 {
t.Fatalf("registered datagram-v2 rejected: %v", err)
@@ -142,8 +348,8 @@ func TestTunnelAdmissionRequiresDeviceSignature(t *testing.T) {
Version: "1", SessionID: "session-1", GatewayID: "gateway-1", Audience: "versevdi-gateway",
Grant: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_", ReconnectSequence: 0,
ClientNonce: "0123456789abcdef", Capabilities: protocol.CapabilityProfile{
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded",
SourceRateControl: "server", ClientDecode: []string{"h264-opus"},
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded",
SourceRateControl: "server", VideoProfiles: []protocol.VideoProfile{rc6Video("h264")}, AudioProfiles: []protocol.AudioProfile{rc6Audio()},
},
}
if _, err := protocol.EncodeTunnelAdmissionRequest(request); err == nil {
@@ -156,18 +362,18 @@ func TestTunnelAdmissionTranscriptIsDomainSeparatedAndLengthDelimited(t *testing
Version: "1", SessionID: "session", GatewayID: "gateway", Audience: "audience",
Grant: strings.Repeat("g", 43), ReconnectSequence: 0, ClientNonce: strings.Repeat("n", 16),
DeviceSignature: strings.Repeat("s", 86), Capabilities: protocol.CapabilityProfile{
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded",
SourceRateControl: "server", ClientDecode: []string{"h264-opus"},
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded",
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 {
t.Fatalf("DeviceAdmissionTranscript() = %q, want %q", got, want)
}
}
func TestSessionAuthorityRejectsProviderRoute(t *testing.T) {
valid := `{"version":"1","session_id":"session-1","gateway_id":"gateway-1","audience":"versevdi-gateway","reconnect_sequence":0,"expires_at":"2099-01-01T00:00:00Z","capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":["h264-opus"]},"provider_profile":"apollo","provider_identity":"provider-1"}`
valid := `{"version":"1","session_id":"session-1","gateway_id":"gateway-1","audience":"versevdi-gateway","reconnect_sequence":0,"expires_at":"2099-01-01T00:00:00Z","capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","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 {
t.Fatalf("valid session authority rejected: %v", err)
}
@@ -176,8 +382,84 @@ func TestSessionAuthorityRejectsProviderRoute(t *testing.T) {
}
}
func TestClientSessionAuthorityIsStrictAndProviderFree(t *testing.T) {
authority := protocol.ClientSessionAuthority{
Version: "1", SessionID: "session-1", GatewayID: "gateway-1", Audience: "versevdi-gateway",
ReconnectSequence: 2, ExpiresAt: "2099-01-01T00:00:00Z", Capabilities: protocol.CapabilityProfile{
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded",
SourceRateControl: "server", VideoProfiles: []protocol.VideoProfile{rc6Video("h264")}, AudioProfiles: []protocol.AudioProfile{rc6Audio()},
},
SelectedDescriptor: rc6Descriptor("h264"),
}
encoded, err := protocol.EncodeClientSessionAuthority(authority)
if err != nil {
t.Fatalf("EncodeClientSessionAuthority() error = %v", err)
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(encoded, &fields); err != nil {
t.Fatalf("encoded client authority is not JSON: %v", err)
}
wantFields := map[string]bool{
"version": true, "session_id": true, "gateway_id": true, "audience": true,
"reconnect_sequence": true, "expires_at": true, "capabilities": true, "selected_descriptor": true,
}
if len(fields) != len(wantFields) {
t.Fatalf("encoded client authority fields = %v; want exactly %v", fields, wantFields)
}
for field := range fields {
if !wantFields[field] {
t.Fatalf("encoded client authority contains forbidden field %q", field)
}
}
if bytes.Contains(encoded, []byte("provider_")) {
t.Fatalf("encoded client authority disclosed provider data: %s", encoded)
}
decoded, err := protocol.DecodeClientSessionAuthority(encoded)
if err != nil || !reflect.DeepEqual(decoded, 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", "selected_descriptor"} {
missing := make(map[string]json.RawMessage, len(fields)-1)
for key, value := range fields {
if key != field {
missing[key] = value
}
}
payload, err := json.Marshal(missing)
if err != nil {
t.Fatal(err)
}
if _, err := protocol.DecodeClientSessionAuthority(payload); err == nil {
t.Fatalf("DecodeClientSessionAuthority accepted missing %q", field)
}
}
for name, value := range map[string]string{
"provider_profile": `"apollo"`,
"provider_identity": `"provider-1"`,
"provider_url": `"https://provider.invalid"`,
"management_host": `"provider.invalid"`,
"unknown": `true`,
} {
payload := append(append([]byte(nil), encoded[:len(encoded)-1]...), []byte(`,"`+name+`":`+value+`}`)...)
if _, err := protocol.DecodeClientSessionAuthority(payload); err == nil {
t.Fatalf("DecodeClientSessionAuthority accepted injected %q", name)
}
}
for _, expiresAt := range []string{"not-a-time", "2099-01-01T00:00:00+00:00", "2099-01-01T00:00:00.100Z"} {
payload := bytes.Replace(encoded, []byte("2099-01-01T00:00:00Z"), []byte(expiresAt), 1)
if _, err := protocol.DecodeClientSessionAuthority(payload); err == nil {
t.Fatalf("DecodeClientSessionAuthority accepted expires_at %q", expiresAt)
}
}
if _, err := protocol.DecodeClientSessionAuthority(append(encoded, []byte(" {}")...)); err == nil {
t.Fatal("DecodeClientSessionAuthority accepted trailing JSON")
}
}
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 {
t.Fatalf("valid provider work rejected: %v", err)
}
@@ -188,10 +470,10 @@ func TestProviderSessionWorkIsStrictAndSessionBound(t *testing.T) {
t.Fatal("provider work accepted missing clipboard policy")
}
for _, invalid := range []string{
strings.Replace(valid, `,"stream_policy":{"resolution_width":2560,"resolution_height":1440,"fps":120,"codec":"HEVC","bitrate_kbps":40000,"audio_enabled":true}`, "", 1),
strings.Replace(valid, `,"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, `"codec":"HEVC"`, `"codec":"VP9"`, 1),
strings.Replace(valid, `"audio_enabled":true`, `"audio_enabled":true,"unknown":false`, 1),
strings.Replace(valid, `"codec":"hevc"`, `"codec":"vp9"`, 1),
strings.Replace(valid, `"packet_duration_ms":5`, `"packet_duration_ms":5,"unknown":false`, 1),
} {
if _, err := protocol.DecodeProviderSessionWork([]byte(invalid)); err == nil {
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")
if not expected or actual != expected:
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
+313 -35
View File
@@ -139,30 +139,60 @@ def go_validation(definition: dict[str, Any]) -> list[str]:
)
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\"}}) }} }}")
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":
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:
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:
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 "minItems" in prop:
lines.append(f"\tif len(v.{field}) < {prop['minItems']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"min_items\"}}) }}")
if "maxItems" in prop:
lines.append(f"\tif len(v.{field}) > {prop['maxItems']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"max_items\"}}) }}")
items = prop.get("items", {})
if items.get("type") == "string" and "minLength" in items:
lines.append(f"\tfor _, item := range v.{field} {{ if len(item) < {items['minLength']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"min_item_length\"}}) }} }}")
if items.get("type") == "string" and "maxLength" in items:
lines.append(f"\tfor _, item := range v.{field} {{ if len(item) > {items['maxLength']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"max_item_length\"}}) }} }}")
if items.get("type") == "string" and "x-max-bytes" in items:
lines.append(f"\tfor _, item := range v.{field} {{ if len(item) > {items['x-max-bytes']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"max_item_bytes\"}}) }} }}")
if "enum" in items:
allowed = " || ".join(f'item == "{value}"' for value in items["enum"])
lines.append(f"\tfor _, item := range v.{field} {{ if !({allowed}) {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_item\"}}) }} }}")
if prop.get("uniqueItems") and items.get("type") == "string":
lines.append(f"\tfor index, item := range v.{field} {{ for prior := 0; prior < index; prior++ {{ if item == v.{field}[prior] {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"duplicate_item\"}}) }} }} }}")
if prop.get("uniqueItems"):
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)
if item_ref:
lines.append(f"\tfor index := range v.{field} {{ if err := v.{field}[index].Validate(); err != nil {{ violations = append(violations, FieldViolation{{Field: fmt.Sprintf(\"{prop_name}[%d]\", index), Code: \"invalid_item\"}}) }} }}")
reference = ref_name(prop)
if reference:
lines.append(f"\tif err := v.{field}.Validate(); err != nil {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_object\"}}) }}")
validation = f"if err := v.{field}.Validate(); err != nil {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_object\"}}) }}"
if prop_name not in required:
lines.append(f"\tif v.{field} != nil {{ {validation} }}")
else:
lines.append(f"\t{validation}")
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\"}) }")
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":
lines.append("\tif v.ProtocolMinVersion > v.ProtocolMaxVersion { violations = append(violations, FieldViolation{Field: \"protocol_version\", Code: \"invalid_order\"}) }")
if name == "ChannelFrame":
@@ -177,6 +207,7 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
"",
"import (",
"\"bytes\"",
"\"encoding/binary\"",
"\"encoding/base64\"",
"\"encoding/json\"",
"\"errors\"",
@@ -203,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 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):
if name == "FieldViolation":
@@ -212,7 +267,12 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
required = set(definition.get("required", []))
for prop_name, prop in definition.get("properties", {}).items():
tag = prop_name + (",omitempty" if prop_name not in required else "")
out.append(f"\t{go_field(prop_name)} {prop_type(prop, 'go')} `json:\"{tag}\"`")
typ = prop_type(prop, "go")
if prop.get("x-optional-pointer"):
typ = "*" + typ
elif prop_name not in required and ref_name(prop):
typ = "*" + typ
out.append(f"\t{go_field(prop_name)} {typ} `json:\"{tag}\"`")
out.extend(["}", ""])
for name in sorted(defs):
out.append(f"func (v {name}) Validate() error {{")
@@ -225,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"\tvar value {name}")
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("\tif err := json.Unmarshal(data, &fields); err != nil { return value, err }")
required_fields = sorted(defs[name].get("required", []))
@@ -234,6 +295,11 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
% (prop_name, prop_name)
)
for prop_name, prop in defs[name].get("properties", {}).items():
if prop_name not in required_fields and ref_name(prop):
out.append(
'\tif raw, ok := fields["%s"]; ok && bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { return value, ValidationError{Violations: []FieldViolation{{Field: "%s", Code: "invalid_object"}}} }'
% (prop_name, prop_name)
)
if "x-max-bytes" in prop and prop.get("type") != "string":
out.append(
'\tif raw, ok := fields["%s"]; ok && len(raw) > %d { return value, ValidationError{Violations: []FieldViolation{{Field: "%s", Code: "max_bytes"}}} }'
@@ -254,29 +320,51 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
out.append("}")
out.append("")
out.extend([
"func DeviceRegistrationProofTranscript(serverID, principalID, deviceID, challenge []byte, expiryUnixMilliseconds int64) ([]byte, error) {",
"\tfor _, value := range []struct { field string; bytes []byte; length int }{{\"server_id\", serverID, 16}, {\"principal_id\", principalID, 16}, {\"device_id\", deviceID, 16}, {\"challenge\", challenge, 32}} {",
"\t\tif len(value.bytes) != value.length { return nil, ValidationError{Violations: []FieldViolation{{Field: value.field, Code: \"invalid_length\"}}} }",
"\t}",
"\tif expiryUnixMilliseconds < 0 { return nil, ValidationError{Violations: []FieldViolation{{Field: \"expiry_unix_milliseconds\", Code: \"minimum\"}}} }",
"\ttranscript := make([]byte, 0, 112)",
"\ttranscript = append(transcript, \"versevdi-device-proof-v1\"...)",
"\ttranscript = append(transcript, serverID...)",
"\ttranscript = append(transcript, principalID...)",
"\ttranscript = append(transcript, deviceID...)",
"\ttranscript = append(transcript, challenge...)",
"\tvar expiry [8]byte",
"\tbinary.BigEndian.PutUint64(expiry[:], uint64(expiryUnixMilliseconds))",
"\treturn append(transcript, expiry[:]...), nil",
"}",
"",
"var ErrNoCapabilityOverlap = errors.New(\"no capability overlap\")",
"",
"func IntersectCapabilityProfiles(profiles ...CapabilityProfile) (CapabilityProfile, error) {",
"\tif len(profiles) == 0 { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
"\tselected := profiles[0]",
"\tif err := selected.Validate(); err != nil { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
"\tcommon := append([]string(nil), selected.ClientDecode...)",
"\tcommonVideo := append([]VideoProfile(nil), selected.VideoProfiles...)",
"\tcommonAudio := append([]AudioProfile(nil), selected.AudioProfiles...)",
"\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\tnext := common[:0]",
"\t\tfor _, candidate := range common { for _, offered := range profile.ClientDecode { if candidate == offered { next = append(next, candidate); break } } }",
"\t\tcommon = next",
"\t\tif len(common) == 0 { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
"\t\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\tnextVideo := commonVideo[:0]",
"\t\tfor _, candidate := range commonVideo { for _, offered := range profile.VideoProfiles { if candidate == offered { nextVideo = append(nextVideo, candidate); break } } }",
"\t\tcommonVideo = nextVideo",
"\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}",
"\tselected.ClientDecode = common",
"\tselected.VideoProfiles = commonVideo",
"\tselected.AudioProfiles = commonAudio",
"\treturn selected, nil",
"}",
"",
])
out.extend([
"func (v TunnelAdmissionRequest) DeviceAdmissionTranscript() []byte {",
"\tfields := []string{v.SessionID, v.GatewayID, v.Audience, v.Grant, fmt.Sprintf(\"%d\", v.ReconnectSequence), v.ClientNonce, v.Capabilities.Transport, v.Capabilities.Framing, v.Capabilities.Media, v.Capabilities.Audio, v.Capabilities.SourceRateControl, fmt.Sprintf(\"%d\", len(v.Capabilities.ClientDecode))}",
"\tfields = append(fields, 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))}",
"\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",
"\ttranscript.WriteString(\"versevdi/tunnel-admission/v1\")",
"\tfor _, field := range fields { fmt.Fprintf(&transcript, \"%d:%s\", len(field), field) }",
@@ -347,23 +435,34 @@ def rust_validation(definition: dict[str, Any]) -> list[str]:
if "enum" in prop:
allowed = " && ".join(f'{value} != \"{item}\"' for item in prop["enum"])
lines.append(f" {prefix}if {allowed} {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_value\")); }}")
if prop.get("format") == "date-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":
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":
numeric = f"*{value}" if prop_name not in required else value
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:
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 "minItems" in prop:
lines.append(f" {prefix}if {value}.len() < {prop['minItems']} {{ return Err(ValidationError::new(\"{prop_name}\", \"min_items\")); }}")
if "maxItems" in prop:
lines.append(f" {prefix}if {value}.len() > {prop['maxItems']} {{ return Err(ValidationError::new(\"{prop_name}\", \"max_items\")); }}")
items = prop.get("items", {})
if items.get("type") == "string" and "minLength" in items:
lines.append(f" {prefix}for item in {value}.iter() {{ if item.as_bytes().len() < {items['minLength']} {{ return Err(ValidationError::new(\"{prop_name}\", \"min_item_length\")); }} }}")
if items.get("type") == "string" and "maxLength" in items:
lines.append(f" {prefix}for item in {value}.iter() {{ if item.as_bytes().len() > {items['maxLength']} {{ return Err(ValidationError::new(\"{prop_name}\", \"max_item_length\")); }} }}")
if items.get("type") == "string" and "x-max-bytes" in items:
lines.append(f" {prefix}for item in {value}.iter() {{ if item.as_bytes().len() > {items['x-max-bytes']} {{ return Err(ValidationError::new(\"{prop_name}\", \"max_item_bytes\")); }} }}")
if "enum" in items:
allowed = " && ".join(f'item != \"{item}\"' for item in items["enum"])
lines.append(f" {prefix}for item in {value}.iter() {{ if {allowed} {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_item\")); }} }}")
if prop.get("uniqueItems") and items.get("type") == "string":
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\")); }} }}")
item_ref = ref_name(items)
if item_ref:
@@ -376,6 +475,22 @@ def rust_validation(definition: dict[str, Any]) -> list[str]:
name = definition["name"]
if name in {"AllocationPolicy", "ManifestBounds"}:
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":
lines.append(" if self.protocolMinVersion > self.protocolMaxVersion { return Err(ValidationError::new(\"protocol_version\", \"invalid_order\")); }")
if name == "ChannelFrame":
@@ -417,6 +532,23 @@ def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibili
" _ => false,",
" }",
"}",
"fn valid_rfc3339_utc(value: &str) -> bool {",
" let bytes = value.as_bytes();",
" if bytes.len() < 20 || bytes.len() > 30 || bytes[4] != b'-' || bytes[7] != b'-' || bytes[10] != b'T' || bytes[13] != b':' || bytes[16] != b':' || *bytes.last().unwrap() != b'Z' { return false; }",
" let digits = |start: usize, end: usize| -> Option<u32> { bytes.get(start..end)?.iter().try_fold(0u32, |value, byte| if byte.is_ascii_digit() { Some(value * 10 + u32::from(*byte - b'0')) } else { None }) };",
" let (year, month, day, hour, minute, second) = match (digits(0, 4), digits(5, 7), digits(8, 10), digits(11, 13), digits(14, 16), digits(17, 19)) { (Some(year), Some(month), Some(day), Some(hour), Some(minute), Some(second)) => (year, month, day, hour, minute, second), _ => return false };",
" if hour > 23 || minute > 59 || second > 59 { return false; }",
" let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);",
" let days = match month { 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, 4 | 6 | 9 | 11 => 30, 2 if leap => 29, 2 => 28, _ => return false };",
" if day == 0 || day > days { return false; }",
" if bytes.len() == 20 { return true; }",
" 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'",
"}",
"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):
@@ -459,9 +591,12 @@ def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibili
out.extend([
" pub fn device_admission_transcript(&self) -> Vec<u8> {",
" let reconnect_sequence = self.reconnectSequence.to_string();",
" let client_decode_count = self.capabilities.clientDecode.len().to_string();",
" let mut fields = vec![self.sessionId.as_str(), self.gatewayId.as_str(), self.audience.as_str(), self.grant.as_str(), reconnect_sequence.as_str(), self.clientNonce.as_str(), self.capabilities.transport.as_str(), self.capabilities.framing.as_str(), self.capabilities.media.as_str(), self.capabilities.audio.as_str(), self.capabilities.sourceRateControl.as_str(), client_decode_count.as_str()];",
" fields.extend(self.capabilities.clientDecode.iter().map(String::as_str));",
" let video_count = self.capabilities.videoProfiles.len().to_string();",
" let audio_count = self.capabilities.audioProfiles.len().to_string();",
" 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\");",
" for field in fields { transcript.push_str(&format!(\"{}:{}\", field.as_bytes().len(), field)); }",
" transcript.into_bytes()",
@@ -469,14 +604,30 @@ def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibili
])
out.extend(["}", ""])
out.extend([
"pub fn device_registration_proof_transcript(server_id: &[u8], principal_id: &[u8], device_id: &[u8], challenge: &[u8], expiry_unix_milliseconds: i64) -> Result<Vec<u8>, ValidationError> {",
" for (field, value, length) in [(\"server_id\", server_id, 16), (\"principal_id\", principal_id, 16), (\"device_id\", device_id, 16), (\"challenge\", challenge, 32)] {",
" if value.len() != length { return Err(ValidationError::new(field, \"invalid_length\")); }",
" }",
" if expiry_unix_milliseconds < 0 { return Err(ValidationError::new(\"expiry_unix_milliseconds\", \"minimum\")); }",
" let mut transcript = Vec::with_capacity(112);",
" transcript.extend_from_slice(b\"versevdi-device-proof-v1\");",
" transcript.extend_from_slice(server_id);",
" transcript.extend_from_slice(principal_id);",
" transcript.extend_from_slice(device_id);",
" transcript.extend_from_slice(challenge);",
" transcript.extend_from_slice(&(expiry_unix_milliseconds as u64).to_be_bytes());",
" Ok(transcript)",
"}",
"",
"pub fn intersect_capability_profiles(profiles: &[CapabilityProfile]) -> Result<CapabilityProfile, ValidationError> {",
" let mut selected = profiles.first().ok_or_else(|| ValidationError::new(\"capabilities\", \"no_overlap\"))?.clone();",
" selected.validate().map_err(|_| ValidationError::new(\"capabilities\", \"no_overlap\"))?;",
" for profile in &profiles[1..] {",
" profile.validate().map_err(|_| ValidationError::new(\"capabilities\", \"no_overlap\"))?;",
" if profile.transport != selected.transport || profile.framing != selected.framing || profile.media != selected.media || profile.audio != selected.audio || profile.sourceRateControl != selected.sourceRateControl { return Err(ValidationError::new(\"capabilities\", \"no_overlap\")); }",
" selected.clientDecode.retain(|candidate| profile.clientDecode.contains(candidate));",
" if selected.clientDecode.is_empty() { return Err(ValidationError::new(\"capabilities\", \"no_overlap\")); }",
" 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.videoProfiles.retain(|candidate| profile.videoProfiles.contains(candidate));",
" 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)",
"}",
@@ -512,9 +663,11 @@ def swift_validation(definition: dict[str, Any]) -> list[str]:
allowed = ", ".join(f'\"{item}\"' for item in prop["enum"])
lines.append(f" {prefix}if ![{allowed}].contains({value}) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_value\") }}")
if prop.get("format") == "date-time":
lines.append(f" {prefix}if ISO8601DateFormatter().date(from: {value}) == nil {{ 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":
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 "minimum" in prop:
lines.append(f" {prefix}if {value} < {prop['minimum']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"minimum\") }}")
@@ -526,11 +679,17 @@ def swift_validation(definition: dict[str, Any]) -> list[str]:
if "maxItems" in prop:
lines.append(f" {prefix}if {value}.count > {prop['maxItems']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"max_items\") }}")
items = prop.get("items", {})
if items.get("type") == "string" and "minLength" in items:
lines.append(f" {prefix}for item in {value} where item.utf8.count < {items['minLength']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"min_item_length\") }}")
if items.get("type") == "string" and "maxLength" in items:
lines.append(f" {prefix}for item in {value} where item.utf8.count > {items['maxLength']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"max_item_length\") }}")
if items.get("type") == "string" and "x-max-bytes" in items:
lines.append(f" {prefix}for item in {value} where item.utf8.count > {items['x-max-bytes']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"max_item_bytes\") }}")
if "enum" in items:
allowed = ", ".join(f'\"{item}\"' for item in items["enum"])
lines.append(f" {prefix}for item in {value} where ![{allowed}].contains(item) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_item\") }}")
if prop.get("uniqueItems") and items.get("type") == "string":
lines.append(f" {prefix}if Set({value}).count != {value}.count {{ throw ContractValidationError(field: \"{prop_name}\", code: \"duplicate_item\") }}")
if prop.get("uniqueItems"):
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)
if item_ref:
lines.append(f" {prefix}for item in {value} {{ try item.validate() }}")
@@ -542,6 +701,22 @@ def swift_validation(definition: dict[str, Any]) -> list[str]:
name = definition["name"]
if name in {"AllocationPolicy", "ManifestBounds"}:
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":
lines.append(" if protocolMinVersion > protocolMaxVersion { throw ContractValidationError(field: \"protocol_version\", code: \"invalid_order\") }")
if name == "ChannelFrame":
@@ -560,6 +735,67 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
f'public let nMinus2WireVersion = "{compatibility["n_minus_2"]}"',
"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 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 {",
" guard !value.isEmpty, value.utf8.allSatisfy({ byte in",
" (byte >= 65 && byte <= 90) || (byte >= 97 && byte <= 122) || (byte >= 48 && byte <= 57) || byte == 45 || byte == 95",
@@ -569,6 +805,28 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
" guard let decoded = Data(base64Encoded: standard) else { return false }",
" return decoded.base64EncodedString().replacingOccurrences(of: \"+\", with: \"-\").replacingOccurrences(of: \"/\", with: \"_\").replacingOccurrences(of: \"=\", with: \"\") == value",
"}",
"private func validRFC3339UTC(_ value: String) -> Bool {",
" let bytes = Array(value.utf8)",
" guard (20...30).contains(bytes.count), bytes[4] == 45, bytes[7] == 45, bytes[10] == 84, bytes[13] == 58, bytes[16] == 58, bytes.last == 90 else { return false }",
" func digits(_ range: Range<Int>) -> Int? {",
" var result = 0",
" for index in range { guard bytes[index] >= 48 && bytes[index] <= 57 else { return nil }; result = result * 10 + Int(bytes[index] - 48) }",
" return result",
" }",
" guard let year = digits(0..<4), let month = digits(5..<7), let day = digits(8..<10), let hour = digits(11..<13), let minute = digits(14..<16), let second = digits(17..<19), hour <= 23, minute <= 59, second <= 59 else { return false }",
" let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)",
" let days: Int",
" switch month { case 1, 3, 5, 7, 8, 10, 12: days = 31; case 4, 6, 9, 11: days = 30; case 2: days = leap ? 29 : 28; default: return false }",
" guard day > 0 && day <= days else { return false }",
" if bytes.count == 20 { return true }",
" 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",
"}",
"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):
@@ -603,17 +861,35 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
typ = swift_type(prop)
if prop_name in required:
decoded.append(f"{field}: try c.decode({typ}.self, forKey: .{field})")
elif ref_name(prop):
decoded.append(f"{field}: try c.contains(.{field}) ? c.decode({typ}.self, forKey: .{field}) : nil")
else:
decoded.append(f"{field}: try c.decodeIfPresent({typ}.self, forKey: .{field})")
out.append(f" try self.init({', '.join(decoded)})")
out.extend([" }", "", " public func validate() throws {"])
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([
"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)] {",
" if value.count != length { throw ContractValidationError(field: field, code: \"invalid_length\") }",
" }",
" if expiryUnixMilliseconds < 0 { throw ContractValidationError(field: \"expiry_unix_milliseconds\", code: \"minimum\") }",
" var transcript = Data(\"versevdi-device-proof-v1\".utf8)",
" transcript.append(serverID)",
" transcript.append(principalID)",
" transcript.append(deviceID)",
" transcript.append(challenge)",
" var expiry = UInt64(expiryUnixMilliseconds).bigEndian",
" Swift.withUnsafeBytes(of: &expiry) { transcript.append(contentsOf: $0) }",
" return transcript",
"}",
"",
"public extension TunnelAdmissionRequest {",
" 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)]",
" fields.append(contentsOf: capabilities.clientDecode)",
" 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)]",
" 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\"",
" for field in fields { transcript += \"\\(field.utf8.count):\\(field)\" }",
" return Data(transcript.utf8)",
@@ -624,14 +900,16 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
" static func intersection(_ profiles: [CapabilityProfile]) throws -> CapabilityProfile {",
" guard let selected = profiles.first else { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
" try selected.validate()",
" var common = selected.clientDecode",
" var commonVideo = selected.videoProfiles",
" var commonAudio = selected.audioProfiles",
" for profile in profiles.dropFirst() {",
" 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\") }",
" common = common.filter { profile.clientDecode.contains($0) }",
" if common.isEmpty { 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\") }",
" commonVideo = commonVideo.filter { profile.videoProfiles.contains($0) }",
" 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)",
" }",
"}",
"",
+133 -6
View File
@@ -2,6 +2,7 @@ package main
import (
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"
@@ -38,7 +39,7 @@ func main() {
if len(fields) != 5 {
panic("invalid fixture row")
}
actual := evaluate(fields[2], fields[3])
actual := evaluate(fields[1], fields[2], fields[3])
if actual != fields[4] {
panic(fmt.Sprintf("%s: got %s want %s", fields[0], actual, fields[4]))
}
@@ -49,7 +50,7 @@ func main() {
fmt.Printf("Go conformance passed normalized=%s fixtures=%s\n", normalizedDigest(results), fixtureHash)
}
func evaluate(kind, input string) string {
func evaluate(version, kind, input string) string {
parts := map[string]string{}
for _, item := range strings.Split(input, ";") {
pair := strings.SplitN(item, "=", 2)
@@ -58,8 +59,22 @@ func evaluate(kind, input string) string {
}
}
switch kind {
case "device_proof_transcript":
serverID, serverErr := hex.DecodeString(parts["server_id"])
principalID, principalErr := hex.DecodeString(parts["principal_id"])
deviceID, deviceErr := hex.DecodeString(parts["device_id"])
challenge, challengeErr := hex.DecodeString(parts["challenge"])
expiry, expiryErr := strconv.ParseInt(parts["expiry_unix_ms"], 10, 64)
if serverErr != nil || principalErr != nil || deviceErr != nil || challengeErr != nil || expiryErr != nil {
return "invalid:fixture"
}
transcript, err := protocol.DeviceRegistrationProofTranscript(serverID, principalID, deviceID, challenge, expiry)
if err != nil {
return "invalid:device_proof"
}
return hex.EncodeToString(transcript)
case "version":
if input == "1" || input == "0" || input == "-1" {
if input == "2" || input == "1" || input == "0" {
return "valid"
}
return "invalid:unsupported_version"
@@ -81,12 +96,21 @@ func evaluate(kind, input string) string {
Version: parts["version"], Purpose: parts["purpose"], SessionID: "session-1",
ReconnectSequence: 0,
Gateway: protocol.ManifestGateway{
ID: parts["gateway_id"], Addresses: []string{"gateway.control.test:443"}, PublicIdentity: parts["gateway_id"],
ID: parts["gateway_id"], Addresses: []string{"gateway.control.test:443"}, PublicIdentity: parts["public_identity"],
},
Tunnel: protocol.ManifestTunnel{Versions: []string{parts["protocol"] + "/1"}, Features: []string{"control.v1"}},
Profile: protocol.ManifestProfile{ID: "standard", Bounds: protocol.ManifestBounds{MinimumKbps: 1, TargetKbps: 2, MaximumKbps: 3}},
Grant: protocol.GrantReference{OpaqueValue: parts["grant"], ExpiresAt: parts["expires_at"], Audience: parts["audience"]},
CorrelationID: "correlation-1",
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 {
return "valid"
@@ -99,6 +123,62 @@ func evaluate(kind, input string) string {
return "valid"
}
return "invalid:unsupported_clipboard"
case "session_request":
if version != "2" {
return "invalid:unsupported_version"
}
if _, supplied := parts["policy_snapshot"]; supplied {
return "invalid:forbidden_field"
}
value := protocol.SessionRequest{
ClientDeviceID: parts["client_device_id"], DeviceKeyID: parts["device_key_id"],
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 {
return "valid"
}
return "invalid:required"
case "browser_authenticated_session":
if _, hasDevice := parts["client_device_id"]; hasDevice {
return "invalid:forbidden_field"
}
if _, hasKey := parts["device_key_id"]; hasKey {
return "invalid:forbidden_field"
}
value := protocol.BrowserAuthenticatedSession{
Username: parts["username"], Provider: parts["provider"], Roles: []string{parts["roles"]},
Role: parts["role"],
}
if value.Validate() == nil {
return "valid"
}
return "invalid:invalid_session"
case "native_authenticated_session":
clientDeviceID, hasDevice := parts["client_device_id"]
deviceKeyID, hasKey := parts["device_key_id"]
if !hasDevice || !hasKey {
return "invalid:required"
}
value := protocol.NativeAuthenticatedSession{
Username: parts["username"], Provider: parts["provider"], Roles: []string{parts["roles"]}, Role: parts["role"],
NativeIdentity: protocol.NativeSessionIdentity{ClientDeviceID: clientDeviceID, DeviceKeyID: deviceKeyID},
}
if value.Validate() == nil {
return "valid"
}
return "invalid:invalid_session"
case "native_tunnel_credential":
value := protocol.NativeTunnelCredential{
ClientDeviceID: parts["client_device_id"], DeviceKeyID: parts["device_key_id"],
CertificateChainPem: parts["certificate_chain_pem"], TrustBundlePem: parts["trust_bundle_pem"],
ExpiresAt: parts["expires_at"],
}
if value.Validate() == nil {
return "valid"
}
return "invalid:invalid_credential"
case "event":
sequence, sequenceErr := strconv.ParseInt(parts["sequence"], 10, 64)
payloadBytes, payloadErr := strconv.Atoi(parts["payload_bytes"])
@@ -125,10 +205,12 @@ func evaluate(kind, input string) string {
}
return "valid"
case "tunnel":
if (parts["offered"] == "1" || parts["offered"] == "0" || parts["offered"] == "-1") && parts["feature"] == "control.v1" {
feature := parts["feature"]
registered := feature == "control.v1" || feature == "control.v2" || feature == "display.request.v1" || feature == "input.absolute.v1" || feature == "input.scroll.v1"
if (parts["offered"] == "2" || parts["offered"] == "1" || parts["offered"] == "0") && registered {
return "valid"
}
if parts["feature"] != "control.v1" {
if !registered {
return "invalid:unsupported_feature"
}
return "invalid:unsupported_version"
@@ -226,6 +308,26 @@ func classifyGatewayInput(encoded string) string {
}
}
}
case 6:
if len(body) != 8 {
return "invalid:length"
}
x, y := uint16(body[0])<<8|uint16(body[1]), uint16(body[2])<<8|uint16(body[3])
width, height := uint16(body[4])<<8|uint16(body[5]), uint16(body[6])<<8|uint16(body[7])
if width == 0 || height == 0 || x >= width || y >= height {
return "invalid:field"
}
case 7:
if len(body) != 4 {
return "invalid:length"
}
case 8:
if len(body) != 8 {
return "invalid:length"
}
if body[0] > 15 || body[3] > 3 {
return "invalid:field"
}
default:
return "invalid:kind"
}
@@ -269,6 +371,22 @@ func classifyGatewayFeedback(encoded string) string {
if len(body) == 0 {
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:
return "invalid:type"
}
@@ -303,6 +421,15 @@ func classifyGatewayFeedback(encoded string) string {
return "invalid:field"
}
func allZero(value []byte) bool {
for _, item := range value {
if item != 0 {
return false
}
}
return true
}
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] {
return false
+95 -6
View File
@@ -9,10 +9,10 @@ fn values(input: &str) -> std::collections::BTreeMap<String, String> {
.collect()
}
fn evaluate(kind: &str, input: &str) -> &'static str {
fn evaluate(version: &str, kind: &str, input: &str) -> &'static str {
let values = values(input);
match kind {
"version" if matches!(input, "1" | "0" | "-1") => "valid",
"version" if matches!(input, "2" | "1" | "0") => "valid",
"version" => "invalid:unsupported_version",
"page" => match values.get("limit").and_then(|value| value.parse::<i64>().ok()) {
Some(limit) if (1..=100).contains(&limit) => "valid",
@@ -24,12 +24,64 @@ fn evaluate(kind: &str, input: &str) -> &'static str {
"manifest"
if values.get("version").map(String::as_str) == Some("1")
&& values.contains_key("gateway_id")
&& values.contains_key("public_identity")
&& values.get("grant").map_or(false, |value| value.len() >= 43)
&& values.get("purpose").map(String::as_str) == Some("launch") => "valid",
"manifest" => "invalid:invalid_manifest",
"clipboard" if values.get("encoding").map(String::as_str) == Some("utf-8")
&& !values.contains_key("file") => "valid",
"clipboard" => "invalid:unsupported_clipboard",
"session_request" if version != "2" => "invalid:unsupported_version",
"session_request" if values.contains_key("policy_snapshot") => "invalid:forbidden_field",
"session_request" => match SessionRequest::new(
values.get("client_device_id").cloned().unwrap_or_default(),
values.get("device_key_id").cloned().unwrap_or_default(),
values.get("pool_id").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,
) {
Ok(_) => "valid",
Err(_) => "invalid:required",
},
"browser_authenticated_session" if values.contains_key("client_device_id") || values.contains_key("device_key_id") => "invalid:forbidden_field",
"browser_authenticated_session" => match BrowserAuthenticatedSession::new(
values.get("username").cloned().unwrap_or_default(),
values.get("provider").cloned().unwrap_or_default(),
vec![values.get("roles").cloned().unwrap_or_default()],
values.get("role").cloned().unwrap_or_default(),
) {
Ok(_) => "valid",
Err(_) => "invalid:invalid_session",
},
"native_authenticated_session" if !values.contains_key("client_device_id") || !values.contains_key("device_key_id") => "invalid:required",
"native_authenticated_session" => {
let identity = match NativeSessionIdentity::new(values["client_device_id"].clone(), values["device_key_id"].clone()) {
Ok(identity) => identity,
Err(_) => return "invalid:required",
};
match NativeAuthenticatedSession::new(
values.get("username").cloned().unwrap_or_default(),
values.get("provider").cloned().unwrap_or_default(),
vec![values.get("roles").cloned().unwrap_or_default()],
values.get("role").cloned().unwrap_or_default(),
identity,
) {
Ok(_) => "valid",
Err(_) => "invalid:invalid_session",
}
}
"native_tunnel_credential" => match NativeTunnelCredential::new(
values.get("client_device_id").cloned().unwrap_or_default(),
values.get("device_key_id").cloned().unwrap_or_default(),
values.get("certificate_chain_pem").cloned().unwrap_or_default(),
values.get("trust_bundle_pem").cloned().unwrap_or_default(),
values.get("expires_at").cloned().unwrap_or_default(),
) {
Ok(_) => "valid",
Err(_) => "invalid:invalid_credential",
},
"event" if values.get("version").map(String::as_str) != Some("1") => {
"invalid:unsupported_version"
}
@@ -46,9 +98,9 @@ fn evaluate(kind: &str, input: &str) -> &'static str {
"event" if values.get("sequence").and_then(|value| value.parse::<i64>().ok()).map_or(true, |sequence| sequence < 1)
|| !values.contains_key("correlation_id") => "invalid:required",
"event" => "valid",
"tunnel" if matches!(values.get("offered").map(String::as_str), Some("1") | Some("0") | Some("-1"))
&& values.get("feature").map(String::as_str) == Some("control.v1") => "valid",
"tunnel" if values.get("feature").map(String::as_str) != Some("control.v1") => {
"tunnel" if matches!(values.get("offered").map(String::as_str), Some("2") | Some("1") | Some("0"))
&& matches!(values.get("feature").map(String::as_str), Some("control.v1") | Some("control.v2") | Some("display.request.v1") | Some("input.absolute.v1") | Some("input.scroll.v1")) => "valid",
"tunnel" if !matches!(values.get("feature").map(String::as_str), Some("control.v1") | Some("control.v2") | Some("display.request.v1") | Some("input.absolute.v1") | Some("input.scroll.v1")) => {
"invalid:unsupported_feature"
}
"tunnel" => "invalid:unsupported_version",
@@ -79,6 +131,20 @@ fn evaluate(kind: &str, input: &str) -> &'static str {
}
}
fn evaluate_device_proof(input: &str) -> String {
let values = values(input);
let server_id = decode_hex(values.get("server_id").map(String::as_str).unwrap_or_default()).expect("server fixture hex");
let principal_id = decode_hex(values.get("principal_id").map(String::as_str).unwrap_or_default()).expect("principal fixture hex");
let device_id = decode_hex(values.get("device_id").map(String::as_str).unwrap_or_default()).expect("device fixture hex");
let challenge = decode_hex(values.get("challenge").map(String::as_str).unwrap_or_default()).expect("challenge fixture hex");
let expiry = values.get("expiry_unix_ms").expect("expiry fixture").parse::<i64>().expect("expiry integer");
device_registration_proof_transcript(&server_id, &principal_id, &device_id, &challenge, expiry)
.expect("valid device proof fixture")
.iter()
.map(|byte| format!("{byte:02x}"))
.collect()
}
fn classify_gateway_input(encoded: &str) -> &'static str {
let raw = match decode_hex(encoded) {
Some(raw) => raw,
@@ -110,6 +176,19 @@ fn classify_gateway_input(encoded: &str) -> &'static str {
5 if body[0] > 15 => "invalid:field",
5 if body[1] == 0 && body[2] == 0 && body[3..].iter().any(|value| *value != 0) => "invalid:field",
5 => "valid",
6 if body.len() != 8 => "invalid:length",
6 => {
let x = u16::from_be_bytes([body[0], body[1]]);
let y = u16::from_be_bytes([body[2], body[3]]);
let width = u16::from_be_bytes([body[4], body[5]]);
let height = u16::from_be_bytes([body[6], body[7]]);
if width != 0 && height != 0 && x < width && y < height { "valid" } else { "invalid:field" }
}
7 if body.len() == 4 => "valid",
7 => "invalid:length",
8 if body.len() != 8 => "invalid:length",
8 if body[0] > 15 || body[3] > 3 => "invalid:field",
8 => "valid",
_ => "invalid:kind",
}
}
@@ -145,6 +224,12 @@ fn classify_gateway_feedback(encoded: &str) -> &'static str {
2 => "invalid:field",
3 if body.is_empty() => "valid",
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",
};
}
@@ -273,7 +358,11 @@ fn main() {
for line in lines {
let fields: Vec<&str> = line.split('\t').collect();
assert_eq!(fields.len(), 5);
let actual = evaluate(fields[2], fields[3]);
let actual = if fields[2] == "device_proof_transcript" {
evaluate_device_proof(fields[3])
} else {
evaluate(fields[1], fields[2], fields[3]).to_owned()
};
assert_eq!(actual, fields[4], "{}", fields[0]);
results.push(format!("{}\t{}", fields[0], actual));
}
+73 -6
View File
@@ -9,17 +9,51 @@ func values(_ input: String) -> [String: String] {
return result
}
func evaluate(_ kind: String, _ input: String) -> String {
func evaluate(_ version: String, _ kind: String, _ input: String) -> String {
let values = values(input)
switch kind {
case "version": return ["1", "0", "-1"].contains(input) ? "valid" : "invalid:unsupported_version"
case "version": return ["2", "1", "0"].contains(input) ? "valid" : "invalid:unsupported_version"
case "page":
guard let raw = values["limit"], let limit = Int(raw), (1...100).contains(limit) else { return "invalid:invalid_limit" }
return "valid"
case "manifest":
for key in ["provider_url", "vm_address", "password", "private_key"] where values[key] != nil { return "invalid:forbidden_field" }
return values["version"] == "1" && values["gateway_id"] != nil && (values["grant"]?.utf8.count ?? 0) >= 43 && values["purpose"] == "launch" ? "valid" : "invalid:invalid_manifest"
return values["version"] == "1" && values["gateway_id"] != nil && values["public_identity"] != nil && (values["grant"]?.utf8.count ?? 0) >= 43 && values["purpose"] == "launch" ? "valid" : "invalid:invalid_manifest"
case "clipboard": return values["encoding"] == "utf-8" && values["file"] == nil ? "valid" : "invalid:unsupported_clipboard"
case "session_request":
guard version == "2" else { return "invalid:unsupported_version" }
if values["policy_snapshot"] != nil { return "invalid:forbidden_field" }
guard (try? SessionRequest(
clientDeviceId: values["client_device_id"] ?? "", deviceKeyId: values["device_key_id"] ?? "",
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
)) != nil else { return "invalid:required" }
return "valid"
case "browser_authenticated_session":
guard values["client_device_id"] == nil, values["device_key_id"] == nil else { return "invalid:forbidden_field" }
guard (try? BrowserAuthenticatedSession(
username: values["username"] ?? "", provider: values["provider"] ?? "",
roles: [values["roles"] ?? ""], role: values["role"] ?? ""
)) != nil else { return "invalid:invalid_session" }
return "valid"
case "native_authenticated_session":
guard let identity = try? NativeSessionIdentity(
clientDeviceId: values["client_device_id"] ?? "", deviceKeyId: values["device_key_id"] ?? ""
), values["client_device_id"] != nil, values["device_key_id"] != nil else { return "invalid:required" }
guard (try? NativeAuthenticatedSession(
username: values["username"] ?? "", provider: values["provider"] ?? "",
roles: [values["roles"] ?? ""], role: values["role"] ?? "", nativeIdentity: identity
)) != nil else { return "invalid:invalid_session" }
return "valid"
case "native_tunnel_credential":
guard (try? NativeTunnelCredential(
clientDeviceId: values["client_device_id"] ?? "", deviceKeyId: values["device_key_id"] ?? "",
certificateChainPem: values["certificate_chain_pem"] ?? "", trustBundlePem: values["trust_bundle_pem"] ?? "",
expiresAt: values["expires_at"] ?? ""
)) != nil else { return "invalid:invalid_credential" }
return "valid"
case "event":
guard values["version"] == "1" else { return "invalid:unsupported_version" }
if let after = Int(values["after"] ?? ""), let earliest = Int(values["earliest"] ?? ""), after > 0, earliest > 0, after < earliest - 1 { return "invalid:gap" }
@@ -27,8 +61,9 @@ func evaluate(_ kind: String, _ input: String) -> String {
guard let sequence = Int(values["sequence"] ?? ""), sequence > 0, values["correlation_id"] != nil else { return "invalid:required" }
return "valid"
case "tunnel":
if ["1", "0", "-1"].contains(values["offered"] ?? "") && values["feature"] == "control.v1" { return "valid" }
return values["feature"] == "control.v1" ? "invalid:unsupported_version" : "invalid:unsupported_feature"
let registered = ["control.v1", "control.v2", "display.request.v1", "input.absolute.v1", "input.scroll.v1"].contains(values["feature"] ?? "")
if ["2", "1", "0"].contains(values["offered"] ?? "") && registered { return "valid" }
return registered ? "invalid:unsupported_version" : "invalid:unsupported_feature"
case "datagram": return classifyDatagram(values["hex"] ?? "")
case "gateway_input": return classifyGatewayInput(values["hex"] ?? "")
case "gateway_feedback": return classifyGatewayFeedback(values["hex"] ?? "")
@@ -48,6 +83,19 @@ func evaluate(_ kind: String, _ input: String) -> String {
}
}
func evaluateDeviceProof(_ input: String) -> String {
let values = values(input)
let serverID = Data(decodeHex(values["server_id"] ?? "")!)
let principalID = Data(decodeHex(values["principal_id"] ?? "")!)
let deviceID = Data(decodeHex(values["device_id"] ?? "")!)
let challenge = Data(decodeHex(values["challenge"] ?? "")!)
let expiry = Int64(values["expiry_unix_ms"] ?? "")!
return try! deviceRegistrationProofTranscript(
serverID: serverID, principalID: principalID, deviceID: deviceID,
challenge: challenge, expiryUnixMilliseconds: expiry
).map { String(format: "%02x", $0) }.joined()
}
func decodeHex(_ encoded: String) -> [UInt8]? {
let characters = Array(encoded)
guard characters.count % 2 == 0 else { return nil }
@@ -82,6 +130,17 @@ func classifyGatewayInput(_ encoded: String) -> String {
guard body[0] <= 15 else { return "invalid:field" }
guard body[1] != 0 || body[2] != 0 || body.dropFirst(3).allSatisfy({ $0 == 0 }) else { return "invalid:field" }
return "valid"
case 6:
guard body.count == 8 else { return "invalid:length" }
let x = Int(body[0]) * 256 + Int(body[1])
let y = Int(body[2]) * 256 + Int(body[3])
let width = Int(body[4]) * 256 + Int(body[5])
let height = Int(body[6]) * 256 + Int(body[7])
return width > 0 && height > 0 && x < width && y < height ? "valid" : "invalid:field"
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"
}
}
@@ -102,6 +161,12 @@ func classifyGatewayFeedback(_ encoded: String) -> String {
case 2:
return validFECStatus(body) ? "valid" : "invalid:field"
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"
}
}
@@ -188,7 +253,9 @@ struct ConformanceMain {
for line in lines {
let fields = line.split(separator: "\t", omittingEmptySubsequences: false).map(String.init)
precondition(fields.count == 5)
let actual = evaluate(fields[2], fields[3])
let actual = fields[2] == "device_proof_transcript"
? evaluateDeviceProof(fields[3])
: evaluate(fields[1], fields[2], fields[3])
precondition(actual == fields[4], fields[0])
results.append("\(fields[0])\t\(actual)")
}
+6 -1
View File
@@ -4,12 +4,17 @@ from __future__ import annotations
import pathlib
import subprocess
import tempfile
import os
ROOT = pathlib.Path(__file__).resolve().parents[1]
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:
raise SystemExit(result.stdout + result.stderr)
print(result.stdout.strip())
+417 -31
View File
@@ -4,16 +4,22 @@
from __future__ import annotations
import pathlib
import re
import shutil
import subprocess
import tempfile
import os
ROOT = pathlib.Path(__file__).resolve().parents[1]
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:
raise RuntimeError("%s\n%s%s" % (" ".join(command), result.stdout, result.stderr))
@@ -24,6 +30,32 @@ def run_failure(command: list[str], directory: pathlib.Path, expected: str) -> N
raise RuntimeError("expected failure: %s\n%s%s" % (" ".join(command), result.stdout, result.stderr))
def protobuf_message_fields(name: str) -> list[tuple[str, int]]:
result = subprocess.run(
["protoc", "--decode=google.protobuf.FileDescriptorSet", "google/protobuf/descriptor.proto"],
input=(ROOT / "gen/protobuf/tunnel-v1.pb").read_bytes(),
capture_output=True,
check=False,
)
if result.returncode != 0:
raise RuntimeError(result.stderr.decode())
lines = result.stdout.decode().splitlines()
marker = f' name: "{name}"'
try:
name_index = lines.index(marker)
start = max(index for index in range(name_index) if lines[index] == " message_type {")
except (ValueError, StopIteration) as exc:
raise RuntimeError(f"protobuf descriptor missing message {name}") from exc
depth = 0
block: list[str] = []
for line in lines[start:]:
depth += line.count("{") - line.count("}")
block.append(line)
if depth == 0:
break
return [(field, int(number)) for field, number in re.findall(r' field \{\n name: "([^"]+)"\n number: (\d+)', "\n".join(block))]
def main() -> int:
with tempfile.TemporaryDirectory(prefix="versevdi-generated-contracts-") as temporary:
workspace = pathlib.Path(temporary)
@@ -31,18 +63,42 @@ def main() -> int:
swift.write_text(
"""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(
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 {
fatalError("unexpected control wire compatibility declaration")
}
_ = try CapabilityProfile(
transport: "quic-tls13", framing: "datagram-v2", media: "encoded",
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"]
sourceRateControl: "server", videoProfiles: [video], audioProfiles: [audio]
)
do {
_ = try CapabilityProfile(
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")
} catch { }
@@ -52,24 +108,62 @@ let request = try TunnelAdmissionRequest(
clientNonce: String(repeating: "n", count: 16), deviceSignature: String(repeating: "s", count: 86), capabilities: capability
)
_ = request
let transcript = "versevdi/tunnel-admission/v17:session7:gateway8:audience43:" + String(repeating: "g", count: 43) + "1:016:" + String(repeating: "n", count: 16) + "10:quic-tls1311:datagram-v17:encoded7:encoded6: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 {
fatalError("unexpected device admission transcript")
}
let proofServerID = Data(repeating: 1, count: 16)
let proofPrincipalID = Data(repeating: 2, count: 16)
let proofDeviceID = Data(repeating: 3, count: 16)
let proofChallenge = Data(repeating: 4, count: 32)
let proofTranscript = try deviceRegistrationProofTranscript(
serverID: proofServerID, principalID: proofPrincipalID, deviceID: proofDeviceID,
challenge: proofChallenge, expiryUnixMilliseconds: 1
)
guard proofTranscript.count == 112,
String(data: proofTranscript.prefix(24), encoding: .utf8) == "versevdi-device-proof-v1",
Array(proofTranscript.suffix(8)) == [0, 0, 0, 0, 0, 0, 0, 1] else {
fatalError("unexpected device registration proof transcript")
}
let invalidProofInputs: [(String, String, Data, Data, Data, Data, Int64)] = [
("server-short", "server_id", Data(repeating: 0, count: 15), proofPrincipalID, proofDeviceID, proofChallenge, 0),
("server-long", "server_id", Data(repeating: 0, count: 17), proofPrincipalID, proofDeviceID, proofChallenge, 0),
("principal-short", "principal_id", proofServerID, Data(repeating: 0, count: 15), proofDeviceID, proofChallenge, 0),
("principal-long", "principal_id", proofServerID, Data(repeating: 0, count: 17), proofDeviceID, proofChallenge, 0),
("device-short", "device_id", proofServerID, proofPrincipalID, Data(repeating: 0, count: 15), proofChallenge, 0),
("device-long", "device_id", proofServerID, proofPrincipalID, Data(repeating: 0, count: 17), proofChallenge, 0),
("challenge-short", "challenge", proofServerID, proofPrincipalID, proofDeviceID, Data(repeating: 0, count: 31), 0),
("challenge-long", "challenge", proofServerID, proofPrincipalID, proofDeviceID, Data(repeating: 0, count: 33), 0),
("negative-expiry", "expiry_unix_milliseconds", proofServerID, proofPrincipalID, proofDeviceID, proofChallenge, -1),
]
for (name, field, serverID, principalID, deviceID, challenge, expiry) in invalidProofInputs {
do {
_ = try deviceRegistrationProofTranscript(
serverID: serverID, principalID: principalID, deviceID: deviceID,
challenge: challenge, expiryUnixMilliseconds: expiry
)
fatalError("\(name) was accepted")
} catch let error as ContractValidationError {
guard error.field == field,
error.code == (field == "expiry_unix_milliseconds" ? "minimum" : "invalid_length") else {
fatalError("\(name) returned the wrong validation error")
}
}
}
let incompatible = try CapabilityProfile(
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(
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 {
guard try CapabilityProfile.intersection([capability, capability]) == capability else {
fatalError("matching capability profiles did not intersect")
}
} catch { fatalError("matching capability profiles did not intersect") }
guard try CapabilityProfile.intersection([gatewayCapability, capability]).clientDecode == ["h264-opus"] else {
guard try CapabilityProfile.intersection([gatewayCapability, capability]).videoProfiles == [video] else {
fatalError("ordered registered profile intersection changed")
}
do {
@@ -95,6 +189,70 @@ for invalid in [
fatalError("invalid tunnel admission request was accepted")
} catch { }
}
let clientAuthority = try ClientSessionAuthority(
version: "1", sessionId: "session", gatewayId: "gateway", audience: "audience",
reconnectSequence: 2, expiresAt: "2099-01-01T00:00:00Z", capabilities: capability, selectedDescriptor: descriptor
)
let clientAuthorityJSON = try clientAuthority.encodeJSON()
let clientAuthorityObject = try JSONSerialization.jsonObject(with: clientAuthorityJSON) as! [String: Any]
guard Set(clientAuthorityObject.keys) == Set([
"version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities", "selected_descriptor"
]), !String(data: clientAuthorityJSON, encoding: .utf8)!.contains("provider_") else {
fatalError("client authority was not exactly provider-free")
}
_ = try ClientSessionAuthority.decodeJSON(clientAuthorityJSON)
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
missing.removeValue(forKey: field)
do {
_ = try ClientSessionAuthority.decodeJSON(try JSONSerialization.data(withJSONObject: missing))
fatalError("client authority accepted missing \(field)")
} catch { }
}
for (field, value) in [
("provider_profile", "apollo"),
("provider_identity", "provider-1"),
("provider_url", "https://provider.invalid"),
("management_host", "provider.invalid"),
("unknown", "true"),
] {
var injected = clientAuthorityObject
injected[field] = value
do {
_ = try ClientSessionAuthority.decodeJSON(try JSONSerialization.data(withJSONObject: injected))
fatalError("client authority accepted injected \(field)")
} catch { }
}
for expiresAt in ["not-a-time", "2099-01-01T00:00:00+00:00", "2099-01-01T00:00:00.100Z"] {
var invalidExpiry = clientAuthorityObject
invalidExpiry["expires_at"] = expiresAt
do {
_ = try ClientSessionAuthority.decodeJSON(try JSONSerialization.data(withJSONObject: invalidExpiry))
fatalError("client authority accepted invalid expiry")
} catch { }
}
do {
_ = try ClientSessionAuthority.decodeJSON(clientAuthorityJSON + Data(" {}".utf8))
fatalError("client authority accepted trailing JSON")
} catch { }
do {
_ = try AllocationPolicy(
minimumKbps: 100, targetKbps: 50, maximumKbps: 25, tier: "standard",
@@ -103,15 +261,92 @@ do {
)
fatalError("invalid allocation bounds were accepted")
} catch { }
let streamPolicy = try ProviderStreamPolicy(
resolutionWidth: 2560, resolutionHeight: 1440, fps: 120,
codec: "HEVC", bitrateKbps: 40000, audioEnabled: true
)
guard streamPolicy.codec == "HEVC" else { fatalError("stream policy changed") }
let displayMode = display
for invalid in [
{ try ProviderStreamPolicy(resolutionWidth: 319, resolutionHeight: 1440, fps: 120, codec: "HEVC", bitrateKbps: 40000, audioEnabled: true) },
{ try ProviderStreamPolicy(resolutionWidth: 2560, resolutionHeight: 1440, fps: 241, codec: "HEVC", bitrateKbps: 40000, audioEnabled: true) },
{ try ProviderStreamPolicy(resolutionWidth: 2560, resolutionHeight: 1440, fps: 120, codec: "VP9", bitrateKbps: 40000, audioEnabled: true) },
{ try DisplayMode(resolutionWidth: 319, resolutionHeight: 1440, fps: 120) },
{ try DisplayMode(resolutionWidth: 2560, resolutionHeight: 199, fps: 120) },
{ try DisplayMode(resolutionWidth: 2560, resolutionHeight: 1440, fps: 241) },
] {
do {
_ = try invalid()
fatalError("invalid display mode was accepted")
} catch { }
}
let policyFreeV2Request = try SessionRequest(
clientDeviceId: "device-1", deviceKeyId: "key-1", poolId: "pool-1",
idempotencyKey: "request-1", videoProfiles: [video], bitratePreference: try BitratePreference(mode: "auto", targetKbps: nil), requestedDisplayMode: nil
).encodeJSON()
guard !String(data: policyFreeV2Request, encoding: .utf8)!.contains("requested_display_mode") else {
fatalError("wire-v2 request encoded an absent display mode")
}
let displayRequest = try SessionRequest(
clientDeviceId: "device-1", deviceKeyId: "key-1", poolId: "pool-1",
idempotencyKey: "request-1", videoProfiles: [video], bitratePreference: try BitratePreference(mode: "explicit", targetKbps: 40000), requestedDisplayMode: displayMode
)
guard try SessionRequest.decodeJSON(displayRequest.encodeJSON()).requestedDisplayMode == displayMode else {
fatalError("display mode did not round-trip")
}
var nullDisplayRequest = try JSONSerialization.jsonObject(with: displayRequest.encodeJSON()) as! [String: Any]
nullDisplayRequest["requested_display_mode"] = NSNull()
do {
_ = try SessionRequest.decodeJSON(try JSONSerialization.data(withJSONObject: nullDisplayRequest))
fatalError("explicit null display mode was accepted")
} catch { }
let nativeIdentity = try NativeSessionIdentity(clientDeviceId: "device-1", deviceKeyId: "key-1")
let browserSession = try BrowserAuthenticatedSession(
username: "alice", provider: "local", roles: ["user"], role: "user"
)
guard !String(data: try browserSession.encodeJSON(), encoding: .utf8)!.contains("native_identity") else {
fatalError("browser session encoded native identity")
}
let nativeSession = try NativeAuthenticatedSession(
username: "alice", provider: "local", roles: ["user"], role: "user", nativeIdentity: nativeIdentity
)
guard try NativeAuthenticatedSession.decodeJSON(nativeSession.encodeJSON()).nativeIdentity == nativeIdentity else {
fatalError("native session identity did not round-trip")
}
do {
_ = try BrowserAuthenticatedSession.decodeJSON(nativeSession.encodeJSON())
fatalError("browser session accepted native identity")
} catch { }
do {
_ = try NativeAuthenticatedSession.decodeJSON(browserSession.encodeJSON())
fatalError("native session accepted missing identity")
} catch { }
var partialNativeSession = try JSONSerialization.jsonObject(with: nativeSession.encodeJSON()) as! [String: Any]
partialNativeSession["native_identity"] = ["client_device_id": "device-1"]
do {
_ = try NativeAuthenticatedSession.decodeJSON(try JSONSerialization.data(withJSONObject: partialNativeSession))
fatalError("partial native identity was accepted")
} catch { }
for roles in [[""], [String(repeating: "r", count: 65)]] {
do {
_ = try BrowserAuthenticatedSession(username: "alice", provider: "local", roles: roles, role: "user")
fatalError("invalid role item length was accepted")
} catch { }
}
_ = try NativeTunnelCredential(
clientDeviceId: "device-1", deviceKeyId: "key-1", certificateChainPem: "certificate",
trustBundlePem: "trust", expiresAt: "2099-01-01T00:00:00Z"
)
for expiresAt in ["2099-01-01T00:00:00+00:00", "2099-01-01T00:00:00.100Z"] {
do {
_ = try NativeTunnelCredential(
clientDeviceId: "device-1", deviceKeyId: "key-1", certificateChainPem: "certificate",
trustBundlePem: "trust", expiresAt: expiresAt
)
fatalError("noncanonical RFC3339 UTC timestamp was accepted")
} catch { }
}
let streamPolicy = try ProviderStreamPolicy(
videoProfile: hevc, audioProfile: audio, displayMode: displayMode,
bitrateTargetKbps: 40000, bitrateMaximumKbps: 50000
)
guard streamPolicy.videoProfile == hevc else { fatalError("stream policy changed") }
for invalid in [
{ try ProviderStreamPolicy(videoProfile: video, audioProfile: audio, displayMode: try DisplayMode(resolutionWidth: 319, resolutionHeight: 1440, fps: 120), bitrateTargetKbps: 40000, bitrateMaximumKbps: 50000) },
{ try ProviderStreamPolicy(videoProfile: video, audioProfile: audio, displayMode: displayMode, bitrateTargetKbps: 50001, bitrateMaximumKbps: 50000) },
{ 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 {
_ = try invalid()
@@ -169,17 +404,35 @@ do {
output.write(
"""
fn main() {
assert_eq!(CURRENT_WIRE_VERSION, "2");
assert_eq!(N_MINUS_1_WIRE_VERSION, "1");
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(
"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();
assert!(CapabilityProfile::new(
"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());
assert!(CapabilityProfile::new(
"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());
let request = TunnelAdmissionRequest::new(
"1".into(), "session".into(), "gateway".into(), "audience".into(),
@@ -187,8 +440,34 @@ fn main() {
).unwrap();
let transcript = "versevdi/tunnel-admission/v17:session7:gateway8:audience43:".to_string()
+ &"g".repeat(43) + "1:016:" + &"n".repeat(16)
+ "10:quic-tls1311:datagram-v17:encoded7:encoded6: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());
let proof_server_id = vec![1u8; 16];
let proof_principal_id = vec![2u8; 16];
let proof_device_id = vec![3u8; 16];
let proof_challenge = vec![4u8; 32];
let proof = device_registration_proof_transcript(
&proof_server_id, &proof_principal_id, &proof_device_id, &proof_challenge, 1,
).unwrap();
assert_eq!(proof.len(), 112);
assert_eq!(&proof[..24], b"versevdi-device-proof-v1");
assert_eq!(&proof[104..], &[0, 0, 0, 0, 0, 0, 0, 1]);
for (server_id, principal_id, device_id, challenge, expiry, field, code) in [
(vec![0; 15], proof_principal_id.clone(), proof_device_id.clone(), proof_challenge.clone(), 0, "server_id", "invalid_length"),
(vec![0; 17], proof_principal_id.clone(), proof_device_id.clone(), proof_challenge.clone(), 0, "server_id", "invalid_length"),
(proof_server_id.clone(), vec![0; 15], proof_device_id.clone(), proof_challenge.clone(), 0, "principal_id", "invalid_length"),
(proof_server_id.clone(), vec![0; 17], proof_device_id.clone(), proof_challenge.clone(), 0, "principal_id", "invalid_length"),
(proof_server_id.clone(), proof_principal_id.clone(), vec![0; 15], proof_challenge.clone(), 0, "device_id", "invalid_length"),
(proof_server_id.clone(), proof_principal_id.clone(), vec![0; 17], proof_challenge.clone(), 0, "device_id", "invalid_length"),
(proof_server_id.clone(), proof_principal_id.clone(), proof_device_id.clone(), vec![0; 31], 0, "challenge", "invalid_length"),
(proof_server_id.clone(), proof_principal_id.clone(), proof_device_id.clone(), vec![0; 33], 0, "challenge", "invalid_length"),
(proof_server_id.clone(), proof_principal_id.clone(), proof_device_id.clone(), proof_challenge.clone(), -1, "expiry_unix_milliseconds", "minimum"),
] {
assert_eq!(
device_registration_proof_transcript(&server_id, &principal_id, &device_id, &challenge, expiry),
Err(ValidationError::new(field, code)),
);
}
assert!(TunnelAdmissionRequest::new(
"2".into(), "session".into(), "gateway".into(), "audience".into(),
"g".repeat(43), 0, "n".repeat(16), "s".repeat(86), capabilities.clone(),
@@ -201,35 +480,80 @@ fn main() {
"1".into(), "session".into(), "gateway".into(), "audience".into(),
"g".repeat(43), 0, "short".into(), "s".repeat(86), capabilities.clone(),
).is_err());
let client_authority = ClientSessionAuthority::new(
"1".into(), "session".into(), "gateway".into(), "audience".into(), 2,
"2099-01-01T00:00:00Z".into(), capabilities.clone(), descriptor.clone(),
).unwrap();
assert_eq!(client_authority.sessionId(), "session");
assert_eq!(client_authority.capabilities(), &capabilities);
assert!(ClientSessionAuthority::new(
"1".into(), "session".into(), "gateway".into(), "audience".into(), 2,
"not-a-time".into(), capabilities.clone(), descriptor.clone(),
).is_err());
assert!(intersect_capability_profiles(&[capabilities.clone(), capabilities.clone()]).is_ok());
let incompatible = CapabilityProfile::new(
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
"encoded".into(), "server".into(), vec!["hevc-opus".into()],
"server".into(), vec![hevc.clone()], vec![audio.clone()],
).unwrap();
let gateway_capability = CapabilityProfile::new(
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
"encoded".into(), "server".into(), vec!["hevc-opus".into(), "h264-opus".into()],
"server".into(), vec![hevc.clone(), video.clone()], vec![audio.clone()],
).unwrap();
assert_eq!(
intersect_capability_profiles(&[gateway_capability, capabilities.clone()]).unwrap().clientDecode(),
&vec!["h264-opus".to_string()],
intersect_capability_profiles(&[gateway_capability, capabilities.clone()]).unwrap().videoProfiles(),
&vec![video.clone()],
);
assert!(intersect_capability_profiles(&[capabilities, incompatible]).is_err());
assert!(AllocationPolicy::new(
100, 50, 25, "standard".into(), "audience".into(), "verse".into(), 1, 60, 300,
).is_err());
assert!(ProviderStreamPolicy::new(
2560, 1440, 120, "HEVC".into(), 40000, true,
let display_mode = DisplayMode::new(2560, 1440, 120).unwrap();
assert!(DisplayMode::new(319, 1440, 120).is_err());
assert!(DisplayMode::new(2560, 199, 120).is_err());
assert!(DisplayMode::new(2560, 1440, 241).is_err());
let policy_free_v2_request = SessionRequest::new(
"device-1".into(), "key-1".into(), "pool-1".into(), "request-1".into(),
vec![video.clone()], BitratePreference::new("auto".into(), None).unwrap(),
None,
).unwrap();
assert!(policy_free_v2_request.requestedDisplayMode().is_none());
let display_request = SessionRequest::new(
"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()),
).unwrap();
assert_eq!(display_request.requestedDisplayMode(), &Some(display_mode));
let native_identity = NativeSessionIdentity::new("device-1".into(), "key-1".into()).unwrap();
assert!(BrowserAuthenticatedSession::new(
"alice".into(), "local".into(), vec!["user".into()], "user".into(),
).is_ok());
assert!(ProviderStreamPolicy::new(
319, 1440, 120, "HEVC".into(), 40000, true,
assert!(NativeAuthenticatedSession::new(
"alice".into(), "local".into(), vec!["user".into()], "user".into(), native_identity,
).is_ok());
assert!(BrowserAuthenticatedSession::new(
"alice".into(), "local".into(), vec![String::new()], "user".into(),
).is_err());
assert!(ProviderStreamPolicy::new(
2560, 1440, 241, "HEVC".into(), 40000, true,
assert!(BrowserAuthenticatedSession::new(
"alice".into(), "local".into(), vec!["r".repeat(65)], "user".into(),
).is_err());
assert!(NativeTunnelCredential::new(
"device-1".into(), "key-1".into(), "certificate".into(), "trust".into(),
"2099-01-01T00:00:00Z".into(),
).is_ok());
for expires_at in ["2099-01-01T00:00:00+00:00", "2099-01-01T00:00:00.100Z"] {
assert!(NativeTunnelCredential::new(
"device-1".into(), "key-1".into(), "certificate".into(), "trust".into(),
expires_at.into(),
).is_err());
}
assert!(ProviderStreamPolicy::new(
2560, 1440, 120, "VP9".into(), 40000, true,
hevc.clone(), audio.clone(), display.clone(), 40000, 50000,
).is_ok());
assert!(DisplayMode::new(319, 1440, 120).is_err());
assert!(ProviderStreamPolicy::new(
video.clone(), audio.clone(), display.clone(), 50001, 50000,
).is_err());
assert!(VideoProfile::new("vp9".into(), 8, "4:2:0".into(), "bt709-limited".into(), "sdr".into()).is_err());
assert!(GatewayTelemetry::new(
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, "ready".into(),
).is_ok());
@@ -257,6 +581,68 @@ fn main() {
)
run(["rustc", str(rust), "-o", str(workspace / "rust-contracts")], ROOT)
run([str(workspace / "rust-contracts")], ROOT)
expected_protobuf_messages = {
"ClientSessionAuthority": [
("version", 1),
("session_id", 2),
("gateway_id", 3),
("audience", 4),
("reconnect_sequence", 5),
("expires_at", 6),
("capabilities", 7),
("selected_descriptor", 8),
],
"GatewayQualityWorkRequest": [
("version", 1),
("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"
shutil.copyfile(ROOT / "gen/rust/protocol.rs", rust_unknown)
with rust_unknown.open("a", encoding="utf-8") as output:
+285 -2
View File
@@ -12,6 +12,17 @@ import sys
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:
schema_path = ROOT / "schemas/control-v1.schema.json"
schema = json.loads(schema_path.read_text(encoding="utf-8"))
@@ -24,7 +35,7 @@ def main() -> int:
assert set(definition["required"]).issubset(definition["properties"]), name
compatibility = json.loads((ROOT / "compatibility.json").read_text(encoding="utf-8"))
assert set([compatibility["current"], compatibility["n_minus_1"], compatibility["n_minus_2"]]) == {"1", "0", "-1"}
assert [compatibility["current"], compatibility["n_minus_1"], compatibility["n_minus_2"]] == ["2", "1", "0"]
assert len(set(compatibility["unsupported"])) == len(compatibility["unsupported"])
for registry in ("registries/features.json", "registries/datagrams.json"):
@@ -36,10 +47,153 @@ def main() -> int:
maximum = entry.get("max_frame_bytes", entry.get("max_payload_bytes"))
assert isinstance(maximum, int) and 1 <= maximum <= maximum_bound
feature_registry = json.loads((ROOT / "registries/features.json").read_text(encoding="utf-8"))
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 {"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"]
assert display_mode["required"] == ["resolution_width", "resolution_height", "fps"]
assert display_mode["properties"]["resolution_width"] == {"type": "integer", "minimum": 320, "maximum": 16384}
assert display_mode["properties"]["resolution_height"] == {"type": "integer", "minimum": 200, "maximum": 8640}
assert display_mode["properties"]["fps"] == {"type": "integer", "minimum": 1, "maximum": 240}
for owner, field in (
("SessionRequest", "requested_display_mode"),
("BrokerSession", "requested_display_mode"),
("BrokerSession", "effective_display_mode"),
("ManifestProfile", "display_mode"),
):
assert field not in defs[owner]["required"]
assert defs[owner]["properties"][field] == {"$ref": "#/$defs/DisplayMode"}
session_request = defs["SessionRequest"]
assert "policy_snapshot" not in session_request["required"]
assert "policy_snapshot" not in session_request["properties"]
assert "policy_snapshot" in defs["BrokerSession"]["required"]
assert defs["BrokerSession"]["properties"]["policy_snapshot"] == {"$ref": "#/$defs/AllocationPolicy"}
native_identity = defs["NativeSessionIdentity"]
assert native_identity["required"] == ["client_device_id", "device_key_id"]
browser_session = defs["BrowserAuthenticatedSession"]
assert browser_session["required"] == ["username", "provider", "roles", "role"]
assert "native_identity" not in browser_session["properties"]
native_session = defs["NativeAuthenticatedSession"]
assert native_session["required"] == ["username", "provider", "roles", "role", "native_identity"]
assert native_session["properties"]["native_identity"] == {"$ref": "#/$defs/NativeSessionIdentity"}
for session_definition in (browser_session, native_session):
assert session_definition["properties"]["roles"]["items"] == {
"type": "string", "minLength": 1, "maxLength": 64, "x-max-bytes": 64
}
tunnel_credential = defs["NativeTunnelCredential"]
assert tunnel_credential["required"] == [
"client_device_id", "device_key_id", "certificate_chain_pem", "trust_bundle_pem", "expires_at"
]
client_authority_expiry = defs["ClientSessionAuthority"]["properties"]["expires_at"]
assert client_authority_expiry["format"] == "date-time"
client_authority_expiry_pattern = re.compile(client_authority_expiry.get("pattern", r"(?!)"))
assert client_authority_expiry_pattern.fullmatch("2099-01-01T00:00:00Z"), "client authority expiry must accept canonical UTC"
for noncanonical_expiry in ("2099-01-01T00:00:00+00:00", "2099-01-01T00:00:00.100Z"):
assert not client_authority_expiry_pattern.fullmatch(noncanonical_expiry), (
f"client authority expiry accepted noncanonical UTC {noncanonical_expiry}"
)
manifest = json.loads((ROOT / "fixtures/valid/manifest.json").read_text(encoding="utf-8"))
assert set(manifest).issubset(set(defs["ConnectionManifest"]["properties"]))
public_identity = manifest["gateway"]["public_identity"]
assert public_identity == "gateway.control.test"
assert public_identity not in {
manifest["gateway"]["id"],
*manifest["gateway"]["addresses"],
"sha256:" + "00" * 32,
"apollo-provider-1",
}
forbidden = json.loads((ROOT / "fixtures/invalid/manifest-provider-field.json").read_text(encoding="utf-8"))
assert "provider_url" not in defs["ConnectionManifest"]["properties"] and "provider_url" in forbidden
session_request_fixture = json.loads((ROOT / "fixtures/valid/session-request.json").read_text(encoding="utf-8"))
assert "policy_snapshot" not in session_request_fixture
rejected_policy_fixture = json.loads((ROOT / "fixtures/invalid/session-request-policy-snapshot.json").read_text(encoding="utf-8"))
assert "policy_snapshot" in rejected_policy_fixture
browser_session_fixture = json.loads((ROOT / "fixtures/valid/authenticated-browser-session.json").read_text(encoding="utf-8"))
assert "native_identity" not in browser_session_fixture
native_session_fixture = json.loads((ROOT / "fixtures/valid/authenticated-native-session.json").read_text(encoding="utf-8"))
assert set(native_session_fixture["native_identity"]) == {"client_device_id", "device_key_id"}
partial_identity_fixture = json.loads((ROOT / "fixtures/invalid/authenticated-session-partial-native-identity.json").read_text(encoding="utf-8"))
assert set(partial_identity_fixture["native_identity"]) != {"client_device_id", "device_key_id"}
browser_native_fixture = json.loads((ROOT / "fixtures/invalid/browser-session-native-identity.json").read_text(encoding="utf-8"))
assert "native_identity" in browser_native_fixture
native_missing_fixture = json.loads((ROOT / "fixtures/invalid/native-session-missing-identity.json").read_text(encoding="utf-8"))
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"))
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"
ids = set()
@@ -51,7 +205,9 @@ def main() -> int:
assert len(fields) == 5, line
assert fields[0] not in ids, fields[0]
ids.add(fields[0])
assert fields[4] == "valid" or fields[4].startswith("invalid:"), line
assert fields[4] == "valid" or fields[4].startswith("invalid:") or (
fields[2] == "device_proof_transcript" and re.fullmatch(r"[0-9a-f]{224}", fields[4])
), line
fixture_manifest = json.loads((ROOT / "fixtures/manifest.json").read_text(encoding="utf-8"))
assert fixture_manifest["files"] == sorted(
@@ -64,11 +220,138 @@ def main() -> int:
fixture_hash.update((ROOT / relative).read_bytes())
fixture_hash.update(b"\0")
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")
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
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
session_endpoint = openapi.split(" /api/v1/auth/session:", 1)[1].split("\n /api/", 1)[0]
assert "$defs/BrowserAuthenticatedSession" in session_endpoint
assert "$defs/NativeAuthenticatedSession" in session_endpoint
login_endpoint = openapi.split(" /api/v1/auth/login:", 1)[1].split("\n /api/", 1)[0]
assert "$defs/BrowserAuthenticatedSession" in login_endpoint
assert "$defs/NativeAuthenticatedSession" not in login_endpoint
tunnel_endpoint = openapi.split(" /api/v1/auth/tunnel-credentials:", 1)[1].split("\n /api/", 1)[0]
assert "- nativeBearer: []" in tunnel_endpoint
assert "browserSession" not in tunnel_endpoint and "requestBody:" not in tunnel_endpoint
assert "$defs/NativeTunnelCredential" in tunnel_endpoint
assert "Cache-Control:" in tunnel_endpoint and "const: no-store" in tunnel_endpoint
csrf_schemes = """ browserCsrfCookie:
type: apiKey
in: cookie
name: versevdi_csrf
description: Must be identical to X-CSRF-Token and is checked against Server session state.
browserCsrfHeader:
type: apiKey
in: header
name: X-CSRF-Token
description: Must be identical to the versevdi_csrf cookie and is checked against Server session state.
"""
assert csrf_schemes in openapi, "missing exact browser CSRF security schemes"
browser_requirement = """ security:
- browserSession: []
browserCsrfCookie: []
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 (
"issueReauthenticationGrant", "logoutSession", "registerDevice", "proveDevice", "revokeDevice",
"requestBrokerSession", "allocateBrokerSession", "reconnectBrokerSession", "cancelBrokerSession",
):
operation = openapi.split(f" operationId: {operation_id}\n", 1)[1].split(" responses:\n", 1)[0]
assert browser_requirement.removeprefix(" ") in operation, f"{operation_id}: missing browser CSRF AND requirement"
for operation_id in ("logoutSession", "requestBrokerSession", "allocateBrokerSession", "reconnectBrokerSession", "cancelBrokerSession"):
operation = openapi.split(f" operationId: {operation_id}\n", 1)[1].split(" responses:\n", 1)[0]
assert " browserCsrfHeader: []\n - nativeBearer: []\n" in operation, f"{operation_id}: native bearer must remain a separate OR requirement"
reconnect_endpoint = openapi.split(" operationId: reconnectBrokerSession\n", 1)[1].split("\n /api/", 1)[0]
assert " '202':\n" in reconnect_endpoint
relaunch_response = reconnect_endpoint.split(" '202':\n", 1)[1].split(" '400':", 1)[0]
relaunch_schema = relaunch_response.split(" content:\n", 1)[1]
assert relaunch_schema == (
" application/json:\n"
" schema:\n"
" $ref: ../schemas/control-v1.schema.json#/$defs/StopOperation\n"
), "reconnect 202 must contain only the exact StopOperation schema reference"
assert "ConnectionManifest" not in relaunch_response
assert "anyOf:" not in relaunch_response and "oneOf:" not in relaunch_response
relaunch_text = " ".join(relaunch_response.split())
assert "durable `session.display_relaunch` termination operation" in relaunch_text
assert "Before initial operation creation and on every replay, the authenticated principal and active client device/key MUST match the broker session" in relaunch_text
assert "MUST create at most one termination operation total per broker session" in relaunch_text
assert "A same-owner/device lost-response retry MUST return that same operation" in relaunch_text
assert "If a user Stop wins first, reconnect MUST return a stable non-202 result" in relaunch_text
assert "a later user Stop MUST converge on that same existing `StopOperation` without creating a second operation or issuing a second Terminate" in relaunch_text
assert "not a manifest" in relaunch_text
assert "does not assert termination completion" in relaunch_text
assert "does not authorize a replacement session before `applied`" in relaunch_text
assert "fresh `SessionRequest` with a new idempotency key" in relaunch_text
assert "Failed or `termination_unconfirmed` outcomes never auto-relaunch" in relaunch_text
assert "Client local Stop or teardown MUST invalidate relaunch generation so a later `applied` state cannot cause a fresh launch" in relaunch_text
for operation_id in ("loginBrowserSession", "rotateNativeCredential", "issueNativeTunnelCredential"):
operation = openapi.split(f" operationId: {operation_id}\n", 1)[1].split(" responses:\n", 1)[0]
assert "browserCsrf" not in operation, f"{operation_id}: excluded operation gained browser CSRF"
for operation_id in ("getAuthenticatedSession", "listResources", "getBrokerSession", "resumeUserEvents"):
operation = openapi.split(f" operationId: {operation_id}\n", 1)[1].split(" responses:\n", 1)[0]
assert "browserCsrf" not in operation, f"{operation_id}: safe GET gained browser CSRF"
assert defs["ManifestGateway"]["properties"]["public_identity"]["description"] == (
"Exact TLS server name; distinct from dial addresses, gateway UUIDs, certificate fingerprints, and provider identities."
)
print("Protocol source validation passed")
return 0
+19
View File
@@ -37,6 +37,17 @@ def classify_input(raw: bytes) -> str:
return "invalid:field"
active_mask = int.from_bytes(body[1:3], "big")
return "valid" if active_mask or not any(body[3:]) else "invalid:field"
if kind == 6:
if len(body) != 8:
return "invalid:length"
x, y, width, height = (int.from_bytes(body[index:index + 2], "big") for index in range(0, 8, 2))
return "valid" if width and height and x < width and y < height else "invalid:field"
if kind == 7:
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"
@@ -61,6 +72,14 @@ def classify_feedback(raw: bytes) -> str:
return "valid" if valid_fec_status(body) else "invalid:field"
if kind == 3:
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"
if kind in (1, 2, 3):
return "invalid:direction"