diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 73ed4fb..b2805ae 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -118,6 +118,16 @@ adapters whose absence cannot stop the city, office, traffic, or sky from running. This is the replacement path for the retired Phaser World 1; it is not a migration of that renderer. +The first hosted adapter is now concrete: the Fastify API owns short-lived, +in-memory authoritative presence sessions and streams validated deltas over +authenticated POST/SSE. `src/realtime` remains renderer- and transport-neutral; +the browser transport, interpolation buffer, procedural peer renderer and small +status UI are separate adapters. A city or full-depth office opts into remote +peers explicitly, while an anonymous/public scene does not even allocate their +assets. Interest changes clear the old coordinate frame before a new one is +shown. Game-state deltas never carry webcam or screen media; those remain +separate consent and authorization lifecycles. + --- ## 3. Three rules that keep Apache 2.0 honest diff --git a/CONTRACT.md b/CONTRACT.md index 7641935..d3eca8a 100644 --- a/CONTRACT.md +++ b/CONTRACT.md @@ -193,6 +193,11 @@ Three backends were designed for one box — three ports, three frameworks, thre public box. - `Cache-Control` is fail-closed: a global hook stamps `private, no-store`, and a route opts in to public caching explicitly. +- Authenticated realtime is a route module in this same process, not another + daemon. Its state is bounded and memory-only. Event credentials travel in a + POST body, never a URL; the server owns interest filtering, motion validation, + token rotation and revocation. The auth subject is not a peer-visible entity + id, and no media is carried on the game-state stream. ### 5.1 Zero-config boot must actually boot diff --git a/index.html b/index.html index 017b72a..78d30b4 100644 --- a/index.html +++ b/index.html @@ -446,6 +446,42 @@ } .tier.god .who { color: var(--amber-ink); } .tier a { color: var(--amber-ink); text-transform: none; letter-spacing: 0.04em; } + #presence-host { + position: fixed; + top: calc(var(--s4) + env(safe-area-inset-top) + 2.5rem); + right: var(--s4); + z-index: 3; + max-width: min(15rem, 65vw); + } + #presence-host:empty { display: none; } + .tera-presence { + display: flex; + align-items: center; + gap: var(--s2); + padding: 5px var(--s2); + color: var(--ink-3); + background: rgba(8, 12, 17, 0.76); + border: 1px solid var(--hairline); + border-radius: var(--r-sm); + box-shadow: var(--shadow); + backdrop-filter: var(--blur); + -webkit-backdrop-filter: var(--blur); + font-size: 9px; + letter-spacing: 0.04em; + } + .tera-presence[data-presence="live"] { color: var(--ink-2); } + .tera-presence__retry { + padding: 2px 6px; + color: var(--amber-ink); + background: transparent; + border: 1px solid rgba(242, 177, 52, 0.3); + border-radius: 3px; + font: inherit; + cursor: pointer; + } + body.presence-on .corner { + top: calc(var(--s4) + env(safe-area-inset-top) + 4.55rem); + } .profile-trigger { min-height: 28px; padding: 4px 8px; @@ -781,6 +817,8 @@ right: var(--s3); } .tier { top: calc(var(--s3) + env(safe-area-inset-top)); right: var(--s3); } + #presence-host { top: calc(var(--s3) + env(safe-area-inset-top) + 2.5rem); right: var(--s3); } + body.presence-on .corner { top: calc(var(--s3) + env(safe-area-inset-top) + 4.55rem); } .minimap-frame { height: 10rem; } .rail { right: var(--s3); bottom: calc(var(--s3) + env(safe-area-inset-bottom)); } .source { left: var(--s3); bottom: calc(var(--s3) + env(safe-area-inset-bottom)); } @@ -949,6 +987,7 @@ +