Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afcd5d99db | ||
|
|
79d9e49497 |
+1
-1
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ go-test:
|
|||||||
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
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,9 +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-display-request 1 tunnel offered=1;feature=display.request.v1 valid
|
tunnel-display-request 2 tunnel offered=2;feature=display.request.v1 valid
|
||||||
tunnel-absolute-input 1 tunnel offered=1;feature=input.absolute.v1 valid
|
tunnel-absolute-input 2 tunnel offered=2;feature=input.absolute.v1 valid
|
||||||
tunnel-scroll-input 1 tunnel offered=1;feature=input.scroll.v1 valid
|
tunnel-scroll-input 2 tunnel offered=2;feature=input.scroll.v1 valid
|
||||||
tunnel-unsupported 1 tunnel offered=2;feature=control.v1 invalid:unsupported_version
|
tunnel-unsupported 2 tunnel offered=3;feature=control.v2 invalid:unsupported_version
|
||||||
tunnel-no-control 1 tunnel offered=1;feature=media.video invalid:unsupported_feature
|
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": {}
|
||||||
|
}
|
||||||
@@ -10,5 +10,5 @@
|
|||||||
"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": "4f8bf520d4f61588235b00776b21202ff701b20d6d11b461037b911c2117852b"
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"client_device_id": "device-1",
|
||||||
|
"device_key_id": "key-1",
|
||||||
|
"pool_id": "pool-1",
|
||||||
|
"idempotency_key": "request-1"
|
||||||
|
}
|
||||||
+469
-14
@@ -13,11 +13,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const SchemaSHA256 = "b2bb0a8ac8ef56dbc0e1443eeb5b3028be9e71ec2f5fd8e73928d71b7cd9340c"
|
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"`
|
||||||
@@ -69,6 +69,13 @@ type BrokerSession struct {
|
|||||||
EffectiveDisplayMode *DisplayMode `json:"effective_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 {
|
||||||
Transport string `json:"transport"`
|
Transport string `json:"transport"`
|
||||||
Framing string `json:"framing"`
|
Framing string `json:"framing"`
|
||||||
@@ -281,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"`
|
||||||
@@ -290,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"`
|
||||||
@@ -396,7 +424,6 @@ type SessionRequest struct {
|
|||||||
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"`
|
RequestedDisplayMode *DisplayMode `json:"requested_display_mode,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -863,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 == "" {
|
||||||
@@ -2689,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"})
|
||||||
}
|
}
|
||||||
@@ -3231,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"})
|
||||||
}
|
}
|
||||||
@@ -3371,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}
|
||||||
}
|
}
|
||||||
@@ -3423,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 {
|
||||||
@@ -3526,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 {
|
||||||
@@ -3875,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}
|
||||||
}
|
}
|
||||||
@@ -4761,12 +5215,6 @@ 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{}) {
|
|
||||||
violations = append(violations, FieldViolation{Field: "policy_snapshot", Code: "required"})
|
|
||||||
}
|
|
||||||
if err := v.PolicySnapshot.Validate(); err != nil {
|
|
||||||
violations = append(violations, FieldViolation{Field: "policy_snapshot", Code: "invalid_object"})
|
|
||||||
}
|
|
||||||
if v.RequestedDisplayMode != nil {
|
if v.RequestedDisplayMode != nil {
|
||||||
if err := v.RequestedDisplayMode.Validate(); err != nil {
|
if err := v.RequestedDisplayMode.Validate(); err != nil {
|
||||||
violations = append(violations, FieldViolation{Field: "requested_display_mode", Code: "invalid_object"})
|
violations = append(violations, FieldViolation{Field: "requested_display_mode", Code: "invalid_object"})
|
||||||
@@ -4796,9 +5244,6 @@ 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"}}}
|
||||||
}
|
}
|
||||||
@@ -5057,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
@@ -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": "992235a56d3467313148f86e47931f247591e4c8de737b55ac9c9eee35725fc5",
|
"generator_sha256": "8a153cf1e99682d010ff91c754ef056c64aece8f8bbca0ca58f8eef2b9039119",
|
||||||
"protocol_version": "1.0.0",
|
"protocol_version": "1.0.0",
|
||||||
"schema_sha256": "b2bb0a8ac8ef56dbc0e1443eeb5b3028be9e71ec2f5fd8e73928d71b7cd9340c"
|
"schema_sha256": "dea3dd210c53d5a2d37050dd6afd8b0ac5bb8edcb7ab25a02e4026489ce8a00f"
|
||||||
}
|
}
|
||||||
|
|||||||
+179
-9
@@ -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 = "b2bb0a8ac8ef56dbc0e1443eeb5b3028be9e71ec2f5fd8e73928d71b7cd9340c";
|
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 {
|
||||||
@@ -165,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")); }
|
||||||
@@ -182,8 +196,10 @@ 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 {
|
if let Some(value) = &self.requestedDisplayMode {
|
||||||
@@ -213,6 +229,40 @@ impl BrokerSession {
|
|||||||
pub fn effectiveDisplayMode(&self) -> &Option<DisplayMode> { &self.effectiveDisplayMode }
|
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)]
|
||||||
pub struct CapabilityProfile {
|
pub struct CapabilityProfile {
|
||||||
transport: String,
|
transport: String,
|
||||||
@@ -428,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(())
|
||||||
@@ -636,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")); }
|
||||||
@@ -789,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 }
|
||||||
@@ -823,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")); }
|
||||||
@@ -893,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(())
|
||||||
@@ -1016,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")); }
|
||||||
@@ -1103,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")); }
|
||||||
@@ -1156,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>,
|
||||||
@@ -1193,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(())
|
||||||
}
|
}
|
||||||
@@ -1206,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,
|
||||||
@@ -1268,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")); }
|
||||||
@@ -1348,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 }
|
||||||
@@ -1414,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 }
|
||||||
@@ -1630,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")); }
|
||||||
@@ -1654,13 +1825,12 @@ pub struct SessionRequest {
|
|||||||
deviceKeyId: String,
|
deviceKeyId: String,
|
||||||
poolId: String,
|
poolId: String,
|
||||||
idempotencyKey: String,
|
idempotencyKey: String,
|
||||||
policySnapshot: AllocationPolicy,
|
|
||||||
requestedDisplayMode: Option<DisplayMode>,
|
requestedDisplayMode: Option<DisplayMode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SessionRequest {
|
impl SessionRequest {
|
||||||
pub fn new(clientDeviceId: String, deviceKeyId: String, poolId: String, idempotencyKey: String, policySnapshot: AllocationPolicy, requestedDisplayMode: Option<DisplayMode>) -> 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, requestedDisplayMode };
|
let value = Self { clientDeviceId, deviceKeyId, poolId, idempotencyKey, requestedDisplayMode };
|
||||||
value.validate()?;
|
value.validate()?;
|
||||||
Ok(value)
|
Ok(value)
|
||||||
}
|
}
|
||||||
@@ -1677,7 +1847,6 @@ 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 {
|
if let Some(value) = &self.requestedDisplayMode {
|
||||||
value.validate().map_err(|_| ValidationError::new("requested_display_mode", "invalid_object"))?;
|
value.validate().map_err(|_| ValidationError::new("requested_display_mode", "invalid_object"))?;
|
||||||
}
|
}
|
||||||
@@ -1687,7 +1856,6 @@ impl SessionRequest {
|
|||||||
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 }
|
pub fn requestedDisplayMode(&self) -> &Option<DisplayMode> { &self.requestedDisplayMode }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1799,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 }
|
||||||
|
|||||||
+227
-23
@@ -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 = "b2bb0a8ac8ef56dbc0e1443eeb5b3028be9e71ec2f5fd8e73928d71b7cd9340c"
|
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
|
||||||
@@ -217,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") }
|
||||||
@@ -235,10 +252,10 @@ 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 {
|
if let value = self.requestedDisplayMode {
|
||||||
@@ -253,6 +270,51 @@ public struct BrokerSession: 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 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 func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) }
|
||||||
|
}
|
||||||
|
|
||||||
public struct CapabilityProfile: Codable, Equatable {
|
public struct CapabilityProfile: Codable, Equatable {
|
||||||
public let transport: String
|
public let transport: String
|
||||||
public let framing: String
|
public let framing: String
|
||||||
@@ -551,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") }
|
||||||
}
|
}
|
||||||
@@ -831,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") }
|
||||||
@@ -1036,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) }
|
||||||
@@ -1089,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") }
|
||||||
@@ -1185,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") }
|
||||||
}
|
}
|
||||||
@@ -1333,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") }
|
||||||
@@ -1451,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") }
|
||||||
@@ -1522,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) }
|
||||||
@@ -1576,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") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1587,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
|
||||||
@@ -1703,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") }
|
||||||
@@ -1781,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) }
|
||||||
@@ -1868,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) }
|
||||||
@@ -2169,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") }
|
||||||
@@ -2186,23 +2392,20 @@ 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?
|
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"
|
case requestedDisplayMode = "requested_display_mode"
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(clientDeviceId: String, deviceKeyId: String, poolId: String, idempotencyKey: String, policySnapshot: AllocationPolicy, requestedDisplayMode: DisplayMode?) 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
|
self.requestedDisplayMode = requestedDisplayMode
|
||||||
try validate()
|
try validate()
|
||||||
}
|
}
|
||||||
@@ -2211,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), requestedDisplayMode: try c.contains(.requestedDisplayMode) ? c.decode(DisplayMode.self, forKey: .requestedDisplayMode) : nil)
|
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 {
|
||||||
@@ -2227,7 +2430,6 @@ 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 {
|
if let value = self.requestedDisplayMode {
|
||||||
try value.validate()
|
try value.validate()
|
||||||
}
|
}
|
||||||
@@ -2372,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
@@ -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: []
|
||||||
|
|||||||
+3
-3
@@ -12,6 +12,6 @@
|
|||||||
|
|
||||||
## 3. Freeze
|
## 3. Freeze
|
||||||
|
|
||||||
- [ ] 3.1 Commit the verified source and generated bindings as one unsigned capability-sized change.
|
- [x] 3.1 Commit the verified source and generated bindings as one unsigned capability-sized change.
|
||||||
- [ ] 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.2 Verify the next Phase 3D Protocol RC name is unused locally and remotely, then create one local immutable candidate tag without publishing it.
|
||||||
- [ ] 3.3 Reconcile and archive this OpenSpec change after the immutable candidate is frozen.
|
- [x] 3.3 Reconcile and archive this OpenSpec change after the immutable candidate is frozen.
|
||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
"$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": "display.request.v1", "direction": "client-to-server", "max_frame_bytes": 65536},
|
||||||
|
|||||||
@@ -178,6 +178,50 @@
|
|||||||
"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": {
|
"DisplayMode": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
@@ -191,13 +235,12 @@
|
|||||||
"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"}
|
"requested_display_mode": {"$ref": "#/$defs/DisplayMode"}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -274,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": {
|
||||||
|
|||||||
@@ -10,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)
|
||||||
@@ -42,11 +42,6 @@ func TestDisplayModeIsOptionalStrictAndBounded(t *testing.T) {
|
|||||||
|
|
||||||
request := protocol.SessionRequest{
|
request := protocol.SessionRequest{
|
||||||
ClientDeviceID: "device-1", DeviceKeyID: "key-1", PoolID: "pool-1", IdempotencyKey: "request-1",
|
ClientDeviceID: "device-1", DeviceKeyID: "key-1", PoolID: "pool-1", IdempotencyKey: "request-1",
|
||||||
PolicySnapshot: protocol.AllocationPolicy{
|
|
||||||
MinimumKbps: 1000, TargetKbps: 2000, MaximumKbps: 3000, Tier: "standard",
|
|
||||||
Audience: "versevdi-gateway", Protocol: "verse", ProtocolVersion: 1,
|
|
||||||
GrantTTLSeconds: 60, ReservationLeaseSeconds: 300,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
legacy, err := protocol.EncodeSessionRequest(request)
|
legacy, err := protocol.EncodeSessionRequest(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -100,6 +95,79 @@ func TestDisplayModeIsOptionalStrictAndBounded(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
|||||||
+51
-1
@@ -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\"}}) }} }}")
|
||||||
@@ -359,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":
|
||||||
@@ -372,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\")); }} }}")
|
||||||
@@ -429,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):
|
||||||
@@ -524,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":
|
||||||
@@ -538,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\") }}")
|
||||||
@@ -581,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):
|
||||||
|
|||||||
@@ -38,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]))
|
||||||
}
|
}
|
||||||
@@ -49,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)
|
||||||
@@ -59,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"
|
||||||
@@ -81,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}},
|
||||||
@@ -99,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"])
|
||||||
@@ -126,8 +180,8 @@ func evaluate(kind, input string) string {
|
|||||||
return "valid"
|
return "valid"
|
||||||
case "tunnel":
|
case "tunnel":
|
||||||
feature := parts["feature"]
|
feature := parts["feature"]
|
||||||
registered := feature == "control.v1" || feature == "display.request.v1" || feature == "input.absolute.v1" || feature == "input.scroll.v1"
|
registered := feature == "control.v1" || feature == "control.v2" || feature == "display.request.v1" || feature == "input.absolute.v1" || feature == "input.scroll.v1"
|
||||||
if (parts["offered"] == "1" || parts["offered"] == "0" || parts["offered"] == "-1") && registered {
|
if (parts["offered"] == "2" || parts["offered"] == "1" || parts["offered"] == "0") && registered {
|
||||||
return "valid"
|
return "valid"
|
||||||
}
|
}
|
||||||
if !registered {
|
if !registered {
|
||||||
|
|||||||
@@ -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"))
|
||||||
&& matches!(values.get("feature").map(String::as_str), Some("control.v1") | Some("display.request.v1") | Some("input.absolute.v1") | Some("input.scroll.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 !matches!(values.get("feature").map(String::as_str), Some("control.v1") | Some("display.request.v1") | Some("input.absolute.v1") | Some("input.scroll.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",
|
||||||
@@ -283,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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,8 @@ 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":
|
||||||
let registered = ["control.v1", "display.request.v1", "input.absolute.v1", "input.scroll.v1"].contains(values["feature"] ?? "")
|
let registered = ["control.v1", "control.v2", "display.request.v1", "input.absolute.v1", "input.scroll.v1"].contains(values["feature"] ?? "")
|
||||||
if ["1", "0", "-1"].contains(values["offered"] ?? "") && registered { return "valid" }
|
if ["2", "1", "0"].contains(values["offered"] ?? "") && registered { return "valid" }
|
||||||
return registered ? "invalid:unsupported_version" : "invalid:unsupported_feature"
|
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"] ?? "")
|
||||||
@@ -197,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)")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ 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(
|
_ = try CapabilityProfile(
|
||||||
transport: "quic-tls13", framing: "datagram-v2", media: "encoded",
|
transport: "quic-tls13", framing: "datagram-v2", media: "encoded",
|
||||||
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"]
|
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"]
|
||||||
@@ -114,23 +117,16 @@ for invalid in [
|
|||||||
fatalError("invalid display mode was accepted")
|
fatalError("invalid display mode was accepted")
|
||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
let allocationPolicy = try AllocationPolicy(
|
let policyFreeV2Request = try SessionRequest(
|
||||||
minimumKbps: 1000, targetKbps: 2000, maximumKbps: 3000, tier: "standard",
|
|
||||||
audience: "versevdi-gateway", protocolValue: "verse", protocolVersion: 1,
|
|
||||||
grantTtlSeconds: 60, reservationLeaseSeconds: 300
|
|
||||||
)
|
|
||||||
let legacyDisplayRequest = try SessionRequest(
|
|
||||||
clientDeviceId: "device-1", deviceKeyId: "key-1", poolId: "pool-1",
|
clientDeviceId: "device-1", deviceKeyId: "key-1", poolId: "pool-1",
|
||||||
idempotencyKey: "request-1", policySnapshot: allocationPolicy,
|
idempotencyKey: "request-1", requestedDisplayMode: nil
|
||||||
requestedDisplayMode: nil
|
|
||||||
).encodeJSON()
|
).encodeJSON()
|
||||||
guard !String(data: legacyDisplayRequest, encoding: .utf8)!.contains("requested_display_mode") else {
|
guard !String(data: policyFreeV2Request, encoding: .utf8)!.contains("requested_display_mode") else {
|
||||||
fatalError("legacy request encoded an absent display mode")
|
fatalError("wire-v2 request encoded an absent display mode")
|
||||||
}
|
}
|
||||||
let displayRequest = try SessionRequest(
|
let displayRequest = try SessionRequest(
|
||||||
clientDeviceId: "device-1", deviceKeyId: "key-1", poolId: "pool-1",
|
clientDeviceId: "device-1", deviceKeyId: "key-1", poolId: "pool-1",
|
||||||
idempotencyKey: "request-1", policySnapshot: allocationPolicy,
|
idempotencyKey: "request-1", requestedDisplayMode: displayMode
|
||||||
requestedDisplayMode: displayMode
|
|
||||||
)
|
)
|
||||||
guard try SessionRequest.decodeJSON(displayRequest.encodeJSON()).requestedDisplayMode == displayMode else {
|
guard try SessionRequest.decodeJSON(displayRequest.encodeJSON()).requestedDisplayMode == displayMode else {
|
||||||
fatalError("display mode did not round-trip")
|
fatalError("display mode did not round-trip")
|
||||||
@@ -141,6 +137,52 @@ do {
|
|||||||
_ = try SessionRequest.decodeJSON(try JSONSerialization.data(withJSONObject: nullDisplayRequest))
|
_ = try SessionRequest.decodeJSON(try JSONSerialization.data(withJSONObject: nullDisplayRequest))
|
||||||
fatalError("explicit null display mode was accepted")
|
fatalError("explicit null display mode was accepted")
|
||||||
} catch { }
|
} 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
|
||||||
@@ -207,6 +249,9 @@ 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()],
|
||||||
@@ -260,20 +305,39 @@ fn main() {
|
|||||||
assert!(DisplayMode::new(319, 1440, 120).is_err());
|
assert!(DisplayMode::new(319, 1440, 120).is_err());
|
||||||
assert!(DisplayMode::new(2560, 199, 120).is_err());
|
assert!(DisplayMode::new(2560, 199, 120).is_err());
|
||||||
assert!(DisplayMode::new(2560, 1440, 241).is_err());
|
assert!(DisplayMode::new(2560, 1440, 241).is_err());
|
||||||
let allocation_policy = AllocationPolicy::new(
|
let policy_free_v2_request = SessionRequest::new(
|
||||||
1000, 2000, 3000, "standard".into(), "versevdi-gateway".into(),
|
|
||||||
"verse".into(), 1, 60, 300,
|
|
||||||
).unwrap();
|
|
||||||
let legacy_display_request = SessionRequest::new(
|
|
||||||
"device-1".into(), "key-1".into(), "pool-1".into(), "request-1".into(),
|
"device-1".into(), "key-1".into(), "pool-1".into(), "request-1".into(),
|
||||||
allocation_policy.clone(), None,
|
None,
|
||||||
).unwrap();
|
).unwrap();
|
||||||
assert!(legacy_display_request.requestedDisplayMode().is_none());
|
assert!(policy_free_v2_request.requestedDisplayMode().is_none());
|
||||||
let display_request = SessionRequest::new(
|
let display_request = SessionRequest::new(
|
||||||
"device-1".into(), "key-1".into(), "pool-1".into(), "request-1".into(),
|
"device-1".into(), "key-1".into(), "pool-1".into(), "request-1".into(),
|
||||||
allocation_policy, Some(display_mode.clone()),
|
Some(display_mode.clone()),
|
||||||
).unwrap();
|
).unwrap();
|
||||||
assert_eq!(display_request.requestedDisplayMode(), &Some(display_mode));
|
assert_eq!(display_request.requestedDisplayMode(), &Some(display_mode));
|
||||||
|
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());
|
||||||
|
|||||||
+63
-2
@@ -24,7 +24,7 @@ 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"):
|
||||||
@@ -38,7 +38,7 @@ def main() -> int:
|
|||||||
|
|
||||||
feature_registry = json.loads((ROOT / "registries/features.json").read_text(encoding="utf-8"))
|
feature_registry = json.loads((ROOT / "registries/features.json").read_text(encoding="utf-8"))
|
||||||
registered_features = {entry["id"] for entry in feature_registry["features"]}
|
registered_features = {entry["id"] for entry in feature_registry["features"]}
|
||||||
assert {"display.request.v1", "input.absolute.v1", "input.scroll.v1"}.issubset(registered_features)
|
assert {"control.v1", "control.v2", "display.request.v1", "input.absolute.v1", "input.scroll.v1"}.issubset(registered_features)
|
||||||
|
|
||||||
display_mode = defs["DisplayMode"]
|
display_mode = defs["DisplayMode"]
|
||||||
assert display_mode["required"] == ["resolution_width", "resolution_height", "fps"]
|
assert display_mode["required"] == ["resolution_width", "resolution_height", "fps"]
|
||||||
@@ -54,10 +54,57 @@ def main() -> int:
|
|||||||
assert field not in defs[owner]["required"]
|
assert field not in defs[owner]["required"]
|
||||||
assert defs[owner]["properties"][field] == {"$ref": "#/$defs/DisplayMode"}
|
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()
|
||||||
@@ -87,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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user