Closes #4
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@ decision requires a superseding ADR and corresponding contract/test updates.
|
||||
- [ADR 0003: state, privacy, availability, and safety budgets](0003-state-privacy-availability-and-budgets.md)
|
||||
- [Threat model](../security/threat-model.md)
|
||||
- [Security promise and test matrix](../security/control-matrix.md)
|
||||
- [Versioned HTTP and UDP contracts](../contracts/README.md)
|
||||
|
||||
These decisions intentionally leave gameplay authority, player identity,
|
||||
simulation, persistence, social features, skill matchmaking, and gameplay
|
||||
|
||||
@@ -11,11 +11,13 @@ FinalFactory.Rendezvous.Server FinalFactory.Rendezvous.TestClient
|
||||
|
||||
- `Contracts` targets `netstandard2.1` and contains only versioned,
|
||||
transport-neutral IDs and wire contracts. It cannot reference Server,
|
||||
LiteNetLib, or Godot.
|
||||
LiteNetLib, or Godot. Its only package is `System.Text.Json`, used for the
|
||||
canonical cross-runtime JSON contract.
|
||||
- `Client` targets `netstandard2.1`, references Contracts and the pinned
|
||||
LiteNetLib package, and contains no Godot or Server dependency.
|
||||
- `Server` targets .NET 10 LTS, references Contracts and LiteNetLib, and owns
|
||||
HTTP hosting, UDP mediation, application policy, and ephemeral state.
|
||||
- `Server` targets .NET 10 LTS, references Contracts, LiteNetLib, and the
|
||||
first-party ASP.NET Core OpenAPI generator, and owns HTTP hosting, UDP
|
||||
mediation, application policy, and ephemeral state.
|
||||
- `TestClient` targets .NET 8, references only the public Client/Contracts seams
|
||||
and LiteNetLib, and must never reach into Server internals.
|
||||
- `Tests` target .NET 10 and may reference every project solely to verify public
|
||||
@@ -32,6 +34,8 @@ engine, transport, or server dependency therefore fails the normal test gate.
|
||||
.NET 8-or-later runtime used by current Godot 4 C# projects.
|
||||
- TestClient runtime: .NET 8.
|
||||
- LiteNetLib: 2.1.4, pinned centrally and restored from the lock files.
|
||||
- Microsoft.OpenApi: patched 2.7.5 line, centrally pinned because the version
|
||||
originally pulled by the .NET 10 generator is affected by CVE-2026-49451.
|
||||
|
||||
The repository uses central package versions, per-project lock files,
|
||||
deterministic compilation, nullable reference types, warnings as errors, current
|
||||
@@ -43,3 +47,5 @@ Primary compatibility references:
|
||||
- [.NET support policy](https://dotnet.microsoft.com/en-us/platform/support/policy)
|
||||
- [Godot stable C# prerequisites](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/c_sharp_basics.html)
|
||||
- [LiteNetLib 2.1.4 on NuGet](https://www.nuget.org/packages/LiteNetLib/2.1.4)
|
||||
- [ASP.NET Core OpenAPI generation](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/overview?view=aspnetcore-10.0)
|
||||
- [Microsoft.OpenApi security advisory](https://github.com/advisories/GHSA-v5pm-xwqc-g5wc)
|
||||
|
||||
@@ -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.
|
||||
@@ -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 | 1–10,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.
|
||||
@@ -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, 1–65535. |
|
||||
| next | 1 | Capability length, 1–192. |
|
||||
| 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.
|
||||
Reference in New Issue
Block a user