Nine test files on the server, none on the client, and no test script in the root package at all — so CI's only gate on the half of this project that runs in a stranger's browser was `tsc --noEmit`, which will tell you the types line up and nothing about whether an anonymous visitor is handed the private office. Both files here were built to be tested and never were. `access.ts` is the only module in the bundle whose output is a set of decisions about what a stranger may see, and its own header carries the reason a test is owed: a shipped line read `canEnterOffice = s.authenticated || !s.passwordLogin`, which is true for `auth: none` and dangerously false for `sso` — where `POST /session` answers 404 precisely *because* credentials are issued elsewhere — so on an SSO deployment every anonymous visitor got the private view while the config still said the box was private. There is now a test named after that bug. The assertions are weighted toward the closed direction on purpose: one that a member gets what a member is owed, and half a dozen that nobody gets more than nothing, because showing a member the public office is a bad afternoon and showing a stranger the private one is what the tiers exist to prevent. The 5xx case is in there too — a box mid-restart is `anon`, not `member` — and so is the `javascript:` entry URL, which a CSP of `script-src 'self' 'unsafe-inline'` does not stop from navigating. `plan.ts` says in its own header that it imports no three.js "so the splitting pass is testable without a WebGL context", and that it "drops rather than throws … every one of those is reported through `problems`" — an array whose whole purpose is to be asserted on, which nothing asserted on. So: a pack wrong in five ways still builds and files five reports; a zero-length wall does not put a NaN in the bounds; an office with no levels stays finite so nothing downstream divides by it; a pack with the required arrays missing is taken, because HTTP will send one. And the wall pass gets the check CONTRACT.md §2's argument deserves — one decomposition, two products — by walking a walker through the door and into the wall beside it, and through a window and being stopped. Also the yaw convention, which nothing stated and `officeMinimap.ts` draws straight from: get the sign wrong and every wall mirrors about its own centre, invisible on a square and obvious on anything else. No new dependency. The server already runs `node --test` over `.ts` on native type stripping, so the client does the same — which matters here, because this repo's "no surprise dependencies" check is an allowlist naming why each one is permitted, and a test runner would have needed an entry and an argument. One source change was needed to make any of it possible. `session.ts` read `import.meta.env.VITE_IDENTITY_URL` at module scope, and `access.ts` imports `authFetch` from it — so one property access made the file that decides what an anonymous visitor sees unreachable from a plain test runner, which is most of why it had no tests. It now reads the way `plan.ts` already reads `DEV`, by the idiom that file documents as being there "so this module stays importable from a plain test runner". 31 tests, 9 suites, all passing, wired into `npm test` and into the CI job beside the server's. `vite build` is unchanged and no test code reaches the bundle.
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.
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.
