feat: freeze v1 transport contracts (#4)
quality-gate / quality (push) Successful in 51s

Closes #4
This commit is contained in:
KyuubiYoru
2026-07-16 04:52:38 +02:00
parent e626b89909
commit 69c8b2d2bc
43 changed files with 3737 additions and 12 deletions
+18
View File
@@ -0,0 +1,18 @@
# Versioned contracts
Tracking: #4
The v1 contract is defined by three artifacts that are reviewed and versioned
together:
- [HTTP v1 semantics](http-v1.md)
- [UDP v1 wire format](udp-v1.md)
- [Generated OpenAPI 3.1 document](../api/rendezvous-v1.json)
The public .NET types live in `FinalFactory.Rendezvous.Contracts`, target
`netstandard2.1`, and contain no Server, Godot, or LiteNetLib dependency. Golden
vectors and a public-API snapshot make accidental wire or source compatibility
changes fail the normal test gate.
Any incompatible change requires a new contract version. Additive JSON fields
may be introduced within v1 because v1 readers ignore unknown object members.
+105
View File
@@ -0,0 +1,105 @@
# HTTP contract v1
Tracking: #4
All production endpoints require HTTPS. JSON uses UTF-8, camel-case property
names, compact output, string-valued camel-case enums, and ISO 8601 timestamps.
Every request that contains a body carries `contractVersion: 1`; browse carries
the same value as a required query parameter.
## Compatibility and parsing
- Contract version matching is exact. Any value other than `1` fails with
`unsupportedContractVersion`; it is never guessed or downgraded.
- Gameplay protocol matching is exact. `buildVersion` is display and diagnostic
text only and never decides compatibility.
- Unknown JSON object properties are ignored so additive v1 responses remain
readable. Unknown enum names, numeric enum values, comments, trailing commas,
invalid identifier strings, and excessive nesting are rejected.
- Game, environment, and region IDs are lowercase URL-safe slugs. Listing,
lease, join-attempt, and mediation IDs are non-empty UUIDs serialized as JSON
strings.
- Clients must honor request cancellation. A disconnected or cancelled request
does not promise a response body; the server should stop work where safe.
## Endpoints
| Method | Path | Purpose |
| --- | --- | --- |
| `POST` | `/v1/sessions` | Register a session and create its renewable lease. |
| `POST` | `/v1/sessions/{listingId}/renew` | Renew the listing lease. |
| `PUT` | `/v1/sessions/{listingId}` | Replace mutable browser fields and capacity. |
| `DELETE` | `/v1/sessions/{listingId}` | Withdraw a listing. |
| `GET` | `/v1/sessions` | Browse compatible public sessions. |
| `GET` | `/v1/sessions/{listingId}` | Resolve a public or explicitly shared unlisted listing. |
| `POST` | `/v1/join-attempts` | Authorize and create a short-lived join attempt. |
| `GET` | `/v1/sessions/{listingId}/join-attempts` | Let an authenticated host poll pending attempts. |
| `POST` | `/v1/join-attempts/{attemptId}/outcome` | Report a bounded connection outcome. |
| `GET` | `/health/live` | Report that the HTTP process is alive. |
| `GET` | `/health/ready` | Report whether the UDP mediator is bound and ready. |
The generated [OpenAPI document](../api/rendezvous-v1.json) is the normative
shape reference for parameters, bodies, and responses. Contract-only endpoints
return `501` until their behavior is implemented by the subsequent directory,
lease, and join-orchestration issues.
Host polling sends its reusable lease credential in
`X-Rendezvous-Lease-Token`; it must never be placed in a URL. Lease credentials
for mutation operations are carried in their request bodies. Public browser
responses contain no IP endpoints, lease tokens, punch capabilities, connection
tickets, player identifiers, or gameplay state.
## Idempotency, cursors, and retries
Registration and join creation require a caller-generated visible-ASCII
`idempotencyKey`. A repeat in the same authorization scope returns the original
result while the key is retained; reusing a key with a different payload fails
with `conflict`. Keys are opaque and must not contain credentials.
Cursors are opaque, endpoint-specific, short-lived values. A client may echo a
cursor only to the endpoint and filters that produced it. Invalid or expired
cursors fail with `invalidRequest`; clients restart browsing from the first page.
Renew, update, delete, and outcome reporting are safe to retry with the same
lease/attempt identity after a transport-level failure.
## Limits
Limits are measured after UTF-8 encoding where stated. Servers reject the
entire request rather than truncate values.
| Item | v1 limit |
| --- | ---: |
| HTTP request body | 16 KiB |
| Browser response body | 256 KiB |
| Browser page | 100 listings |
| Metadata document | 4 KiB, 32 keys |
| Metadata key / value | 64 / 256 UTF-8 bytes |
| Game / environment / region ID | 64 / 32 / 32 characters |
| Display name / build version | 128 / 64 UTF-8 bytes |
| Idempotency key | 64 visible ASCII characters |
| Cursor | 512 visible ASCII characters |
| Diagnostic code | 64 visible ASCII characters |
| Error message | 256 UTF-8 bytes |
| Reusable HTTP credential | 1,024 characters |
| Session capacity | 110,000 players |
## Error mapping
Errors use `ApiError` with a stable `code`, bounded safe `message`, optional
`correlationId`, and optional `retryAfterSeconds`. Messages are diagnostic and
must not be parsed. Secrets and raw credentials are never echoed.
| HTTP | Codes |
| ---: | --- |
| 400 | `invalidRequest`, `unsupportedContractVersion` |
| 401 | `authenticationRequired` |
| 403 | `forbidden` |
| 404 | `notFound` |
| 409 | `conflict`, `incompatibleProtocol`, `replayRejected`, `capacityExceeded` |
| 410 | `expired`, `staleHost` |
| 429 | `rateLimited` (with retry guidance when known) |
| 503 | `serviceUnavailable` (with retry guidance when known) |
| 500 | `internalError` |
Malformed input must receive the same bounded error family regardless of which
parser or validation stage rejected it.
+53
View File
@@ -0,0 +1,53 @@
# UDP presence contract v1
Tracking: #4
The UDP mediator accepts a single bounded presence envelope from a host or
client. It associates the authenticated mediation handle with the packet's
observed public source endpoint and the sender's reported local endpoint. It
does not carry gameplay packets.
All multi-byte integers use network byte order. UUID bytes use the canonical
RFC 4122 textual order (the byte pairs from the 32 hexadecimal digits), not the
mixed-endian layout returned by `Guid.ToByteArray()`.
## Datagram layout
| Offset | Size | Field |
| ---: | ---: | --- |
| 0 | 2 | Magic bytes `52 56` (`RV`). |
| 2 | 1 | Contract version, exactly `01`. |
| 3 | 1 | Message type: host presence `01`, client presence `02`. |
| 4 | 1 | Flags, exactly `00` in v1. |
| 5 | 16 | Non-empty mediation-handle UUID. |
| 21 | 1 | Address family: IPv4 `04`, IPv6 `06`. |
| 22 | 1 | Address length: `04` for IPv4, `10` for IPv6. |
| 23 | 4 or 16 | Raw local IP address bytes. |
| next | 2 | Local UDP port, 165535. |
| next | 1 | Capability length, 1192. |
| next | variable | ASCII base64url capability, without padding. |
No trailing bytes are permitted. The whole datagram is limited to 1,200 bytes,
well below common Internet path MTUs. The v1 capability limit is 192 characters,
which also keeps any value passed through LiteNetLib's 256-character NAT token
surface safely below that library boundary.
## Validation and failure behavior
Decoders return one stable failure category: oversized, truncated, invalid
magic, unsupported version, unknown message type, non-zero flags, invalid
handle, invalid address family, invalid address, invalid port, invalid
capability, or trailing data. Unknown versions and message types are rejected;
they are never interpreted as v1.
The address-family byte, encoded address length, and parsed address must agree.
The service derives the public endpoint from the UDP packet source and never
trusts a client-supplied public address. Reported local endpoints are candidates
only and grant no authority.
Capabilities are short-lived, single-purpose, scoped to one mediation handle,
and compared without exposing them in logs. A valid-looking packet does not
prove authorization until the capability is checked. Invalid packets receive
no UDP response, preventing the mediator from becoming an amplification oracle.
Replay, expiry, pairing, and rate-limit policy are defined by later mediator
issues; the v1 envelope deliberately leaves no unbounded or reflected payload.