1
0
This repository has been archived on 2026-08-25. You can view files and clone it. You cannot open issues or pull requests or push a commit.
karti 3e9b97ed8b Signing in puts people in the building
`member` and `anon` were told apart inside the office by the client and by
nothing else. `access.ts` picked an `officeDepth`, `createOfficeScene` built a
presence layer at full depth — and then nobody ever called `setPresence`, so
both tiers rendered the identical empty room. A tier that changes nothing you
can see is not a tier, and `routes/markers.ts` had already written down why one
drawn only in the browser is worse than none: it is a UI hiding a control over a
body the API hands to whoever asks.

So the refusal happens on the server now.

`GET /api/v1/offices/:id/presence` is the one route that always takes a session,
whatever else the deployment is configured for. `markers.ts` serves its feed to
anonymous callers when no feed is configured, on the grounds that there is
nothing there to protect; that reasoning does not transfer, and the difference is
the whole point — a marker is a company at an address and a presence is a person
at a desk.

The ordering inside the handler is the security property, not a detail. It
resolves the viewer *before* it looks at the id, so an anonymous caller gets an
identical 401 for a real office, a private one and one that was never created.
Check the office first and 404-for-unknown against 401-for-known tells them apart
perfectly, which is the enumeration oracle CONTRACT.md §6 forbids, wearing a
different status code. Three requests and one `deepEqual` hold that down.

`TERA_PRESENCE_DIR` is a second directory rather than a `people` field on the
pack, and that is the design. `types.ts` says a `Presence` binds to a `seatId`
and never to a coordinate so the geometry can be published while the people
cannot — which buys nothing if both live in one file, because an operator who
wants a public floorplan then has to strip the roster out by hand, and the first
time they forget the leak is permanent. Two directories makes the safe thing the
default thing. An office with no roster is 200 and empty, never 404: "no such
office" and "nobody has told me who is in this one" are different problems with
different fixes, and one 404 sends an operator after the wrong one.

On the client, occupancy arrives after the room is on screen rather than before —
the building is worth looking at while a second request is in flight. An API that
answers is believed, including when it answers with nobody; an office where
everyone has gone home is a real fact and overwriting it with invented people to
liven up the demo is the one thing this must never do. An API that does not
answer falls back to a fabricated roster, exactly as the markers do, because a
clone with no server is the flagship case and a member shown the same empty room
as a stranger has been told the tier means something when it does not.

Those twenty-five people are invented and the page says so. `sample.ts` says it
to a reader of the source; `#office-badge` now says "Sample occupancy — these
people are invented" to the person looking at the room, and it is not suppressed
when a real deployment's API merely happened to be down — that is exactly the
case where a member would otherwise read invented names as their colleagues.
Fabricated names at real desks look like a staff list, and a screenshot of one
must not be possible to take without the caption.

The floor plan marks the occupied desks, one colour for everybody where the
scene has four: at three device pixels a hue is a guess. The plan answers "is
anyone there" and the room answers "who, and what are they doing". Hovering a
desk names them, and the readout reads as an address getting more specific —
metres, then room, then person.

server: 127 tests pass, 11 of them new. Client typechecks and builds; the office
chunk absorbed the plan renderer and the entry chunk moved 2.3 kB for the sample
roster. Checked in the browser at office.lumbridgecorp.com: FULL VIEW, the badge,
figures at the benches, dots on the plan, and "3.7, 16.7 m · Alcatraz ·
Clementine Roux" under the pointer.
2026-08-06 03:46:01 -07:00

Tera

The map view of Lumbridge Simulate — cities from above, in three.js. Its other half, Spaces, is the offices you walk into: one engine and one asset library, seen from outside and from inside.

Apache 2.0. Runs at tera.lumbridgecorp.com.

San Francisco


What it is

An engine plus data packs. The engine renders terrain, coastline, a built city on real street grids, bridges, roads, markers and air traffic. A city pack is pure data — coastlines, hills, districts, landmarks, camera chapters — so adding a city is a data contribution anyone can review, not a fork.

San Francisco ships today. Los Angeles / Orange County / Riverside is next; New York after that.

A plan view sits top right: the board drawn flat, with the footprint of the camera's own frustum on it, so you can see where you are looking from outside the shot. Click or drag it to move the camera; scroll it to dolly. It is a 2D canvas rather than a second WebGL context, drawn from the same city pack, and it follows the sun into the night along with everything else.

Who sees what

Three tiers, resolved once at boot by src/access.ts:

anonymous signed in admin
the map, the plan view, the named chapters
observed weather and live aircraft
the office public depth — shell, furniture, viewpoints, nobody home full depth, with presence full depth
the marker feed per TERA_MARKERS_ACCESS
the godmode panel (G) — date, season, weather override, counters, pose editor

The sky is public on purpose. Cloud cover over San Francisco is a government sensor reading, and the aircraft are broadcasting their positions unencrypted to anyone with a receiver; neither is something an account can grant you access to. Gating them cost the only moment that makes this project land — real fog rolling off the Pacific onto a city you recognise, at the real time of day, on a first visit.

The markers are the one feed that can carry something private, so the server decides. TERA_MARKERS_ACCESS is members by default and an operator has to say public out loud, which /api/v1/health then announces in degraded[]. The default is the safe answer rather than the common one, because the failure mode is silent: nothing errors, nothing looks broken, the data is just readable by the internet.

These are drawing decisions, not a security boundary, and src/access.ts says so at length. Live data and office presence are withheld by the API, from a caller it does not recognise; the client tier stops the app asking for something it will not get. Admin is granted only by TERA_ADMIN_SUBJECTS on the server — never inferred in the browser, and never from an API that failed to answer. A deployment with no API at all is open, because "clone it and it works" is the promise; it is not "clone it and you are an administrator".

Quick start

npm install
npm run dev

Using the engine

import { createScene } from "@lumbridge/tera/engine/scene.ts";
import { createStage } from "@lumbridge/tera/engine/stage.ts";
import SAN_FRANCISCO from "@lumbridge/tera/cities/sf.ts";

// One stage per canvas, for the life of the page. Cities are put on it and
// taken off again; a renderer per city leaks its shadow map on every switch.
const stage = createStage(canvas);

const scene = await createScene(stage, {
  city: SAN_FRANCISCO,
  markerPalette: { hiring: 0x4ade80, closed: 0xef4444 },
});

scene?.setMarkers([
  { id: "1", lat: 37.7765, lng: -122.4241, label: "Somewhere", colorKey: "hiring" },
]);

createScene is async because the heightfield is built in a Worker — half a million samples, about 730 ms on the Bay Area, and not on the main thread. It resolves to null if the build was abandoned through options.signal, which is what makes switching city mid-build cheap.

The engine renders Marker[] and looks colours up by colorKey in a palette you supply. It does not know what your markers mean — that mapping lives in your adapter. This is what lets one renderer serve a private map coloured by one scheme and a public map coloured by another, without either being a fork.

Adding a city

Write src/cities/<id>.ts exporting a City. Trace the coastline and parks by hand, place hills as radial peaks, and give each district its street bearing.

Two rules, and they are not stylistic:

  • Do not import geometry from OpenStreetMap. OSM and Nominatim output is ODbL — share-alike, and incompatible with this repo's licence.
  • Do not commit logos or brand assets. They are trademarks, not code.

See ARCHITECTURE.md §3 for the full reasoning, and NOTICE for the attribution and data-provenance statement.

Aircraft

The engine takes a FlightSource. Two ship here: SimulatedFlights (original, flies real approach and departure corridors) and AdsbFlights (open community ADS-B feeds such as adsb.lol).

FlightRadar24 is deliberately absent — their terms forbid scraping and forbid redistributing their data, so a client for it cannot live in an Apache-2.0 repository. Commercial sources belong in private deployments. The best long-term answer is an RTL-SDR receiver: first-party data with nothing to comply with.

Layout

src/engine/    renderer — terrain, blocks, structures, markers, flights, scene, minimap
src/cities/    data packs — pure geography, no code
src/adapters/  where outside data plugs in
src/tools/     instruments — god-only, dynamically imported, never statically

engine never imports cities; neither imports adapters.

Nothing under src/tools/ may be reached by a static import from the app. It is loaded by one await import() behind access.can.debug, so a visitor who is not an admin does not download the code at all — which is the strongest available reading of "nothing here runs for a non-god visitor": not a hidden panel, not a disabled panel, no panel. src/tools/index.ts states the rule and what silently undoes it.

Licence

Apache License 2.0 — see LICENSE and NOTICE.

S
Description
Immutable Apache-2.0 Tera baseline through 2026-08-24; current development is proprietary.
Readme Apache-2.0 9 MiB
Languages
TypeScript 93.3%
JavaScript 5.3%
HTML 1.3%