feat: add presence-gated session leases (#7)
quality-gate / quality (push) Successful in 55s
quality-gate / quality (push) Successful in 55s
Closes #7
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
# ADR 0005: authenticated session lease and presence lifecycle
|
||||
|
||||
- Status: Accepted
|
||||
- Date: 2026-07-16
|
||||
- Tracking: #7
|
||||
|
||||
## Context
|
||||
|
||||
A host needs to publish a player-facing session without letting an HTTP request
|
||||
claim a public endpoint or remain visible after the gameplay socket disappears.
|
||||
Registration retries must be safe, credentials must remain opaque, and policy or
|
||||
ownership checks cannot race state mutation.
|
||||
|
||||
## Decision
|
||||
|
||||
The four host HTTP operations require `Authorization: Bearer <publisher credential>`.
|
||||
The signed principal supplies the authoritative game, environment, publisher trust
|
||||
mode, subject, and allowed regions. Request fields never widen that scope. Creation
|
||||
and update apply the enabled `GamePolicy` to exact protocol, region, visibility,
|
||||
bounded display/build/capacity values, and the allowlisted metadata schema.
|
||||
|
||||
Capacity reported by a host is advisory directory information. Rendezvous bounds
|
||||
and publishes it but never treats it as final admission authority; the game host
|
||||
still decides identity, bans, reserved slots, and whether a connection may join.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> AwaitingPresence: authorized register
|
||||
AwaitingPresence --> Listed: valid host UDP presence
|
||||
Listed --> AwaitingPresence: presence deadline passes
|
||||
AwaitingPresence --> AwaitingPresence: lease renew or data update
|
||||
Listed --> Listed: lease renew, data update, or presence refresh
|
||||
AwaitingPresence --> Removed: lease expiry or delete
|
||||
Listed --> Removed: lease expiry or delete
|
||||
Removed --> [*]
|
||||
```
|
||||
|
||||
Registration returns a listing ID, lease ID/token, host-presence handle/capability,
|
||||
lease expiry, a 30-second renewal suggestion, and a 10-second presence-refresh
|
||||
suggestion. The authoritative ceilings remain 60 seconds for the lease and 20
|
||||
seconds for presence. Timing suggestions are server-controlled, not client-selected.
|
||||
|
||||
The lease token and presence capability are 256-bit opaque values derived with
|
||||
HMAC-SHA256 from an in-memory per-process secret, a purpose label, the publisher
|
||||
subject, the idempotency key, a canonical request fingerprint, and a random
|
||||
per-registration derivation salt. Opaque IDs use separate purpose labels. Exact
|
||||
retries read the retained non-secret salt and therefore reproduce the original
|
||||
response without retaining plaintext credentials. Once the bounded idempotency
|
||||
record expires, a new salt rotates IDs and capabilities so an old token cannot
|
||||
regain authority. Metadata order is canonicalized before fingerprinting. The store
|
||||
retains the salt and only a second keyed fingerprint of each token. Restart rotates
|
||||
the derivation secret while the matching ephemeral state disappears.
|
||||
|
||||
Renew, update, and delete require both the same publisher subject and the lease
|
||||
capability. Cross-owner or wrong-capability access returns the same not-found shape.
|
||||
Update may change display name, build label, advisory capacity, and metadata only;
|
||||
game, environment, region, protocol, visibility, trust mode, and opaque IDs remain
|
||||
canonical. Delete is idempotent and does not reveal whether another publisher owns
|
||||
the supplied ID.
|
||||
|
||||
### UDP presence
|
||||
|
||||
Only a structurally valid `HostPresence` datagram with the issued capability can
|
||||
refresh presence. The public endpoint is the UDP packet's observed source on the
|
||||
host's gameplay socket; the HTTP API never accepts one. The bounded local candidate
|
||||
comes from the authenticated datagram. Invalid, unknown, or client-presence packets
|
||||
receive no response. Presence expiry demotes public visibility but keeps the lease,
|
||||
so the same handle can restore visibility without changing session identity.
|
||||
|
||||
Public listing responses contain bounded listing data only. They never contain
|
||||
public/local endpoints, lease tokens, presence capabilities, fingerprints, store
|
||||
keys, or canonical player identity.
|
||||
|
||||
## Failure semantics
|
||||
|
||||
- malformed or policy-invalid fields return a stable typed `InvalidRequest`;
|
||||
- an unsupported gameplay protocol returns `IncompatibleProtocol`;
|
||||
- missing/invalid publisher authentication returns `AuthenticationRequired`;
|
||||
- cross-scope authorization returns `Forbidden` without resource disclosure;
|
||||
- wrong owner/capability or expired state returns the tenant-hidden `NotFound`;
|
||||
- idempotency reuse with changed input returns `Conflict`;
|
||||
- publisher/global exhaustion returns `CapacityExceeded`; and
|
||||
- drain or loss of atomic state returns `ServiceUnavailable` and authorizes no join.
|
||||
|
||||
## Consequences
|
||||
|
||||
- HTTP registration alone can never make a public session browseable.
|
||||
- Plaintext session capabilities are returned to the intended host but are not
|
||||
retained, logged, included in public listing DTOs, or exported as metrics.
|
||||
- Re-registration after restart is the recovery path; there is no durable session
|
||||
identity or gameplay state in Rendezvous.
|
||||
Reference in New Issue
Block a user