From 92ebf8abb57aba6d9f7fe5f5d5f421ff1c07f4a0 Mon Sep 17 00:00:00 2001 From: Kartios Date: Tue, 11 Aug 2026 20:22:12 -0700 Subject: [PATCH] feat: add authenticated realtime presence --- ARCHITECTURE.md | 10 + CONTRACT.md | 5 + index.html | 39 ++ server/README.md | 27 ++ server/src/app.ts | 4 +- server/src/realtime/index.ts | 13 + server/src/realtime/service.ts | 664 +++++++++++++++++++++++++++++ server/src/routes/realtime.ts | 186 ++++++++ server/src/services.ts | 3 + server/src/test/realtime.test.ts | 663 ++++++++++++++++++++++++++++ src/engine/scene.ts | 44 ++ src/interiors/officeScene.ts | 39 ++ src/main.ts | 281 ++++++++++++ src/realtime/client.ts | 567 ++++++++++++++++++++++++ src/realtime/index.ts | 23 + src/realtime/presenceIndicator.ts | 121 ++++++ src/realtime/scenePeers.ts | 359 ++++++++++++++++ src/test/presenceIndicator.test.ts | 114 +++++ src/test/realtimeClient.test.ts | 412 ++++++++++++++++++ src/test/scenePeers.test.ts | 173 ++++++++ 20 files changed, 3746 insertions(+), 1 deletion(-) create mode 100644 server/src/realtime/index.ts create mode 100644 server/src/realtime/service.ts create mode 100644 server/src/routes/realtime.ts create mode 100644 server/src/test/realtime.test.ts create mode 100644 src/realtime/client.ts create mode 100644 src/realtime/presenceIndicator.ts create mode 100644 src/realtime/scenePeers.ts create mode 100644 src/test/presenceIndicator.test.ts create mode 100644 src/test/realtimeClient.test.ts create mode 100644 src/test/scenePeers.test.ts 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 @@ +