protocol: add canonical Phase 3A contract sources
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
# VerseVDI Protocol repository guidance
|
||||
|
||||
This repository is the sole source of the VerseVDI Phase 3 wire contract. It
|
||||
contains OpenAPI, JSON Schema, Protobuf, registries, fixtures, and generated
|
||||
language bindings. It contains no Connection Server business policy,
|
||||
database/auth implementation, provider credentials, or media implementation.
|
||||
|
||||
## Source and generation rules
|
||||
|
||||
- Edit `schemas/`, `openapi/`, `proto/`, `registries/`, and `fixtures/` only as
|
||||
protocol sources. Never hand-edit `gen/`; run `python3 tools/generate.py`.
|
||||
- `VERSION` and `compatibility.json` are the version authority. Current, N-1,
|
||||
and N-2 fixtures must remain executable.
|
||||
- `make verify` runs schema/fixture checks, Protobuf lint/descriptor checks,
|
||||
the historical-descriptor breaking check, deterministic generation, Go tests,
|
||||
and native conformance runners.
|
||||
- Generated output must be byte-for-byte clean after a second generation.
|
||||
- `gen/protobuf/phase3a-baseline.binpb` is a generated historical descriptor used
|
||||
only by Buf breaking checks; regenerate it from an approved baseline tag,
|
||||
never by hand.
|
||||
- The Connection Server consumes generated Go types through adapters; it does
|
||||
not own a competing wire schema.
|
||||
|
||||
## Scope boundaries
|
||||
|
||||
Phase 3A covers control, authorization, resources, broker session actions,
|
||||
gateway-only grants/manifests, resumable control events, registered control
|
||||
datagrams, and bounded text clipboard. It does not implement a gateway,
|
||||
provider transport, video/audio forwarding, file transfer, or a second auth or
|
||||
datastore path.
|
||||
|
||||
## Git and release safety
|
||||
|
||||
Keep private Gitea work and public-source boundaries explicit. Preserve dirty
|
||||
work from other owners. Do not push, merge, publish, or change release
|
||||
credentials unless the task separately authorizes it. A local immutable
|
||||
candidate tag is allowed only after matching Go/Rust/Swift fixture hashes and
|
||||
clean-checkout evidence; it is not a publication.
|
||||
@@ -1,2 +1,27 @@
|
||||
# VerseVDI-Protocol
|
||||
|
||||
The Phase 3A Protocol repository is the sole source of VerseVDI wire contracts.
|
||||
OpenAPI, JSON Schema, Protobuf, registries, and conformance fixtures live here;
|
||||
the Connection Server consumes generated Go bindings and remains the authority
|
||||
for authentication, authorization, persistence, licensing, and provider policy.
|
||||
|
||||
## Scope
|
||||
|
||||
Phase 3A defines versioned control requests/responses, native device contracts,
|
||||
subject-scoped resources, broker actions, gateway-only manifests, resumable
|
||||
control events, registered control datagrams, and bounded text clipboard. It
|
||||
does not implement a gateway, provider transport, media forwarding, file
|
||||
transfer, or a second authentication or datastore path.
|
||||
|
||||
## Verification
|
||||
|
||||
Run `make verify` after editing a source schema, registry, Protobuf definition,
|
||||
or fixture. The command validates source bounds and scope, regenerates Go/Rust/
|
||||
Swift bindings and Protobuf descriptors, compiles generated bindings, runs the
|
||||
shared Go/Rust/Swift corpus, checks datagram frames, and fails if regeneration
|
||||
would dirty the checkout.
|
||||
|
||||
Generated files under `gen/` are never edited by hand. `VERSION`,
|
||||
`compatibility.json`, and the content-addressed fixture manifest identify the
|
||||
release inputs. A Protocol release candidate is not valid until all consumers
|
||||
report the same fixture hash and clean-checkout regeneration evidence.
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
version: v2
|
||||
modules:
|
||||
- path: proto
|
||||
lint:
|
||||
use:
|
||||
- STANDARD
|
||||
breaking:
|
||||
use:
|
||||
- FILE
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"protocol": "versevdi-control",
|
||||
"current": "1",
|
||||
"n_minus_1": "0",
|
||||
"n_minus_2": "-1",
|
||||
"unsupported": ["-2", "2"],
|
||||
"feature_registry": "registries/features.json",
|
||||
"datagram_registry": "registries/datagrams.json"
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
# Phase 3A interface inventory
|
||||
|
||||
This inventory records the wire boundary consumed by the closed Connection Server.
|
||||
The Protocol repository owns only schemas, registries, fixtures, and generated bindings;
|
||||
the Server remains the authority for authentication, authorization, persistence, and
|
||||
provider selection.
|
||||
|
||||
## Existing Server routes adapted by generated contracts
|
||||
|
||||
| Route | Channel/guard | Contract boundary | Phase 2 behavior |
|
||||
|---|---|---|---|
|
||||
| `POST /api/v1/auth/refresh` | native credential body; rate limited | `RefreshRequest` -> `NativeCredential` | opaque refresh rotation and replay-family revocation remain PostgreSQL-owned |
|
||||
| `POST /api/v1/auth/devices` | authenticated browser session + CSRF | `DeviceRegistrationRequest` -> `DeviceChallenge` | device challenge remains one-use and hash-backed |
|
||||
| `POST /api/v1/auth/devices/:id/prove` | authenticated browser session + CSRF | `DeviceProofRequest` -> `NativeCredential` | canonical Ed25519 proof and native session issuance remain Server-owned |
|
||||
| `DELETE /api/v1/auth/devices/:id` | authenticated browser session + CSRF | no body | principal-scoped device revocation remains Server-owned |
|
||||
| `GET /api/v1/resources` | authenticated browser/native session | `ResourceList` | assigned desktop and entitled pool projections omit machine/provider data |
|
||||
| `POST /api/v1/broker/sessions` | authenticated session + CSRF | `SessionRequest` -> `BrokerSession` | existing assignment, idempotency, queue, and PostgreSQL state machine remain authoritative |
|
||||
| `GET /api/v1/broker/sessions/:id` | authenticated session | `BrokerSession` | owner-scoped lookup is preserved |
|
||||
| `POST /api/v1/broker/sessions/:id/allocate` | authenticated session + CSRF | `ConnectionManifest` | gateway-only grant boundary; no provider/direct-host fields |
|
||||
| `POST /api/v1/broker/sessions/:id/reconnect` | authenticated session + CSRF | `ReconnectRequest` -> `ConnectionManifest` | device proof, expected version, grant rotation, and monotonic reconnect sequence remain authoritative |
|
||||
| `POST /api/v1/broker/sessions/:id/cancel` | authenticated session + CSRF | no body | existing compensation path remains authoritative |
|
||||
|
||||
## Contract rules
|
||||
|
||||
- Unknown fields are rejected for signed/security-sensitive requests and generated
|
||||
decoders are strict.
|
||||
- Responses are explicit projections; domain, sqlc, provider, VM, and credential
|
||||
structures are never serialized directly.
|
||||
- Resource projections contain only opaque assignment/pool identifiers and safe state;
|
||||
machine identifiers and provider endpoints are not wire fields.
|
||||
- A manifest contains only the versioned gateway/tunnel/profile/grant allow-list. The
|
||||
opaque grant is stored as a hash in the Server database and is single-use.
|
||||
- Events and QUIC/tunnel sources are defined here before any production media or gateway
|
||||
implementation; the Phase 3A claim stops at control contracts and conformance.
|
||||
|
||||
## Compatibility notes
|
||||
|
||||
The existing routes and authentication channels remain unchanged. Phase 3A adds the
|
||||
`page` member to the resource response and replaces the legacy flat manifest projection
|
||||
with the versioned gateway-only manifest. Both changes are intentional contract changes:
|
||||
the former is additive, and the latter is required to make provider/direct-host fields
|
||||
unrepresentable. The Server integration tests retain the old safe resource members and
|
||||
assert the new allow-list explicitly.
|
||||
@@ -0,0 +1,27 @@
|
||||
# VerseVDI control datagram v1
|
||||
|
||||
Phase 3A reserves a bounded control-datagram envelope. It does not forward video,
|
||||
audio, provider traffic, VM traffic, or arbitrary binary clipboard data.
|
||||
|
||||
All multi-byte integers are unsigned big-endian. The fixed header is 21 bytes:
|
||||
|
||||
| Offset | Size | Field | Rule |
|
||||
|---:|---:|---|---|
|
||||
| 0 | 2 | magic | ASCII `VD` (`0x56 0x44`) |
|
||||
| 2 | 1 | version | `1` only |
|
||||
| 3 | 1 | channel | registered channel identifier |
|
||||
| 4 | 1 | flags | registry-defined; unknown bits reject |
|
||||
| 5 | 4 | sequence | session-local wrapping sequence |
|
||||
| 9 | 8 | timestamp_ms | sender timestamp, bounded by transport skew policy |
|
||||
| 17 | 1 | fragment_index | zero-based; `0` when not fragmented |
|
||||
| 18 | 1 | fragment_count | `1` when not fragmented; index less than count |
|
||||
| 19 | 2 | payload_length | exact payload byte count |
|
||||
|
||||
The complete frame is at most 65,536 bytes and `payload_length` is at most 65,515.
|
||||
Truncated, oversized, unknown-version, unknown-channel, invalid-fragment, and
|
||||
length-mismatch frames are rejected before allocation proportional to the claimed
|
||||
payload. Media/provider identifiers are not registered channels.
|
||||
|
||||
Registered Phase 3A channels are `control.ack.v1`, `control.cancel.v1`, and
|
||||
`clipboard.text.v1`. Clipboard payloads are UTF-8 JSON text contracts and remain
|
||||
subject to the 65,536-byte text limit and explicit authorization.
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"version": "1",
|
||||
"header_bytes": 21,
|
||||
"maximum_frame_bytes": 65536,
|
||||
"channels": [
|
||||
{"id": 1, "name": "control.ack.v1", "direction": "bidirectional", "max_payload_bytes": 1024},
|
||||
{"id": 2, "name": "control.cancel.v1", "direction": "client-to-server", "max_payload_bytes": 2048},
|
||||
{"id": 3, "name": "clipboard.text.v1", "direction": "bidirectional", "max_payload_bytes": 65515}
|
||||
],
|
||||
"reserved_rejected": ["video", "audio", "provider", "vm", "file-transfer", "clipboard.binary"]
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: VerseVDI control contract
|
||||
version: 1.0.0
|
||||
description: Bounded Phase 3A control-plane wire contract; no provider or media endpoints.
|
||||
servers:
|
||||
- url: /
|
||||
tags:
|
||||
- name: authentication
|
||||
- name: resources
|
||||
- name: broker
|
||||
- name: events
|
||||
paths:
|
||||
/api/v1/auth/login:
|
||||
post:
|
||||
tags: [authentication]
|
||||
operationId: loginBrowserSession
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../schemas/control-v1.schema.json#/$defs/LoginRequest
|
||||
responses:
|
||||
'200':
|
||||
description: Browser session established through secure cookies.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [username, provider, roles, role]
|
||||
properties:
|
||||
username: {type: string, minLength: 1, maxLength: 256}
|
||||
provider: {type: string, minLength: 1, maxLength: 64}
|
||||
roles: {type: array, maxItems: 16, items: {type: string, maxLength: 64}}
|
||||
role: {type: string, enum: [user, admin]}
|
||||
'400': {$ref: '#/components/responses/InvalidRequest'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'503': {$ref: '#/components/responses/Unavailable'}
|
||||
/api/v1/auth/refresh:
|
||||
post:
|
||||
tags: [authentication]
|
||||
operationId: rotateNativeCredential
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../schemas/control-v1.schema.json#/$defs/RefreshRequest
|
||||
responses:
|
||||
'200':
|
||||
description: Rotated opaque native credential.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../schemas/control-v1.schema.json#/$defs/NativeCredential
|
||||
'400': {$ref: '#/components/responses/InvalidRequest'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
/api/v1/auth/session:
|
||||
get:
|
||||
tags: [authentication]
|
||||
operationId: getAuthenticatedSession
|
||||
security:
|
||||
- browserSession: []
|
||||
- nativeBearer: []
|
||||
responses:
|
||||
'200':
|
||||
description: Authenticated subject summary.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [username, provider, roles, role]
|
||||
properties:
|
||||
username: {type: string, minLength: 1, maxLength: 256}
|
||||
provider: {type: string, minLength: 1, maxLength: 64}
|
||||
roles: {type: array, maxItems: 16, items: {type: string, maxLength: 64}}
|
||||
role: {type: string, enum: [user, admin]}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
/api/v1/auth/reauth:
|
||||
post:
|
||||
tags: [authentication]
|
||||
operationId: issueReauthenticationGrant
|
||||
security:
|
||||
- browserSession: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../schemas/control-v1.schema.json#/$defs/ReauthRequest
|
||||
responses:
|
||||
'200':
|
||||
description: Short-lived opaque step-up grant.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../schemas/control-v1.schema.json#/$defs/ReauthGrant
|
||||
'400': {$ref: '#/components/responses/InvalidRequest'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
/api/v1/auth/logout:
|
||||
post:
|
||||
tags: [authentication]
|
||||
operationId: logoutSession
|
||||
security:
|
||||
- browserSession: []
|
||||
- nativeBearer: []
|
||||
responses:
|
||||
'204': {description: Session revoked and browser cookies cleared.}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
/api/v1/auth/devices:
|
||||
post:
|
||||
tags: [authentication]
|
||||
operationId: registerDevice
|
||||
security:
|
||||
- browserSession: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../schemas/control-v1.schema.json#/$defs/DeviceRegistrationRequest
|
||||
responses:
|
||||
'201':
|
||||
description: Single-use device challenge.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../schemas/control-v1.schema.json#/$defs/DeviceChallenge
|
||||
'400': {$ref: '#/components/responses/InvalidRequest'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
/api/v1/auth/devices/{device_id}/prove:
|
||||
post:
|
||||
tags: [authentication]
|
||||
operationId: proveDevice
|
||||
security:
|
||||
- browserSession: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DeviceID'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../schemas/control-v1.schema.json#/$defs/DeviceProofRequest
|
||||
responses:
|
||||
'200':
|
||||
description: Opaque native credential.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../schemas/control-v1.schema.json#/$defs/NativeCredential
|
||||
'400': {$ref: '#/components/responses/InvalidRequest'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
/api/v1/auth/devices/{device_id}:
|
||||
delete:
|
||||
tags: [authentication]
|
||||
operationId: revokeDevice
|
||||
security:
|
||||
- browserSession: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DeviceID'
|
||||
responses:
|
||||
'204': {description: Device and its native authority revoked.}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
/api/v1/resources:
|
||||
get:
|
||||
tags: [resources]
|
||||
operationId: listResources
|
||||
security:
|
||||
- browserSession: []
|
||||
- nativeBearer: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Limit'
|
||||
- $ref: '#/components/parameters/Cursor'
|
||||
responses:
|
||||
'200':
|
||||
description: Subject-scoped bounded resources and entitlements.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../schemas/control-v1.schema.json#/$defs/ResourceList
|
||||
'400': {$ref: '#/components/responses/InvalidRequest'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
/api/v1/broker/sessions:
|
||||
post:
|
||||
tags: [broker]
|
||||
operationId: requestBrokerSession
|
||||
security:
|
||||
- browserSession: []
|
||||
- nativeBearer: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/IdempotencyKey'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../schemas/control-v1.schema.json#/$defs/SessionRequest
|
||||
responses:
|
||||
'201':
|
||||
description: Durable broker session.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../schemas/control-v1.schema.json#/$defs/BrokerSession
|
||||
'202':
|
||||
description: Durable session waiting for an eligible desktop or capacity.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../schemas/control-v1.schema.json#/$defs/BrokerSession
|
||||
'400': {$ref: '#/components/responses/InvalidRequest'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/api/v1/broker/sessions/{session_id}:
|
||||
get:
|
||||
tags: [broker]
|
||||
operationId: getBrokerSession
|
||||
security:
|
||||
- browserSession: []
|
||||
- nativeBearer: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/SessionID'
|
||||
responses:
|
||||
'200':
|
||||
description: Subject-scoped broker session projection.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../schemas/control-v1.schema.json#/$defs/BrokerSession
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
/api/v1/broker/sessions/{session_id}/allocate:
|
||||
post:
|
||||
tags: [broker]
|
||||
operationId: allocateBrokerSession
|
||||
security:
|
||||
- browserSession: []
|
||||
- nativeBearer: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/SessionID'
|
||||
responses:
|
||||
'200':
|
||||
description: Gateway-only launch manifest with a single-use grant.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../schemas/control-v1.schema.json#/$defs/ConnectionManifest
|
||||
'202':
|
||||
description: Session remains waiting for an eligible desktop or capacity.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../schemas/control-v1.schema.json#/$defs/BrokerSession
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/api/v1/broker/sessions/{session_id}/reconnect:
|
||||
post:
|
||||
tags: [broker]
|
||||
operationId: reconnectBrokerSession
|
||||
security:
|
||||
- browserSession: []
|
||||
- nativeBearer: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/SessionID'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../schemas/control-v1.schema.json#/$defs/ReconnectRequest
|
||||
responses:
|
||||
'200':
|
||||
description: Gateway-only reconnect manifest with a monotonic sequence.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../schemas/control-v1.schema.json#/$defs/ConnectionManifest
|
||||
'400': {$ref: '#/components/responses/InvalidRequest'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/api/v1/broker/sessions/{session_id}/cancel:
|
||||
post:
|
||||
tags: [broker]
|
||||
operationId: cancelBrokerSession
|
||||
security:
|
||||
- browserSession: []
|
||||
- nativeBearer: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/SessionID'
|
||||
responses:
|
||||
'204': {description: Cancellation accepted idempotently.}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/api/v1/events:
|
||||
get:
|
||||
tags: [events]
|
||||
operationId: resumeUserEvents
|
||||
security:
|
||||
- browserSession: []
|
||||
- nativeBearer: []
|
||||
parameters:
|
||||
- name: version
|
||||
in: query
|
||||
required: false
|
||||
schema: {type: string, enum: ['1']}
|
||||
- name: cursor
|
||||
in: query
|
||||
required: false
|
||||
schema: {type: string, maxLength: 512}
|
||||
- name: Upgrade
|
||||
in: header
|
||||
required: false
|
||||
schema: {type: string, enum: [websocket]}
|
||||
- name: Sec-WebSocket-Version
|
||||
in: header
|
||||
required: false
|
||||
schema: {type: string, enum: ['13']}
|
||||
responses:
|
||||
'200':
|
||||
description: Bounded resumable NDJSON event snapshot; WebSocket upgrades emit the same envelopes as text frames.
|
||||
headers:
|
||||
X-VerseVDI-Event-Cursor:
|
||||
schema: {type: string, maxLength: 512}
|
||||
content:
|
||||
application/x-ndjson:
|
||||
schema:
|
||||
$ref: ../schemas/control-v1.schema.json#/$defs/EventEnvelope
|
||||
'400': {$ref: '#/components/responses/InvalidRequest'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
'426': {description: WebSocket version 13 is required.}
|
||||
'503': {$ref: '#/components/responses/Unavailable'}
|
||||
components:
|
||||
securitySchemes:
|
||||
browserSession:
|
||||
type: apiKey
|
||||
in: cookie
|
||||
name: versevdi_session
|
||||
nativeBearer:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: opaque
|
||||
parameters:
|
||||
DeviceID:
|
||||
name: device_id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string, minLength: 1, maxLength: 128}
|
||||
SessionID:
|
||||
name: session_id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string, minLength: 1, maxLength: 128}
|
||||
Limit:
|
||||
name: limit
|
||||
in: query
|
||||
required: false
|
||||
schema: {type: integer, minimum: 1, maximum: 100, default: 100}
|
||||
Cursor:
|
||||
name: cursor
|
||||
in: query
|
||||
required: false
|
||||
schema: {type: string, maxLength: 512}
|
||||
IdempotencyKey:
|
||||
name: Idempotency-Key
|
||||
in: header
|
||||
required: true
|
||||
schema: {type: string, minLength: 1, maxLength: 256}
|
||||
responses:
|
||||
InvalidRequest:
|
||||
description: The request is invalid.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../schemas/control-v1.schema.json#/$defs/ErrorEnvelope
|
||||
Unauthorized:
|
||||
description: Authentication or credential validation failed without revealing credential state.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../schemas/control-v1.schema.json#/$defs/ErrorEnvelope
|
||||
Forbidden:
|
||||
description: The authenticated subject is not entitled to the requested action.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../schemas/control-v1.schema.json#/$defs/ErrorEnvelope
|
||||
NotFound:
|
||||
description: The resource is unavailable to this subject.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../schemas/control-v1.schema.json#/$defs/ErrorEnvelope
|
||||
Conflict:
|
||||
description: The expected version, protocol version, idempotency, or event retention boundary conflicts.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../schemas/control-v1.schema.json#/$defs/ErrorEnvelope
|
||||
Unavailable:
|
||||
description: The authoritative service is temporarily unavailable.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../schemas/control-v1.schema.json#/$defs/ErrorEnvelope
|
||||
@@ -0,0 +1,20 @@
|
||||
schema: spec-driven
|
||||
|
||||
# Project context (optional)
|
||||
# This is shown to AI when creating artifacts.
|
||||
# Add your tech stack, conventions, style guides, domain knowledge, etc.
|
||||
# Example:
|
||||
# context: |
|
||||
# Tech stack: TypeScript, React, Node.js
|
||||
# We use conventional commits
|
||||
# Domain: e-commerce platform
|
||||
|
||||
# Per-artifact rules (optional)
|
||||
# Add custom rules for specific artifacts.
|
||||
# Example:
|
||||
# rules:
|
||||
# proposal:
|
||||
# - Keep proposals under 500 words
|
||||
# - Always include a "Non-goals" section
|
||||
# tasks:
|
||||
# - Break tasks into chunks of max 2 hours
|
||||
@@ -0,0 +1,34 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package versevdi.control.v1;
|
||||
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
option go_package = "github.com/sechmachine/VerseVDI-Protocol/gen/go/protocol";
|
||||
|
||||
message VersionNegotiation {
|
||||
repeated string supported_versions = 1;
|
||||
repeated string features = 2;
|
||||
}
|
||||
|
||||
message ResourceLink {
|
||||
string type = 1;
|
||||
string id = 2;
|
||||
int64 version = 3;
|
||||
}
|
||||
|
||||
message EventEnvelope {
|
||||
string event_id = 1;
|
||||
int64 sequence = 2;
|
||||
string type = 3;
|
||||
int64 version = 4;
|
||||
ResourceLink resource = 5;
|
||||
google.protobuf.Timestamp occurred_at = 6;
|
||||
bytes payload_json = 7;
|
||||
string correlation_id = 8;
|
||||
}
|
||||
|
||||
message ClipboardText {
|
||||
string text = 1;
|
||||
string encoding = 2;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package versevdi.tunnel.v1;
|
||||
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
option go_package = "github.com/sechmachine/VerseVDI-Protocol/gen/go/tunnel";
|
||||
|
||||
message VersionNegotiation {
|
||||
repeated string supported_versions = 1;
|
||||
repeated string features = 2;
|
||||
}
|
||||
|
||||
message AdmissionRequest {
|
||||
string session_id = 1;
|
||||
string protocol_version = 2;
|
||||
string audience = 3;
|
||||
string purpose = 4;
|
||||
string grant = 5;
|
||||
}
|
||||
|
||||
message AdmissionResult {
|
||||
bool admitted = 1;
|
||||
string session_id = 2;
|
||||
string failure_code = 3;
|
||||
int64 session_version = 4;
|
||||
}
|
||||
|
||||
message SessionClose {
|
||||
string session_id = 1;
|
||||
string code = 2;
|
||||
string reason = 3;
|
||||
google.protobuf.Timestamp occurred_at = 4;
|
||||
}
|
||||
|
||||
message Health {
|
||||
string session_id = 1;
|
||||
int64 sequence = 2;
|
||||
google.protobuf.Timestamp observed_at = 3;
|
||||
}
|
||||
|
||||
message BoundedError {
|
||||
string code = 1;
|
||||
string message = 2;
|
||||
}
|
||||
|
||||
message ChannelOpen {
|
||||
string channel_id = 1;
|
||||
string direction = 2;
|
||||
uint32 maximum_frame_bytes = 3;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"max_frame_bytes": 65536,
|
||||
"datagrams": [
|
||||
{"id": "control.ack.v1", "direction": "bidirectional", "max_payload_bytes": 1024},
|
||||
{"id": "control.cancel.v1", "direction": "client-to-server", "max_payload_bytes": 2048},
|
||||
{"id": "clipboard.text.v1", "direction": "bidirectional", "max_payload_bytes": 65536}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"features": [
|
||||
{"id": "control.v1", "direction": "bidirectional", "max_frame_bytes": 65536},
|
||||
{"id": "events.resume.v1", "direction": "server-to-client", "max_frame_bytes": 65536},
|
||||
{"id": "clipboard.text.v1", "direction": "bidirectional", "max_frame_bytes": 65536}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://protocol.versevdi.invalid/schemas/control-v1.schema.json",
|
||||
"title": "VerseVDI Phase 3A control contract",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"$defs": {
|
||||
"FieldViolation": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["field", "code"],
|
||||
"properties": {
|
||||
"field": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"code": {"type": "string", "minLength": 1, "maxLength": 64}
|
||||
}
|
||||
},
|
||||
"ErrorEnvelope": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["status", "error", "code", "message", "resolution", "request_id", "violations"],
|
||||
"properties": {
|
||||
"status": {"type": "boolean"},
|
||||
"error": {"type": "string", "minLength": 1, "maxLength": 512},
|
||||
"code": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"message": {"type": "string", "minLength": 1, "maxLength": 512},
|
||||
"resolution": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"request_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"violations": {"type": "array", "maxItems": 16, "items": {"$ref": "#/$defs/FieldViolation"}}
|
||||
}
|
||||
},
|
||||
"PageInfo": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["limit", "next_cursor"],
|
||||
"properties": {
|
||||
"limit": {"type": "integer", "minimum": 1, "maximum": 100},
|
||||
"next_cursor": {"type": "string", "maxLength": 512}
|
||||
}
|
||||
},
|
||||
"ResourceLink": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["type", "id", "version"],
|
||||
"properties": {
|
||||
"type": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||
"id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"version": {"type": "integer", "minimum": 1}
|
||||
}
|
||||
},
|
||||
"Resource": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "kind", "name", "state", "version", "links"],
|
||||
"properties": {
|
||||
"id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"kind": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||
"name": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||
"state": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||
"assignment_state": {"type": "string", "maxLength": 64},
|
||||
"version": {"type": "integer", "minimum": 1},
|
||||
"links": {"type": "array", "maxItems": 16, "items": {"$ref": "#/$defs/ResourceLink"}}
|
||||
}
|
||||
},
|
||||
"AssignedDesktop": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["assignment_id", "pool_id", "name", "availability"],
|
||||
"properties": {
|
||||
"assignment_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"pool_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"name": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||
"availability": {"type": "string", "minLength": 1, "maxLength": 64}
|
||||
}
|
||||
},
|
||||
"EntitledPool": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["pool_id", "name", "assignment_state"],
|
||||
"properties": {
|
||||
"pool_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"name": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||
"assignment_state": {"type": "string", "minLength": 1, "maxLength": 64}
|
||||
}
|
||||
},
|
||||
"ResourceList": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["assigned_desktops", "entitled_pools", "page"],
|
||||
"properties": {
|
||||
"assigned_desktops": {"type": "array", "maxItems": 100, "items": {"$ref": "#/$defs/AssignedDesktop"}},
|
||||
"entitled_pools": {"type": "array", "maxItems": 100, "items": {"$ref": "#/$defs/EntitledPool"}},
|
||||
"page": {"$ref": "#/$defs/PageInfo"}
|
||||
}
|
||||
},
|
||||
"DeviceRegistrationRequest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "platform", "device_subject", "algorithm", "public_key"],
|
||||
"properties": {
|
||||
"name": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"platform": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||
"device_subject": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||
"algorithm": {"type": "string", "const": "ed25519"},
|
||||
"public_key": {"type": "string", "minLength": 1, "maxLength": 256}
|
||||
}
|
||||
},
|
||||
"DeviceChallenge": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["device_id", "server_id", "principal_id", "challenge", "expires_at", "algorithm", "signature_format"],
|
||||
"properties": {
|
||||
"device_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"server_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"principal_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"challenge": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||
"expires_at": {"type": "string", "format": "date-time", "maxLength": 64},
|
||||
"algorithm": {"type": "string", "const": "ed25519"},
|
||||
"signature_format": {"type": "string", "const": "ed25519-domain-separated-v1"}
|
||||
}
|
||||
},
|
||||
"DeviceProofRequest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["challenge", "signature"],
|
||||
"properties": {
|
||||
"challenge": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||
"signature": {"type": "string", "minLength": 1, "maxLength": 256}
|
||||
}
|
||||
},
|
||||
"LoginRequest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["username", "password"],
|
||||
"properties": {
|
||||
"provider": {"type": "string", "enum": ["ldap", "local"]},
|
||||
"username": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||
"password": {"type": "string", "minLength": 1, "maxLength": 1024}
|
||||
}
|
||||
},
|
||||
"ReauthRequest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["password", "purpose"],
|
||||
"properties": {
|
||||
"password": {"type": "string", "minLength": 1, "maxLength": 1024},
|
||||
"purpose": {"type": "string", "enum": ["identity_change", "key_change", "backup_enable", "external_database_tls_disabled", "assignment_change"]}
|
||||
}
|
||||
},
|
||||
"ReauthGrant": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["token", "purpose", "expires_at"],
|
||||
"properties": {
|
||||
"token": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||
"purpose": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||
"expires_at": {"type": "string", "format": "date-time", "maxLength": 64}
|
||||
}
|
||||
},
|
||||
"RefreshRequest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["family_id", "refresh_token"],
|
||||
"properties": {
|
||||
"family_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"refresh_token": {"type": "string", "minLength": 1, "maxLength": 256}
|
||||
}
|
||||
},
|
||||
"NativeCredential": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["family_id", "access_token", "refresh_token", "expires_at"],
|
||||
"properties": {
|
||||
"device_id": {"type": "string", "maxLength": 128},
|
||||
"family_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"access_token": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||
"refresh_token": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||
"expires_at": {"type": "string", "format": "date-time", "maxLength": 64},
|
||||
"refresh_expires_at": {"type": "string", "format": "date-time", "maxLength": 64}
|
||||
}
|
||||
},
|
||||
"SessionRequest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["client_device_id", "device_key_id", "pool_id", "idempotency_key", "policy_snapshot"],
|
||||
"properties": {
|
||||
"client_device_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"device_key_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"pool_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"idempotency_key": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||
"policy_snapshot": {"$ref": "#/$defs/AllocationPolicy"}
|
||||
}
|
||||
},
|
||||
"ReconnectRequest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["client_device_id", "device_key_id", "expected_version"],
|
||||
"properties": {
|
||||
"client_device_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"device_key_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"expected_version": {"type": "integer", "minimum": 1}
|
||||
}
|
||||
},
|
||||
"BrokerSession": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "principal_id", "pool_id", "state", "policy_snapshot", "cleanup_state", "idempotency_key", "correlation_id", "requested_at", "version"],
|
||||
"properties": {
|
||||
"id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"principal_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"pool_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"assignment_id": {"type": "string", "maxLength": 128},
|
||||
"state": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||
"policy_snapshot": {"$ref": "#/$defs/AllocationPolicy"},
|
||||
"reconnect_deadline": {"type": "string", "format": "date-time", "maxLength": 64},
|
||||
"outcome": {"type": "string", "maxLength": 64},
|
||||
"failure_code": {"type": "string", "maxLength": 128},
|
||||
"cleanup_state": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||
"idempotency_key": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||
"correlation_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"requested_at": {"type": "string", "format": "date-time", "maxLength": 64},
|
||||
"ended_at": {"type": "string", "format": "date-time", "maxLength": 64},
|
||||
"version": {"type": "integer", "minimum": 1}
|
||||
}
|
||||
},
|
||||
"AllocationPolicy": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["minimum_kbps", "target_kbps", "maximum_kbps", "tier", "audience", "protocol", "protocol_version", "grant_ttl_seconds", "reservation_lease_seconds"],
|
||||
"properties": {
|
||||
"minimum_kbps": {"type": "integer", "minimum": 1, "maximum": 100000000},
|
||||
"target_kbps": {"type": "integer", "minimum": 1, "maximum": 100000000},
|
||||
"maximum_kbps": {"type": "integer", "minimum": 1, "maximum": 100000000},
|
||||
"tier": {"type": "string", "enum": ["standard", "priority", "premium"]},
|
||||
"audience": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||
"protocol": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||
"protocol_version": {"type": "integer", "minimum": 1, "maximum": 100},
|
||||
"grant_ttl_seconds": {"type": "integer", "minimum": 5, "maximum": 300},
|
||||
"reservation_lease_seconds": {"type": "integer", "minimum": 5, "maximum": 3600}
|
||||
}
|
||||
},
|
||||
"ConnectionManifest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["version", "purpose", "session_id", "reconnect_sequence", "gateway", "tunnel", "profile", "grant", "correlation_id"],
|
||||
"properties": {
|
||||
"version": {"type": "string", "const": "1"},
|
||||
"purpose": {"type": "string", "enum": ["launch", "reconnect"]},
|
||||
"session_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"reconnect_sequence": {"type": "integer", "minimum": 0},
|
||||
"gateway": {"$ref": "#/$defs/ManifestGateway"},
|
||||
"tunnel": {"$ref": "#/$defs/ManifestTunnel"},
|
||||
"profile": {"$ref": "#/$defs/ManifestProfile"},
|
||||
"grant": {"$ref": "#/$defs/GrantReference"},
|
||||
"correlation_id": {"type": "string", "minLength": 1, "maxLength": 128}
|
||||
}
|
||||
},
|
||||
"ManifestGateway": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "addresses", "public_identity"],
|
||||
"properties": {
|
||||
"id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"addresses": {"type": "array", "minItems": 1, "maxItems": 4, "items": {"type": "string", "minLength": 1, "maxLength": 256}},
|
||||
"public_identity": {"type": "string", "minLength": 1, "maxLength": 256}
|
||||
}
|
||||
},
|
||||
"ManifestTunnel": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["versions", "features"],
|
||||
"properties": {
|
||||
"versions": {"type": "array", "minItems": 1, "maxItems": 4, "items": {"type": "string", "minLength": 1, "maxLength": 64}},
|
||||
"features": {"type": "array", "maxItems": 32, "items": {"type": "string", "minLength": 1, "maxLength": 64}}
|
||||
}
|
||||
},
|
||||
"ManifestProfile": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "bounds"],
|
||||
"properties": {
|
||||
"id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"bounds": {"$ref": "#/$defs/ManifestBounds"}
|
||||
}
|
||||
},
|
||||
"ManifestBounds": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["minimum_kbps", "target_kbps", "maximum_kbps"],
|
||||
"properties": {
|
||||
"minimum_kbps": {"type": "integer", "minimum": 1, "maximum": 100000000},
|
||||
"target_kbps": {"type": "integer", "minimum": 1, "maximum": 100000000},
|
||||
"maximum_kbps": {"type": "integer", "minimum": 1, "maximum": 100000000}
|
||||
}
|
||||
},
|
||||
"GrantReference": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["opaque_value", "expires_at", "audience"],
|
||||
"properties": {
|
||||
"opaque_value": {"type": "string", "minLength": 43, "maxLength": 256},
|
||||
"expires_at": {"type": "string", "format": "date-time", "maxLength": 64},
|
||||
"audience": {"type": "string", "minLength": 1, "maxLength": 128}
|
||||
}
|
||||
},
|
||||
"EventResume": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["cursor", "last_sequence"],
|
||||
"properties": {
|
||||
"cursor": {"type": "string", "maxLength": 512},
|
||||
"last_sequence": {"type": "integer", "minimum": 0}
|
||||
}
|
||||
},
|
||||
"EventEnvelope": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["event_id", "sequence", "type", "version", "resource", "occurred_at", "correlation_id", "payload"],
|
||||
"properties": {
|
||||
"event_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"sequence": {"type": "integer", "minimum": 1},
|
||||
"type": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"version": {"type": "integer", "minimum": 1},
|
||||
"resource": {"$ref": "#/$defs/ResourceLink"},
|
||||
"occurred_at": {"type": "string", "format": "date-time", "maxLength": 64},
|
||||
"correlation_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"payload": {"type": "object", "x-max-bytes": 16384}
|
||||
}
|
||||
},
|
||||
"ClipboardText": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["text", "encoding"],
|
||||
"properties": {
|
||||
"text": {"type": "string", "maxLength": 65536},
|
||||
"encoding": {"type": "string", "const": "utf-8"}
|
||||
}
|
||||
},
|
||||
"VersionNegotiation": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["supported_versions", "features"],
|
||||
"properties": {
|
||||
"supported_versions": {"type": "array", "minItems": 1, "maxItems": 3, "items": {"type": "string", "maxLength": 16}},
|
||||
"features": {"type": "array", "maxItems": 64, "items": {"type": "string", "maxLength": 64}}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user