feat: implement scoped join attempts and tickets (#10)
quality-gate / quality (push) Successful in 1m1s
quality-gate / quality (push) Successful in 1m1s
Closes #10
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
# ADR 0008: scoped join attempts and one-time connection tickets
|
||||
|
||||
- Status: Accepted
|
||||
- Date: 2026-07-16
|
||||
- Tracking: #10
|
||||
|
||||
## Decision
|
||||
|
||||
Join creation is an unauthenticated public operation because v1 does not treat a
|
||||
Rendezvous caller as game identity. The HTTP source address is normalized and
|
||||
converted to a process-keyed opaque subject for idempotency and bounded policy
|
||||
accounting; raw addresses and the derived subject are never returned or logged.
|
||||
A successful request means only that this network client may try to connect to
|
||||
this active session. It does not reserve capacity or grant gameplay admission.
|
||||
|
||||
Creation validates the v1 contract, caller idempotency key, enabled tenant policy,
|
||||
exact gameplay protocol, listing scope, live lease, and fresh authenticated host
|
||||
presence in one atomic store operation. A listing advertised as full remains
|
||||
joinable because its player count is advisory and the game host owns the final
|
||||
capacity, identity, ban, and admission decision.
|
||||
|
||||
Each attempt derives independent host-punch, client-punch, and connection-ticket
|
||||
credentials plus opaque attempt and mediation IDs from a process-ephemeral HMAC
|
||||
key, the client subject, the complete canonical request fingerprint, a fresh salt,
|
||||
and a purpose/role label. Credentials are 32-byte base64url values (43 characters),
|
||||
below both the 192-character Rendezvous capability ceiling and LiteNetLib's
|
||||
256-character NAT token ceiling. State retains keyed credential fingerprints,
|
||||
derivation inputs, and salt—not issued plaintext. All diagnostic string
|
||||
representations redact credentials and derivation material.
|
||||
|
||||
The client receives only its punch capability. A host polls its own listing with
|
||||
the lease token in `X-Rendezvous-Lease-Token` and receives only host-role
|
||||
capabilities through a signed, listing-bound, five-minute cursor. Replaying an
|
||||
identical join request returns the same live attempt; changing the request under
|
||||
the same owner/key conflicts. A client may cancel with its punch capability in
|
||||
`X-Rendezvous-Client-Punch-Capability`; cancellation atomically removes the
|
||||
attempt. Listing deletion, expiry, revocation, or process restart removes every
|
||||
associated attempt and credential fingerprint.
|
||||
|
||||
Endpoint binding remains role- and capability-specific. The first endpoint
|
||||
observed for a role wins atomically; an exact UDP duplicate is idempotent, while
|
||||
endpoint or role substitution is rejected. An introduction is consumable once
|
||||
only after both roles bind, so concurrent attempts for the same listing cannot
|
||||
cross-wire.
|
||||
|
||||
The connection ticket is distinct from both punch capabilities and is reproduced
|
||||
only after introduction succeeds. Its window begins at that moment and lasts at
|
||||
most 20 seconds without outliving the 30-second attempt. The server has an atomic
|
||||
fingerprint-consumption seam for mediator tests and revocation. On the game host,
|
||||
the SDK's bounded `ConnectionTicketValidator` stores a process-keyed digest,
|
||||
accepts an exact ticket once under a lock, rejects altered/cross-attempt/expired/
|
||||
revoked/replayed tickets, and zeroes retained digests and key material on disposal.
|
||||
Issue #11 carries the ticket in the authenticated introduction; issue #12 wires
|
||||
authorization and consumption into the caller-owned LiteNetLib coordinator.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A join attempt is transport authorization, never proof of player identity or a
|
||||
game slot.
|
||||
- Network-address-derived subjects are process-local abuse/idempotency scopes,
|
||||
not stable user identifiers; stronger authenticated player scopes require a
|
||||
future game-owned identity contract.
|
||||
- Cancellation after a ticket has reached a host must also revoke that host's
|
||||
local validator entry; coordinator wiring owns that race in issue #12.
|
||||
- Capability and ticket plaintext never enter browser results, state snapshots,
|
||||
logs, metrics, or generated string representations.
|
||||
Reference in New Issue
Block a user