Compare commits

...
Author SHA1 Message Date
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
sechmachine c554cac00d feat(protocol): carry complete encoded media frames
Verify Protocol / verify (push) Canceled after 0s
Verify Protocol / module (push) Successful in 53s
2026-07-30 22:23:22 +07:00
sechmachine d1d00d6472 ci: pin Protocol verification actions
Verify Protocol / module (push) Successful in 26s
Verify Protocol / verify (push) Canceled after 0s
2026-07-30 18:33:35 +07:00
sechmachine 03e14a9ae3 docs(openspec): archive terminal receipt repair
Verify Protocol / module (push) Successful in 20s
Verify Protocol / verify (push) Canceled after 0s
2026-07-30 18:31:28 +07:00
sechmachine 995f63a27f docs(openspec): record gateway rc9 publication
Verify Protocol / module (push) Successful in 1m46s
Verify Protocol / verify (push) Canceled after 0s
2026-07-30 14:24:08 +07:00
sechmachine 30bb1a2fa3 fix(protocol): align terminal receipt conformance
Verify Protocol / verify (push) Canceled after 0s
Verify Protocol / module (push) Successful in 1m50s
2026-07-30 14:20:48 +07:00
sechmachine 021ecf425f docs(protocol): archive terminal receipt contract
Verify Protocol / module (push) Successful in 1m43s
Verify Protocol / verify (push) Canceled after 0s
2026-07-30 10:50:57 +07:00
69 changed files with 2776 additions and 261 deletions
+3 -3
View File
@@ -20,8 +20,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-go@v7 - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with: with:
go-version: "1.26.5" go-version: "1.26.5"
cache: true cache: true
@@ -44,7 +44,7 @@ jobs:
runs-on: macos-26 runs-on: macos-26
timeout-minutes: 30 timeout-minutes: 30
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Assert pinned toolchain - name: Assert pinned toolchain
shell: bash shell: bash
run: | run: |
+1 -1
View File
@@ -21,10 +21,10 @@
# Go workspace file # Go workspace file
go.work go.work
go.work.sum go.work.sum
/.build
# env file # env file
.env .env
# IDE files # IDE files
/.idea /.idea
+6 -2
View File
@@ -1,4 +1,4 @@
.PHONY: verify generate proto-lint proto-breaking source-verify scope-verify conformance frame-verify go-test binding-compile strict-contracts clean-generated .PHONY: verify generate proto-lint proto-breaking source-verify scope-verify ci-verify conformance frame-verify go-test binding-compile strict-contracts clean-generated
PYTHON ?= python3 PYTHON ?= python3
PROTOC ?= protoc PROTOC ?= protoc
@@ -23,12 +23,16 @@ scope-verify:
$(PYTHON) -B tools/test_check_scope.py $(PYTHON) -B tools/test_check_scope.py
$(PYTHON) -B tools/check_scope.py $(PYTHON) -B tools/check_scope.py
ci-verify:
$(PYTHON) -B tools/check_ci_actions.py
go-test: go-test:
go test ./gen/go/... ./tests/go go test ./gen/go/... ./tests/go
binding-compile: binding-compile:
rustc --crate-type lib gen/rust/protocol.rs -o /tmp/versevdi-protocol-generated.rlib rustc --crate-type lib gen/rust/protocol.rs -o /tmp/versevdi-protocol-generated.rlib
swiftc -typecheck gen/swift/Protocol.swift swiftc -typecheck gen/swift/Protocol.swift
swift build
strict-contracts: strict-contracts:
$(PYTHON) -B tools/test_generated_contracts.py $(PYTHON) -B tools/test_generated_contracts.py
@@ -45,4 +49,4 @@ frame-verify:
clean-generated: clean-generated:
$(PYTHON) tools/generate.py --check $(PYTHON) tools/generate.py --check
verify: generate proto-lint proto-breaking source-verify scope-verify go-test binding-compile strict-contracts conformance frame-verify clean-generated verify: generate proto-lint proto-breaking source-verify scope-verify ci-verify go-test binding-compile strict-contracts conformance frame-verify clean-generated
+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", "$schema": "https://json-schema.org/draft/2020-12/schema",
"protocol": "versevdi-control", "protocol": "versevdi-control",
"current": "1", "current": "2",
"n_minus_1": "0", "n_minus_1": "1",
"n_minus_2": "-1", "n_minus_2": "0",
"unsupported": ["-2", "2"], "unsupported": ["-1", "3"],
"feature_registry": "registries/features.json", "feature_registry": "registries/features.json",
"datagram_registry": "registries/datagrams.json" "datagram_registry": "registries/datagrams.json"
} }
+26 -6
View File
@@ -1,11 +1,31 @@
id version kind input expected id version kind input expected
version-current 1 version 1 valid version-current 2 version 2 valid
version-n-minus-1 0 version 0 valid version-n-minus-1 1 version 1 valid
version-n-minus-2 -1 version -1 valid version-n-minus-2 0 version 0 valid
version-unsupported 2 version 2 invalid:unsupported_version version-unsupported 3 version 3 invalid:unsupported_version
page-valid 1 page limit=20;cursor=opaque valid page-valid 1 page limit=20;cursor=opaque valid
page-limit-high 1 page limit=101 invalid:invalid_limit 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-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-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-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-text-valid 1 clipboard encoding=utf-8;text=hello%20world valid
clipboard-file 1 clipboard encoding=octet-stream;file=/tmp/a invalid:unsupported_clipboard 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
+1 -1
View File
@@ -1,7 +1,7 @@
id version kind input expected id version kind input expected
valid-empty-control 1 datagram hex=564401010000000000000000000000000000010000 valid valid-empty-control 1 datagram hex=564401010000000000000000000000000000010000 valid
invalid-short 1 datagram hex=564401 invalid:truncated invalid-short 1 datagram hex=564401 invalid:truncated
invalid-version 1 datagram hex=564402010000000000000000000000000000010000 invalid:unsupported_version invalid-version 1 datagram hex=564403010000000000000000000000000000010000 invalid:unsupported_version
invalid-channel 1 datagram hex=564401990000000000000000000000000000010000 invalid:unknown_channel invalid-channel 1 datagram hex=564401990000000000000000000000000000010000 invalid:unknown_channel
invalid-length 1 datagram hex=564401010000000000000000000000000000010001 invalid:length_mismatch invalid-length 1 datagram hex=564401010000000000000000000000000000010001 invalid:length_mismatch
valid-video-empty 1 datagram hex=5644010a0000000000000000000000000000010000 valid valid-video-empty 1 datagram hex=5644010a0000000000000000000000000000010000 valid
1 id version kind input expected
2 valid-empty-control 1 datagram hex=564401010000000000000000000000000000010000 valid
3 invalid-short 1 datagram hex=564401 invalid:truncated
4 invalid-version 1 datagram hex=564402010000000000000000000000000000010000 hex=564403010000000000000000000000000000010000 invalid:unsupported_version
5 invalid-channel 1 datagram hex=564401990000000000000000000000000000010000 invalid:unknown_channel
6 invalid-length 1 datagram hex=564401010000000000000000000000000000010001 invalid:length_mismatch
7 valid-video-empty 1 datagram hex=5644010a0000000000000000000000000000010000 valid
+10
View File
@@ -0,0 +1,10 @@
id version kind input expected
v2-valid-video-single 2 datagram hex=5644020a00000000010000000000000002000000010003010203 valid
v2-valid-video-last-fragment 2 datagram hex=5644020a00000000010000000000000002037a037b0000 valid
v2-invalid-short 2 datagram hex=564402 invalid:truncated
v2-invalid-version 2 datagram hex=5644030a00000000010000000000000002000000010000 invalid:unsupported_version
v2-invalid-channel 2 datagram hex=5644020d00000000010000000000000002000000010000 invalid:unknown_channel
v2-invalid-fragment-zero 2 datagram hex=5644020a00000000010000000000000002000000000000 invalid:fragment
v2-invalid-fragment-index 2 datagram hex=5644020a00000000010000000000000002000100010000 invalid:fragment
v2-invalid-fragment-count-limit 2 datagram hex=5644020a000000000100000000000000020000037c0000 invalid:fragment_limit
v2-invalid-length 2 datagram hex=5644020a00000000010000000000000002000000010001 invalid:length_mismatch
1 id version kind input expected
2 v2-valid-video-single 2 datagram hex=5644020a00000000010000000000000002000000010003010203 valid
3 v2-valid-video-last-fragment 2 datagram hex=5644020a00000000010000000000000002037a037b0000 valid
4 v2-invalid-short 2 datagram hex=564402 invalid:truncated
5 v2-invalid-version 2 datagram hex=5644030a00000000010000000000000002000000010000 invalid:unsupported_version
6 v2-invalid-channel 2 datagram hex=5644020d00000000010000000000000002000000010000 invalid:unknown_channel
7 v2-invalid-fragment-zero 2 datagram hex=5644020a00000000010000000000000002000000000000 invalid:fragment
8 v2-invalid-fragment-index 2 datagram hex=5644020a00000000010000000000000002000100010000 invalid:fragment
9 v2-invalid-fragment-count-limit 2 datagram hex=5644020a000000000100000000000000020000037c0000 invalid:fragment_limit
10 v2-invalid-length 2 datagram hex=5644020a00000000010000000000000002000000010001 invalid:length_mismatch
@@ -7,8 +7,11 @@ valid-relative-mouse 1 gateway_input hex=564749310304fffe0003 valid
valid-utf8-scalar 1 gateway_input hex=564749310403e29883 valid valid-utf8-scalar 1 gateway_input hex=564749310403e29883 valid
valid-controller 1 gateway_input hex=5647493105110200030004ffff00010002000300040005 valid valid-controller 1 gateway_input hex=5647493105110200030004ffff00010002000300040005 valid
valid-controller-release 1 gateway_input hex=5647493105110200000000000000000000000000000000 valid valid-controller-release 1 gateway_input hex=5647493105110200000000000000000000000000000000 valid
valid-absolute-mouse 1 gateway_input hex=56474931060804d202370a0005a0 valid
valid-scroll 1 gateway_input hex=564749310704ff880078 valid
valid-idr 1 gateway_feedback hex=5647463100010000 valid valid-idr 1 gateway_feedback hex=5647463100010000 valid
valid-fec 1 gateway_feedback hex=56474631000200150000002a000500030002000a000200080002140001 valid valid-fec 1 gateway_feedback hex=56474631000200150000002a000500030002000a000200080002140001 valid
valid-terminal-receipt 1 gateway_feedback hex=5647463100030000 valid
valid-termination 1 gateway_feedback hex=564746310110000400000001 valid valid-termination 1 gateway_feedback hex=564746310110000400000001 valid
valid-rumble 1 gateway_feedback hex=56474631011100050112345678 valid valid-rumble 1 gateway_feedback hex=56474631011100050112345678 valid
valid-hdr 1 gateway_feedback hex=564746310112000101 valid valid-hdr 1 gateway_feedback hex=564746310112000101 valid
@@ -17,6 +20,15 @@ invalid-input-kind 1 gateway_input hex=564749317f00 invalid:kind
invalid-input-reserved 1 gateway_input hex=564749310203010101 invalid:reserved invalid-input-reserved 1 gateway_input hex=564749310203010101 invalid:reserved
invalid-input-utf8 1 gateway_input hex=564749310402c328 invalid:utf8 invalid-input-utf8 1 gateway_input hex=564749310402c328 invalid:utf8
invalid-input-length 1 gateway_input hex=564749310104010200 invalid:length 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-feedback-direction 1 gateway_feedback hex=5647463101020000 invalid:direction
invalid-feedback-type 1 gateway_feedback hex=5647463100030000 invalid:type 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-length 1 gateway_feedback hex=5647463101100003000000 invalid:length 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-idr 1 gateway_feedback hex=5647463100010000 valid
13 valid-fec 1 gateway_feedback hex=56474631000200150000002a000500030002000a000200080002140001 valid
14 valid-terminal-receipt 1 gateway_feedback hex=5647463100030000 valid
15 valid-termination 1 gateway_feedback hex=564746310110000400000001 valid
16 valid-rumble 1 gateway_feedback hex=56474631011100050112345678 valid
17 valid-hdr 1 gateway_feedback hex=564746310112000101 valid
20 invalid-input-reserved 1 gateway_input hex=564749310203010101 invalid:reserved
21 invalid-input-utf8 1 gateway_input hex=564749310402c328 invalid:utf8
22 invalid-input-length 1 gateway_input hex=564749310104010200 invalid:length
23 invalid-absolute-zero-viewport 1 gateway_input hex=56474931060800000000000005a0 invalid:field
24 invalid-absolute-x-out-of-range 1 gateway_input hex=5647493106080a0000000a0005a0 invalid:field
25 invalid-absolute-y-out-of-range 1 gateway_input hex=564749310608000005a00a0005a0 invalid:field
26 invalid-absolute-length 1 gateway_input hex=56474931060700000000010001 invalid:length
27 invalid-scroll-length 1 gateway_input hex=5647493107020000 invalid:length
28 invalid-feedback-direction 1 gateway_feedback hex=5647463101020000 invalid:direction
29 invalid-feedback-type invalid-terminal-receipt-direction 1 gateway_feedback hex=5647463100030000 hex=5647463101030000 invalid:type invalid:direction
30 invalid-terminal-receipt-body 1 gateway_feedback hex=5647463100030001ff invalid:length
31 invalid-terminal-receipt-truncated 1 gateway_feedback hex=56474631000300 invalid:truncated
32 invalid-terminal-receipt-length 1 gateway_feedback hex=5647463100030001 invalid:length
33 invalid-feedback-type 1 gateway_feedback hex=5647463100040000 invalid:type
34 invalid-feedback-length 1 gateway_feedback hex=5647463101100003000000 invalid:length
+8 -5
View File
@@ -1,6 +1,9 @@
id version kind input expected id version kind input expected
tunnel-current 1 tunnel offered=1;feature=control.v1 valid tunnel-current 2 tunnel offered=2;feature=control.v2 valid
tunnel-n-minus-1 0 tunnel offered=0;feature=control.v1 valid tunnel-n-minus-1 1 tunnel offered=1;feature=control.v1 valid
tunnel-n-minus-2 -1 tunnel offered=-1;feature=control.v1 valid tunnel-n-minus-2 0 tunnel offered=0;feature=control.v1 valid
tunnel-unsupported 1 tunnel offered=2;feature=control.v1 invalid:unsupported_version tunnel-display-request 2 tunnel offered=2;feature=display.request.v1 valid
tunnel-no-control 1 tunnel offered=1;feature=media.video invalid:unsupported_feature 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,10 @@
{
"username": "alice",
"provider": "local",
"roles": ["user"],
"role": "user",
"native_identity": {
"client_device_id": "device-1",
"device_key_id": "key-1"
}
}
@@ -6,7 +6,7 @@
"gateway": { "gateway": {
"id": "gateway-1", "id": "gateway-1",
"addresses": ["gateway.control.test:443"], "addresses": ["gateway.control.test:443"],
"public_identity": "gateway-1" "public_identity": "gateway.control.test"
}, },
"tunnel": { "tunnel": {
"versions": ["verse-gateway-v1/1"], "versions": ["verse-gateway-v1/1"],
@@ -0,0 +1,6 @@
{
"username": "alice",
"provider": "local",
"roles": ["user"],
"role": "user"
}
@@ -0,0 +1,7 @@
{
"client_device_id": "device-1",
"device_key_id": "key-1",
"pool_id": "pool-1",
"idempotency_key": "request-1",
"policy_snapshot": {}
}
+2 -1
View File
@@ -3,11 +3,12 @@
"files": [ "files": [
"fixtures/conformance/control-v1.tsv", "fixtures/conformance/control-v1.tsv",
"fixtures/conformance/datagram-v1.tsv", "fixtures/conformance/datagram-v1.tsv",
"fixtures/conformance/datagram-v2.tsv",
"fixtures/conformance/events-v1.tsv", "fixtures/conformance/events-v1.tsv",
"fixtures/conformance/gateway-clipboard-audit-v1.tsv", "fixtures/conformance/gateway-clipboard-audit-v1.tsv",
"fixtures/conformance/gateway-clipboard-v1.tsv", "fixtures/conformance/gateway-clipboard-v1.tsv",
"fixtures/conformance/gateway-input-feedback-v1.tsv", "fixtures/conformance/gateway-input-feedback-v1.tsv",
"fixtures/conformance/tunnel-v1.tsv" "fixtures/conformance/tunnel-v1.tsv"
], ],
"corpus_sha256": "69d5b12a533ff0d9786784b99aecc8a74a7ec2c6855b75c52e46ecff5bd3e6c5" "corpus_sha256": "ed69937656f395b30f520861948f82ed3c0b21ea86e9b33c7949ed09942e701d"
} }
@@ -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"
}
}
+1 -1
View File
@@ -6,7 +6,7 @@
"gateway": { "gateway": {
"id": "gateway-1", "id": "gateway-1",
"addresses": ["gateway.control.test:443"], "addresses": ["gateway.control.test:443"],
"public_identity": "gateway-1" "public_identity": "gateway.control.test"
}, },
"tunnel": { "tunnel": {
"versions": ["verse-gateway-v1/1"], "versions": ["verse-gateway-v1/1"],
@@ -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"
}
+6
View File
@@ -0,0 +1,6 @@
{
"client_device_id": "device-1",
"device_key_id": "key-1",
"pool_id": "pool-1",
"idempotency_key": "request-1"
}
+31
View File
@@ -0,0 +1,31 @@
# VerseVDI complete media datagram v2
Datagram v2 carries one complete encoded video or audio unit under one sequence.
It is negotiated explicitly as `datagram-v2`; v1 bytes are never reinterpreted.
The gateway relays encoded bytes without codec processing or provider exposure.
All multi-byte integers are unsigned big-endian. The fixed header is 23 bytes:
| Offset | Size | Field | Rule |
|---:|---:|---|---|
| 0 | 2 | magic | ASCII `VD` (`0x56 0x44`) |
| 2 | 1 | version | `2` only |
| 3 | 1 | channel | `media.video.v1` (10) or `media.audio.v1` (11) |
| 4 | 1 | flags | zero; unknown bits reject |
| 5 | 4 | sequence | session-local wrapping complete-unit sequence |
| 9 | 8 | timestamp_ms | sender timestamp, bounded by transport skew policy |
| 17 | 2 | fragment_index | zero-based |
| 19 | 2 | fragment_count | 1 through 891; index less than count |
| 21 | 2 | payload_length | exact payload byte count, at most 1,177 |
Each QUIC datagram is at most 1,200 bytes. One complete unit is at most
1,048,576 encoded bytes and 891 fragments. A sender rejects a larger unit
before fragmentation.
A receiver retains at most four incomplete media units and only received
fragment bytes. Fragments for one unit must agree on channel, sequence,
timestamp, flags, and count. Exact duplicates are ignored; conflicting
duplicates reject that unit. Bounded reorder is accepted. An incomplete unit
expires after 250 milliseconds, and accepting a fifth incomplete unit evicts
the oldest. Reassembly checks the 1,048,576-byte ceiling before appending and
emits only after every fragment is present.
+21 -10
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, is deliberately provider-neutral: it never carries provider routes,
certificates, credentials, opaque provider packets, clipboard bytes, files, or certificates, credentials, opaque provider packets, clipboard bytes, files, or
client-folder data. It does not change the v1 datagram header or any existing 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`) ## `input.sequenced.v1` payload (`VGI1`)
All multibyte fields are unsigned big-endian. The payload has exactly six bytes All multibyte fields are big-endian and unsigned unless a field is explicitly
of header followed by the declared body: marked signed. The payload has exactly six bytes of header followed by the
declared body:
| Offset | Size | Field | Rule | | Offset | Size | Field | Rule |
|---:|---:|---|---| |---:|---:|---|---|
@@ -32,12 +34,16 @@ during cleanup.
| `0x03` | relative mouse | `delta_x` (i16), `delta_y` (i16). | | `0x03` | relative mouse | `delta_x` (i16), `delta_y` (i16). |
| `0x04` | UTF-8 scalar | exactly one valid UTF-8 Unicode scalar, one through four bytes. | | `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. | | `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. |
Keyboard, mouse button, UTF-8, and controller messages are delivered over the Keyboard, mouse button, UTF-8, controller, absolute mouse, and scroll messages
gateway's reliable ordered input flow. Relative mouse is a state change, not a are delivered over the gateway's reliable ordered input flow. Relative and
pressed-state entry. The gateway maps the validated values to the provider's absolute mouse movement and scroll are state changes, not pressed-state
separate keyboard, mouse, UTF-8, and controller control messages; it does not entries. Kinds `0x06` and `0x07` are accepted only when the session advertises
forward this envelope to the provider. `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`) ## Reliable control payload (`VGF1`)
@@ -53,8 +59,9 @@ is the following exact envelope:
| 6 | 2 | payload length | exact payload byte count | | 6 | 2 | payload length | exact payload byte count |
| 8 | N | payload | exact type-specific body | | 8 | N | payload | exact type-specific body |
The client-to-gateway types are `0x01` IDR request (empty) and `0x02` FEC The client-to-gateway types are `0x01` IDR request (empty), `0x02` FEC
status: `frame_index` (u32), `highest_received_sequence` (u16), status, and `0x03` terminal receipt (empty). FEC status contains
`frame_index` (u32), `highest_received_sequence` (u16),
`next_contiguous_sequence` (u16), `missing_before_highest` (u16), `next_contiguous_sequence` (u16), `missing_before_highest` (u16),
`total_data_packets` (u16), `total_parity_packets` (u16), `total_data_packets` (u16), `total_parity_packets` (u16),
`received_data_packets` (u16), `received_parity_packets` (u16), `received_data_packets` (u16), `received_parity_packets` (u16),
@@ -63,6 +70,10 @@ status: `frame_index` (u32), `highest_received_sequence` (u16),
the provider's unsequenced ENet FEC delivery; it does not put it on the reliable the provider's unsequenced ENet FEC delivery; it does not put it on the reliable
provider input path. provider input path.
The terminal receipt is valid only from client to gateway with an exact
zero-byte payload. Session-state authorization remains a gateway responsibility;
the Protocol grammar defines only its fixed wire shape.
The gateway-to-client types are `0x10` host termination (`exit_code` u32), The gateway-to-client types are `0x10` host termination (`exit_code` u32),
`0x11` rumble (`controller` u8, `low_frequency` u16, `0x11` rumble (`controller` u8, `low_frequency` u16,
`high_frequency` u16), and `0x12` HDR mode (`enabled` exactly `0` or `1`). The `high_frequency` u16), and `0x12` HDR mode (`enabled` exactly `0` or `1`). The
+1
View File
@@ -1,6 +1,7 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"version": "1", "version": "1",
"framing_profiles": ["datagram-v1", "datagram-v2"],
"header_bytes": 21, "header_bytes": 21,
"maximum_frame_bytes": 65536, "maximum_frame_bytes": 65536,
"channels": [ "channels": [
+611 -39
View File
@@ -13,11 +13,11 @@ import (
"time" "time"
) )
const SchemaSHA256 = "3aec8dd72bdbb6b9657c8df3160252c93034c7c1032d471e01eae2ef91e47716" const SchemaSHA256 = "dea3dd210c53d5a2d37050dd6afd8b0ac5bb8edcb7ab25a02e4026489ce8a00f"
const ProtocolVersion = "1.0.0" const ProtocolVersion = "1.0.0"
const CurrentWireVersion = "1" const CurrentWireVersion = "2"
const NMinus1WireVersion = "0" const NMinus1WireVersion = "1"
const NMinus2WireVersion = "-1" const NMinus2WireVersion = "0"
type FieldViolation struct { type FieldViolation struct {
Field string `json:"field"` Field string `json:"field"`
@@ -50,21 +50,30 @@ type AssignedDesktop struct {
} }
type BrokerSession struct { type BrokerSession struct {
ID string `json:"id"` ID string `json:"id"`
PrincipalID string `json:"principal_id"` PrincipalID string `json:"principal_id"`
PoolID string `json:"pool_id"` PoolID string `json:"pool_id"`
AssignmentID string `json:"assignment_id,omitempty"` AssignmentID string `json:"assignment_id,omitempty"`
State string `json:"state"` State string `json:"state"`
PolicySnapshot AllocationPolicy `json:"policy_snapshot"` PolicySnapshot AllocationPolicy `json:"policy_snapshot"`
ReconnectDeadline string `json:"reconnect_deadline,omitempty"` ReconnectDeadline string `json:"reconnect_deadline,omitempty"`
Outcome string `json:"outcome,omitempty"` Outcome string `json:"outcome,omitempty"`
FailureCode string `json:"failure_code,omitempty"` FailureCode string `json:"failure_code,omitempty"`
CleanupState string `json:"cleanup_state"` CleanupState string `json:"cleanup_state"`
IdempotencyKey string `json:"idempotency_key"` IdempotencyKey string `json:"idempotency_key"`
CorrelationID string `json:"correlation_id"` CorrelationID string `json:"correlation_id"`
RequestedAt string `json:"requested_at"` RequestedAt string `json:"requested_at"`
EndedAt string `json:"ended_at,omitempty"` EndedAt string `json:"ended_at,omitempty"`
Version int64 `json:"version"` Version int64 `json:"version"`
RequestedDisplayMode *DisplayMode `json:"requested_display_mode,omitempty"`
EffectiveDisplayMode *DisplayMode `json:"effective_display_mode,omitempty"`
}
type BrowserAuthenticatedSession struct {
Username string `json:"username"`
Provider string `json:"provider"`
Roles []string `json:"roles"`
Role string `json:"role"`
} }
type CapabilityProfile struct { type CapabilityProfile struct {
@@ -134,6 +143,12 @@ type DeviceRegistrationRequest struct {
PublicKey string `json:"public_key"` PublicKey string `json:"public_key"`
} }
type DisplayMode struct {
ResolutionWidth int64 `json:"resolution_width"`
ResolutionHeight int64 `json:"resolution_height"`
Fps int64 `json:"fps"`
}
type EntitledPool struct { type EntitledPool struct {
PoolID string `json:"pool_id"` PoolID string `json:"pool_id"`
Name string `json:"name"` Name string `json:"name"`
@@ -263,8 +278,9 @@ type ManifestGateway struct {
} }
type ManifestProfile struct { type ManifestProfile struct {
ID string `json:"id"` ID string `json:"id"`
Bounds ManifestBounds `json:"bounds"` Bounds ManifestBounds `json:"bounds"`
DisplayMode *DisplayMode `json:"display_mode,omitempty"`
} }
type ManifestTunnel struct { type ManifestTunnel struct {
@@ -272,6 +288,14 @@ type ManifestTunnel struct {
Features []string `json:"features"` Features []string `json:"features"`
} }
type NativeAuthenticatedSession struct {
Username string `json:"username"`
Provider string `json:"provider"`
Roles []string `json:"roles"`
Role string `json:"role"`
NativeIdentity NativeSessionIdentity `json:"native_identity"`
}
type NativeCredential struct { type NativeCredential struct {
DeviceID string `json:"device_id,omitempty"` DeviceID string `json:"device_id,omitempty"`
FamilyID string `json:"family_id"` FamilyID string `json:"family_id"`
@@ -281,6 +305,19 @@ type NativeCredential struct {
RefreshExpiresAt string `json:"refresh_expires_at,omitempty"` RefreshExpiresAt string `json:"refresh_expires_at,omitempty"`
} }
type NativeSessionIdentity struct {
ClientDeviceID string `json:"client_device_id"`
DeviceKeyID string `json:"device_key_id"`
}
type NativeTunnelCredential struct {
ClientDeviceID string `json:"client_device_id"`
DeviceKeyID string `json:"device_key_id"`
CertificateChainPem string `json:"certificate_chain_pem"`
TrustBundlePem string `json:"trust_bundle_pem"`
ExpiresAt string `json:"expires_at"`
}
type PageInfo struct { type PageInfo struct {
Limit int64 `json:"limit"` Limit int64 `json:"limit"`
NextCursor string `json:"next_cursor"` NextCursor string `json:"next_cursor"`
@@ -383,11 +420,11 @@ type SessionAuthority struct {
} }
type SessionRequest struct { type SessionRequest struct {
ClientDeviceID string `json:"client_device_id"` ClientDeviceID string `json:"client_device_id"`
DeviceKeyID string `json:"device_key_id"` DeviceKeyID string `json:"device_key_id"`
PoolID string `json:"pool_id"` PoolID string `json:"pool_id"`
IdempotencyKey string `json:"idempotency_key"` IdempotencyKey string `json:"idempotency_key"`
PolicySnapshot AllocationPolicy `json:"policy_snapshot"` RequestedDisplayMode *DisplayMode `json:"requested_display_mode,omitempty"`
} }
type StableError struct { type StableError struct {
@@ -767,6 +804,16 @@ func (v BrokerSession) Validate() error {
if v.Version != 0 && v.Version < 1 { if v.Version != 0 && v.Version < 1 {
violations = append(violations, FieldViolation{Field: "version", Code: "minimum"}) violations = append(violations, FieldViolation{Field: "version", Code: "minimum"})
} }
if v.RequestedDisplayMode != nil {
if err := v.RequestedDisplayMode.Validate(); err != nil {
violations = append(violations, FieldViolation{Field: "requested_display_mode", Code: "invalid_object"})
}
}
if v.EffectiveDisplayMode != nil {
if err := v.EffectiveDisplayMode.Validate(); err != nil {
violations = append(violations, FieldViolation{Field: "effective_display_mode", Code: "invalid_object"})
}
}
if len(violations) > 0 { if len(violations) > 0 {
return ValidationError{Violations: violations} return ValidationError{Violations: violations}
} }
@@ -812,6 +859,12 @@ func DecodeBrokerSession(data []byte) (BrokerSession, error) {
if raw, ok := fields["version"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { if raw, ok := fields["version"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "version", Code: "required"}}} return value, ValidationError{Violations: []FieldViolation{{Field: "version", Code: "required"}}}
} }
if raw, ok := fields["requested_display_mode"]; ok && bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "requested_display_mode", Code: "invalid_object"}}}
}
if raw, ok := fields["effective_display_mode"]; ok && bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "effective_display_mode", Code: "invalid_object"}}}
}
decoder := json.NewDecoder(bytes.NewReader(data)) decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields() decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil { if err := decoder.Decode(&value); err != nil {
@@ -837,6 +890,105 @@ func EncodeBrokerSession(value BrokerSession) ([]byte, error) {
return json.Marshal(value) return json.Marshal(value)
} }
func (v BrowserAuthenticatedSession) Validate() error {
var violations []FieldViolation
if v.Username == "" {
violations = append(violations, FieldViolation{Field: "username", Code: "required"})
}
if len(v.Username) < 1 && v.Username != "" {
violations = append(violations, FieldViolation{Field: "username", Code: "min_length"})
}
if len(v.Username) > 256 {
violations = append(violations, FieldViolation{Field: "username", Code: "max_length"})
}
if v.Provider == "" {
violations = append(violations, FieldViolation{Field: "provider", Code: "required"})
}
if len(v.Provider) < 1 && v.Provider != "" {
violations = append(violations, FieldViolation{Field: "provider", Code: "min_length"})
}
if len(v.Provider) > 64 {
violations = append(violations, FieldViolation{Field: "provider", Code: "max_length"})
}
if v.Roles == nil {
violations = append(violations, FieldViolation{Field: "roles", Code: "required"})
}
if len(v.Roles) > 16 {
violations = append(violations, FieldViolation{Field: "roles", Code: "max_items"})
}
for _, item := range v.Roles {
if len(item) < 1 {
violations = append(violations, FieldViolation{Field: "roles", Code: "min_item_length"})
}
}
for _, item := range v.Roles {
if len(item) > 64 {
violations = append(violations, FieldViolation{Field: "roles", Code: "max_item_length"})
}
}
for _, item := range v.Roles {
if len(item) > 64 {
violations = append(violations, FieldViolation{Field: "roles", Code: "max_item_bytes"})
}
}
if v.Role == "" {
violations = append(violations, FieldViolation{Field: "role", Code: "required"})
}
if v.Role != "" && !(v.Role == "user" || v.Role == "admin") {
violations = append(violations, FieldViolation{Field: "role", Code: "invalid_value"})
}
if len(violations) > 0 {
return ValidationError{Violations: violations}
}
return nil
}
func DecodeBrowserAuthenticatedSession(data []byte) (BrowserAuthenticatedSession, error) {
var value BrowserAuthenticatedSession
if len(data) > 1024*1024 {
return value, errors.New("protocol payload exceeds limit")
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(data, &fields); err != nil {
return value, err
}
if raw, ok := fields["provider"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "provider", Code: "required"}}}
}
if raw, ok := fields["role"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "role", Code: "required"}}}
}
if raw, ok := fields["roles"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "roles", Code: "required"}}}
}
if raw, ok := fields["username"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "username", Code: "required"}}}
}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil {
return value, err
}
var trailing any
if err := decoder.Decode(&trailing); err != io.EOF {
if err == nil {
return value, errors.New("trailing JSON value")
}
return value, err
}
if err := value.Validate(); err != nil {
return value, err
}
return value, nil
}
func EncodeBrowserAuthenticatedSession(value BrowserAuthenticatedSession) ([]byte, error) {
if err := value.Validate(); err != nil {
return nil, err
}
return json.Marshal(value)
}
func (v CapabilityProfile) Validate() error { func (v CapabilityProfile) Validate() error {
var violations []FieldViolation var violations []FieldViolation
if v.Transport == "" { if v.Transport == "" {
@@ -851,11 +1003,8 @@ func (v CapabilityProfile) Validate() error {
if v.Framing == "" { if v.Framing == "" {
violations = append(violations, FieldViolation{Field: "framing", Code: "required"}) violations = append(violations, FieldViolation{Field: "framing", Code: "required"})
} }
if len(v.Framing) < 1 && v.Framing != "" { if v.Framing != "" && !(v.Framing == "datagram-v1" || v.Framing == "datagram-v2") {
violations = append(violations, FieldViolation{Field: "framing", Code: "min_length"}) violations = append(violations, FieldViolation{Field: "framing", Code: "invalid_value"})
}
if len(v.Framing) > 64 {
violations = append(violations, FieldViolation{Field: "framing", Code: "max_length"})
} }
if v.Media == "" { if v.Media == "" {
violations = append(violations, FieldViolation{Field: "media", Code: "required"}) violations = append(violations, FieldViolation{Field: "media", Code: "required"})
@@ -1628,6 +1777,84 @@ func EncodeDeviceRegistrationRequest(value DeviceRegistrationRequest) ([]byte, e
return json.Marshal(value) return json.Marshal(value)
} }
func (v DisplayMode) Validate() error {
var violations []FieldViolation
if v.ResolutionWidth == 0 {
violations = append(violations, FieldViolation{Field: "resolution_width", Code: "required"})
}
if v.ResolutionWidth != 0 && v.ResolutionWidth < 320 {
violations = append(violations, FieldViolation{Field: "resolution_width", Code: "minimum"})
}
if v.ResolutionWidth > 16384 {
violations = append(violations, FieldViolation{Field: "resolution_width", Code: "maximum"})
}
if v.ResolutionHeight == 0 {
violations = append(violations, FieldViolation{Field: "resolution_height", Code: "required"})
}
if v.ResolutionHeight != 0 && v.ResolutionHeight < 200 {
violations = append(violations, FieldViolation{Field: "resolution_height", Code: "minimum"})
}
if v.ResolutionHeight > 8640 {
violations = append(violations, FieldViolation{Field: "resolution_height", Code: "maximum"})
}
if v.Fps == 0 {
violations = append(violations, FieldViolation{Field: "fps", Code: "required"})
}
if v.Fps != 0 && v.Fps < 1 {
violations = append(violations, FieldViolation{Field: "fps", Code: "minimum"})
}
if v.Fps > 240 {
violations = append(violations, FieldViolation{Field: "fps", Code: "maximum"})
}
if len(violations) > 0 {
return ValidationError{Violations: violations}
}
return nil
}
func DecodeDisplayMode(data []byte) (DisplayMode, error) {
var value DisplayMode
if len(data) > 1024*1024 {
return value, errors.New("protocol payload exceeds limit")
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(data, &fields); err != nil {
return value, err
}
if raw, ok := fields["fps"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "fps", Code: "required"}}}
}
if raw, ok := fields["resolution_height"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "resolution_height", Code: "required"}}}
}
if raw, ok := fields["resolution_width"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "resolution_width", Code: "required"}}}
}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil {
return value, err
}
var trailing any
if err := decoder.Decode(&trailing); err != io.EOF {
if err == nil {
return value, errors.New("trailing JSON value")
}
return value, err
}
if err := value.Validate(); err != nil {
return value, err
}
return value, nil
}
func EncodeDisplayMode(value DisplayMode) ([]byte, error) {
if err := value.Validate(); err != nil {
return nil, err
}
return json.Marshal(value)
}
func (v EntitledPool) Validate() error { func (v EntitledPool) Validate() error {
var violations []FieldViolation var violations []FieldViolation
if v.PoolID == "" { if v.PoolID == "" {
@@ -2588,6 +2815,16 @@ func (v GatewayRegistration) Validate() error {
if len(v.Features) > 64 { if len(v.Features) > 64 {
violations = append(violations, FieldViolation{Field: "features", Code: "max_items"}) violations = append(violations, FieldViolation{Field: "features", Code: "max_items"})
} }
for _, item := range v.Features {
if len(item) < 1 {
violations = append(violations, FieldViolation{Field: "features", Code: "min_item_length"})
}
}
for _, item := range v.Features {
if len(item) > 64 {
violations = append(violations, FieldViolation{Field: "features", Code: "max_item_length"})
}
}
if reflect.DeepEqual(v.Capabilities, CapabilityProfile{}) { if reflect.DeepEqual(v.Capabilities, CapabilityProfile{}) {
violations = append(violations, FieldViolation{Field: "capabilities", Code: "required"}) violations = append(violations, FieldViolation{Field: "capabilities", Code: "required"})
} }
@@ -3130,6 +3367,16 @@ func (v ManifestGateway) Validate() error {
if len(v.Addresses) > 4 { if len(v.Addresses) > 4 {
violations = append(violations, FieldViolation{Field: "addresses", Code: "max_items"}) violations = append(violations, FieldViolation{Field: "addresses", Code: "max_items"})
} }
for _, item := range v.Addresses {
if len(item) < 1 {
violations = append(violations, FieldViolation{Field: "addresses", Code: "min_item_length"})
}
}
for _, item := range v.Addresses {
if len(item) > 256 {
violations = append(violations, FieldViolation{Field: "addresses", Code: "max_item_length"})
}
}
if v.PublicIdentity == "" { if v.PublicIdentity == "" {
violations = append(violations, FieldViolation{Field: "public_identity", Code: "required"}) violations = append(violations, FieldViolation{Field: "public_identity", Code: "required"})
} }
@@ -3205,6 +3452,11 @@ func (v ManifestProfile) Validate() error {
if err := v.Bounds.Validate(); err != nil { if err := v.Bounds.Validate(); err != nil {
violations = append(violations, FieldViolation{Field: "bounds", Code: "invalid_object"}) violations = append(violations, FieldViolation{Field: "bounds", Code: "invalid_object"})
} }
if v.DisplayMode != nil {
if err := v.DisplayMode.Validate(); err != nil {
violations = append(violations, FieldViolation{Field: "display_mode", Code: "invalid_object"})
}
}
if len(violations) > 0 { if len(violations) > 0 {
return ValidationError{Violations: violations} return ValidationError{Violations: violations}
} }
@@ -3226,6 +3478,9 @@ func DecodeManifestProfile(data []byte) (ManifestProfile, error) {
if raw, ok := fields["id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { if raw, ok := fields["id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "id", Code: "required"}}} return value, ValidationError{Violations: []FieldViolation{{Field: "id", Code: "required"}}}
} }
if raw, ok := fields["display_mode"]; ok && bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "display_mode", Code: "invalid_object"}}}
}
decoder := json.NewDecoder(bytes.NewReader(data)) decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields() decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil { if err := decoder.Decode(&value); err != nil {
@@ -3262,12 +3517,32 @@ func (v ManifestTunnel) Validate() error {
if len(v.Versions) > 4 { if len(v.Versions) > 4 {
violations = append(violations, FieldViolation{Field: "versions", Code: "max_items"}) violations = append(violations, FieldViolation{Field: "versions", Code: "max_items"})
} }
for _, item := range v.Versions {
if len(item) < 1 {
violations = append(violations, FieldViolation{Field: "versions", Code: "min_item_length"})
}
}
for _, item := range v.Versions {
if len(item) > 64 {
violations = append(violations, FieldViolation{Field: "versions", Code: "max_item_length"})
}
}
if v.Features == nil { if v.Features == nil {
violations = append(violations, FieldViolation{Field: "features", Code: "required"}) violations = append(violations, FieldViolation{Field: "features", Code: "required"})
} }
if len(v.Features) > 32 { if len(v.Features) > 32 {
violations = append(violations, FieldViolation{Field: "features", Code: "max_items"}) violations = append(violations, FieldViolation{Field: "features", Code: "max_items"})
} }
for _, item := range v.Features {
if len(item) < 1 {
violations = append(violations, FieldViolation{Field: "features", Code: "min_item_length"})
}
}
for _, item := range v.Features {
if len(item) > 64 {
violations = append(violations, FieldViolation{Field: "features", Code: "max_item_length"})
}
}
if len(violations) > 0 { if len(violations) > 0 {
return ValidationError{Violations: violations} return ValidationError{Violations: violations}
} }
@@ -3314,6 +3589,114 @@ func EncodeManifestTunnel(value ManifestTunnel) ([]byte, error) {
return json.Marshal(value) return json.Marshal(value)
} }
func (v NativeAuthenticatedSession) Validate() error {
var violations []FieldViolation
if v.Username == "" {
violations = append(violations, FieldViolation{Field: "username", Code: "required"})
}
if len(v.Username) < 1 && v.Username != "" {
violations = append(violations, FieldViolation{Field: "username", Code: "min_length"})
}
if len(v.Username) > 256 {
violations = append(violations, FieldViolation{Field: "username", Code: "max_length"})
}
if v.Provider == "" {
violations = append(violations, FieldViolation{Field: "provider", Code: "required"})
}
if len(v.Provider) < 1 && v.Provider != "" {
violations = append(violations, FieldViolation{Field: "provider", Code: "min_length"})
}
if len(v.Provider) > 64 {
violations = append(violations, FieldViolation{Field: "provider", Code: "max_length"})
}
if v.Roles == nil {
violations = append(violations, FieldViolation{Field: "roles", Code: "required"})
}
if len(v.Roles) > 16 {
violations = append(violations, FieldViolation{Field: "roles", Code: "max_items"})
}
for _, item := range v.Roles {
if len(item) < 1 {
violations = append(violations, FieldViolation{Field: "roles", Code: "min_item_length"})
}
}
for _, item := range v.Roles {
if len(item) > 64 {
violations = append(violations, FieldViolation{Field: "roles", Code: "max_item_length"})
}
}
for _, item := range v.Roles {
if len(item) > 64 {
violations = append(violations, FieldViolation{Field: "roles", Code: "max_item_bytes"})
}
}
if v.Role == "" {
violations = append(violations, FieldViolation{Field: "role", Code: "required"})
}
if v.Role != "" && !(v.Role == "user" || v.Role == "admin") {
violations = append(violations, FieldViolation{Field: "role", Code: "invalid_value"})
}
if reflect.DeepEqual(v.NativeIdentity, NativeSessionIdentity{}) {
violations = append(violations, FieldViolation{Field: "native_identity", Code: "required"})
}
if err := v.NativeIdentity.Validate(); err != nil {
violations = append(violations, FieldViolation{Field: "native_identity", Code: "invalid_object"})
}
if len(violations) > 0 {
return ValidationError{Violations: violations}
}
return nil
}
func DecodeNativeAuthenticatedSession(data []byte) (NativeAuthenticatedSession, error) {
var value NativeAuthenticatedSession
if len(data) > 1024*1024 {
return value, errors.New("protocol payload exceeds limit")
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(data, &fields); err != nil {
return value, err
}
if raw, ok := fields["native_identity"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "native_identity", Code: "required"}}}
}
if raw, ok := fields["provider"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "provider", Code: "required"}}}
}
if raw, ok := fields["role"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "role", Code: "required"}}}
}
if raw, ok := fields["roles"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "roles", Code: "required"}}}
}
if raw, ok := fields["username"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "username", Code: "required"}}}
}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil {
return value, err
}
var trailing any
if err := decoder.Decode(&trailing); err != io.EOF {
if err == nil {
return value, errors.New("trailing JSON value")
}
return value, err
}
if err := value.Validate(); err != nil {
return value, err
}
return value, nil
}
func EncodeNativeAuthenticatedSession(value NativeAuthenticatedSession) ([]byte, error) {
if err := value.Validate(); err != nil {
return nil, err
}
return json.Marshal(value)
}
func (v NativeCredential) Validate() error { func (v NativeCredential) Validate() error {
var violations []FieldViolation var violations []FieldViolation
if len(v.DeviceID) > 128 { if len(v.DeviceID) > 128 {
@@ -3417,6 +3800,176 @@ func EncodeNativeCredential(value NativeCredential) ([]byte, error) {
return json.Marshal(value) return json.Marshal(value)
} }
func (v NativeSessionIdentity) Validate() error {
var violations []FieldViolation
if v.ClientDeviceID == "" {
violations = append(violations, FieldViolation{Field: "client_device_id", Code: "required"})
}
if len(v.ClientDeviceID) < 1 && v.ClientDeviceID != "" {
violations = append(violations, FieldViolation{Field: "client_device_id", Code: "min_length"})
}
if len(v.ClientDeviceID) > 128 {
violations = append(violations, FieldViolation{Field: "client_device_id", Code: "max_length"})
}
if v.DeviceKeyID == "" {
violations = append(violations, FieldViolation{Field: "device_key_id", Code: "required"})
}
if len(v.DeviceKeyID) < 1 && v.DeviceKeyID != "" {
violations = append(violations, FieldViolation{Field: "device_key_id", Code: "min_length"})
}
if len(v.DeviceKeyID) > 128 {
violations = append(violations, FieldViolation{Field: "device_key_id", Code: "max_length"})
}
if len(violations) > 0 {
return ValidationError{Violations: violations}
}
return nil
}
func DecodeNativeSessionIdentity(data []byte) (NativeSessionIdentity, error) {
var value NativeSessionIdentity
if len(data) > 1024*1024 {
return value, errors.New("protocol payload exceeds limit")
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(data, &fields); err != nil {
return value, err
}
if raw, ok := fields["client_device_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "client_device_id", Code: "required"}}}
}
if raw, ok := fields["device_key_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "device_key_id", Code: "required"}}}
}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil {
return value, err
}
var trailing any
if err := decoder.Decode(&trailing); err != io.EOF {
if err == nil {
return value, errors.New("trailing JSON value")
}
return value, err
}
if err := value.Validate(); err != nil {
return value, err
}
return value, nil
}
func EncodeNativeSessionIdentity(value NativeSessionIdentity) ([]byte, error) {
if err := value.Validate(); err != nil {
return nil, err
}
return json.Marshal(value)
}
func (v NativeTunnelCredential) Validate() error {
var violations []FieldViolation
if v.ClientDeviceID == "" {
violations = append(violations, FieldViolation{Field: "client_device_id", Code: "required"})
}
if len(v.ClientDeviceID) < 1 && v.ClientDeviceID != "" {
violations = append(violations, FieldViolation{Field: "client_device_id", Code: "min_length"})
}
if len(v.ClientDeviceID) > 128 {
violations = append(violations, FieldViolation{Field: "client_device_id", Code: "max_length"})
}
if v.DeviceKeyID == "" {
violations = append(violations, FieldViolation{Field: "device_key_id", Code: "required"})
}
if len(v.DeviceKeyID) < 1 && v.DeviceKeyID != "" {
violations = append(violations, FieldViolation{Field: "device_key_id", Code: "min_length"})
}
if len(v.DeviceKeyID) > 128 {
violations = append(violations, FieldViolation{Field: "device_key_id", Code: "max_length"})
}
if v.CertificateChainPem == "" {
violations = append(violations, FieldViolation{Field: "certificate_chain_pem", Code: "required"})
}
if len(v.CertificateChainPem) < 1 && v.CertificateChainPem != "" {
violations = append(violations, FieldViolation{Field: "certificate_chain_pem", Code: "min_length"})
}
if len(v.CertificateChainPem) > 65536 {
violations = append(violations, FieldViolation{Field: "certificate_chain_pem", Code: "max_length"})
}
if v.TrustBundlePem == "" {
violations = append(violations, FieldViolation{Field: "trust_bundle_pem", Code: "required"})
}
if len(v.TrustBundlePem) < 1 && v.TrustBundlePem != "" {
violations = append(violations, FieldViolation{Field: "trust_bundle_pem", Code: "min_length"})
}
if len(v.TrustBundlePem) > 65536 {
violations = append(violations, FieldViolation{Field: "trust_bundle_pem", Code: "max_length"})
}
if v.ExpiresAt == "" {
violations = append(violations, FieldViolation{Field: "expires_at", Code: "required"})
}
if len(v.ExpiresAt) > 64 {
violations = append(violations, FieldViolation{Field: "expires_at", Code: "max_length"})
}
if v.ExpiresAt != "" {
if parsed, err := time.Parse(time.RFC3339Nano, v.ExpiresAt); err != nil || parsed.UTC().Format(time.RFC3339Nano) != v.ExpiresAt {
violations = append(violations, FieldViolation{Field: "expires_at", Code: "invalid_time"})
}
}
if len(violations) > 0 {
return ValidationError{Violations: violations}
}
return nil
}
func DecodeNativeTunnelCredential(data []byte) (NativeTunnelCredential, error) {
var value NativeTunnelCredential
if len(data) > 1024*1024 {
return value, errors.New("protocol payload exceeds limit")
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(data, &fields); err != nil {
return value, err
}
if raw, ok := fields["certificate_chain_pem"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "certificate_chain_pem", Code: "required"}}}
}
if raw, ok := fields["client_device_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "client_device_id", Code: "required"}}}
}
if raw, ok := fields["device_key_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "device_key_id", Code: "required"}}}
}
if raw, ok := fields["expires_at"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "expires_at", Code: "required"}}}
}
if raw, ok := fields["trust_bundle_pem"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "trust_bundle_pem", Code: "required"}}}
}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil {
return value, err
}
var trailing any
if err := decoder.Decode(&trailing); err != io.EOF {
if err == nil {
return value, errors.New("trailing JSON value")
}
return value, err
}
if err := value.Validate(); err != nil {
return value, err
}
return value, nil
}
func EncodeNativeTunnelCredential(value NativeTunnelCredential) ([]byte, error) {
if err := value.Validate(); err != nil {
return nil, err
}
return json.Marshal(value)
}
func (v PageInfo) Validate() error { func (v PageInfo) Validate() error {
var violations []FieldViolation var violations []FieldViolation
if v.Limit == 0 { if v.Limit == 0 {
@@ -3766,6 +4319,16 @@ func (v ProviderState) Validate() error {
if len(v.Channels) > 8 { if len(v.Channels) > 8 {
violations = append(violations, FieldViolation{Field: "channels", Code: "max_items"}) violations = append(violations, FieldViolation{Field: "channels", Code: "max_items"})
} }
for _, item := range v.Channels {
if len(item) < 1 {
violations = append(violations, FieldViolation{Field: "channels", Code: "min_item_length"})
}
}
for _, item := range v.Channels {
if len(item) > 64 {
violations = append(violations, FieldViolation{Field: "channels", Code: "max_item_length"})
}
}
if len(violations) > 0 { if len(violations) > 0 {
return ValidationError{Violations: violations} return ValidationError{Violations: violations}
} }
@@ -4652,11 +5215,10 @@ func (v SessionRequest) Validate() error {
if len(v.IdempotencyKey) > 256 { if len(v.IdempotencyKey) > 256 {
violations = append(violations, FieldViolation{Field: "idempotency_key", Code: "max_length"}) violations = append(violations, FieldViolation{Field: "idempotency_key", Code: "max_length"})
} }
if reflect.DeepEqual(v.PolicySnapshot, AllocationPolicy{}) { if v.RequestedDisplayMode != nil {
violations = append(violations, FieldViolation{Field: "policy_snapshot", Code: "required"}) if err := v.RequestedDisplayMode.Validate(); err != nil {
} violations = append(violations, FieldViolation{Field: "requested_display_mode", Code: "invalid_object"})
if err := v.PolicySnapshot.Validate(); err != nil { }
violations = append(violations, FieldViolation{Field: "policy_snapshot", Code: "invalid_object"})
} }
if len(violations) > 0 { if len(violations) > 0 {
return ValidationError{Violations: violations} return ValidationError{Violations: violations}
@@ -4682,12 +5244,12 @@ func DecodeSessionRequest(data []byte) (SessionRequest, error) {
if raw, ok := fields["idempotency_key"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { if raw, ok := fields["idempotency_key"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "idempotency_key", Code: "required"}}} return value, ValidationError{Violations: []FieldViolation{{Field: "idempotency_key", Code: "required"}}}
} }
if raw, ok := fields["policy_snapshot"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "policy_snapshot", Code: "required"}}}
}
if raw, ok := fields["pool_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { if raw, ok := fields["pool_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "pool_id", Code: "required"}}} return value, ValidationError{Violations: []FieldViolation{{Field: "pool_id", Code: "required"}}}
} }
if raw, ok := fields["requested_display_mode"]; ok && bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "requested_display_mode", Code: "invalid_object"}}}
}
decoder := json.NewDecoder(bytes.NewReader(data)) decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields() decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil { if err := decoder.Decode(&value); err != nil {
@@ -4940,12 +5502,22 @@ func (v VersionNegotiation) Validate() error {
if len(v.SupportedVersions) > 3 { if len(v.SupportedVersions) > 3 {
violations = append(violations, FieldViolation{Field: "supported_versions", Code: "max_items"}) violations = append(violations, FieldViolation{Field: "supported_versions", Code: "max_items"})
} }
for _, item := range v.SupportedVersions {
if len(item) > 16 {
violations = append(violations, FieldViolation{Field: "supported_versions", Code: "max_item_length"})
}
}
if v.Features == nil { if v.Features == nil {
violations = append(violations, FieldViolation{Field: "features", Code: "required"}) violations = append(violations, FieldViolation{Field: "features", Code: "required"})
} }
if len(v.Features) > 64 { if len(v.Features) > 64 {
violations = append(violations, FieldViolation{Field: "features", Code: "max_items"}) violations = append(violations, FieldViolation{Field: "features", Code: "max_items"})
} }
for _, item := range v.Features {
if len(item) > 64 {
violations = append(violations, FieldViolation{Field: "features", Code: "max_item_length"})
}
}
if len(violations) > 0 { if len(violations) > 0 {
return ValidationError{Violations: violations} return ValidationError{Violations: violations}
} }
+7 -7
View File
@@ -1,18 +1,18 @@
{ {
"compatibility": { "compatibility": {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"current": "1", "current": "2",
"datagram_registry": "registries/datagrams.json", "datagram_registry": "registries/datagrams.json",
"feature_registry": "registries/features.json", "feature_registry": "registries/features.json",
"n_minus_1": "0", "n_minus_1": "1",
"n_minus_2": "-1", "n_minus_2": "0",
"protocol": "versevdi-control", "protocol": "versevdi-control",
"unsupported": [ "unsupported": [
"-2", "-1",
"2" "3"
] ]
}, },
"generator_sha256": "00fdba050eb924a54dd3d63aac0a38560341b675f0de4e3e9631ee895057a9b6", "generator_sha256": "8a153cf1e99682d010ff91c754ef056c64aece8f8bbca0ca58f8eef2b9039119",
"protocol_version": "1.0.0", "protocol_version": "1.0.0",
"schema_sha256": "3aec8dd72bdbb6b9657c8df3160252c93034c7c1032d471e01eae2ef91e47716" "schema_sha256": "dea3dd210c53d5a2d37050dd6afd8b0ac5bb8edcb7ab25a02e4026489ce8a00f"
} }
+231 -16
View File
@@ -1,9 +1,9 @@
// Code generated by tools/generate.py; DO NOT EDIT. // Code generated by tools/generate.py; DO NOT EDIT.
#![allow(non_snake_case)] #![allow(non_snake_case)]
pub const SCHEMA_SHA256: &str = "3aec8dd72bdbb6b9657c8df3160252c93034c7c1032d471e01eae2ef91e47716"; pub const SCHEMA_SHA256: &str = "dea3dd210c53d5a2d37050dd6afd8b0ac5bb8edcb7ab25a02e4026489ce8a00f";
pub const CURRENT_WIRE_VERSION: &str = "1"; pub const CURRENT_WIRE_VERSION: &str = "2";
pub const N_MINUS_1_WIRE_VERSION: &str = "0"; pub const N_MINUS_1_WIRE_VERSION: &str = "1";
pub const N_MINUS_2_WIRE_VERSION: &str = "-1"; pub const N_MINUS_2_WIRE_VERSION: &str = "0";
pub type JsonObject = std::collections::BTreeMap<String, String>; pub type JsonObject = std::collections::BTreeMap<String, String>;
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@@ -30,6 +30,19 @@ fn valid_base64_url(value: &str) -> bool {
_ => false, _ => 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'
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct AllocationPolicy { pub struct AllocationPolicy {
@@ -136,11 +149,13 @@ pub struct BrokerSession {
requestedAt: String, requestedAt: String,
endedAt: Option<String>, endedAt: Option<String>,
version: i64, version: i64,
requestedDisplayMode: Option<DisplayMode>,
effectiveDisplayMode: Option<DisplayMode>,
} }
impl BrokerSession { impl BrokerSession {
pub fn new(id: String, principalId: String, poolId: String, assignmentId: Option<String>, state: String, policySnapshot: AllocationPolicy, reconnectDeadline: Option<String>, outcome: Option<String>, failureCode: Option<String>, cleanupState: String, idempotencyKey: String, correlationId: String, requestedAt: String, endedAt: Option<String>, version: i64) -> Result<Self, ValidationError> { pub fn new(id: String, principalId: String, poolId: String, assignmentId: Option<String>, state: String, policySnapshot: AllocationPolicy, reconnectDeadline: Option<String>, outcome: Option<String>, failureCode: Option<String>, cleanupState: String, idempotencyKey: String, correlationId: String, requestedAt: String, endedAt: Option<String>, version: i64, requestedDisplayMode: Option<DisplayMode>, effectiveDisplayMode: Option<DisplayMode>) -> Result<Self, ValidationError> {
let value = Self { id, principalId, poolId, assignmentId, state, policySnapshot, reconnectDeadline, outcome, failureCode, cleanupState, idempotencyKey, correlationId, requestedAt, endedAt, version }; let value = Self { id, principalId, poolId, assignmentId, state, policySnapshot, reconnectDeadline, outcome, failureCode, cleanupState, idempotencyKey, correlationId, requestedAt, endedAt, version, requestedDisplayMode, effectiveDisplayMode };
value.validate()?; value.validate()?;
Ok(value) Ok(value)
} }
@@ -163,6 +178,7 @@ impl BrokerSession {
self.policySnapshot.validate().map_err(|_| ValidationError::new("policy_snapshot", "invalid_object"))?; self.policySnapshot.validate().map_err(|_| ValidationError::new("policy_snapshot", "invalid_object"))?;
if let Some(value) = &self.reconnectDeadline { if let Some(value) = &self.reconnectDeadline {
if value.len() > 64 { return Err(ValidationError::new("reconnect_deadline", "max_length")); } if value.len() > 64 { return Err(ValidationError::new("reconnect_deadline", "max_length")); }
if !valid_rfc3339_utc(value.as_str()) { return Err(ValidationError::new("reconnect_deadline", "invalid_time")); }
} }
if let Some(value) = &self.outcome { if let Some(value) = &self.outcome {
if value.len() > 64 { return Err(ValidationError::new("outcome", "max_length")); } if value.len() > 64 { return Err(ValidationError::new("outcome", "max_length")); }
@@ -180,10 +196,18 @@ impl BrokerSession {
if !self.correlationId.is_empty() && self.correlationId.len() < 1 { return Err(ValidationError::new("correlation_id", "min_length")); } if !self.correlationId.is_empty() && self.correlationId.len() < 1 { return Err(ValidationError::new("correlation_id", "min_length")); }
if self.correlationId.len() > 128 { return Err(ValidationError::new("correlation_id", "max_length")); } if self.correlationId.len() > 128 { return Err(ValidationError::new("correlation_id", "max_length")); }
if self.requestedAt.len() > 64 { return Err(ValidationError::new("requested_at", "max_length")); } if self.requestedAt.len() > 64 { return Err(ValidationError::new("requested_at", "max_length")); }
if !valid_rfc3339_utc(self.requestedAt.as_str()) { return Err(ValidationError::new("requested_at", "invalid_time")); }
if let Some(value) = &self.endedAt { if let Some(value) = &self.endedAt {
if value.len() > 64 { return Err(ValidationError::new("ended_at", "max_length")); } if value.len() > 64 { return Err(ValidationError::new("ended_at", "max_length")); }
if !valid_rfc3339_utc(value.as_str()) { return Err(ValidationError::new("ended_at", "invalid_time")); }
} }
if self.version < 1 { return Err(ValidationError::new("version", "minimum")); } if self.version < 1 { return Err(ValidationError::new("version", "minimum")); }
if let Some(value) = &self.requestedDisplayMode {
value.validate().map_err(|_| ValidationError::new("requested_display_mode", "invalid_object"))?;
}
if let Some(value) = &self.effectiveDisplayMode {
value.validate().map_err(|_| ValidationError::new("effective_display_mode", "invalid_object"))?;
}
Ok(()) Ok(())
} }
pub fn id(&self) -> &String { &self.id } pub fn id(&self) -> &String { &self.id }
@@ -201,6 +225,42 @@ impl BrokerSession {
pub fn requestedAt(&self) -> &String { &self.requestedAt } pub fn requestedAt(&self) -> &String { &self.requestedAt }
pub fn endedAt(&self) -> &Option<String> { &self.endedAt } pub fn endedAt(&self) -> &Option<String> { &self.endedAt }
pub fn version(&self) -> &i64 { &self.version } pub fn version(&self) -> &i64 { &self.version }
pub fn requestedDisplayMode(&self) -> &Option<DisplayMode> { &self.requestedDisplayMode }
pub fn effectiveDisplayMode(&self) -> &Option<DisplayMode> { &self.effectiveDisplayMode }
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BrowserAuthenticatedSession {
username: String,
provider: String,
roles: Vec<String>,
role: String,
}
impl BrowserAuthenticatedSession {
pub fn new(username: String, provider: String, roles: Vec<String>, role: String) -> Result<Self, ValidationError> {
let value = Self { username, provider, roles, role };
value.validate()?;
Ok(value)
}
pub fn validate(&self) -> Result<(), ValidationError> {
if self.username.is_empty() { return Err(ValidationError::new("username", "required")); }
if !self.username.is_empty() && self.username.len() < 1 { return Err(ValidationError::new("username", "min_length")); }
if self.username.len() > 256 { return Err(ValidationError::new("username", "max_length")); }
if self.provider.is_empty() { return Err(ValidationError::new("provider", "required")); }
if !self.provider.is_empty() && self.provider.len() < 1 { return Err(ValidationError::new("provider", "min_length")); }
if self.provider.len() > 64 { return Err(ValidationError::new("provider", "max_length")); }
if self.roles.len() > 16 { return Err(ValidationError::new("roles", "max_items")); }
for item in self.roles.iter() { if item.as_bytes().len() < 1 { return Err(ValidationError::new("roles", "min_item_length")); } }
for item in self.roles.iter() { if item.as_bytes().len() > 64 { return Err(ValidationError::new("roles", "max_item_length")); } }
for item in self.roles.iter() { if item.as_bytes().len() > 64 { return Err(ValidationError::new("roles", "max_item_bytes")); } }
if self.role != "user" && self.role != "admin" { return Err(ValidationError::new("role", "invalid_value")); }
Ok(())
}
pub fn username(&self) -> &String { &self.username }
pub fn provider(&self) -> &String { &self.provider }
pub fn roles(&self) -> &Vec<String> { &self.roles }
pub fn role(&self) -> &String { &self.role }
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@@ -223,9 +283,7 @@ impl CapabilityProfile {
if self.transport.is_empty() { return Err(ValidationError::new("transport", "required")); } if self.transport.is_empty() { return Err(ValidationError::new("transport", "required")); }
if !self.transport.is_empty() && self.transport.len() < 1 { return Err(ValidationError::new("transport", "min_length")); } if !self.transport.is_empty() && self.transport.len() < 1 { return Err(ValidationError::new("transport", "min_length")); }
if self.transport.len() > 64 { return Err(ValidationError::new("transport", "max_length")); } if self.transport.len() > 64 { return Err(ValidationError::new("transport", "max_length")); }
if self.framing.is_empty() { return Err(ValidationError::new("framing", "required")); } if self.framing != "datagram-v1" && self.framing != "datagram-v2" { return Err(ValidationError::new("framing", "invalid_value")); }
if !self.framing.is_empty() && self.framing.len() < 1 { return Err(ValidationError::new("framing", "min_length")); }
if self.framing.len() > 64 { return Err(ValidationError::new("framing", "max_length")); }
if self.media.is_empty() { return Err(ValidationError::new("media", "required")); } if self.media.is_empty() { return Err(ValidationError::new("media", "required")); }
if !self.media.is_empty() && self.media.len() < 1 { return Err(ValidationError::new("media", "min_length")); } if !self.media.is_empty() && self.media.len() < 1 { return Err(ValidationError::new("media", "min_length")); }
if self.media.len() > 64 { return Err(ValidationError::new("media", "max_length")); } if self.media.len() > 64 { return Err(ValidationError::new("media", "max_length")); }
@@ -420,6 +478,7 @@ impl DeviceChallenge {
if !self.challenge.is_empty() && self.challenge.len() < 1 { return Err(ValidationError::new("challenge", "min_length")); } if !self.challenge.is_empty() && self.challenge.len() < 1 { return Err(ValidationError::new("challenge", "min_length")); }
if self.challenge.len() > 256 { return Err(ValidationError::new("challenge", "max_length")); } if self.challenge.len() > 256 { return Err(ValidationError::new("challenge", "max_length")); }
if self.expiresAt.len() > 64 { return Err(ValidationError::new("expires_at", "max_length")); } if self.expiresAt.len() > 64 { return Err(ValidationError::new("expires_at", "max_length")); }
if !valid_rfc3339_utc(self.expiresAt.as_str()) { return Err(ValidationError::new("expires_at", "invalid_time")); }
if self.algorithm != "ed25519" { return Err(ValidationError::new("algorithm", "invalid_value")); } if self.algorithm != "ed25519" { return Err(ValidationError::new("algorithm", "invalid_value")); }
if self.signatureFormat != "ed25519-domain-separated-v1" { return Err(ValidationError::new("signature_format", "invalid_value")); } if self.signatureFormat != "ed25519-domain-separated-v1" { return Err(ValidationError::new("signature_format", "invalid_value")); }
Ok(()) Ok(())
@@ -496,6 +555,33 @@ impl DeviceRegistrationRequest {
pub fn publicKey(&self) -> &String { &self.publicKey } pub fn publicKey(&self) -> &String { &self.publicKey }
} }
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DisplayMode {
resolutionWidth: i64,
resolutionHeight: i64,
fps: i64,
}
impl DisplayMode {
pub fn new(resolutionWidth: i64, resolutionHeight: i64, fps: i64) -> Result<Self, ValidationError> {
let value = Self { resolutionWidth, resolutionHeight, fps };
value.validate()?;
Ok(value)
}
pub fn validate(&self) -> Result<(), ValidationError> {
if self.resolutionWidth < 320 { return Err(ValidationError::new("resolution_width", "minimum")); }
if self.resolutionWidth > 16384 { return Err(ValidationError::new("resolution_width", "maximum")); }
if self.resolutionHeight < 200 { return Err(ValidationError::new("resolution_height", "minimum")); }
if self.resolutionHeight > 8640 { return Err(ValidationError::new("resolution_height", "maximum")); }
if self.fps < 1 { return Err(ValidationError::new("fps", "minimum")); }
if self.fps > 240 { return Err(ValidationError::new("fps", "maximum")); }
Ok(())
}
pub fn resolutionWidth(&self) -> &i64 { &self.resolutionWidth }
pub fn resolutionHeight(&self) -> &i64 { &self.resolutionHeight }
pub fn fps(&self) -> &i64 { &self.fps }
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct EntitledPool { pub struct EntitledPool {
poolId: String, poolId: String,
@@ -601,6 +687,7 @@ impl EventEnvelope {
if self.version < 1 { return Err(ValidationError::new("version", "minimum")); } if self.version < 1 { return Err(ValidationError::new("version", "minimum")); }
self.resource.validate().map_err(|_| ValidationError::new("resource", "invalid_object"))?; self.resource.validate().map_err(|_| ValidationError::new("resource", "invalid_object"))?;
if self.occurredAt.len() > 64 { return Err(ValidationError::new("occurred_at", "max_length")); } if self.occurredAt.len() > 64 { return Err(ValidationError::new("occurred_at", "max_length")); }
if !valid_rfc3339_utc(self.occurredAt.as_str()) { return Err(ValidationError::new("occurred_at", "invalid_time")); }
if self.correlationId.is_empty() { return Err(ValidationError::new("correlation_id", "required")); } if self.correlationId.is_empty() { return Err(ValidationError::new("correlation_id", "required")); }
if !self.correlationId.is_empty() && self.correlationId.len() < 1 { return Err(ValidationError::new("correlation_id", "min_length")); } if !self.correlationId.is_empty() && self.correlationId.len() < 1 { return Err(ValidationError::new("correlation_id", "min_length")); }
if self.correlationId.len() > 128 { return Err(ValidationError::new("correlation_id", "max_length")); } if self.correlationId.len() > 128 { return Err(ValidationError::new("correlation_id", "max_length")); }
@@ -754,6 +841,7 @@ impl GatewayDrain {
if !self.reason.is_empty() && self.reason.len() < 1 { return Err(ValidationError::new("reason", "min_length")); } if !self.reason.is_empty() && self.reason.len() < 1 { return Err(ValidationError::new("reason", "min_length")); }
if self.reason.len() > 256 { return Err(ValidationError::new("reason", "max_length")); } if self.reason.len() > 256 { return Err(ValidationError::new("reason", "max_length")); }
if self.deadline.len() > 64 { return Err(ValidationError::new("deadline", "max_length")); } if self.deadline.len() > 64 { return Err(ValidationError::new("deadline", "max_length")); }
if !valid_rfc3339_utc(self.deadline.as_str()) { return Err(ValidationError::new("deadline", "invalid_time")); }
Ok(()) Ok(())
} }
pub fn version(&self) -> &String { &self.version } pub fn version(&self) -> &String { &self.version }
@@ -788,6 +876,7 @@ impl GatewayHeartbeat {
if self.gatewayId.len() > 128 { return Err(ValidationError::new("gateway_id", "max_length")); } if self.gatewayId.len() > 128 { return Err(ValidationError::new("gateway_id", "max_length")); }
if self.sequence < 1 { return Err(ValidationError::new("sequence", "minimum")); } if self.sequence < 1 { return Err(ValidationError::new("sequence", "minimum")); }
if self.observedAt.len() > 64 { return Err(ValidationError::new("observed_at", "max_length")); } if self.observedAt.len() > 64 { return Err(ValidationError::new("observed_at", "max_length")); }
if !valid_rfc3339_utc(self.observedAt.as_str()) { return Err(ValidationError::new("observed_at", "invalid_time")); }
if self.activeConnections < 0 { return Err(ValidationError::new("active_connections", "minimum")); } if self.activeConnections < 0 { return Err(ValidationError::new("active_connections", "minimum")); }
if self.activeConnections > 1000000 { return Err(ValidationError::new("active_connections", "maximum")); } if self.activeConnections > 1000000 { return Err(ValidationError::new("active_connections", "maximum")); }
if self.egressKbps < 0 { return Err(ValidationError::new("egress_kbps", "minimum")); } if self.egressKbps < 0 { return Err(ValidationError::new("egress_kbps", "minimum")); }
@@ -858,6 +947,8 @@ impl GatewayRegistration {
if self.bandwidthCapacityKbps < 1 { return Err(ValidationError::new("bandwidth_capacity_kbps", "minimum")); } if self.bandwidthCapacityKbps < 1 { return Err(ValidationError::new("bandwidth_capacity_kbps", "minimum")); }
if self.bandwidthCapacityKbps > 1000000000 { return Err(ValidationError::new("bandwidth_capacity_kbps", "maximum")); } if self.bandwidthCapacityKbps > 1000000000 { return Err(ValidationError::new("bandwidth_capacity_kbps", "maximum")); }
if self.features.len() > 64 { return Err(ValidationError::new("features", "max_items")); } if self.features.len() > 64 { return Err(ValidationError::new("features", "max_items")); }
for item in self.features.iter() { if item.as_bytes().len() < 1 { return Err(ValidationError::new("features", "min_item_length")); } }
for item in self.features.iter() { if item.as_bytes().len() > 64 { return Err(ValidationError::new("features", "max_item_length")); } }
self.capabilities.validate().map_err(|_| ValidationError::new("capabilities", "invalid_object"))?; self.capabilities.validate().map_err(|_| ValidationError::new("capabilities", "invalid_object"))?;
if self.protocolMinVersion > self.protocolMaxVersion { return Err(ValidationError::new("protocol_version", "invalid_order")); } if self.protocolMinVersion > self.protocolMaxVersion { return Err(ValidationError::new("protocol_version", "invalid_order")); }
Ok(()) Ok(())
@@ -981,6 +1072,7 @@ impl GrantReference {
if !self.opaqueValue.is_empty() && self.opaqueValue.len() < 43 { return Err(ValidationError::new("opaque_value", "min_length")); } if !self.opaqueValue.is_empty() && self.opaqueValue.len() < 43 { return Err(ValidationError::new("opaque_value", "min_length")); }
if self.opaqueValue.len() > 256 { return Err(ValidationError::new("opaque_value", "max_length")); } if self.opaqueValue.len() > 256 { return Err(ValidationError::new("opaque_value", "max_length")); }
if self.expiresAt.len() > 64 { return Err(ValidationError::new("expires_at", "max_length")); } if self.expiresAt.len() > 64 { return Err(ValidationError::new("expires_at", "max_length")); }
if !valid_rfc3339_utc(self.expiresAt.as_str()) { return Err(ValidationError::new("expires_at", "invalid_time")); }
if self.audience.is_empty() { return Err(ValidationError::new("audience", "required")); } if self.audience.is_empty() { return Err(ValidationError::new("audience", "required")); }
if !self.audience.is_empty() && self.audience.len() < 1 { return Err(ValidationError::new("audience", "min_length")); } if !self.audience.is_empty() && self.audience.len() < 1 { return Err(ValidationError::new("audience", "min_length")); }
if self.audience.len() > 128 { return Err(ValidationError::new("audience", "max_length")); } if self.audience.len() > 128 { return Err(ValidationError::new("audience", "max_length")); }
@@ -1068,6 +1160,8 @@ impl ManifestGateway {
if self.id.len() > 128 { return Err(ValidationError::new("id", "max_length")); } if self.id.len() > 128 { return Err(ValidationError::new("id", "max_length")); }
if self.addresses.len() < 1 { return Err(ValidationError::new("addresses", "min_items")); } if self.addresses.len() < 1 { return Err(ValidationError::new("addresses", "min_items")); }
if self.addresses.len() > 4 { return Err(ValidationError::new("addresses", "max_items")); } if self.addresses.len() > 4 { return Err(ValidationError::new("addresses", "max_items")); }
for item in self.addresses.iter() { if item.as_bytes().len() < 1 { return Err(ValidationError::new("addresses", "min_item_length")); } }
for item in self.addresses.iter() { if item.as_bytes().len() > 256 { return Err(ValidationError::new("addresses", "max_item_length")); } }
if self.publicIdentity.is_empty() { return Err(ValidationError::new("public_identity", "required")); } if self.publicIdentity.is_empty() { return Err(ValidationError::new("public_identity", "required")); }
if !self.publicIdentity.is_empty() && self.publicIdentity.len() < 1 { return Err(ValidationError::new("public_identity", "min_length")); } if !self.publicIdentity.is_empty() && self.publicIdentity.len() < 1 { return Err(ValidationError::new("public_identity", "min_length")); }
if self.publicIdentity.len() > 256 { return Err(ValidationError::new("public_identity", "max_length")); } if self.publicIdentity.len() > 256 { return Err(ValidationError::new("public_identity", "max_length")); }
@@ -1082,11 +1176,12 @@ impl ManifestGateway {
pub struct ManifestProfile { pub struct ManifestProfile {
id: String, id: String,
bounds: ManifestBounds, bounds: ManifestBounds,
displayMode: Option<DisplayMode>,
} }
impl ManifestProfile { impl ManifestProfile {
pub fn new(id: String, bounds: ManifestBounds) -> Result<Self, ValidationError> { pub fn new(id: String, bounds: ManifestBounds, displayMode: Option<DisplayMode>) -> Result<Self, ValidationError> {
let value = Self { id, bounds }; let value = Self { id, bounds, displayMode };
value.validate()?; value.validate()?;
Ok(value) Ok(value)
} }
@@ -1095,10 +1190,14 @@ impl ManifestProfile {
if !self.id.is_empty() && self.id.len() < 1 { return Err(ValidationError::new("id", "min_length")); } if !self.id.is_empty() && self.id.len() < 1 { return Err(ValidationError::new("id", "min_length")); }
if self.id.len() > 128 { return Err(ValidationError::new("id", "max_length")); } if self.id.len() > 128 { return Err(ValidationError::new("id", "max_length")); }
self.bounds.validate().map_err(|_| ValidationError::new("bounds", "invalid_object"))?; self.bounds.validate().map_err(|_| ValidationError::new("bounds", "invalid_object"))?;
if let Some(value) = &self.displayMode {
value.validate().map_err(|_| ValidationError::new("display_mode", "invalid_object"))?;
}
Ok(()) Ok(())
} }
pub fn id(&self) -> &String { &self.id } pub fn id(&self) -> &String { &self.id }
pub fn bounds(&self) -> &ManifestBounds { &self.bounds } pub fn bounds(&self) -> &ManifestBounds { &self.bounds }
pub fn displayMode(&self) -> &Option<DisplayMode> { &self.displayMode }
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@@ -1116,13 +1215,54 @@ impl ManifestTunnel {
pub fn validate(&self) -> Result<(), ValidationError> { pub fn validate(&self) -> Result<(), ValidationError> {
if self.versions.len() < 1 { return Err(ValidationError::new("versions", "min_items")); } if self.versions.len() < 1 { return Err(ValidationError::new("versions", "min_items")); }
if self.versions.len() > 4 { return Err(ValidationError::new("versions", "max_items")); } if self.versions.len() > 4 { return Err(ValidationError::new("versions", "max_items")); }
for item in self.versions.iter() { if item.as_bytes().len() < 1 { return Err(ValidationError::new("versions", "min_item_length")); } }
for item in self.versions.iter() { if item.as_bytes().len() > 64 { return Err(ValidationError::new("versions", "max_item_length")); } }
if self.features.len() > 32 { return Err(ValidationError::new("features", "max_items")); } if self.features.len() > 32 { return Err(ValidationError::new("features", "max_items")); }
for item in self.features.iter() { if item.as_bytes().len() < 1 { return Err(ValidationError::new("features", "min_item_length")); } }
for item in self.features.iter() { if item.as_bytes().len() > 64 { return Err(ValidationError::new("features", "max_item_length")); } }
Ok(()) Ok(())
} }
pub fn versions(&self) -> &Vec<String> { &self.versions } pub fn versions(&self) -> &Vec<String> { &self.versions }
pub fn features(&self) -> &Vec<String> { &self.features } pub fn features(&self) -> &Vec<String> { &self.features }
} }
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NativeAuthenticatedSession {
username: String,
provider: String,
roles: Vec<String>,
role: String,
nativeIdentity: NativeSessionIdentity,
}
impl NativeAuthenticatedSession {
pub fn new(username: String, provider: String, roles: Vec<String>, role: String, nativeIdentity: NativeSessionIdentity) -> Result<Self, ValidationError> {
let value = Self { username, provider, roles, role, nativeIdentity };
value.validate()?;
Ok(value)
}
pub fn validate(&self) -> Result<(), ValidationError> {
if self.username.is_empty() { return Err(ValidationError::new("username", "required")); }
if !self.username.is_empty() && self.username.len() < 1 { return Err(ValidationError::new("username", "min_length")); }
if self.username.len() > 256 { return Err(ValidationError::new("username", "max_length")); }
if self.provider.is_empty() { return Err(ValidationError::new("provider", "required")); }
if !self.provider.is_empty() && self.provider.len() < 1 { return Err(ValidationError::new("provider", "min_length")); }
if self.provider.len() > 64 { return Err(ValidationError::new("provider", "max_length")); }
if self.roles.len() > 16 { return Err(ValidationError::new("roles", "max_items")); }
for item in self.roles.iter() { if item.as_bytes().len() < 1 { return Err(ValidationError::new("roles", "min_item_length")); } }
for item in self.roles.iter() { if item.as_bytes().len() > 64 { return Err(ValidationError::new("roles", "max_item_length")); } }
for item in self.roles.iter() { if item.as_bytes().len() > 64 { return Err(ValidationError::new("roles", "max_item_bytes")); } }
if self.role != "user" && self.role != "admin" { return Err(ValidationError::new("role", "invalid_value")); }
self.nativeIdentity.validate().map_err(|_| ValidationError::new("native_identity", "invalid_object"))?;
Ok(())
}
pub fn username(&self) -> &String { &self.username }
pub fn provider(&self) -> &String { &self.provider }
pub fn roles(&self) -> &Vec<String> { &self.roles }
pub fn role(&self) -> &String { &self.role }
pub fn nativeIdentity(&self) -> &NativeSessionIdentity { &self.nativeIdentity }
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct NativeCredential { pub struct NativeCredential {
deviceId: Option<String>, deviceId: Option<String>,
@@ -1153,8 +1293,10 @@ impl NativeCredential {
if !self.refreshToken.is_empty() && self.refreshToken.len() < 1 { return Err(ValidationError::new("refresh_token", "min_length")); } if !self.refreshToken.is_empty() && self.refreshToken.len() < 1 { return Err(ValidationError::new("refresh_token", "min_length")); }
if self.refreshToken.len() > 256 { return Err(ValidationError::new("refresh_token", "max_length")); } if self.refreshToken.len() > 256 { return Err(ValidationError::new("refresh_token", "max_length")); }
if self.expiresAt.len() > 64 { return Err(ValidationError::new("expires_at", "max_length")); } if self.expiresAt.len() > 64 { return Err(ValidationError::new("expires_at", "max_length")); }
if !valid_rfc3339_utc(self.expiresAt.as_str()) { return Err(ValidationError::new("expires_at", "invalid_time")); }
if let Some(value) = &self.refreshExpiresAt { if let Some(value) = &self.refreshExpiresAt {
if value.len() > 64 { return Err(ValidationError::new("refresh_expires_at", "max_length")); } if value.len() > 64 { return Err(ValidationError::new("refresh_expires_at", "max_length")); }
if !valid_rfc3339_utc(value.as_str()) { return Err(ValidationError::new("refresh_expires_at", "invalid_time")); }
} }
Ok(()) Ok(())
} }
@@ -1166,6 +1308,70 @@ impl NativeCredential {
pub fn refreshExpiresAt(&self) -> &Option<String> { &self.refreshExpiresAt } pub fn refreshExpiresAt(&self) -> &Option<String> { &self.refreshExpiresAt }
} }
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NativeSessionIdentity {
clientDeviceId: String,
deviceKeyId: String,
}
impl NativeSessionIdentity {
pub fn new(clientDeviceId: String, deviceKeyId: String) -> Result<Self, ValidationError> {
let value = Self { clientDeviceId, deviceKeyId };
value.validate()?;
Ok(value)
}
pub fn validate(&self) -> Result<(), ValidationError> {
if self.clientDeviceId.is_empty() { return Err(ValidationError::new("client_device_id", "required")); }
if !self.clientDeviceId.is_empty() && self.clientDeviceId.len() < 1 { return Err(ValidationError::new("client_device_id", "min_length")); }
if self.clientDeviceId.len() > 128 { return Err(ValidationError::new("client_device_id", "max_length")); }
if self.deviceKeyId.is_empty() { return Err(ValidationError::new("device_key_id", "required")); }
if !self.deviceKeyId.is_empty() && self.deviceKeyId.len() < 1 { return Err(ValidationError::new("device_key_id", "min_length")); }
if self.deviceKeyId.len() > 128 { return Err(ValidationError::new("device_key_id", "max_length")); }
Ok(())
}
pub fn clientDeviceId(&self) -> &String { &self.clientDeviceId }
pub fn deviceKeyId(&self) -> &String { &self.deviceKeyId }
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NativeTunnelCredential {
clientDeviceId: String,
deviceKeyId: String,
certificateChainPem: String,
trustBundlePem: String,
expiresAt: String,
}
impl NativeTunnelCredential {
pub fn new(clientDeviceId: String, deviceKeyId: String, certificateChainPem: String, trustBundlePem: String, expiresAt: String) -> Result<Self, ValidationError> {
let value = Self { clientDeviceId, deviceKeyId, certificateChainPem, trustBundlePem, expiresAt };
value.validate()?;
Ok(value)
}
pub fn validate(&self) -> Result<(), ValidationError> {
if self.clientDeviceId.is_empty() { return Err(ValidationError::new("client_device_id", "required")); }
if !self.clientDeviceId.is_empty() && self.clientDeviceId.len() < 1 { return Err(ValidationError::new("client_device_id", "min_length")); }
if self.clientDeviceId.len() > 128 { return Err(ValidationError::new("client_device_id", "max_length")); }
if self.deviceKeyId.is_empty() { return Err(ValidationError::new("device_key_id", "required")); }
if !self.deviceKeyId.is_empty() && self.deviceKeyId.len() < 1 { return Err(ValidationError::new("device_key_id", "min_length")); }
if self.deviceKeyId.len() > 128 { return Err(ValidationError::new("device_key_id", "max_length")); }
if self.certificateChainPem.is_empty() { return Err(ValidationError::new("certificate_chain_pem", "required")); }
if !self.certificateChainPem.is_empty() && self.certificateChainPem.len() < 1 { return Err(ValidationError::new("certificate_chain_pem", "min_length")); }
if self.certificateChainPem.len() > 65536 { return Err(ValidationError::new("certificate_chain_pem", "max_length")); }
if self.trustBundlePem.is_empty() { return Err(ValidationError::new("trust_bundle_pem", "required")); }
if !self.trustBundlePem.is_empty() && self.trustBundlePem.len() < 1 { return Err(ValidationError::new("trust_bundle_pem", "min_length")); }
if self.trustBundlePem.len() > 65536 { return Err(ValidationError::new("trust_bundle_pem", "max_length")); }
if self.expiresAt.len() > 64 { return Err(ValidationError::new("expires_at", "max_length")); }
if !valid_rfc3339_utc(self.expiresAt.as_str()) { return Err(ValidationError::new("expires_at", "invalid_time")); }
Ok(())
}
pub fn clientDeviceId(&self) -> &String { &self.clientDeviceId }
pub fn deviceKeyId(&self) -> &String { &self.deviceKeyId }
pub fn certificateChainPem(&self) -> &String { &self.certificateChainPem }
pub fn trustBundlePem(&self) -> &String { &self.trustBundlePem }
pub fn expiresAt(&self) -> &String { &self.expiresAt }
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct PageInfo { pub struct PageInfo {
limit: i64, limit: i64,
@@ -1228,6 +1434,7 @@ impl ProviderSessionWork {
if self.gatewayId.len() > 128 { return Err(ValidationError::new("gateway_id", "max_length")); } if self.gatewayId.len() > 128 { return Err(ValidationError::new("gateway_id", "max_length")); }
if self.reconnectSequence < 0 { return Err(ValidationError::new("reconnect_sequence", "minimum")); } if self.reconnectSequence < 0 { return Err(ValidationError::new("reconnect_sequence", "minimum")); }
if self.expiresAt.len() > 64 { return Err(ValidationError::new("expires_at", "max_length")); } if self.expiresAt.len() > 64 { return Err(ValidationError::new("expires_at", "max_length")); }
if !valid_rfc3339_utc(self.expiresAt.as_str()) { return Err(ValidationError::new("expires_at", "invalid_time")); }
if self.providerProfile != "apollo" { return Err(ValidationError::new("provider_profile", "invalid_value")); } if self.providerProfile != "apollo" { return Err(ValidationError::new("provider_profile", "invalid_value")); }
if self.providerIdentity.is_empty() { return Err(ValidationError::new("provider_identity", "required")); } if self.providerIdentity.is_empty() { return Err(ValidationError::new("provider_identity", "required")); }
if !self.providerIdentity.is_empty() && self.providerIdentity.len() < 1 { return Err(ValidationError::new("provider_identity", "min_length")); } if !self.providerIdentity.is_empty() && self.providerIdentity.len() < 1 { return Err(ValidationError::new("provider_identity", "min_length")); }
@@ -1308,6 +1515,8 @@ impl ProviderState {
if self.sessionId.len() > 128 { return Err(ValidationError::new("session_id", "max_length")); } if self.sessionId.len() > 128 { return Err(ValidationError::new("session_id", "max_length")); }
if self.state != "starting" && self.state != "ready" && self.state != "disconnected" && self.state != "terminating" && self.state != "terminated" && self.state != "cleanup_pending" && self.state != "failed" { return Err(ValidationError::new("state", "invalid_value")); } if self.state != "starting" && self.state != "ready" && self.state != "disconnected" && self.state != "terminating" && self.state != "terminated" && self.state != "cleanup_pending" && self.state != "failed" { return Err(ValidationError::new("state", "invalid_value")); }
if self.channels.len() > 8 { return Err(ValidationError::new("channels", "max_items")); } if self.channels.len() > 8 { return Err(ValidationError::new("channels", "max_items")); }
for item in self.channels.iter() { if item.as_bytes().len() < 1 { return Err(ValidationError::new("channels", "min_item_length")); } }
for item in self.channels.iter() { if item.as_bytes().len() > 64 { return Err(ValidationError::new("channels", "max_item_length")); } }
Ok(()) Ok(())
} }
pub fn version(&self) -> &String { &self.version } pub fn version(&self) -> &String { &self.version }
@@ -1374,6 +1583,7 @@ impl ReauthGrant {
if !self.purpose.is_empty() && self.purpose.len() < 1 { return Err(ValidationError::new("purpose", "min_length")); } if !self.purpose.is_empty() && self.purpose.len() < 1 { return Err(ValidationError::new("purpose", "min_length")); }
if self.purpose.len() > 64 { return Err(ValidationError::new("purpose", "max_length")); } if self.purpose.len() > 64 { return Err(ValidationError::new("purpose", "max_length")); }
if self.expiresAt.len() > 64 { return Err(ValidationError::new("expires_at", "max_length")); } if self.expiresAt.len() > 64 { return Err(ValidationError::new("expires_at", "max_length")); }
if !valid_rfc3339_utc(self.expiresAt.as_str()) { return Err(ValidationError::new("expires_at", "invalid_time")); }
Ok(()) Ok(())
} }
pub fn token(&self) -> &String { &self.token } pub fn token(&self) -> &String { &self.token }
@@ -1590,6 +1800,7 @@ impl SessionAuthority {
if self.audience.len() > 256 { return Err(ValidationError::new("audience", "max_length")); } if self.audience.len() > 256 { return Err(ValidationError::new("audience", "max_length")); }
if self.reconnectSequence < 0 { return Err(ValidationError::new("reconnect_sequence", "minimum")); } if self.reconnectSequence < 0 { return Err(ValidationError::new("reconnect_sequence", "minimum")); }
if self.expiresAt.len() > 64 { return Err(ValidationError::new("expires_at", "max_length")); } if self.expiresAt.len() > 64 { return Err(ValidationError::new("expires_at", "max_length")); }
if !valid_rfc3339_utc(self.expiresAt.as_str()) { return Err(ValidationError::new("expires_at", "invalid_time")); }
self.capabilities.validate().map_err(|_| ValidationError::new("capabilities", "invalid_object"))?; self.capabilities.validate().map_err(|_| ValidationError::new("capabilities", "invalid_object"))?;
if self.providerProfile != "apollo" { return Err(ValidationError::new("provider_profile", "invalid_value")); } if self.providerProfile != "apollo" { return Err(ValidationError::new("provider_profile", "invalid_value")); }
if self.providerIdentity.is_empty() { return Err(ValidationError::new("provider_identity", "required")); } if self.providerIdentity.is_empty() { return Err(ValidationError::new("provider_identity", "required")); }
@@ -1614,12 +1825,12 @@ pub struct SessionRequest {
deviceKeyId: String, deviceKeyId: String,
poolId: String, poolId: String,
idempotencyKey: String, idempotencyKey: String,
policySnapshot: AllocationPolicy, requestedDisplayMode: Option<DisplayMode>,
} }
impl SessionRequest { impl SessionRequest {
pub fn new(clientDeviceId: String, deviceKeyId: String, poolId: String, idempotencyKey: String, policySnapshot: AllocationPolicy) -> Result<Self, ValidationError> { pub fn new(clientDeviceId: String, deviceKeyId: String, poolId: String, idempotencyKey: String, requestedDisplayMode: Option<DisplayMode>) -> Result<Self, ValidationError> {
let value = Self { clientDeviceId, deviceKeyId, poolId, idempotencyKey, policySnapshot }; let value = Self { clientDeviceId, deviceKeyId, poolId, idempotencyKey, requestedDisplayMode };
value.validate()?; value.validate()?;
Ok(value) Ok(value)
} }
@@ -1636,14 +1847,16 @@ impl SessionRequest {
if self.idempotencyKey.is_empty() { return Err(ValidationError::new("idempotency_key", "required")); } if self.idempotencyKey.is_empty() { return Err(ValidationError::new("idempotency_key", "required")); }
if !self.idempotencyKey.is_empty() && self.idempotencyKey.len() < 1 { return Err(ValidationError::new("idempotency_key", "min_length")); } if !self.idempotencyKey.is_empty() && self.idempotencyKey.len() < 1 { return Err(ValidationError::new("idempotency_key", "min_length")); }
if self.idempotencyKey.len() > 256 { return Err(ValidationError::new("idempotency_key", "max_length")); } if self.idempotencyKey.len() > 256 { return Err(ValidationError::new("idempotency_key", "max_length")); }
self.policySnapshot.validate().map_err(|_| ValidationError::new("policy_snapshot", "invalid_object"))?; if let Some(value) = &self.requestedDisplayMode {
value.validate().map_err(|_| ValidationError::new("requested_display_mode", "invalid_object"))?;
}
Ok(()) Ok(())
} }
pub fn clientDeviceId(&self) -> &String { &self.clientDeviceId } pub fn clientDeviceId(&self) -> &String { &self.clientDeviceId }
pub fn deviceKeyId(&self) -> &String { &self.deviceKeyId } pub fn deviceKeyId(&self) -> &String { &self.deviceKeyId }
pub fn poolId(&self) -> &String { &self.poolId } pub fn poolId(&self) -> &String { &self.poolId }
pub fn idempotencyKey(&self) -> &String { &self.idempotencyKey } pub fn idempotencyKey(&self) -> &String { &self.idempotencyKey }
pub fn policySnapshot(&self) -> &AllocationPolicy { &self.policySnapshot } pub fn requestedDisplayMode(&self) -> &Option<DisplayMode> { &self.requestedDisplayMode }
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@@ -1754,7 +1967,9 @@ impl VersionNegotiation {
pub fn validate(&self) -> Result<(), ValidationError> { pub fn validate(&self) -> Result<(), ValidationError> {
if self.supportedVersions.len() < 1 { return Err(ValidationError::new("supported_versions", "min_items")); } if self.supportedVersions.len() < 1 { return Err(ValidationError::new("supported_versions", "min_items")); }
if self.supportedVersions.len() > 3 { return Err(ValidationError::new("supported_versions", "max_items")); } if self.supportedVersions.len() > 3 { return Err(ValidationError::new("supported_versions", "max_items")); }
for item in self.supportedVersions.iter() { if item.as_bytes().len() > 16 { return Err(ValidationError::new("supported_versions", "max_item_length")); } }
if self.features.len() > 64 { return Err(ValidationError::new("features", "max_items")); } if self.features.len() > 64 { return Err(ValidationError::new("features", "max_items")); }
for item in self.features.iter() { if item.as_bytes().len() > 64 { return Err(ValidationError::new("features", "max_item_length")); } }
Ok(()) Ok(())
} }
pub fn supportedVersions(&self) -> &Vec<String> { &self.supportedVersions } pub fn supportedVersions(&self) -> &Vec<String> { &self.supportedVersions }
+293 -30
View File
@@ -1,10 +1,10 @@
// Code generated by tools/generate.py; DO NOT EDIT. // Code generated by tools/generate.py; DO NOT EDIT.
import Foundation import Foundation
public typealias JSONObject = [String: String] public typealias JSONObject = [String: String]
public let schemaSHA256 = "3aec8dd72bdbb6b9657c8df3160252c93034c7c1032d471e01eae2ef91e47716" public let schemaSHA256 = "dea3dd210c53d5a2d37050dd6afd8b0ac5bb8edcb7ab25a02e4026489ce8a00f"
public let currentWireVersion = "1" public let currentWireVersion = "2"
public let nMinus1WireVersion = "0" public let nMinus1WireVersion = "1"
public let nMinus2WireVersion = "-1" public let nMinus2WireVersion = "0"
public struct ContractValidationError: Error, Equatable { public let field: String; public let code: String } public struct ContractValidationError: Error, Equatable { public let field: String; public let code: String }
private struct AnyCodingKey: CodingKey { let stringValue: String; let intValue: Int?; init?(stringValue: String) { self.stringValue = stringValue; self.intValue = nil }; init?(intValue: Int) { self.stringValue = String(intValue); self.intValue = intValue } } private struct AnyCodingKey: CodingKey { let stringValue: String; let intValue: Int?; init?(stringValue: String) { self.stringValue = stringValue; self.intValue = nil }; init?(intValue: Int) { self.stringValue = String(intValue); self.intValue = intValue } }
private func validBase64URL(_ value: String) -> Bool { private func validBase64URL(_ value: String) -> Bool {
@@ -16,6 +16,23 @@ private func validBase64URL(_ value: String) -> Bool {
guard let decoded = Data(base64Encoded: standard) else { return false } guard let decoded = Data(base64Encoded: standard) else { return false }
return decoded.base64EncodedString().replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "=", with: "") == value 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
}
public struct AllocationPolicy: Codable, Equatable { public struct AllocationPolicy: Codable, Equatable {
public let minimumKbps: Int64 public let minimumKbps: Int64
@@ -148,6 +165,8 @@ public struct BrokerSession: Codable, Equatable {
public let requestedAt: String public let requestedAt: String
public let endedAt: String? public let endedAt: String?
public let version: Int64 public let version: Int64
public let requestedDisplayMode: DisplayMode?
public let effectiveDisplayMode: DisplayMode?
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case id = "id" case id = "id"
case principalId = "principal_id" case principalId = "principal_id"
@@ -164,9 +183,11 @@ public struct BrokerSession: Codable, Equatable {
case requestedAt = "requested_at" case requestedAt = "requested_at"
case endedAt = "ended_at" case endedAt = "ended_at"
case version = "version" case version = "version"
case requestedDisplayMode = "requested_display_mode"
case effectiveDisplayMode = "effective_display_mode"
} }
public init(id: String, principalId: String, poolId: String, assignmentId: String?, state: String, policySnapshot: AllocationPolicy, reconnectDeadline: String?, outcome: String?, failureCode: String?, cleanupState: String, idempotencyKey: String, correlationId: String, requestedAt: String, endedAt: String?, version: Int64) throws { public init(id: String, principalId: String, poolId: String, assignmentId: String?, state: String, policySnapshot: AllocationPolicy, reconnectDeadline: String?, outcome: String?, failureCode: String?, cleanupState: String, idempotencyKey: String, correlationId: String, requestedAt: String, endedAt: String?, version: Int64, requestedDisplayMode: DisplayMode?, effectiveDisplayMode: DisplayMode?) throws {
self.id = id self.id = id
self.principalId = principalId self.principalId = principalId
self.poolId = poolId self.poolId = poolId
@@ -182,6 +203,8 @@ public struct BrokerSession: Codable, Equatable {
self.requestedAt = requestedAt self.requestedAt = requestedAt
self.endedAt = endedAt self.endedAt = endedAt
self.version = version self.version = version
self.requestedDisplayMode = requestedDisplayMode
self.effectiveDisplayMode = effectiveDisplayMode
try validate() try validate()
} }
@@ -189,7 +212,7 @@ public struct BrokerSession: Codable, Equatable {
let all = try decoder.container(keyedBy: AnyCodingKey.self) let all = try decoder.container(keyedBy: AnyCodingKey.self)
for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") }
let c = try decoder.container(keyedBy: CodingKeys.self) let c = try decoder.container(keyedBy: CodingKeys.self)
try self.init(id: try c.decode(String.self, forKey: .id), principalId: try c.decode(String.self, forKey: .principalId), poolId: try c.decode(String.self, forKey: .poolId), assignmentId: try c.decodeIfPresent(String.self, forKey: .assignmentId), state: try c.decode(String.self, forKey: .state), policySnapshot: try c.decode(AllocationPolicy.self, forKey: .policySnapshot), reconnectDeadline: try c.decodeIfPresent(String.self, forKey: .reconnectDeadline), outcome: try c.decodeIfPresent(String.self, forKey: .outcome), failureCode: try c.decodeIfPresent(String.self, forKey: .failureCode), cleanupState: try c.decode(String.self, forKey: .cleanupState), idempotencyKey: try c.decode(String.self, forKey: .idempotencyKey), correlationId: try c.decode(String.self, forKey: .correlationId), requestedAt: try c.decode(String.self, forKey: .requestedAt), endedAt: try c.decodeIfPresent(String.self, forKey: .endedAt), version: try c.decode(Int64.self, forKey: .version)) try self.init(id: try c.decode(String.self, forKey: .id), principalId: try c.decode(String.self, forKey: .principalId), poolId: try c.decode(String.self, forKey: .poolId), assignmentId: try c.decodeIfPresent(String.self, forKey: .assignmentId), state: try c.decode(String.self, forKey: .state), policySnapshot: try c.decode(AllocationPolicy.self, forKey: .policySnapshot), reconnectDeadline: try c.decodeIfPresent(String.self, forKey: .reconnectDeadline), outcome: try c.decodeIfPresent(String.self, forKey: .outcome), failureCode: try c.decodeIfPresent(String.self, forKey: .failureCode), cleanupState: try c.decode(String.self, forKey: .cleanupState), idempotencyKey: try c.decode(String.self, forKey: .idempotencyKey), correlationId: try c.decode(String.self, forKey: .correlationId), requestedAt: try c.decode(String.self, forKey: .requestedAt), endedAt: try c.decodeIfPresent(String.self, forKey: .endedAt), version: try c.decode(Int64.self, forKey: .version), requestedDisplayMode: try c.contains(.requestedDisplayMode) ? c.decode(DisplayMode.self, forKey: .requestedDisplayMode) : nil, effectiveDisplayMode: try c.contains(.effectiveDisplayMode) ? c.decode(DisplayMode.self, forKey: .effectiveDisplayMode) : nil)
} }
public func validate() throws { public func validate() throws {
@@ -211,7 +234,7 @@ public struct BrokerSession: Codable, Equatable {
try self.policySnapshot.validate() try self.policySnapshot.validate()
if let value = self.reconnectDeadline { if let value = self.reconnectDeadline {
if value.utf8.count > 64 { throw ContractValidationError(field: "reconnect_deadline", code: "max_length") } if value.utf8.count > 64 { throw ContractValidationError(field: "reconnect_deadline", code: "max_length") }
if ISO8601DateFormatter().date(from: value) == nil { throw ContractValidationError(field: "reconnect_deadline", code: "invalid_time") } if !validRFC3339UTC(value) { throw ContractValidationError(field: "reconnect_deadline", code: "invalid_time") }
} }
if let value = self.outcome { if let value = self.outcome {
if value.utf8.count > 64 { throw ContractValidationError(field: "outcome", code: "max_length") } if value.utf8.count > 64 { throw ContractValidationError(field: "outcome", code: "max_length") }
@@ -229,12 +252,63 @@ public struct BrokerSession: Codable, Equatable {
if !self.correlationId.isEmpty && self.correlationId.utf8.count < 1 { throw ContractValidationError(field: "correlation_id", code: "min_length") } if !self.correlationId.isEmpty && self.correlationId.utf8.count < 1 { throw ContractValidationError(field: "correlation_id", code: "min_length") }
if self.correlationId.utf8.count > 128 { throw ContractValidationError(field: "correlation_id", code: "max_length") } if self.correlationId.utf8.count > 128 { throw ContractValidationError(field: "correlation_id", code: "max_length") }
if self.requestedAt.utf8.count > 64 { throw ContractValidationError(field: "requested_at", code: "max_length") } if self.requestedAt.utf8.count > 64 { throw ContractValidationError(field: "requested_at", code: "max_length") }
if ISO8601DateFormatter().date(from: self.requestedAt) == nil { throw ContractValidationError(field: "requested_at", code: "invalid_time") } if !validRFC3339UTC(self.requestedAt) { throw ContractValidationError(field: "requested_at", code: "invalid_time") }
if let value = self.endedAt { if let value = self.endedAt {
if value.utf8.count > 64 { throw ContractValidationError(field: "ended_at", code: "max_length") } if value.utf8.count > 64 { throw ContractValidationError(field: "ended_at", code: "max_length") }
if ISO8601DateFormatter().date(from: value) == nil { throw ContractValidationError(field: "ended_at", code: "invalid_time") } if !validRFC3339UTC(value) { throw ContractValidationError(field: "ended_at", code: "invalid_time") }
} }
if self.version < 1 { throw ContractValidationError(field: "version", code: "minimum") } if self.version < 1 { throw ContractValidationError(field: "version", code: "minimum") }
if let value = self.requestedDisplayMode {
try value.validate()
}
if let value = self.effectiveDisplayMode {
try value.validate()
}
}
public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }
public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) }
}
public struct BrowserAuthenticatedSession: Codable, Equatable {
public let username: String
public let provider: String
public let roles: [String]
public let role: String
enum CodingKeys: String, CodingKey {
case username = "username"
case provider = "provider"
case roles = "roles"
case role = "role"
}
public init(username: String, provider: String, roles: [String], role: String) throws {
self.username = username
self.provider = provider
self.roles = roles
self.role = role
try validate()
}
public init(from decoder: Decoder) throws {
let all = try decoder.container(keyedBy: AnyCodingKey.self)
for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") }
let c = try decoder.container(keyedBy: CodingKeys.self)
try self.init(username: try c.decode(String.self, forKey: .username), provider: try c.decode(String.self, forKey: .provider), roles: try c.decode([String].self, forKey: .roles), role: try c.decode(String.self, forKey: .role))
}
public func validate() throws {
if self.username.isEmpty { throw ContractValidationError(field: "username", code: "required") }
if !self.username.isEmpty && self.username.utf8.count < 1 { throw ContractValidationError(field: "username", code: "min_length") }
if self.username.utf8.count > 256 { throw ContractValidationError(field: "username", code: "max_length") }
if self.provider.isEmpty { throw ContractValidationError(field: "provider", code: "required") }
if !self.provider.isEmpty && self.provider.utf8.count < 1 { throw ContractValidationError(field: "provider", code: "min_length") }
if self.provider.utf8.count > 64 { throw ContractValidationError(field: "provider", code: "max_length") }
if self.roles.count > 16 { throw ContractValidationError(field: "roles", code: "max_items") }
for item in self.roles where item.utf8.count < 1 { throw ContractValidationError(field: "roles", code: "min_item_length") }
for item in self.roles where item.utf8.count > 64 { throw ContractValidationError(field: "roles", code: "max_item_length") }
for item in self.roles where item.utf8.count > 64 { throw ContractValidationError(field: "roles", code: "max_item_bytes") }
if !["user", "admin"].contains(self.role) { throw ContractValidationError(field: "role", code: "invalid_value") }
} }
public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) } public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }
@@ -278,9 +352,7 @@ public struct CapabilityProfile: Codable, Equatable {
if self.transport.isEmpty { throw ContractValidationError(field: "transport", code: "required") } if self.transport.isEmpty { throw ContractValidationError(field: "transport", code: "required") }
if !self.transport.isEmpty && self.transport.utf8.count < 1 { throw ContractValidationError(field: "transport", code: "min_length") } if !self.transport.isEmpty && self.transport.utf8.count < 1 { throw ContractValidationError(field: "transport", code: "min_length") }
if self.transport.utf8.count > 64 { throw ContractValidationError(field: "transport", code: "max_length") } if self.transport.utf8.count > 64 { throw ContractValidationError(field: "transport", code: "max_length") }
if self.framing.isEmpty { throw ContractValidationError(field: "framing", code: "required") } if !["datagram-v1", "datagram-v2"].contains(self.framing) { throw ContractValidationError(field: "framing", code: "invalid_value") }
if !self.framing.isEmpty && self.framing.utf8.count < 1 { throw ContractValidationError(field: "framing", code: "min_length") }
if self.framing.utf8.count > 64 { throw ContractValidationError(field: "framing", code: "max_length") }
if self.media.isEmpty { throw ContractValidationError(field: "media", code: "required") } if self.media.isEmpty { throw ContractValidationError(field: "media", code: "required") }
if !self.media.isEmpty && self.media.utf8.count < 1 { throw ContractValidationError(field: "media", code: "min_length") } if !self.media.isEmpty && self.media.utf8.count < 1 { throw ContractValidationError(field: "media", code: "min_length") }
if self.media.utf8.count > 64 { throw ContractValidationError(field: "media", code: "max_length") } if self.media.utf8.count > 64 { throw ContractValidationError(field: "media", code: "max_length") }
@@ -541,7 +613,7 @@ public struct DeviceChallenge: Codable, Equatable {
if !self.challenge.isEmpty && self.challenge.utf8.count < 1 { throw ContractValidationError(field: "challenge", code: "min_length") } if !self.challenge.isEmpty && self.challenge.utf8.count < 1 { throw ContractValidationError(field: "challenge", code: "min_length") }
if self.challenge.utf8.count > 256 { throw ContractValidationError(field: "challenge", code: "max_length") } if self.challenge.utf8.count > 256 { throw ContractValidationError(field: "challenge", code: "max_length") }
if self.expiresAt.utf8.count > 64 { throw ContractValidationError(field: "expires_at", code: "max_length") } if self.expiresAt.utf8.count > 64 { throw ContractValidationError(field: "expires_at", code: "max_length") }
if ISO8601DateFormatter().date(from: self.expiresAt) == nil { throw ContractValidationError(field: "expires_at", code: "invalid_time") } if !validRFC3339UTC(self.expiresAt) { throw ContractValidationError(field: "expires_at", code: "invalid_time") }
if self.algorithm != "ed25519" { throw ContractValidationError(field: "algorithm", code: "invalid_value") } if self.algorithm != "ed25519" { throw ContractValidationError(field: "algorithm", code: "invalid_value") }
if self.signatureFormat != "ed25519-domain-separated-v1" { throw ContractValidationError(field: "signature_format", code: "invalid_value") } if self.signatureFormat != "ed25519-domain-separated-v1" { throw ContractValidationError(field: "signature_format", code: "invalid_value") }
} }
@@ -634,6 +706,43 @@ public struct DeviceRegistrationRequest: Codable, Equatable {
public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) } public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) }
} }
public struct DisplayMode: Codable, Equatable {
public let resolutionWidth: Int64
public let resolutionHeight: Int64
public let fps: Int64
enum CodingKeys: String, CodingKey {
case resolutionWidth = "resolution_width"
case resolutionHeight = "resolution_height"
case fps = "fps"
}
public init(resolutionWidth: Int64, resolutionHeight: Int64, fps: Int64) throws {
self.resolutionWidth = resolutionWidth
self.resolutionHeight = resolutionHeight
self.fps = fps
try validate()
}
public init(from decoder: Decoder) throws {
let all = try decoder.container(keyedBy: AnyCodingKey.self)
for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") }
let c = try decoder.container(keyedBy: CodingKeys.self)
try self.init(resolutionWidth: try c.decode(Int64.self, forKey: .resolutionWidth), resolutionHeight: try c.decode(Int64.self, forKey: .resolutionHeight), fps: try c.decode(Int64.self, forKey: .fps))
}
public func validate() throws {
if self.resolutionWidth < 320 { throw ContractValidationError(field: "resolution_width", code: "minimum") }
if self.resolutionWidth > 16384 { throw ContractValidationError(field: "resolution_width", code: "maximum") }
if self.resolutionHeight < 200 { throw ContractValidationError(field: "resolution_height", code: "minimum") }
if self.resolutionHeight > 8640 { throw ContractValidationError(field: "resolution_height", code: "maximum") }
if self.fps < 1 { throw ContractValidationError(field: "fps", code: "minimum") }
if self.fps > 240 { throw ContractValidationError(field: "fps", code: "maximum") }
}
public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }
public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) }
}
public struct EntitledPool: Codable, Equatable { public struct EntitledPool: Codable, Equatable {
public let poolId: String public let poolId: String
public let name: String public let name: String
@@ -784,7 +893,7 @@ public struct EventEnvelope: Codable, Equatable {
if self.version < 1 { throw ContractValidationError(field: "version", code: "minimum") } if self.version < 1 { throw ContractValidationError(field: "version", code: "minimum") }
try self.resource.validate() try self.resource.validate()
if self.occurredAt.utf8.count > 64 { throw ContractValidationError(field: "occurred_at", code: "max_length") } if self.occurredAt.utf8.count > 64 { throw ContractValidationError(field: "occurred_at", code: "max_length") }
if ISO8601DateFormatter().date(from: self.occurredAt) == nil { throw ContractValidationError(field: "occurred_at", code: "invalid_time") } if !validRFC3339UTC(self.occurredAt) { throw ContractValidationError(field: "occurred_at", code: "invalid_time") }
if self.correlationId.isEmpty { throw ContractValidationError(field: "correlation_id", code: "required") } if self.correlationId.isEmpty { throw ContractValidationError(field: "correlation_id", code: "required") }
if !self.correlationId.isEmpty && self.correlationId.utf8.count < 1 { throw ContractValidationError(field: "correlation_id", code: "min_length") } if !self.correlationId.isEmpty && self.correlationId.utf8.count < 1 { throw ContractValidationError(field: "correlation_id", code: "min_length") }
if self.correlationId.utf8.count > 128 { throw ContractValidationError(field: "correlation_id", code: "max_length") } if self.correlationId.utf8.count > 128 { throw ContractValidationError(field: "correlation_id", code: "max_length") }
@@ -989,7 +1098,7 @@ public struct GatewayDrain: Codable, Equatable {
if !self.reason.isEmpty && self.reason.utf8.count < 1 { throw ContractValidationError(field: "reason", code: "min_length") } if !self.reason.isEmpty && self.reason.utf8.count < 1 { throw ContractValidationError(field: "reason", code: "min_length") }
if self.reason.utf8.count > 256 { throw ContractValidationError(field: "reason", code: "max_length") } if self.reason.utf8.count > 256 { throw ContractValidationError(field: "reason", code: "max_length") }
if self.deadline.utf8.count > 64 { throw ContractValidationError(field: "deadline", code: "max_length") } if self.deadline.utf8.count > 64 { throw ContractValidationError(field: "deadline", code: "max_length") }
if ISO8601DateFormatter().date(from: self.deadline) == nil { throw ContractValidationError(field: "deadline", code: "invalid_time") } if !validRFC3339UTC(self.deadline) { throw ContractValidationError(field: "deadline", code: "invalid_time") }
} }
public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) } public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }
@@ -1042,7 +1151,7 @@ public struct GatewayHeartbeat: Codable, Equatable {
if self.gatewayId.utf8.count > 128 { throw ContractValidationError(field: "gateway_id", code: "max_length") } if self.gatewayId.utf8.count > 128 { throw ContractValidationError(field: "gateway_id", code: "max_length") }
if self.sequence < 1 { throw ContractValidationError(field: "sequence", code: "minimum") } if self.sequence < 1 { throw ContractValidationError(field: "sequence", code: "minimum") }
if self.observedAt.utf8.count > 64 { throw ContractValidationError(field: "observed_at", code: "max_length") } if self.observedAt.utf8.count > 64 { throw ContractValidationError(field: "observed_at", code: "max_length") }
if ISO8601DateFormatter().date(from: self.observedAt) == nil { throw ContractValidationError(field: "observed_at", code: "invalid_time") } if !validRFC3339UTC(self.observedAt) { throw ContractValidationError(field: "observed_at", code: "invalid_time") }
if self.activeConnections < 0 { throw ContractValidationError(field: "active_connections", code: "minimum") } if self.activeConnections < 0 { throw ContractValidationError(field: "active_connections", code: "minimum") }
if self.activeConnections > 1000000 { throw ContractValidationError(field: "active_connections", code: "maximum") } if self.activeConnections > 1000000 { throw ContractValidationError(field: "active_connections", code: "maximum") }
if self.egressKbps < 0 { throw ContractValidationError(field: "egress_kbps", code: "minimum") } if self.egressKbps < 0 { throw ContractValidationError(field: "egress_kbps", code: "minimum") }
@@ -1138,6 +1247,8 @@ public struct GatewayRegistration: Codable, Equatable {
if self.bandwidthCapacityKbps < 1 { throw ContractValidationError(field: "bandwidth_capacity_kbps", code: "minimum") } if self.bandwidthCapacityKbps < 1 { throw ContractValidationError(field: "bandwidth_capacity_kbps", code: "minimum") }
if self.bandwidthCapacityKbps > 1000000000 { throw ContractValidationError(field: "bandwidth_capacity_kbps", code: "maximum") } if self.bandwidthCapacityKbps > 1000000000 { throw ContractValidationError(field: "bandwidth_capacity_kbps", code: "maximum") }
if self.features.count > 64 { throw ContractValidationError(field: "features", code: "max_items") } if self.features.count > 64 { throw ContractValidationError(field: "features", code: "max_items") }
for item in self.features where item.utf8.count < 1 { throw ContractValidationError(field: "features", code: "min_item_length") }
for item in self.features where item.utf8.count > 64 { throw ContractValidationError(field: "features", code: "max_item_length") }
try self.capabilities.validate() try self.capabilities.validate()
if protocolMinVersion > protocolMaxVersion { throw ContractValidationError(field: "protocol_version", code: "invalid_order") } if protocolMinVersion > protocolMaxVersion { throw ContractValidationError(field: "protocol_version", code: "invalid_order") }
} }
@@ -1286,7 +1397,7 @@ public struct GrantReference: Codable, Equatable {
if !self.opaqueValue.isEmpty && self.opaqueValue.utf8.count < 43 { throw ContractValidationError(field: "opaque_value", code: "min_length") } if !self.opaqueValue.isEmpty && self.opaqueValue.utf8.count < 43 { throw ContractValidationError(field: "opaque_value", code: "min_length") }
if self.opaqueValue.utf8.count > 256 { throw ContractValidationError(field: "opaque_value", code: "max_length") } if self.opaqueValue.utf8.count > 256 { throw ContractValidationError(field: "opaque_value", code: "max_length") }
if self.expiresAt.utf8.count > 64 { throw ContractValidationError(field: "expires_at", code: "max_length") } if self.expiresAt.utf8.count > 64 { throw ContractValidationError(field: "expires_at", code: "max_length") }
if ISO8601DateFormatter().date(from: self.expiresAt) == nil { throw ContractValidationError(field: "expires_at", code: "invalid_time") } if !validRFC3339UTC(self.expiresAt) { throw ContractValidationError(field: "expires_at", code: "invalid_time") }
if self.audience.isEmpty { throw ContractValidationError(field: "audience", code: "required") } if self.audience.isEmpty { throw ContractValidationError(field: "audience", code: "required") }
if !self.audience.isEmpty && self.audience.utf8.count < 1 { throw ContractValidationError(field: "audience", code: "min_length") } if !self.audience.isEmpty && self.audience.utf8.count < 1 { throw ContractValidationError(field: "audience", code: "min_length") }
if self.audience.utf8.count > 128 { throw ContractValidationError(field: "audience", code: "max_length") } if self.audience.utf8.count > 128 { throw ContractValidationError(field: "audience", code: "max_length") }
@@ -1404,6 +1515,8 @@ public struct ManifestGateway: Codable, Equatable {
if self.id.utf8.count > 128 { throw ContractValidationError(field: "id", code: "max_length") } if self.id.utf8.count > 128 { throw ContractValidationError(field: "id", code: "max_length") }
if self.addresses.count < 1 { throw ContractValidationError(field: "addresses", code: "min_items") } if self.addresses.count < 1 { throw ContractValidationError(field: "addresses", code: "min_items") }
if self.addresses.count > 4 { throw ContractValidationError(field: "addresses", code: "max_items") } if self.addresses.count > 4 { throw ContractValidationError(field: "addresses", code: "max_items") }
for item in self.addresses where item.utf8.count < 1 { throw ContractValidationError(field: "addresses", code: "min_item_length") }
for item in self.addresses where item.utf8.count > 256 { throw ContractValidationError(field: "addresses", code: "max_item_length") }
if self.publicIdentity.isEmpty { throw ContractValidationError(field: "public_identity", code: "required") } if self.publicIdentity.isEmpty { throw ContractValidationError(field: "public_identity", code: "required") }
if !self.publicIdentity.isEmpty && self.publicIdentity.utf8.count < 1 { throw ContractValidationError(field: "public_identity", code: "min_length") } if !self.publicIdentity.isEmpty && self.publicIdentity.utf8.count < 1 { throw ContractValidationError(field: "public_identity", code: "min_length") }
if self.publicIdentity.utf8.count > 256 { throw ContractValidationError(field: "public_identity", code: "max_length") } if self.publicIdentity.utf8.count > 256 { throw ContractValidationError(field: "public_identity", code: "max_length") }
@@ -1416,14 +1529,17 @@ public struct ManifestGateway: Codable, Equatable {
public struct ManifestProfile: Codable, Equatable { public struct ManifestProfile: Codable, Equatable {
public let id: String public let id: String
public let bounds: ManifestBounds public let bounds: ManifestBounds
public let displayMode: DisplayMode?
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case id = "id" case id = "id"
case bounds = "bounds" case bounds = "bounds"
case displayMode = "display_mode"
} }
public init(id: String, bounds: ManifestBounds) throws { public init(id: String, bounds: ManifestBounds, displayMode: DisplayMode?) throws {
self.id = id self.id = id
self.bounds = bounds self.bounds = bounds
self.displayMode = displayMode
try validate() try validate()
} }
@@ -1431,7 +1547,7 @@ public struct ManifestProfile: Codable, Equatable {
let all = try decoder.container(keyedBy: AnyCodingKey.self) let all = try decoder.container(keyedBy: AnyCodingKey.self)
for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") }
let c = try decoder.container(keyedBy: CodingKeys.self) let c = try decoder.container(keyedBy: CodingKeys.self)
try self.init(id: try c.decode(String.self, forKey: .id), bounds: try c.decode(ManifestBounds.self, forKey: .bounds)) try self.init(id: try c.decode(String.self, forKey: .id), bounds: try c.decode(ManifestBounds.self, forKey: .bounds), displayMode: try c.contains(.displayMode) ? c.decode(DisplayMode.self, forKey: .displayMode) : nil)
} }
public func validate() throws { public func validate() throws {
@@ -1439,6 +1555,9 @@ public struct ManifestProfile: Codable, Equatable {
if !self.id.isEmpty && self.id.utf8.count < 1 { throw ContractValidationError(field: "id", code: "min_length") } if !self.id.isEmpty && self.id.utf8.count < 1 { throw ContractValidationError(field: "id", code: "min_length") }
if self.id.utf8.count > 128 { throw ContractValidationError(field: "id", code: "max_length") } if self.id.utf8.count > 128 { throw ContractValidationError(field: "id", code: "max_length") }
try self.bounds.validate() try self.bounds.validate()
if let value = self.displayMode {
try value.validate()
}
} }
public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) } public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }
@@ -1469,7 +1588,60 @@ public struct ManifestTunnel: Codable, Equatable {
public func validate() throws { public func validate() throws {
if self.versions.count < 1 { throw ContractValidationError(field: "versions", code: "min_items") } if self.versions.count < 1 { throw ContractValidationError(field: "versions", code: "min_items") }
if self.versions.count > 4 { throw ContractValidationError(field: "versions", code: "max_items") } if self.versions.count > 4 { throw ContractValidationError(field: "versions", code: "max_items") }
for item in self.versions where item.utf8.count < 1 { throw ContractValidationError(field: "versions", code: "min_item_length") }
for item in self.versions where item.utf8.count > 64 { throw ContractValidationError(field: "versions", code: "max_item_length") }
if self.features.count > 32 { throw ContractValidationError(field: "features", code: "max_items") } if self.features.count > 32 { throw ContractValidationError(field: "features", code: "max_items") }
for item in self.features where item.utf8.count < 1 { throw ContractValidationError(field: "features", code: "min_item_length") }
for item in self.features where item.utf8.count > 64 { throw ContractValidationError(field: "features", code: "max_item_length") }
}
public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }
public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) }
}
public struct NativeAuthenticatedSession: Codable, Equatable {
public let username: String
public let provider: String
public let roles: [String]
public let role: String
public let nativeIdentity: NativeSessionIdentity
enum CodingKeys: String, CodingKey {
case username = "username"
case provider = "provider"
case roles = "roles"
case role = "role"
case nativeIdentity = "native_identity"
}
public init(username: String, provider: String, roles: [String], role: String, nativeIdentity: NativeSessionIdentity) throws {
self.username = username
self.provider = provider
self.roles = roles
self.role = role
self.nativeIdentity = nativeIdentity
try validate()
}
public init(from decoder: Decoder) throws {
let all = try decoder.container(keyedBy: AnyCodingKey.self)
for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") }
let c = try decoder.container(keyedBy: CodingKeys.self)
try self.init(username: try c.decode(String.self, forKey: .username), provider: try c.decode(String.self, forKey: .provider), roles: try c.decode([String].self, forKey: .roles), role: try c.decode(String.self, forKey: .role), nativeIdentity: try c.decode(NativeSessionIdentity.self, forKey: .nativeIdentity))
}
public func validate() throws {
if self.username.isEmpty { throw ContractValidationError(field: "username", code: "required") }
if !self.username.isEmpty && self.username.utf8.count < 1 { throw ContractValidationError(field: "username", code: "min_length") }
if self.username.utf8.count > 256 { throw ContractValidationError(field: "username", code: "max_length") }
if self.provider.isEmpty { throw ContractValidationError(field: "provider", code: "required") }
if !self.provider.isEmpty && self.provider.utf8.count < 1 { throw ContractValidationError(field: "provider", code: "min_length") }
if self.provider.utf8.count > 64 { throw ContractValidationError(field: "provider", code: "max_length") }
if self.roles.count > 16 { throw ContractValidationError(field: "roles", code: "max_items") }
for item in self.roles where item.utf8.count < 1 { throw ContractValidationError(field: "roles", code: "min_item_length") }
for item in self.roles where item.utf8.count > 64 { throw ContractValidationError(field: "roles", code: "max_item_length") }
for item in self.roles where item.utf8.count > 64 { throw ContractValidationError(field: "roles", code: "max_item_bytes") }
if !["user", "admin"].contains(self.role) { throw ContractValidationError(field: "role", code: "invalid_value") }
try self.nativeIdentity.validate()
} }
public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) } public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }
@@ -1523,10 +1695,10 @@ public struct NativeCredential: Codable, Equatable {
if !self.refreshToken.isEmpty && self.refreshToken.utf8.count < 1 { throw ContractValidationError(field: "refresh_token", code: "min_length") } if !self.refreshToken.isEmpty && self.refreshToken.utf8.count < 1 { throw ContractValidationError(field: "refresh_token", code: "min_length") }
if self.refreshToken.utf8.count > 256 { throw ContractValidationError(field: "refresh_token", code: "max_length") } if self.refreshToken.utf8.count > 256 { throw ContractValidationError(field: "refresh_token", code: "max_length") }
if self.expiresAt.utf8.count > 64 { throw ContractValidationError(field: "expires_at", code: "max_length") } if self.expiresAt.utf8.count > 64 { throw ContractValidationError(field: "expires_at", code: "max_length") }
if ISO8601DateFormatter().date(from: self.expiresAt) == nil { throw ContractValidationError(field: "expires_at", code: "invalid_time") } if !validRFC3339UTC(self.expiresAt) { throw ContractValidationError(field: "expires_at", code: "invalid_time") }
if let value = self.refreshExpiresAt { if let value = self.refreshExpiresAt {
if value.utf8.count > 64 { throw ContractValidationError(field: "refresh_expires_at", code: "max_length") } if value.utf8.count > 64 { throw ContractValidationError(field: "refresh_expires_at", code: "max_length") }
if ISO8601DateFormatter().date(from: value) == nil { throw ContractValidationError(field: "refresh_expires_at", code: "invalid_time") } if !validRFC3339UTC(value) { throw ContractValidationError(field: "refresh_expires_at", code: "invalid_time") }
} }
} }
@@ -1534,6 +1706,91 @@ public struct NativeCredential: Codable, Equatable {
public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) } public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) }
} }
public struct NativeSessionIdentity: Codable, Equatable {
public let clientDeviceId: String
public let deviceKeyId: String
enum CodingKeys: String, CodingKey {
case clientDeviceId = "client_device_id"
case deviceKeyId = "device_key_id"
}
public init(clientDeviceId: String, deviceKeyId: String) throws {
self.clientDeviceId = clientDeviceId
self.deviceKeyId = deviceKeyId
try validate()
}
public init(from decoder: Decoder) throws {
let all = try decoder.container(keyedBy: AnyCodingKey.self)
for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") }
let c = try decoder.container(keyedBy: CodingKeys.self)
try self.init(clientDeviceId: try c.decode(String.self, forKey: .clientDeviceId), deviceKeyId: try c.decode(String.self, forKey: .deviceKeyId))
}
public func validate() throws {
if self.clientDeviceId.isEmpty { throw ContractValidationError(field: "client_device_id", code: "required") }
if !self.clientDeviceId.isEmpty && self.clientDeviceId.utf8.count < 1 { throw ContractValidationError(field: "client_device_id", code: "min_length") }
if self.clientDeviceId.utf8.count > 128 { throw ContractValidationError(field: "client_device_id", code: "max_length") }
if self.deviceKeyId.isEmpty { throw ContractValidationError(field: "device_key_id", code: "required") }
if !self.deviceKeyId.isEmpty && self.deviceKeyId.utf8.count < 1 { throw ContractValidationError(field: "device_key_id", code: "min_length") }
if self.deviceKeyId.utf8.count > 128 { throw ContractValidationError(field: "device_key_id", code: "max_length") }
}
public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }
public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) }
}
public struct NativeTunnelCredential: Codable, Equatable {
public let clientDeviceId: String
public let deviceKeyId: String
public let certificateChainPem: String
public let trustBundlePem: String
public let expiresAt: String
enum CodingKeys: String, CodingKey {
case clientDeviceId = "client_device_id"
case deviceKeyId = "device_key_id"
case certificateChainPem = "certificate_chain_pem"
case trustBundlePem = "trust_bundle_pem"
case expiresAt = "expires_at"
}
public init(clientDeviceId: String, deviceKeyId: String, certificateChainPem: String, trustBundlePem: String, expiresAt: String) throws {
self.clientDeviceId = clientDeviceId
self.deviceKeyId = deviceKeyId
self.certificateChainPem = certificateChainPem
self.trustBundlePem = trustBundlePem
self.expiresAt = expiresAt
try validate()
}
public init(from decoder: Decoder) throws {
let all = try decoder.container(keyedBy: AnyCodingKey.self)
for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") }
let c = try decoder.container(keyedBy: CodingKeys.self)
try self.init(clientDeviceId: try c.decode(String.self, forKey: .clientDeviceId), deviceKeyId: try c.decode(String.self, forKey: .deviceKeyId), certificateChainPem: try c.decode(String.self, forKey: .certificateChainPem), trustBundlePem: try c.decode(String.self, forKey: .trustBundlePem), expiresAt: try c.decode(String.self, forKey: .expiresAt))
}
public func validate() throws {
if self.clientDeviceId.isEmpty { throw ContractValidationError(field: "client_device_id", code: "required") }
if !self.clientDeviceId.isEmpty && self.clientDeviceId.utf8.count < 1 { throw ContractValidationError(field: "client_device_id", code: "min_length") }
if self.clientDeviceId.utf8.count > 128 { throw ContractValidationError(field: "client_device_id", code: "max_length") }
if self.deviceKeyId.isEmpty { throw ContractValidationError(field: "device_key_id", code: "required") }
if !self.deviceKeyId.isEmpty && self.deviceKeyId.utf8.count < 1 { throw ContractValidationError(field: "device_key_id", code: "min_length") }
if self.deviceKeyId.utf8.count > 128 { throw ContractValidationError(field: "device_key_id", code: "max_length") }
if self.certificateChainPem.isEmpty { throw ContractValidationError(field: "certificate_chain_pem", code: "required") }
if !self.certificateChainPem.isEmpty && self.certificateChainPem.utf8.count < 1 { throw ContractValidationError(field: "certificate_chain_pem", code: "min_length") }
if self.certificateChainPem.utf8.count > 65536 { throw ContractValidationError(field: "certificate_chain_pem", code: "max_length") }
if self.trustBundlePem.isEmpty { throw ContractValidationError(field: "trust_bundle_pem", code: "required") }
if !self.trustBundlePem.isEmpty && self.trustBundlePem.utf8.count < 1 { throw ContractValidationError(field: "trust_bundle_pem", code: "min_length") }
if self.trustBundlePem.utf8.count > 65536 { throw ContractValidationError(field: "trust_bundle_pem", code: "max_length") }
if self.expiresAt.utf8.count > 64 { throw ContractValidationError(field: "expires_at", code: "max_length") }
if !validRFC3339UTC(self.expiresAt) { throw ContractValidationError(field: "expires_at", code: "invalid_time") }
}
public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }
public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) }
}
public struct PageInfo: Codable, Equatable { public struct PageInfo: Codable, Equatable {
public let limit: Int64 public let limit: Int64
public let nextCursor: String public let nextCursor: String
@@ -1650,7 +1907,7 @@ public struct ProviderSessionWork: Codable, Equatable {
if self.gatewayId.utf8.count > 128 { throw ContractValidationError(field: "gateway_id", code: "max_length") } if self.gatewayId.utf8.count > 128 { throw ContractValidationError(field: "gateway_id", code: "max_length") }
if self.reconnectSequence < 0 { throw ContractValidationError(field: "reconnect_sequence", code: "minimum") } if self.reconnectSequence < 0 { throw ContractValidationError(field: "reconnect_sequence", code: "minimum") }
if self.expiresAt.utf8.count > 64 { throw ContractValidationError(field: "expires_at", code: "max_length") } if self.expiresAt.utf8.count > 64 { throw ContractValidationError(field: "expires_at", code: "max_length") }
if ISO8601DateFormatter().date(from: self.expiresAt) == nil { throw ContractValidationError(field: "expires_at", code: "invalid_time") } if !validRFC3339UTC(self.expiresAt) { throw ContractValidationError(field: "expires_at", code: "invalid_time") }
if self.providerProfile != "apollo" { throw ContractValidationError(field: "provider_profile", code: "invalid_value") } if self.providerProfile != "apollo" { throw ContractValidationError(field: "provider_profile", code: "invalid_value") }
if self.providerIdentity.isEmpty { throw ContractValidationError(field: "provider_identity", code: "required") } if self.providerIdentity.isEmpty { throw ContractValidationError(field: "provider_identity", code: "required") }
if !self.providerIdentity.isEmpty && self.providerIdentity.utf8.count < 1 { throw ContractValidationError(field: "provider_identity", code: "min_length") } if !self.providerIdentity.isEmpty && self.providerIdentity.utf8.count < 1 { throw ContractValidationError(field: "provider_identity", code: "min_length") }
@@ -1728,6 +1985,8 @@ public struct ProviderState: Codable, Equatable {
if self.sessionId.utf8.count > 128 { throw ContractValidationError(field: "session_id", code: "max_length") } if self.sessionId.utf8.count > 128 { throw ContractValidationError(field: "session_id", code: "max_length") }
if !["starting", "ready", "disconnected", "terminating", "terminated", "cleanup_pending", "failed"].contains(self.state) { throw ContractValidationError(field: "state", code: "invalid_value") } if !["starting", "ready", "disconnected", "terminating", "terminated", "cleanup_pending", "failed"].contains(self.state) { throw ContractValidationError(field: "state", code: "invalid_value") }
if self.channels.count > 8 { throw ContractValidationError(field: "channels", code: "max_items") } if self.channels.count > 8 { throw ContractValidationError(field: "channels", code: "max_items") }
for item in self.channels where item.utf8.count < 1 { throw ContractValidationError(field: "channels", code: "min_item_length") }
for item in self.channels where item.utf8.count > 64 { throw ContractValidationError(field: "channels", code: "max_item_length") }
} }
public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) } public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }
@@ -1815,7 +2074,7 @@ public struct ReauthGrant: Codable, Equatable {
if !self.purpose.isEmpty && self.purpose.utf8.count < 1 { throw ContractValidationError(field: "purpose", code: "min_length") } if !self.purpose.isEmpty && self.purpose.utf8.count < 1 { throw ContractValidationError(field: "purpose", code: "min_length") }
if self.purpose.utf8.count > 64 { throw ContractValidationError(field: "purpose", code: "max_length") } if self.purpose.utf8.count > 64 { throw ContractValidationError(field: "purpose", code: "max_length") }
if self.expiresAt.utf8.count > 64 { throw ContractValidationError(field: "expires_at", code: "max_length") } if self.expiresAt.utf8.count > 64 { throw ContractValidationError(field: "expires_at", code: "max_length") }
if ISO8601DateFormatter().date(from: self.expiresAt) == nil { throw ContractValidationError(field: "expires_at", code: "invalid_time") } if !validRFC3339UTC(self.expiresAt) { throw ContractValidationError(field: "expires_at", code: "invalid_time") }
} }
public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) } public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }
@@ -2116,7 +2375,7 @@ public struct SessionAuthority: Codable, Equatable {
if self.audience.utf8.count > 256 { throw ContractValidationError(field: "audience", code: "max_length") } if self.audience.utf8.count > 256 { throw ContractValidationError(field: "audience", code: "max_length") }
if self.reconnectSequence < 0 { throw ContractValidationError(field: "reconnect_sequence", code: "minimum") } if self.reconnectSequence < 0 { throw ContractValidationError(field: "reconnect_sequence", code: "minimum") }
if self.expiresAt.utf8.count > 64 { throw ContractValidationError(field: "expires_at", code: "max_length") } if self.expiresAt.utf8.count > 64 { throw ContractValidationError(field: "expires_at", code: "max_length") }
if ISO8601DateFormatter().date(from: self.expiresAt) == nil { throw ContractValidationError(field: "expires_at", code: "invalid_time") } if !validRFC3339UTC(self.expiresAt) { throw ContractValidationError(field: "expires_at", code: "invalid_time") }
try self.capabilities.validate() try self.capabilities.validate()
if !["apollo"].contains(self.providerProfile) { throw ContractValidationError(field: "provider_profile", code: "invalid_value") } if !["apollo"].contains(self.providerProfile) { throw ContractValidationError(field: "provider_profile", code: "invalid_value") }
if self.providerIdentity.isEmpty { throw ContractValidationError(field: "provider_identity", code: "required") } if self.providerIdentity.isEmpty { throw ContractValidationError(field: "provider_identity", code: "required") }
@@ -2133,21 +2392,21 @@ public struct SessionRequest: Codable, Equatable {
public let deviceKeyId: String public let deviceKeyId: String
public let poolId: String public let poolId: String
public let idempotencyKey: String public let idempotencyKey: String
public let policySnapshot: AllocationPolicy public let requestedDisplayMode: DisplayMode?
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case clientDeviceId = "client_device_id" case clientDeviceId = "client_device_id"
case deviceKeyId = "device_key_id" case deviceKeyId = "device_key_id"
case poolId = "pool_id" case poolId = "pool_id"
case idempotencyKey = "idempotency_key" case idempotencyKey = "idempotency_key"
case policySnapshot = "policy_snapshot" case requestedDisplayMode = "requested_display_mode"
} }
public init(clientDeviceId: String, deviceKeyId: String, poolId: String, idempotencyKey: String, policySnapshot: AllocationPolicy) throws { public init(clientDeviceId: String, deviceKeyId: String, poolId: String, idempotencyKey: String, requestedDisplayMode: DisplayMode?) throws {
self.clientDeviceId = clientDeviceId self.clientDeviceId = clientDeviceId
self.deviceKeyId = deviceKeyId self.deviceKeyId = deviceKeyId
self.poolId = poolId self.poolId = poolId
self.idempotencyKey = idempotencyKey self.idempotencyKey = idempotencyKey
self.policySnapshot = policySnapshot self.requestedDisplayMode = requestedDisplayMode
try validate() try validate()
} }
@@ -2155,7 +2414,7 @@ public struct SessionRequest: Codable, Equatable {
let all = try decoder.container(keyedBy: AnyCodingKey.self) let all = try decoder.container(keyedBy: AnyCodingKey.self)
for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") }
let c = try decoder.container(keyedBy: CodingKeys.self) let c = try decoder.container(keyedBy: CodingKeys.self)
try self.init(clientDeviceId: try c.decode(String.self, forKey: .clientDeviceId), deviceKeyId: try c.decode(String.self, forKey: .deviceKeyId), poolId: try c.decode(String.self, forKey: .poolId), idempotencyKey: try c.decode(String.self, forKey: .idempotencyKey), policySnapshot: try c.decode(AllocationPolicy.self, forKey: .policySnapshot)) try self.init(clientDeviceId: try c.decode(String.self, forKey: .clientDeviceId), deviceKeyId: try c.decode(String.self, forKey: .deviceKeyId), poolId: try c.decode(String.self, forKey: .poolId), idempotencyKey: try c.decode(String.self, forKey: .idempotencyKey), requestedDisplayMode: try c.contains(.requestedDisplayMode) ? c.decode(DisplayMode.self, forKey: .requestedDisplayMode) : nil)
} }
public func validate() throws { public func validate() throws {
@@ -2171,7 +2430,9 @@ public struct SessionRequest: Codable, Equatable {
if self.idempotencyKey.isEmpty { throw ContractValidationError(field: "idempotency_key", code: "required") } if self.idempotencyKey.isEmpty { throw ContractValidationError(field: "idempotency_key", code: "required") }
if !self.idempotencyKey.isEmpty && self.idempotencyKey.utf8.count < 1 { throw ContractValidationError(field: "idempotency_key", code: "min_length") } if !self.idempotencyKey.isEmpty && self.idempotencyKey.utf8.count < 1 { throw ContractValidationError(field: "idempotency_key", code: "min_length") }
if self.idempotencyKey.utf8.count > 256 { throw ContractValidationError(field: "idempotency_key", code: "max_length") } if self.idempotencyKey.utf8.count > 256 { throw ContractValidationError(field: "idempotency_key", code: "max_length") }
try self.policySnapshot.validate() if let value = self.requestedDisplayMode {
try value.validate()
}
} }
public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) } public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }
@@ -2313,7 +2574,9 @@ public struct VersionNegotiation: Codable, Equatable {
public func validate() throws { public func validate() throws {
if self.supportedVersions.count < 1 { throw ContractValidationError(field: "supported_versions", code: "min_items") } if self.supportedVersions.count < 1 { throw ContractValidationError(field: "supported_versions", code: "min_items") }
if self.supportedVersions.count > 3 { throw ContractValidationError(field: "supported_versions", code: "max_items") } if self.supportedVersions.count > 3 { throw ContractValidationError(field: "supported_versions", code: "max_items") }
for item in self.supportedVersions where item.utf8.count > 16 { throw ContractValidationError(field: "supported_versions", code: "max_item_length") }
if self.features.count > 64 { throw ContractValidationError(field: "features", code: "max_items") } if self.features.count > 64 { throw ContractValidationError(field: "features", code: "max_items") }
for item in self.features where item.utf8.count > 64 { throw ContractValidationError(field: "features", code: "max_item_length") }
} }
public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) } public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }
+27 -19
View File
@@ -2,7 +2,7 @@ openapi: 3.1.0
info: info:
title: VerseVDI control contract title: VerseVDI control contract
version: 1.0.0 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: servers:
- url: / - url: /
tags: tags:
@@ -23,18 +23,11 @@ paths:
$ref: ../schemas/control-v1.schema.json#/$defs/LoginRequest $ref: ../schemas/control-v1.schema.json#/$defs/LoginRequest
responses: responses:
'200': '200':
description: Browser session established through secure cookies. description: Browser session established through secure cookies; native identity is structurally forbidden.
content: content:
application/json: application/json:
schema: schema:
type: object $ref: ../schemas/control-v1.schema.json#/$defs/BrowserAuthenticatedSession
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]}
'400': {$ref: '#/components/responses/InvalidRequest'} '400': {$ref: '#/components/responses/InvalidRequest'}
'401': {$ref: '#/components/responses/Unauthorized'} '401': {$ref: '#/components/responses/Unauthorized'}
'503': {$ref: '#/components/responses/Unavailable'} '503': {$ref: '#/components/responses/Unavailable'}
@@ -66,18 +59,32 @@ paths:
- nativeBearer: [] - nativeBearer: []
responses: responses:
'200': '200':
description: Authenticated subject summary. description: Route-context-specific authenticated subject summary. Browser responses forbid native identity; native bearer responses require it.
content: content:
application/json: application/json:
schema: schema:
type: object oneOf:
additionalProperties: false - $ref: ../schemas/control-v1.schema.json#/$defs/BrowserAuthenticatedSession
required: [username, provider, roles, role] - $ref: ../schemas/control-v1.schema.json#/$defs/NativeAuthenticatedSession
properties: '401': {$ref: '#/components/responses/Unauthorized'}
username: {type: string, minLength: 1, maxLength: 256} /api/v1/auth/tunnel-credentials:
provider: {type: string, minLength: 1, maxLength: 64} post:
roles: {type: array, maxItems: 16, items: {type: string, maxLength: 64}} tags: [authentication]
role: {type: string, enum: [user, admin]} 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'} '401': {$ref: '#/components/responses/Unauthorized'}
/api/v1/auth/reauth: /api/v1/auth/reauth:
post: post:
@@ -189,6 +196,7 @@ paths:
post: post:
tags: [broker] tags: [broker]
operationId: requestBrokerSession operationId: requestBrokerSession
description: Control wire version 2 endpoint. Legacy version-1 SessionRequest payloads containing client-supplied policy_snapshot are rejected.
security: security:
- browserSession: [] - browserSession: []
- nativeBearer: [] - nativeBearer: []
@@ -0,0 +1,27 @@
## Context
RC8's canonical requirement names a terminal receipt, but its frame grammar, fixed fixture, and all Protocol validators reject the exact receipt accepted by Data Plane. The existing VGF1 envelope and generated conformance machinery already cover the required boundary.
## Goals / Non-Goals
**Goals:**
- Make one fixed empty client-direction type `0x03` receipt valid in every Protocol validator.
- Preserve all existing VGF1 direction, type, size, and payload validation.
- Make independent clients able to construct the receipt from Protocol-owned evidence.
**Non-Goals:**
- No new envelope, feedback framework, transport, or acknowledgement protocol.
- No change to gateway-to-client termination, rumble, HDR, client IDR, or FEC payloads.
## Decisions
- Extend the existing VGF1 grammar and fixed TSV corpus; generated and native validators remain consumers of that single contract.
- Reserve type `0x03` only for client direction with a zero-length payload. Direction and exact-length checks remain prior trust-boundary requirements.
- Publish the verified change as a new immutable version; RC8 remains unchanged.
## Risks / Trade-offs
- [A validator diverges from the fixed corpus] → Require normalized cross-language conformance in `make verify`.
- [Receipt handling is accepted outside terminal state] → Keep state authorization in Data Plane; Protocol validates only the wire shape.
@@ -0,0 +1,23 @@
## Why
The canonical terminal-receipt requirement contradicts the fixed VGF1 grammar and every generated validator, so RC8 cannot provide an executable cross-language contract for gateway-owned terminal closure.
## What Changes
- Define client-direction VGF1 type `0x03` as an empty terminal receipt in the existing frame grammar.
- Add fixed conformance vectors for valid receipt handling and invalid direction, body, truncation, length, and unknown-type cases.
- Generate consistent Go, Rust, Swift, and Python validation behavior from the Protocol source.
## Capabilities
### New Capabilities
None.
### Modified Capabilities
- `gateway-input-feedback`: Make the already-required terminal receipt executable and cross-language conformant without weakening other VGF1 validation.
## Impact
Protocol frame documentation, conformance fixtures, generators, generated bindings, native validator tools, and immutable Protocol consumers. RC8 remains unchanged and a new immutable Protocol version is required.
@@ -0,0 +1,27 @@
## MODIFIED Requirements
### Requirement: Bounded provider feedback control envelope
The registered bidirectional reliable `control.ack.v1` flow SHALL define an ASCII `VGF1` envelope
with a direction byte, type byte, big-endian payload length, and exact payload
bytes. Only host termination, rumble, and HDR feedback SHALL be valid from the
gateway to the client. Only IDR, FEC/loss feedback, and client-direction type
`0x03` with an empty payload as terminal receipt SHALL be valid from the client
to the gateway. The fixed conformance corpus and every generated or native
Protocol validator SHALL accept that exact receipt and reject unknown types,
wrong direction, nonempty receipt bodies, truncation, and length mismatch. The
terminal receipt SHALL be valid only while the same session awaits receipt of
its one terminal event and MUST NOT be forwarded to the provider. The envelope
SHALL contain no provider address, certificate, credential, or opaque provider
packet.
#### Scenario: Host termination forwarding
- **WHEN** the Apollo adapter receives an authenticated host termination packet
- **THEN** the gateway forwards a bounded `VGF1` termination envelope over reliable Verse control and reports the provider state separately
#### Scenario: Terminal event receipt
- **WHEN** a client receives the reliable typed terminal event
- **THEN** it sends the fixed empty client-direction type `0x03` receipt and the gateway owns bounded tunnel closure without forwarding the receipt to the provider
#### Scenario: Unauthorized or malformed feedback
- **WHEN** feedback is disabled by policy, has an invalid direction/type/length, contains a forbidden provider field, or sends a terminal receipt outside the awaiting-terminal state
- **THEN** the gateway rejects it without forwarding or provider mutation
@@ -0,0 +1,15 @@
## 1. Red Conformance
- [x] 1.1 Add fixed valid and invalid terminal-receipt vectors and prove Python, Go, Rust, and Swift reject the required valid vector
- [x] 1.2 Add a Data Plane independent-client regression that consumes the Protocol fixed vector rather than the production encoder
## 2. Contract Repair
- [x] 2.1 Update the VGF1 grammar and Protocol validator sources for the exact empty client-direction type `0x03` receipt
- [x] 2.2 Regenerate bindings normally and prove deterministic generation has no drift
- [x] 2.3 Run complete Protocol verification and strict OpenSpec validation
## 3. Immutable Release
- [x] 3.1 Verify the next version is unused locally and remotely, publish one immutable annotated tag, and verify its object and peeled commit
- [x] 3.2 Resolve the version from separate empty caches and pin exact fetched checksums in Data Plane and Connection Server
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-30
@@ -5,5 +5,5 @@
## 2. Immutable release ## 2. Immutable release
- [ ] 2.1 Publish one new never-reused immutable Protocol version after final contract verification - [x] 2.1 Publish one new never-reused immutable Protocol version after final contract verification
- [ ] 2.2 Resolve the version from separate empty consumer caches and record exact checksums - [x] 2.2 Resolve the version from separate empty consumer caches and record exact checksums
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-30
@@ -0,0 +1,29 @@
## Context
RC9 datagram-v1 uses one-byte fragment fields and caps a media unit at 16 path-MTU fragments. The reviewed Apollo assembler can recover an encoded frame up to 1,028,152 bytes, so the replacement contract must carry at least that size while rejecting proportional allocation from untrusted header claims.
## Goals / Non-Goals
**Goals:**
- Preserve datagram-v1 byte-for-byte and add explicitly negotiated datagram-v2.
- Keep each QUIC datagram at 1,200 bytes or less.
- Bound one complete encoded unit to 1 MiB, at most 891 fragments, four incomplete units, and a 250 ms reassembly lifetime.
- Define duplicate, reorder, timeout, and malformed behavior for every validator.
**Non-Goals:**
- Provider packet exposure, codec parsing, decode/transcode, retransmission, or a generic transport framework.
## Decisions
- Datagram-v2 widens fragment index and count to unsigned 16-bit network-order fields, making a 23-byte header and 1,177-byte media payload. This is the smallest header change that covers the reviewed provider maximum without unrelated sequences.
- Sequence identifies one complete unit. All fragments must agree on channel, sequence, timestamp, count, and flags.
- Reassembly stores only received fragment bytes, rejects conflicting duplicates, permits exact duplicates and bounded reorder, evicts oldest state above four units, and expires state after 250 ms.
- Capability validation registers `datagram-v1` and `datagram-v2`; exact profile equality keeps downgrade behavior fail closed.
## Risks / Trade-offs
- [More fragments increase per-frame overhead] → retain the 1,200-byte PMTU envelope and the 1 MiB frame ceiling.
- [Four concurrent 1 MiB units can consume bounded memory] → allocate only received bytes and evict/expire incomplete state.
- [RC9 peers cannot validate datagram-v2] → require a new immutable Protocol release and exact consumer pins; never mutate RC9.
@@ -0,0 +1,24 @@
## Why
The RC9 datagram contract limits one encoded media unit to 18,864 bytes, below the bounded complete frames required by the reviewed Apollo adapter and the fixed Phase 3C profiles. A new immutable Protocol release must define complete-frame transport and reassembly bounds before consumers can relay realistic encoded frames without mutation.
## What Changes
- Add a versioned PMTU-safe media datagram header with wider fragment fields.
- Bound complete encoded frames, fragments, reassembly state, duplicates, reorder, timeout, and allocation.
- Preserve RC9 datagram-v1 unchanged and require explicit negotiation of the new framing profile.
- Add fixed Python, Go, Rust, and Swift conformance vectors for valid and malformed framing.
## Capabilities
### New Capabilities
- `gateway-media-framing`: Versioned complete encoded-frame fragmentation and bounded reassembly contract.
### Modified Capabilities
None.
## Impact
Protocol framing documentation, registries, capability schema, fixed conformance fixtures, validators, generated Go/Rust/Swift bindings, and immutable consumer versioning. Requirements: SYS-002, SYS-003, P3C-006, P3C-007, P3C-008, P3C-030, P3C-038, VER-001, VER-002, VER-028.
@@ -0,0 +1,34 @@
## ADDED 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,20 @@
## 1. Red conformance
- [x] 1.1 Add fixed datagram-v2 and capability vectors that fail current Python, Go, Rust, and Swift validators
- [x] 1.2 Prove unknown version, malformed length, invalid fragment, and oversized complete-unit cases remain rejected
## 2. Contract implementation
- [x] 2.1 Add the versioned datagram-v2 frame grammar and registered framing values
- [x] 2.2 Update generator/native validators and regenerate Go, Rust, and Swift bindings normally
## 3. Verification
- [x] 3.1 Run focused cross-language conformance and strict OpenSpec validation
- [x] 3.2 Run complete `make verify` and prove a second generation has no drift
## 4. Immutable boundary
- [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.
+33 -22
View File
@@ -7,20 +7,32 @@ used across the authenticated Verse gateway boundary.
### Requirement: Typed sequenced input envelope ### Requirement: Typed sequenced input envelope
The `input.sequenced.v1` payload SHALL begin with ASCII `VGI1`, a one-byte 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 event kind, and one-byte payload length. It SHALL contain exactly one bounded
keyboard, mouse-button, relative-mouse, UTF-8 scalar, or controller-state keyboard, mouse-button, relative-mouse, UTF-8 scalar, controller-state,
event. False keyboard/mouse state and zeroed controller state are explicit absolute-mouse, or high-resolution-scroll event. Absolute mouse kind `0x06`
releases. Multibyte integer fields SHALL be big-endian. Unknown kinds, SHALL contain big-endian u16 x, y, viewport width, and viewport height, reject
length mismatches, malformed UTF-8, unsupported controller indices, and zero viewports and coordinates outside the viewport, and require negotiated
reserved fields SHALL be rejected before provider translation. `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 #### Scenario: Keyboard state change
- **WHEN** a client sends a valid keyboard press or release envelope - **WHEN** a client sends a valid keyboard press or release envelope
- **THEN** the gateway forwards the corresponding typed provider input on its - **THEN** the gateway forwards the corresponding typed provider input on its
reliable keyboard channel and records the pressed state for cleanup. 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 #### Scenario: Invalid input envelope
- **WHEN** a client sends an envelope with an unknown event kind, invalid - **WHEN** a client sends an unknown or unadvertised kind, invalid length,
length, malformed UTF-8 scalar, or nonzero reserved field 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 - **THEN** the gateway rejects it without sending provider input or changing
pressed state. pressed state.
@@ -40,28 +52,27 @@ an implementation-specific release-all provider command.
The registered bidirectional reliable `control.ack.v1` flow SHALL define an ASCII `VGF1` envelope The registered bidirectional reliable `control.ack.v1` flow SHALL define an ASCII `VGF1` envelope
with a direction byte, type byte, big-endian payload length, and exact payload with a direction byte, type byte, big-endian payload length, and exact payload
bytes. Only host termination, rumble, and HDR feedback SHALL be valid from the bytes. Only host termination, rumble, and HDR feedback SHALL be valid from the
gateway to the client. Only IDR, FEC/loss feedback, and an empty terminal receipt gateway to the client. Only IDR, FEC/loss feedback, and client-direction type
SHALL be valid from the client to the gateway. The terminal receipt SHALL be `0x03` with an empty payload as terminal receipt SHALL be valid from the client
valid only while the same session awaits receipt of its one terminal event and to the gateway. The fixed conformance corpus and every generated or native
MUST NOT be forwarded to the provider. The envelope SHALL contain no provider Protocol validator SHALL accept that exact receipt and reject unknown types,
address, certificate, credential, or opaque provider packet. wrong direction, nonempty receipt bodies, truncation, and length mismatch. The
terminal receipt SHALL be valid only while the same session awaits receipt of
its one terminal event and MUST NOT be forwarded to the provider. The envelope
SHALL contain no provider address, certificate, credential, or opaque provider
packet.
#### Scenario: Host termination forwarding #### Scenario: Host termination forwarding
- **WHEN** the Apollo adapter receives an authenticated host termination - **WHEN** the Apollo adapter receives an authenticated host termination packet
packet - **THEN** the gateway forwards a bounded `VGF1` termination envelope over reliable Verse control and reports the provider state separately
- **THEN** the gateway forwards a bounded `VGF1` termination envelope over
reliable Verse control and reports the provider state separately.
#### Scenario: Terminal event receipt #### Scenario: Terminal event receipt
- **WHEN** a client receives the reliable typed terminal event - **WHEN** a client receives the reliable typed terminal event
- **THEN** it sends the empty terminal receipt and the gateway owns bounded - **THEN** it sends the fixed empty client-direction type `0x03` receipt and the gateway owns bounded tunnel closure without forwarding the receipt to the provider
tunnel closure without forwarding the receipt to the provider.
#### Scenario: Unauthorized or malformed feedback #### Scenario: Unauthorized or malformed feedback
- **WHEN** feedback is disabled by policy, has an invalid direction/type/length, - **WHEN** feedback is disabled by policy, has an invalid direction/type/length, contains a forbidden provider field, or sends a terminal receipt outside the awaiting-terminal state
contains a forbidden provider field, or sends a terminal receipt outside the - **THEN** the gateway rejects it without forwarding or provider mutation
awaiting-terminal state
- **THEN** the gateway rejects it without forwarding or provider mutation.
### Requirement: Policy-bound text clipboard envelope ### Requirement: Policy-bound text clipboard envelope
The reliable `clipboard.text.v1` flow SHALL carry only a typed UTF-8 text The reliable `clipboard.text.v1` flow SHALL carry only a typed UTF-8 text
@@ -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.
+4
View File
@@ -1,6 +1,10 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"max_frame_bytes": 65536, "max_frame_bytes": 65536,
"framing_profiles": [
{"id": "datagram-v1", "header_bytes": 21, "media_payload_bytes": 1179, "max_fragments": 16, "max_complete_bytes": 18864},
{"id": "datagram-v2", "header_bytes": 23, "media_payload_bytes": 1177, "max_fragments": 891, "max_complete_bytes": 1048576}
],
"datagrams": [ "datagrams": [
{"id": "control.ack.v1", "direction": "bidirectional", "max_payload_bytes": 1024, "payload_profile": "gateway-feedback-v1"}, {"id": "control.ack.v1", "direction": "bidirectional", "max_payload_bytes": 1024, "payload_profile": "gateway-feedback-v1"},
{"id": "control.cancel.v1", "direction": "client-to-server", "max_payload_bytes": 2048}, {"id": "control.cancel.v1", "direction": "client-to-server", "max_payload_bytes": 2048},
+5 -1
View File
@@ -2,7 +2,11 @@
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"features": [ "features": [
{"id": "control.v1", "direction": "bidirectional", "max_frame_bytes": 65536}, {"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": "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}
] ]
} }
+68 -6
View File
@@ -178,16 +178,70 @@
"refresh_expires_at": {"type": "string", "format": "date-time", "maxLength": 64} "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}
}
},
"SessionRequest": { "SessionRequest": {
"type": "object", "type": "object",
"additionalProperties": false, "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"],
"properties": { "properties": {
"client_device_id": {"type": "string", "minLength": 1, "maxLength": 128}, "client_device_id": {"type": "string", "minLength": 1, "maxLength": 128},
"device_key_id": {"type": "string", "minLength": 1, "maxLength": 128}, "device_key_id": {"type": "string", "minLength": 1, "maxLength": 128},
"pool_id": {"type": "string", "minLength": 1, "maxLength": 128}, "pool_id": {"type": "string", "minLength": 1, "maxLength": 128},
"idempotency_key": {"type": "string", "minLength": 1, "maxLength": 256}, "idempotency_key": {"type": "string", "minLength": 1, "maxLength": 256},
"policy_snapshot": {"$ref": "#/$defs/AllocationPolicy"} "requested_display_mode": {"$ref": "#/$defs/DisplayMode"}
} }
}, },
"ReconnectRequest": { "ReconnectRequest": {
@@ -219,7 +273,9 @@
"correlation_id": {"type": "string", "minLength": 1, "maxLength": 128}, "correlation_id": {"type": "string", "minLength": 1, "maxLength": 128},
"requested_at": {"type": "string", "format": "date-time", "maxLength": 64}, "requested_at": {"type": "string", "format": "date-time", "maxLength": 64},
"ended_at": {"type": "string", "format": "date-time", "maxLength": 64}, "ended_at": {"type": "string", "format": "date-time", "maxLength": 64},
"version": {"type": "integer", "minimum": 1} "version": {"type": "integer", "minimum": 1},
"requested_display_mode": {"$ref": "#/$defs/DisplayMode"},
"effective_display_mode": {"$ref": "#/$defs/DisplayMode"}
} }
}, },
"AllocationPolicy": { "AllocationPolicy": {
@@ -261,7 +317,12 @@
"properties": { "properties": {
"id": {"type": "string", "minLength": 1, "maxLength": 128}, "id": {"type": "string", "minLength": 1, "maxLength": 128},
"addresses": {"type": "array", "minItems": 1, "maxItems": 4, "items": {"type": "string", "minLength": 1, "maxLength": 256}}, "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": { "ManifestTunnel": {
@@ -279,7 +340,8 @@
"required": ["id", "bounds"], "required": ["id", "bounds"],
"properties": { "properties": {
"id": {"type": "string", "minLength": 1, "maxLength": 128}, "id": {"type": "string", "minLength": 1, "maxLength": 128},
"bounds": {"$ref": "#/$defs/ManifestBounds"} "bounds": {"$ref": "#/$defs/ManifestBounds"},
"display_mode": {"$ref": "#/$defs/DisplayMode"}
} }
}, },
"ManifestBounds": { "ManifestBounds": {
@@ -372,7 +434,7 @@
"required": ["transport", "framing", "media", "audio", "source_rate_control", "client_decode"], "required": ["transport", "framing", "media", "audio", "source_rate_control", "client_decode"],
"properties": { "properties": {
"transport": {"type": "string", "minLength": 1, "maxLength": 64}, "transport": {"type": "string", "minLength": 1, "maxLength": 64},
"framing": {"type": "string", "minLength": 1, "maxLength": 64}, "framing": {"type": "string", "enum": ["datagram-v1", "datagram-v2"]},
"media": {"type": "string", "minLength": 1, "maxLength": 64}, "media": {"type": "string", "minLength": 1, "maxLength": 64},
"audio": {"type": "string", "minLength": 1, "maxLength": 64}, "audio": {"type": "string", "minLength": 1, "maxLength": 64},
"source_rate_control": {"type": "string", "minLength": 1, "maxLength": 64}, "source_rate_control": {"type": "string", "minLength": 1, "maxLength": 64},
+162 -1
View File
@@ -1,6 +1,7 @@
package protocol_test package protocol_test
import ( import (
"bytes"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
@@ -9,7 +10,7 @@ import (
) )
func TestManifestRejectsForbiddenAndUnknownFields(t *testing.T) { func TestManifestRejectsForbiddenAndUnknownFields(t *testing.T) {
valid := `{"version":"1","purpose":"launch","session_id":"session-1","reconnect_sequence":0,"gateway":{"id":"gateway-1","addresses":["gateway.control.test:443"],"public_identity":"gateway-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"}`
manifest, err := protocol.DecodeConnectionManifest([]byte(valid)) manifest, err := protocol.DecodeConnectionManifest([]byte(valid))
if err != nil || manifest.Gateway.ID != "gateway-1" { if err != nil || manifest.Gateway.ID != "gateway-1" {
t.Fatalf("valid manifest = %+v, err = %v", manifest, err) t.Fatalf("valid manifest = %+v, err = %v", manifest, err)
@@ -22,6 +23,151 @@ 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",
}
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"}`
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) { func TestPageInfoRejectsOutOfBoundsLimit(t *testing.T) {
if _, err := protocol.DecodePageInfo([]byte(`{"limit":101,"next_cursor":""}`)); err == nil { if _, err := protocol.DecodePageInfo([]byte(`{"limit":101,"next_cursor":""}`)); err == nil {
t.Fatal("DecodePageInfo accepted limit above the contract maximum") t.Fatal("DecodePageInfo accepted limit above the contract maximum")
@@ -122,6 +268,21 @@ 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"},
}
if err := valid.Validate(); err != nil {
t.Fatalf("registered datagram-v2 rejected: %v", err)
}
invalid := valid
invalid.Framing = "datagram-v3"
if err := invalid.Validate(); err == nil {
t.Fatal("unregistered framing accepted")
}
}
func TestTunnelAdmissionRequiresDeviceSignature(t *testing.T) { func TestTunnelAdmissionRequiresDeviceSignature(t *testing.T) {
request := protocol.TunnelAdmissionRequest{ request := protocol.TunnelAdmissionRequest{
Version: "1", SessionID: "session-1", GatewayID: "gateway-1", Audience: "versevdi-gateway", Version: "1", SessionID: "session-1", GatewayID: "gateway-1", Audience: "versevdi-gateway",
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env python3
"""Require immutable commits for third-party Gitea workflow actions."""
from __future__ import annotations
import pathlib
import re
ROOT = pathlib.Path(__file__).resolve().parents[1]
USE = re.compile(r"^\s*-\s+uses:\s+([^@\s]+)@([^\s#]+)", re.MULTILINE)
for workflow in sorted((ROOT / ".gitea/workflows").glob("*.y*ml")):
for action, revision in USE.findall(workflow.read_text(encoding="utf-8")):
if not action.startswith("./") and not re.fullmatch(r"[0-9a-f]{40}", revision):
raise SystemExit(f"{workflow.relative_to(ROOT)}: mutable action {action}@{revision}")
print("Protocol CI action references are immutable")
+1
View File
@@ -87,6 +87,7 @@ def check_text_boundaries() -> None:
ROOT / "openapi/control-v1.yaml", ROOT / "openapi/control-v1.yaml",
ROOT / "proto/versevdi/control/v1/control.proto", ROOT / "proto/versevdi/control/v1/control.proto",
ROOT / "frames/datagram-v1.md", ROOT / "frames/datagram-v1.md",
ROOT / "frames/datagram-v2.md",
ROOT / "frames/registry.json", ROOT / "frames/registry.json",
ROOT / "registries/features.json", ROOT / "registries/features.json",
ROOT / "registries/datagrams.json", ROOT / "registries/datagrams.json",
+67 -3
View File
@@ -150,6 +150,12 @@ def go_validation(definition: dict[str, Any]) -> list[str]:
if "maxItems" in prop: if "maxItems" in prop:
lines.append(f"\tif len(v.{field}) > {prop['maxItems']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"max_items\"}}) }}") lines.append(f"\tif len(v.{field}) > {prop['maxItems']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"max_items\"}}) }}")
items = prop.get("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: if "enum" in items:
allowed = " || ".join(f'item == "{value}"' for value in items["enum"]) allowed = " || ".join(f'item == "{value}"' for value in items["enum"])
lines.append(f"\tfor _, item := range v.{field} {{ if !({allowed}) {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_item\"}}) }} }}") lines.append(f"\tfor _, item := range v.{field} {{ if !({allowed}) {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_item\"}}) }} }}")
@@ -160,7 +166,11 @@ def go_validation(definition: dict[str, Any]) -> list[str]:
lines.append(f"\tfor index := range v.{field} {{ if err := v.{field}[index].Validate(); err != nil {{ violations = append(violations, FieldViolation{{Field: fmt.Sprintf(\"{prop_name}[%d]\", index), Code: \"invalid_item\"}}) }} }}") lines.append(f"\tfor index := range v.{field} {{ if err := v.{field}[index].Validate(); err != nil {{ violations = append(violations, FieldViolation{{Field: fmt.Sprintf(\"{prop_name}[%d]\", index), Code: \"invalid_item\"}}) }} }}")
reference = ref_name(prop) reference = ref_name(prop)
if reference: 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"}: if name in {"AllocationPolicy", "ManifestBounds"}:
lines.append("\tif v.MinimumKbps > v.TargetKbps || v.TargetKbps > v.MaximumKbps { violations = append(violations, FieldViolation{Field: \"bounds\", Code: \"invalid_order\"}) }") lines.append("\tif v.MinimumKbps > v.TargetKbps || v.TargetKbps > v.MaximumKbps { violations = append(violations, FieldViolation{Field: \"bounds\", Code: \"invalid_order\"}) }")
if name == "GatewayRegistration": if name == "GatewayRegistration":
@@ -212,7 +222,10 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
required = set(definition.get("required", [])) required = set(definition.get("required", []))
for prop_name, prop in definition.get("properties", {}).items(): for prop_name, prop in definition.get("properties", {}).items():
tag = prop_name + (",omitempty" if prop_name not in required else "") tag = prop_name + (",omitempty" if prop_name not in required else "")
out.append(f"\t{go_field(prop_name)} {prop_type(prop, 'go')} `json:\"{tag}\"`") typ = prop_type(prop, "go")
if prop_name not in required and ref_name(prop):
typ = "*" + typ
out.append(f"\t{go_field(prop_name)} {typ} `json:\"{tag}\"`")
out.extend(["}", ""]) out.extend(["}", ""])
for name in sorted(defs): for name in sorted(defs):
out.append(f"func (v {name}) Validate() error {{") out.append(f"func (v {name}) Validate() error {{")
@@ -234,6 +247,11 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
% (prop_name, prop_name) % (prop_name, prop_name)
) )
for prop_name, prop in defs[name].get("properties", {}).items(): 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": if "x-max-bytes" in prop and prop.get("type") != "string":
out.append( out.append(
'\tif raw, ok := fields["%s"]; ok && len(raw) > %d { return value, ValidationError{Violations: []FieldViolation{{Field: "%s", Code: "max_bytes"}}} }' '\tif raw, ok := fields["%s"]; ok && len(raw) > %d { return value, ValidationError{Violations: []FieldViolation{{Field: "%s", Code: "max_bytes"}}} }'
@@ -347,6 +365,8 @@ def rust_validation(definition: dict[str, Any]) -> list[str]:
if "enum" in prop: if "enum" in prop:
allowed = " && ".join(f'{value} != \"{item}\"' for item in prop["enum"]) allowed = " && ".join(f'{value} != \"{item}\"' for item in prop["enum"])
lines.append(f" {prefix}if {allowed} {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_value\")); }}") 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": if prop.get("format") == "base64url":
lines.append(f" {prefix}if !valid_base64_url({value}.as_str()) {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_format\")); }}") lines.append(f" {prefix}if !valid_base64_url({value}.as_str()) {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_format\")); }}")
if prop.get("type") == "integer": if prop.get("type") == "integer":
@@ -360,6 +380,12 @@ def rust_validation(definition: dict[str, Any]) -> list[str]:
if "maxItems" in prop: if "maxItems" in prop:
lines.append(f" {prefix}if {value}.len() > {prop['maxItems']} {{ return Err(ValidationError::new(\"{prop_name}\", \"max_items\")); }}") lines.append(f" {prefix}if {value}.len() > {prop['maxItems']} {{ return Err(ValidationError::new(\"{prop_name}\", \"max_items\")); }}")
items = prop.get("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: if "enum" in items:
allowed = " && ".join(f'item != \"{item}\"' for item in items["enum"]) allowed = " && ".join(f'item != \"{item}\"' for item in items["enum"])
lines.append(f" {prefix}for item in {value}.iter() {{ if {allowed} {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_item\")); }} }}") lines.append(f" {prefix}for item in {value}.iter() {{ if {allowed} {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_item\")); }} }}")
@@ -417,6 +443,19 @@ def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibili
" _ => false,", " _ => 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'",
"}",
"", "",
] ]
for name in sorted(defs): for name in sorted(defs):
@@ -512,7 +551,7 @@ def swift_validation(definition: dict[str, Any]) -> list[str]:
allowed = ", ".join(f'\"{item}\"' for item in prop["enum"]) 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\") }}") lines.append(f" {prefix}if ![{allowed}].contains({value}) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_value\") }}")
if prop.get("format") == "date-time": 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": if prop.get("format") == "base64url":
lines.append(f" {prefix}if !validBase64URL({value}) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_format\") }}") lines.append(f" {prefix}if !validBase64URL({value}) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_format\") }}")
if prop.get("type") == "integer": if prop.get("type") == "integer":
@@ -526,6 +565,12 @@ def swift_validation(definition: dict[str, Any]) -> list[str]:
if "maxItems" in prop: if "maxItems" in prop:
lines.append(f" {prefix}if {value}.count > {prop['maxItems']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"max_items\") }}") lines.append(f" {prefix}if {value}.count > {prop['maxItems']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"max_items\") }}")
items = prop.get("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: if "enum" in items:
allowed = ", ".join(f'\"{item}\"' for item in items["enum"]) allowed = ", ".join(f'\"{item}\"' for item in items["enum"])
lines.append(f" {prefix}for item in {value} where ![{allowed}].contains(item) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_item\") }}") lines.append(f" {prefix}for item in {value} where ![{allowed}].contains(item) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_item\") }}")
@@ -569,6 +614,23 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
" guard let decoded = Data(base64Encoded: standard) else { return false }", " guard let decoded = Data(base64Encoded: standard) else { return false }",
" return decoded.base64EncodedString().replacingOccurrences(of: \"+\", with: \"-\").replacingOccurrences(of: \"/\", with: \"_\").replacingOccurrences(of: \"=\", with: \"\") == value", " 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",
"}",
"", "",
] ]
for name in sorted(defs): for name in sorted(defs):
@@ -603,6 +665,8 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
typ = swift_type(prop) typ = swift_type(prop)
if prop_name in required: if prop_name in required:
decoded.append(f"{field}: try c.decode({typ}.self, forKey: .{field})") 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: else:
decoded.append(f"{field}: try c.decodeIfPresent({typ}.self, forKey: .{field})") decoded.append(f"{field}: try c.decodeIfPresent({typ}.self, forKey: .{field})")
out.append(f" try self.init({', '.join(decoded)})") out.append(f" try self.init({', '.join(decoded)})")
+104 -18
View File
@@ -14,11 +14,6 @@ import (
protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol" protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol"
) )
const (
datagramHeaderBytes = 21
maximumFrameBytes = 65536
)
func main() { func main() {
entries, err := os.ReadDir("fixtures/conformance") entries, err := os.ReadDir("fixtures/conformance")
if err != nil { if err != nil {
@@ -43,7 +38,7 @@ func main() {
if len(fields) != 5 { if len(fields) != 5 {
panic("invalid fixture row") panic("invalid fixture row")
} }
actual := evaluate(fields[2], fields[3]) actual := evaluate(fields[1], fields[2], fields[3])
if actual != fields[4] { if actual != fields[4] {
panic(fmt.Sprintf("%s: got %s want %s", fields[0], actual, fields[4])) panic(fmt.Sprintf("%s: got %s want %s", fields[0], actual, fields[4]))
} }
@@ -54,7 +49,7 @@ func main() {
fmt.Printf("Go conformance passed normalized=%s fixtures=%s\n", normalizedDigest(results), fixtureHash) 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{} parts := map[string]string{}
for _, item := range strings.Split(input, ";") { for _, item := range strings.Split(input, ";") {
pair := strings.SplitN(item, "=", 2) pair := strings.SplitN(item, "=", 2)
@@ -64,7 +59,7 @@ func evaluate(kind, input string) string {
} }
switch kind { switch kind {
case "version": case "version":
if input == "1" || input == "0" || input == "-1" { if input == "2" || input == "1" || input == "0" {
return "valid" return "valid"
} }
return "invalid:unsupported_version" return "invalid:unsupported_version"
@@ -86,7 +81,7 @@ func evaluate(kind, input string) string {
Version: parts["version"], Purpose: parts["purpose"], SessionID: "session-1", Version: parts["version"], Purpose: parts["purpose"], SessionID: "session-1",
ReconnectSequence: 0, ReconnectSequence: 0,
Gateway: protocol.ManifestGateway{ 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"}}, 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}}, Profile: protocol.ManifestProfile{ID: "standard", Bounds: protocol.ManifestBounds{MinimumKbps: 1, TargetKbps: 2, MaximumKbps: 3}},
@@ -104,6 +99,60 @@ func evaluate(kind, input string) string {
return "valid" return "valid"
} }
return "invalid:unsupported_clipboard" 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"],
}
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": case "event":
sequence, sequenceErr := strconv.ParseInt(parts["sequence"], 10, 64) sequence, sequenceErr := strconv.ParseInt(parts["sequence"], 10, 64)
payloadBytes, payloadErr := strconv.Atoi(parts["payload_bytes"]) payloadBytes, payloadErr := strconv.Atoi(parts["payload_bytes"])
@@ -130,10 +179,12 @@ func evaluate(kind, input string) string {
} }
return "valid" return "valid"
case "tunnel": 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" return "valid"
} }
if parts["feature"] != "control.v1" { if !registered {
return "invalid:unsupported_feature" return "invalid:unsupported_feature"
} }
return "invalid:unsupported_version" return "invalid:unsupported_version"
@@ -231,6 +282,19 @@ 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"
}
default: default:
return "invalid:kind" return "invalid:kind"
} }
@@ -270,12 +334,16 @@ func classifyGatewayFeedback(encoded string) string {
return "valid" return "valid"
} }
return "invalid:field" return "invalid:field"
case 3:
if len(body) == 0 {
return "valid"
}
default: default:
return "invalid:type" return "invalid:type"
} }
return "invalid:length" return "invalid:length"
} }
if kind == 1 || kind == 2 { if kind == 1 || kind == 2 || kind == 3 {
return "invalid:direction" return "invalid:direction"
} }
switch kind { switch kind {
@@ -316,16 +384,24 @@ func classifyDatagram(encoded string) string {
if err != nil { if err != nil {
return "invalid:hex" return "invalid:hex"
} }
if len(raw) < datagramHeaderBytes { if len(raw) < 3 {
return "invalid:truncated" return "invalid:truncated"
} }
if string(raw[:2]) != "VD" { if string(raw[:2]) != "VD" {
return "invalid:magic" return "invalid:magic"
} }
if raw[2] != 1 { if raw[2] != 1 && raw[2] != 2 {
return "invalid:unsupported_version" return "invalid:unsupported_version"
} }
headerBytes := 21
limits := map[byte]int{1: 1024, 2: 2048, 3: 65515, 10: 1179, 11: 1179, 12: 1179} limits := map[byte]int{1: 1024, 2: 2048, 3: 65515, 10: 1179, 11: 1179, 12: 1179}
if raw[2] == 2 {
headerBytes = 23
limits = map[byte]int{10: 1177, 11: 1177}
}
if len(raw) < headerBytes {
return "invalid:truncated"
}
limit, ok := limits[raw[3]] limit, ok := limits[raw[3]]
if !ok { if !ok {
return "invalid:unknown_channel" return "invalid:unknown_channel"
@@ -333,17 +409,27 @@ func classifyDatagram(encoded string) string {
if raw[4] != 0 { if raw[4] != 0 {
return "invalid:flags" return "invalid:flags"
} }
if raw[18] == 0 || raw[17] >= raw[18] { fragmentIndex, fragmentCount := int(raw[17]), int(raw[18])
payloadOffset := 19
if raw[2] == 2 {
fragmentIndex = int(raw[17])<<8 | int(raw[18])
fragmentCount = int(raw[19])<<8 | int(raw[20])
payloadOffset = 21
if fragmentCount > 891 {
return "invalid:fragment_limit"
}
}
if fragmentCount == 0 || fragmentIndex >= fragmentCount {
return "invalid:fragment" return "invalid:fragment"
} }
payloadLength := int(raw[19])<<8 | int(raw[20]) payloadLength := int(raw[payloadOffset])<<8 | int(raw[payloadOffset+1])
if payloadLength > limit { if payloadLength > limit {
return "invalid:payload_limit" return "invalid:payload_limit"
} }
if len(raw) != datagramHeaderBytes+payloadLength { if len(raw) != headerBytes+payloadLength {
return "invalid:length_mismatch" return "invalid:length_mismatch"
} }
if len(raw) > maximumFrameBytes { if raw[2] == 1 && len(raw) > 65536 || raw[2] == 2 && len(raw) > 1200 {
return "invalid:frame_limit" return "invalid:frame_limit"
} }
return "valid" return "valid"
+94 -18
View File
@@ -9,10 +9,10 @@ fn values(input: &str) -> std::collections::BTreeMap<String, String> {
.collect() .collect()
} }
fn evaluate(kind: &str, input: &str) -> &'static str { fn evaluate(version: &str, kind: &str, input: &str) -> &'static str {
let values = values(input); let values = values(input);
match kind { match kind {
"version" if matches!(input, "1" | "0" | "-1") => "valid", "version" if matches!(input, "2" | "1" | "0") => "valid",
"version" => "invalid:unsupported_version", "version" => "invalid:unsupported_version",
"page" => match values.get("limit").and_then(|value| value.parse::<i64>().ok()) { "page" => match values.get("limit").and_then(|value| value.parse::<i64>().ok()) {
Some(limit) if (1..=100).contains(&limit) => "valid", Some(limit) if (1..=100).contains(&limit) => "valid",
@@ -24,12 +24,62 @@ fn evaluate(kind: &str, input: &str) -> &'static str {
"manifest" "manifest"
if values.get("version").map(String::as_str) == Some("1") if values.get("version").map(String::as_str) == Some("1")
&& values.contains_key("gateway_id") && values.contains_key("gateway_id")
&& values.contains_key("public_identity")
&& values.get("grant").map_or(false, |value| value.len() >= 43) && values.get("grant").map_or(false, |value| value.len() >= 43)
&& values.get("purpose").map(String::as_str) == Some("launch") => "valid", && values.get("purpose").map(String::as_str) == Some("launch") => "valid",
"manifest" => "invalid:invalid_manifest", "manifest" => "invalid:invalid_manifest",
"clipboard" if values.get("encoding").map(String::as_str) == Some("utf-8") "clipboard" if values.get("encoding").map(String::as_str) == Some("utf-8")
&& !values.contains_key("file") => "valid", && !values.contains_key("file") => "valid",
"clipboard" => "invalid:unsupported_clipboard", "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(),
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") => { "event" if values.get("version").map(String::as_str) != Some("1") => {
"invalid:unsupported_version" "invalid:unsupported_version"
} }
@@ -46,9 +96,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) "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", || !values.contains_key("correlation_id") => "invalid:required",
"event" => "valid", "event" => "valid",
"tunnel" if matches!(values.get("offered").map(String::as_str), Some("1") | Some("0") | Some("-1")) "tunnel" if matches!(values.get("offered").map(String::as_str), Some("2") | Some("1") | Some("0"))
&& values.get("feature").map(String::as_str) == Some("control.v1") => "valid", && 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 values.get("feature").map(String::as_str) != Some("control.v1") => { "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" "invalid:unsupported_feature"
} }
"tunnel" => "invalid:unsupported_version", "tunnel" => "invalid:unsupported_version",
@@ -110,6 +160,16 @@ fn classify_gateway_input(encoded: &str) -> &'static str {
5 if body[0] > 15 => "invalid:field", 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 if body[1] == 0 && body[2] == 0 && body[3..].iter().any(|value| *value != 0) => "invalid:field",
5 => "valid", 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",
_ => "invalid:kind", _ => "invalid:kind",
} }
} }
@@ -143,10 +203,12 @@ fn classify_gateway_feedback(encoded: &str) -> &'static str {
1 => "invalid:length", 1 => "invalid:length",
2 if valid_fec_status(body) => "valid", 2 if valid_fec_status(body) => "valid",
2 => "invalid:field", 2 => "invalid:field",
3 if body.is_empty() => "valid",
3 => "invalid:length",
_ => "invalid:type", _ => "invalid:type",
}; };
} }
if kind == 1 || kind == 2 { if kind == 1 || kind == 2 || kind == 3 {
return "invalid:direction"; return "invalid:direction";
} }
match kind { match kind {
@@ -187,36 +249,50 @@ fn classify_datagram(encoded: &str) -> &'static str {
Some(raw) => raw, Some(raw) => raw,
None => return "invalid:hex", None => return "invalid:hex",
}; };
if raw.len() < 21 { if raw.len() < 3 {
return "invalid:truncated"; return "invalid:truncated";
} }
if raw[0..2] != *b"VD" { if raw[0..2] != *b"VD" {
return "invalid:magic"; return "invalid:magic";
} }
if raw[2] != 1 { if raw[2] != 1 && raw[2] != 2 {
return "invalid:unsupported_version"; return "invalid:unsupported_version";
} }
let limit = match raw[3] { let header_bytes = if raw[2] == 1 { 21 } else { 23 };
1 => 1024, if raw.len() < header_bytes {
2 => 2048, return "invalid:truncated";
3 => 65515, }
10 | 11 | 12 => 1179, let limit = match (raw[2], raw[3]) {
(1, 1) => 1024,
(1, 2) => 2048,
(1, 3) => 65515,
(1, 10 | 11 | 12) => 1179,
(2, 10 | 11) => 1177,
_ => return "invalid:unknown_channel", _ => return "invalid:unknown_channel",
}; };
if raw[4] != 0 { if raw[4] != 0 {
return "invalid:flags"; return "invalid:flags";
} }
if raw[18] == 0 || raw[17] >= raw[18] { let (fragment_index, fragment_count, payload_offset) = if raw[2] == 1 {
(raw[17] as usize, raw[18] as usize, 19)
} else {
let count = ((raw[19] as usize) << 8) | raw[20] as usize;
if count > 891 {
return "invalid:fragment_limit";
}
((((raw[17] as usize) << 8) | raw[18] as usize), count, 21)
};
if fragment_count == 0 || fragment_index >= fragment_count {
return "invalid:fragment"; return "invalid:fragment";
} }
let payload_length = ((raw[19] as usize) << 8) | raw[20] as usize; let payload_length = ((raw[payload_offset] as usize) << 8) | raw[payload_offset + 1] as usize;
if payload_length > limit { if payload_length > limit {
return "invalid:payload_limit"; return "invalid:payload_limit";
} }
if raw.len() != 21 + payload_length { if raw.len() != header_bytes + payload_length {
return "invalid:length_mismatch"; return "invalid:length_mismatch";
} }
if raw.len() > 65536 { if raw[2] == 1 && raw.len() > 65536 || raw[2] == 2 && raw.len() > 1200 {
return "invalid:frame_limit"; return "invalid:frame_limit";
} }
"valid" "valid"
@@ -257,7 +333,7 @@ fn main() {
for line in lines { for line in lines {
let fields: Vec<&str> = line.split('\t').collect(); let fields: Vec<&str> = line.split('\t').collect();
assert_eq!(fields.len(), 5); assert_eq!(fields.len(), 5);
let actual = evaluate(fields[2], fields[3]); let actual = evaluate(fields[1], fields[2], fields[3]);
assert_eq!(actual, fields[4], "{}", fields[0]); assert_eq!(actual, fields[4], "{}", fields[0]);
results.push(format!("{}\t{}", fields[0], actual)); results.push(format!("{}\t{}", fields[0], actual));
} }
+67 -18
View File
@@ -9,17 +9,49 @@ func values(_ input: String) -> [String: String] {
return result return result
} }
func evaluate(_ kind: String, _ input: String) -> String { func evaluate(_ version: String, _ kind: String, _ input: String) -> String {
let values = values(input) let values = values(input)
switch kind { 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": case "page":
guard let raw = values["limit"], let limit = Int(raw), (1...100).contains(limit) else { return "invalid:invalid_limit" } guard let raw = values["limit"], let limit = Int(raw), (1...100).contains(limit) else { return "invalid:invalid_limit" }
return "valid" return "valid"
case "manifest": case "manifest":
for key in ["provider_url", "vm_address", "password", "private_key"] where values[key] != nil { return "invalid:forbidden_field" } 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 "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"] ?? "",
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": case "event":
guard values["version"] == "1" else { return "invalid:unsupported_version" } guard values["version"] == "1" else { return "invalid:unsupported_version" }
if let after = Int(values["after"] ?? ""), let earliest = Int(values["earliest"] ?? ""), after > 0, earliest > 0, after < earliest - 1 { return "invalid:gap" } if let after = Int(values["after"] ?? ""), let earliest = Int(values["earliest"] ?? ""), after > 0, earliest > 0, after < earliest - 1 { return "invalid:gap" }
@@ -27,8 +59,9 @@ func evaluate(_ kind: String, _ input: String) -> String {
guard let sequence = Int(values["sequence"] ?? ""), sequence > 0, values["correlation_id"] != nil else { return "invalid:required" } guard let sequence = Int(values["sequence"] ?? ""), sequence > 0, values["correlation_id"] != nil else { return "invalid:required" }
return "valid" return "valid"
case "tunnel": case "tunnel":
if ["1", "0", "-1"].contains(values["offered"] ?? "") && values["feature"] == "control.v1" { return "valid" } let registered = ["control.v1", "control.v2", "display.request.v1", "input.absolute.v1", "input.scroll.v1"].contains(values["feature"] ?? "")
return values["feature"] == "control.v1" ? "invalid:unsupported_version" : "invalid:unsupported_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 "datagram": return classifyDatagram(values["hex"] ?? "")
case "gateway_input": return classifyGatewayInput(values["hex"] ?? "") case "gateway_input": return classifyGatewayInput(values["hex"] ?? "")
case "gateway_feedback": return classifyGatewayFeedback(values["hex"] ?? "") case "gateway_feedback": return classifyGatewayFeedback(values["hex"] ?? "")
@@ -82,6 +115,14 @@ func classifyGatewayInput(_ encoded: String) -> String {
guard body[0] <= 15 else { return "invalid:field" } 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" } guard body[1] != 0 || body[2] != 0 || body.dropFirst(3).allSatisfy({ $0 == 0 }) else { return "invalid:field" }
return "valid" 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"
default: return "invalid:kind" default: return "invalid:kind"
} }
} }
@@ -101,10 +142,11 @@ func classifyGatewayFeedback(_ encoded: String) -> String {
case 1: return body.isEmpty ? "valid" : "invalid:length" case 1: return body.isEmpty ? "valid" : "invalid:length"
case 2: case 2:
return validFECStatus(body) ? "valid" : "invalid:field" return validFECStatus(body) ? "valid" : "invalid:field"
case 3: return body.isEmpty ? "valid" : "invalid:length"
default: return "invalid:type" default: return "invalid:type"
} }
} }
if kind == 1 || kind == 2 { return "invalid:direction" } if kind == 1 || kind == 2 || kind == 3 { return "invalid:direction" }
switch kind { switch kind {
case 0x10: return body.count == 4 ? "valid" : "invalid:length" case 0x10: return body.count == 4 ? "valid" : "invalid:length"
case 0x11: case 0x11:
@@ -128,23 +170,30 @@ func validFECStatus(_ body: [UInt8]) -> Bool {
func classifyDatagram(_ encoded: String) -> String { func classifyDatagram(_ encoded: String) -> String {
guard let raw = decodeHex(encoded) else { return "invalid:hex" } guard let raw = decodeHex(encoded) else { return "invalid:hex" }
guard raw.count >= 21 else { return "invalid:truncated" } guard raw.count >= 3 else { return "invalid:truncated" }
guard raw[0] == 0x56 && raw[1] == 0x44 else { return "invalid:magic" } guard raw[0] == 0x56 && raw[1] == 0x44 else { return "invalid:magic" }
guard raw[2] == 1 else { return "invalid:unsupported_version" } guard raw[2] == 1 || raw[2] == 2 else { return "invalid:unsupported_version" }
let headerBytes = raw[2] == 1 ? 21 : 23
guard raw.count >= headerBytes else { return "invalid:truncated" }
let limit: Int let limit: Int
switch raw[3] { switch (raw[2], raw[3]) {
case 1: limit = 1024 case (1, 1): limit = 1024
case 2: limit = 2048 case (1, 2): limit = 2048
case 3: limit = 65515 case (1, 3): limit = 65515
case 10, 11, 12: limit = 1179 case (1, 10), (1, 11), (1, 12): limit = 1179
case (2, 10), (2, 11): limit = 1177
default: return "invalid:unknown_channel" default: return "invalid:unknown_channel"
} }
guard raw[4] == 0 else { return "invalid:flags" } guard raw[4] == 0 else { return "invalid:flags" }
guard raw[18] > 0 && raw[17] < raw[18] else { return "invalid:fragment" } let fragmentIndex = raw[2] == 1 ? Int(raw[17]) : Int(raw[17]) * 256 + Int(raw[18])
let payloadLength = Int(raw[19]) * 256 + Int(raw[20]) let fragmentCount = raw[2] == 1 ? Int(raw[18]) : Int(raw[19]) * 256 + Int(raw[20])
if raw[2] == 2 && fragmentCount > 891 { return "invalid:fragment_limit" }
guard fragmentCount > 0 && fragmentIndex < fragmentCount else { return "invalid:fragment" }
let payloadOffset = raw[2] == 1 ? 19 : 21
let payloadLength = Int(raw[payloadOffset]) * 256 + Int(raw[payloadOffset + 1])
guard payloadLength <= limit else { return "invalid:payload_limit" } guard payloadLength <= limit else { return "invalid:payload_limit" }
guard raw.count == 21 + payloadLength else { return "invalid:length_mismatch" } guard raw.count == headerBytes + payloadLength else { return "invalid:length_mismatch" }
guard raw.count <= 65536 else { return "invalid:frame_limit" } guard raw[2] == 1 ? raw.count <= 65536 : raw.count <= 1200 else { return "invalid:frame_limit" }
return "valid" return "valid"
} }
@@ -180,7 +229,7 @@ struct ConformanceMain {
for line in lines { for line in lines {
let fields = line.split(separator: "\t", omittingEmptySubsequences: false).map(String.init) let fields = line.split(separator: "\t", omittingEmptySubsequences: false).map(String.init)
precondition(fields.count == 5) precondition(fields.count == 5)
let actual = evaluate(fields[2], fields[3]) let actual = evaluate(fields[1], fields[2], fields[3])
precondition(actual == fields[4], fields[0]) precondition(actual == fields[4], fields[0])
results.append("\(fields[0])\t\(actual)") results.append("\(fields[0])\t\(actual)")
} }
+1
View File
@@ -15,6 +15,7 @@ TEXT_PATHS = (
"proto/versevdi/control/v1/control.proto", "proto/versevdi/control/v1/control.proto",
"proto/versevdi/tunnel/v1/tunnel.proto", "proto/versevdi/tunnel/v1/tunnel.proto",
"frames/datagram-v1.md", "frames/datagram-v1.md",
"frames/datagram-v2.md",
"frames/registry.json", "frames/registry.json",
"registries/features.json", "registries/features.json",
"registries/datagrams.json", "registries/datagrams.json",
+139
View File
@@ -35,6 +35,20 @@ let capability = try CapabilityProfile(
transport: "quic-tls13", framing: "datagram-v1", media: "encoded", transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"] audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"]
) )
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"]
)
do {
_ = try CapabilityProfile(
transport: "quic-tls13", framing: "datagram-v3", media: "encoded",
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"]
)
fatalError("unregistered framing was accepted")
} catch { }
let request = try TunnelAdmissionRequest( let request = try TunnelAdmissionRequest(
version: "1", sessionId: "session", gatewayId: "gateway", audience: "audience", version: "1", sessionId: "session", gatewayId: "gateway", audience: "audience",
grant: String(repeating: "g", count: 43), reconnectSequence: 0, grant: String(repeating: "g", count: 43), reconnectSequence: 0,
@@ -92,6 +106,83 @@ do {
) )
fatalError("invalid allocation bounds were accepted") fatalError("invalid allocation bounds were accepted")
} catch { } } catch { }
let displayMode = try DisplayMode(resolutionWidth: 2560, resolutionHeight: 1440, fps: 120)
for invalid in [
{ 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", 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", 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( let streamPolicy = try ProviderStreamPolicy(
resolutionWidth: 2560, resolutionHeight: 1440, fps: 120, resolutionWidth: 2560, resolutionHeight: 1440, fps: 120,
codec: "HEVC", bitrateKbps: 40000, audioEnabled: true codec: "HEVC", bitrateKbps: 40000, audioEnabled: true
@@ -158,10 +249,21 @@ do {
output.write( output.write(
""" """
fn main() { 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 capabilities = CapabilityProfile::new( let capabilities = CapabilityProfile::new(
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(), "quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
"encoded".into(), "server".into(), vec!["h264-opus".into()], "encoded".into(), "server".into(), vec!["h264-opus".into()],
).unwrap(); ).unwrap();
assert!(CapabilityProfile::new(
"quic-tls13".into(), "datagram-v2".into(), "encoded".into(),
"encoded".into(), "server".into(), vec!["h264-opus".into()],
).is_ok());
assert!(CapabilityProfile::new(
"quic-tls13".into(), "datagram-v3".into(), "encoded".into(),
"encoded".into(), "server".into(), vec!["h264-opus".into()],
).is_err());
let request = TunnelAdmissionRequest::new( let request = TunnelAdmissionRequest::new(
"1".into(), "session".into(), "gateway".into(), "audience".into(), "1".into(), "session".into(), "gateway".into(), "audience".into(),
"g".repeat(43), 0, "n".repeat(16), "s".repeat(86), capabilities.clone(), "g".repeat(43), 0, "n".repeat(16), "s".repeat(86), capabilities.clone(),
@@ -199,6 +301,43 @@ fn main() {
assert!(AllocationPolicy::new( assert!(AllocationPolicy::new(
100, 50, 25, "standard".into(), "audience".into(), "verse".into(), 1, 60, 300, 100, 50, 25, "standard".into(), "audience".into(), "verse".into(), 1, 60, 300,
).is_err()); ).is_err());
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(),
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(),
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!(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!(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( assert!(ProviderStreamPolicy::new(
2560, 1440, 120, "HEVC".into(), 40000, true, 2560, 1440, 120, "HEVC".into(), 40000, true,
).is_ok()); ).is_ok());
+82 -2
View File
@@ -24,21 +24,87 @@ def main() -> int:
assert set(definition["required"]).issubset(definition["properties"]), name assert set(definition["required"]).issubset(definition["properties"]), name
compatibility = json.loads((ROOT / "compatibility.json").read_text(encoding="utf-8")) 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"]) assert len(set(compatibility["unsupported"])) == len(compatibility["unsupported"])
for registry in ("registries/features.json", "registries/datagrams.json"): for registry in ("registries/features.json", "registries/datagrams.json"):
value = json.loads((ROOT / registry).read_text(encoding="utf-8")) value = json.loads((ROOT / registry).read_text(encoding="utf-8"))
entries = value.get("features", value.get("datagrams")) entries = value.get("features", value.get("datagrams"))
assert entries and len({entry["id"] for entry in entries}) == len(entries) assert entries and len({entry["id"] for entry in entries}) == len(entries)
maximum_bound = 1_048_576 if registry.endswith("datagrams.json") else 65_536
for entry in entries: for entry in entries:
maximum = entry.get("max_frame_bytes", entry.get("max_payload_bytes")) maximum = entry.get("max_frame_bytes", entry.get("max_payload_bytes"))
assert isinstance(maximum, int) and 1 <= maximum <= 65536 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)
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"
]
manifest = json.loads((ROOT / "fixtures/valid/manifest.json").read_text(encoding="utf-8")) manifest = json.loads((ROOT / "fixtures/valid/manifest.json").read_text(encoding="utf-8"))
assert set(manifest).issubset(set(defs["ConnectionManifest"]["properties"])) 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")) 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 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"])
expected_header = "id\tversion\tkind\tinput\texpected" expected_header = "id\tversion\tkind\tinput\texpected"
ids = set() ids = set()
@@ -68,6 +134,20 @@ def main() -> int:
assert "openapi: 3.1.0" in openapi assert "openapi: 3.1.0" in openapi
assert "/api/v1/auth/refresh:" in openapi and "/api/v1/resources:" in openapi and "/api/v1/events:" in openapi assert "/api/v1/auth/refresh:" in openapi and "/api/v1/resources:" in openapi and "/api/v1/events:" in openapi
assert "provider_url" not in openapi and "vm_address" not in openapi assert "provider_url" not in openapi and "vm_address" not in openapi
session_endpoint = openapi.split(" /api/v1/auth/session:", 1)[1].split("\n /api/", 1)[0]
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
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") print("Protocol source validation passed")
return 0 return 0
+33 -22
View File
@@ -7,47 +7,58 @@ import binascii
import pathlib import pathlib
ROOT = pathlib.Path(__file__).resolve().parents[1] ROOT = pathlib.Path(__file__).resolve().parents[1]
HEADER_BYTES = 21 V1_CHANNEL_LIMITS = {1: 1024, 2: 2048, 3: 65515, 10: 1179, 11: 1179, 12: 1179}
MAX_FRAME_BYTES = 65536 V2_CHANNEL_LIMITS = {10: 1177, 11: 1177}
CHANNEL_LIMITS = {1: 1024, 2: 2048, 3: 65515, 10: 1179, 11: 1179, 12: 1179}
def classify(raw: bytes) -> str: def classify(raw: bytes) -> str:
if len(raw) < HEADER_BYTES: if len(raw) < 3:
return "invalid:truncated" return "invalid:truncated"
if raw[:2] != b"VD": if raw[:2] != b"VD":
return "invalid:magic" return "invalid:magic"
if raw[2] != 1: if raw[2] not in (1, 2):
return "invalid:unsupported_version" return "invalid:unsupported_version"
if raw[3] not in CHANNEL_LIMITS: header_bytes = 21 if raw[2] == 1 else 23
if len(raw) < header_bytes:
return "invalid:truncated"
limits = V1_CHANNEL_LIMITS if raw[2] == 1 else V2_CHANNEL_LIMITS
if raw[3] not in limits:
return "invalid:unknown_channel" return "invalid:unknown_channel"
if raw[4] != 0: if raw[4] != 0:
return "invalid:flags" return "invalid:flags"
fragment_index, fragment_count = raw[17], raw[18] if raw[2] == 1:
fragment_index, fragment_count = raw[17], raw[18]
payload_length = int.from_bytes(raw[19:21], "big")
else:
fragment_index = int.from_bytes(raw[17:19], "big")
fragment_count = int.from_bytes(raw[19:21], "big")
payload_length = int.from_bytes(raw[21:23], "big")
if fragment_count > 891:
return "invalid:fragment_limit"
if fragment_count == 0 or fragment_index >= fragment_count: if fragment_count == 0 or fragment_index >= fragment_count:
return "invalid:fragment" return "invalid:fragment"
payload_length = int.from_bytes(raw[19:21], "big") if payload_length > limits[raw[3]]:
if payload_length > CHANNEL_LIMITS[raw[3]]:
return "invalid:payload_limit" return "invalid:payload_limit"
if len(raw) != HEADER_BYTES + payload_length: if len(raw) != header_bytes + payload_length:
return "invalid:length_mismatch" return "invalid:length_mismatch"
if len(raw) > MAX_FRAME_BYTES: if raw[2] == 1 and len(raw) > 65536 or raw[2] == 2 and len(raw) > 1200:
return "invalid:frame_limit" return "invalid:frame_limit"
return "valid" return "valid"
def main() -> None: def main() -> None:
lines = (ROOT / "fixtures/conformance/datagram-v1.tsv").read_text(encoding="utf-8").splitlines() for fixture in ("datagram-v1.tsv", "datagram-v2.tsv"):
assert lines[0] == "id\tversion\tkind\tinput\texpected" lines = (ROOT / "fixtures/conformance" / fixture).read_text(encoding="utf-8").splitlines()
for line in lines[1:]: assert lines[0] == "id\tversion\tkind\tinput\texpected"
identifier, version, kind, input_value, expected = line.split("\t") for line in lines[1:]:
assert kind == "datagram" and version == "1" identifier, version, kind, input_value, expected = line.split("\t")
encoded = input_value.removeprefix("hex=") assert kind == "datagram" and version in ("1", "2")
try: encoded = input_value.removeprefix("hex=")
actual = classify(binascii.unhexlify(encoded)) try:
except binascii.Error: actual = classify(binascii.unhexlify(encoded))
actual = "invalid:hex" except binascii.Error:
assert actual == expected, f"{identifier}: {actual} != {expected}" actual = "invalid:hex"
assert actual == expected, f"{fixture}:{identifier}: {actual} != {expected}"
print("Datagram frame validation passed") print("Datagram frame validation passed")
+10 -1
View File
@@ -37,6 +37,13 @@ def classify_input(raw: bytes) -> str:
return "invalid:field" return "invalid:field"
active_mask = int.from_bytes(body[1:3], "big") active_mask = int.from_bytes(body[1:3], "big")
return "valid" if active_mask or not any(body[3:]) else "invalid:field" 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"
return "invalid:kind" return "invalid:kind"
@@ -59,8 +66,10 @@ def classify_feedback(raw: bytes) -> str:
return "valid" if not body else "invalid:length" return "valid" if not body else "invalid:length"
if kind == 2: if kind == 2:
return "valid" if valid_fec_status(body) else "invalid:field" return "valid" if valid_fec_status(body) else "invalid:field"
if kind == 3:
return "valid" if not body else "invalid:length"
return "invalid:type" return "invalid:type"
if kind in (1, 2): if kind in (1, 2, 3):
return "invalid:direction" return "invalid:direction"
if kind == 0x10: if kind == 0x10:
return "valid" if len(body) == 4 else "invalid:length" return "valid" if len(body) == 4 else "invalid:length"