1
0

feat: real fire on the boards, the LA office as a twin, and a night sky worth reading

The world stops being a simulation of California and starts being California.

**THE PROMOTION GATE WAS THE FIRST COMMIT, BEFORE ANY ORANGE PIXEL EXISTED.**
On today's live store the SoCal board contains 22 incidents. Every one has NULL
acreage and fifteen are nameless LA County dispatch numbers. Drawn naively that
is 22 orange marks over Los Angeles on a day nothing is burning — in a frame that
contains no other warm colour, so one glyph would be the most salient object on
the board and twenty-two would spend its credibility permanently.

`acres >= 10 AND contained < 80 AND type != 'RX' AND last_seen = max(last_seen)`
returns 0 on SoCal, exactly 5 on California, 0 on the Bay — same body, same day,
three correct answers. The empty board is a deliverable, not a fallback: it says
"No active fire on this board — CAL FIRE and WFIGS, just now", states that 21
records were gated and why, lists the largest fires burning OUTSIDE the frame
with distances, and counts the hot pixels it is deliberately not drawing.

**The privacy leak is structurally impossible rather than carefully avoided.**
cloud-1 serves a projection; the four home-relative columns never leave that box.
`observations.threat` was the one that nearly got through — it is
`(16/distance)^2 x log10(acres) x momentum x containment x wind-alignment`, so
with acreage and containment public it inverts to a distance circle around a
house and three fires give an intersection. A grep of the built bundle for
distance_km, bearing_deg, threat, 7762 and the street name returns nothing.

**Deliberately not used, and both would have produced a confident wrong answer:**
the store's `air` table retains only the last parameter of each poll, so all 78
rows read "Good" while the live feed reports ozone 101 "Unhealthy for Sensitive
Groups" — haze driven off it would clear the sky during a smoke event. And
`weather` is written only inside the NWS alerts loop, so a quiet day stores no
wind at all. Tera's own per-region NWS wind is already correct and already what
the clouds drift on.

Satellite detections are drawn as evidence and never as incidents. The permanent
industrial heat source 4.7 km from the owner's house is flagged persistent and
dropped, asserted by a test that first proves it is present in the fixture.
MODIS integer confidence and VIIRS string confidence are branched on `sat`.

**The LA office is a twin.** Its entire authored second storey — Model Loft,
Model Bay, The Materials Room, 430 lines nobody had ever stood in — is reachable
on foot: a walker crosses level-1 to level-2 in 73 fixed steps, floorY 0 to 5,
verified against the real pack rather than a synthetic plan. Its two studio
devices read real hardware through a field-allowlisted bridge: mute, volume and
reachability only. Never level, because there is no passive level upstream and
obtaining one would record a room with people in it. Never dB, because upstream
is gainPct across four different native scales. The bridge refuses all writes.

Fixed at its root: an anonymous visitor was getting permanently at-rest
instruments backing off against a 401. The tier moves into `createDeviceSource`,
so anon gets the living simulator three file headers already promised.

**Item 8 is closed, not fixed, and the correction is the point.** The Bay Area
"stutter" was GPU power management — the card sat at 500 MHz of 2725 through
every run that reproduced it, 4096/2048/1024/256 shadow maps all render in
1.21-1.31 ms, and two consecutive runs over a byte-identical dist gave 33.4 then
16.7. The allowance is removed and the cell is back to 16.7. Geometry is the
gate; frame time is advisory.

Item 7 was re-scoped after measuring: 1,069,006 of the Bay Area's 2,265,056
triangles were the second submission of the same buildings into the shadow pass.
Mobile now has its own triangle caps and bay-area mobile draws 1,266,096.

Also: bridges and the freeway corridor light up at night as emission, not lights
— 1,614 deck lamps and 18 tower heads on the Bay in two draw calls. The single
change that made US-101 legible was moving its edge lines from the lit material
to the unlit one: retroreflective paint, the argument the SFO night frame already
makes. California went 21,991 lamps to 4,051, clustered at the 17 town districts,
because a rural interurban corridor genuinely is unlit.

Tests 1137 -> 1340, server 280. All ten budget cells pass on first attempt with
no cap raised.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-22 18:01:11 -07:00
parent b7f5c41da5
commit b25f217e3e
91 changed files with 15111 additions and 401 deletions
+2
View File
@@ -16,6 +16,7 @@ import Fastify, { type FastifyError, type FastifyInstance } from "fastify";
import { registerCachePolicy } from "./cache.ts";
import { loadConfig, type Config } from "./config.ts";
import { registerDevices } from "./routes/devices.ts";
import { registerFires } from "./routes/fires.ts";
import { registerFlights } from "./routes/flights.ts";
import { registerHealth } from "./routes/health.ts";
import { registerMarkers } from "./routes/markers.ts";
@@ -50,6 +51,7 @@ export function buildApp(config: Config = loadConfig()): FastifyInstance {
registerHealth(app, services);
registerFlights(app, services);
registerSatellites(app, services);
registerFires(app, services);
registerWeather(app, services);
registerMarkers(app, services);
registerMedia(app, services);
+151 -3
View File
@@ -25,6 +25,7 @@ import { adsbAttribution, checkAdsbEndpoint, FIRST_PARTY_RECEIVER } from "./flig
import type {
AuthMode,
DevicesSourceId,
FiresSourceId,
FlightsSourceId,
MarkersSourceId,
SatellitesSourceId,
@@ -97,6 +98,58 @@ export interface SatellitesConfig {
ttlSeconds: number;
}
/**
* Where wildfire data comes from, and how long it may be held.
*
* `url` is the base of a **projection endpoint on another machine** — not a
* database path, and deliberately not one. The upstream store is keyed on a
* private home and carries four columns computed from the distance to it; the
* machine that owns those columns is the machine that filters them out, so what
* this box can even ask for is already safe. `fires/cloud1.ts` has the argument
* in full.
*/
export interface FiresConfig {
source: FiresSourceId;
/** Base URL, no trailing slash. Empty on every source but `cloud1`. */
url: string;
/**
* Sent as `x-tera-key` when set. Optional, because the transport is a tailnet
* and the body is public-domain agency data — the secret was the distance, and
* it is already gone. Set it anyway on a box that can be reached from more
* than one tailnet.
*/
key: string;
ttlSeconds: number;
/** How many hours of satellite overpasses to ask for. Clamped at the source. */
detectionWindowHours: number;
}
/**
* A first-party hardware bridge — an operator's own studio, over their own
* network.
*
* Read-only by construction: there is no command path through it and
* `devices/firstParty.ts` refuses one out loud rather than by omission. A POST
* from a public web page that unmutes a microphone in an occupied room is a
* different product decision and it has not been made.
*/
export interface StudioConfig {
/** Base URL of the bridge, no trailing slash. Empty when unconfigured. */
url: string;
/** Sent as `x-studio-key`. */
key: string;
/**
* How long a snapshot is held here, in seconds.
*
* Deliberately much longer than `TERA_DEVICES_TTL`, and that gap is the whole
* reason this is its own number. The upstream's `/state` cold-probes three
* machines over SSH — half a second, measured — and it holds its own ten-second
* cache. Polling it at the five-second device TTL, per open tab, would fan SSH
* out to three machines the operator actually uses, forever.
*/
ttlSeconds: number;
}
export interface DevicesConfig {
source: DevicesSourceId;
/**
@@ -232,8 +285,11 @@ export interface Config {
weather: WeatherConfig;
flights: FlightsConfig;
satellites: SatellitesConfig;
fires: FiresConfig;
markers: MarkersConfig;
devices: DevicesConfig;
/** The first-party device bridge. Read by `devices/firstParty.ts` only. */
studio: StudioConfig;
offices: { dir: string };
/**
* Where the rosters are. Separate from `offices.dir` because the two hold
@@ -255,7 +311,9 @@ export function loadConfig(env: Env = process.env): Config {
const weather = loadWeather(env, degraded);
const flights = loadFlights(env, degraded);
const satellites = loadSatellites(env, degraded);
const devices = loadDevices(env, degraded);
const fires = loadFires(env, degraded);
const studio = loadStudio(env);
const devices = loadDevices(env, studio, degraded);
const auth = loadAuth(env, degraded);
const ice = loadIce(env, degraded);
// After auth, because a marker feed with nobody able to sign in is worth a
@@ -285,8 +343,10 @@ export function loadConfig(env: Env = process.env): Config {
weather,
flights,
satellites,
fires,
markers,
devices,
studio,
offices: { dir: str(env, "TERA_OFFICES_DIR", "") },
presence: { dir: str(env, "TERA_PRESENCE_DIR", "") },
auth,
@@ -493,7 +553,7 @@ function radius(asked: number, degraded: string[]): number {
return clamped;
}
const DEVICE_SOURCES: DevicesSourceId[] = ["none", "sim", "homeassistant"];
const DEVICE_SOURCES: DevicesSourceId[] = ["none", "sim", "homeassistant", "first-party"];
/**
* `none` by default, and the default is the honest one rather than the
@@ -515,7 +575,7 @@ const DEVICE_SOURCES: DevicesSourceId[] = ["none", "sim", "homeassistant"];
* confusion `DeviceProvenance` exists to prevent, and it would do it in the one
* direction that matters.
*/
function loadDevices(env: Env, degraded: string[]): DevicesConfig {
function loadDevices(env: Env, studio: StudioConfig, degraded: string[]): DevicesConfig {
const asked = str(env, "TERA_DEVICES_SOURCE", "none");
let source = oneOf(asked, DEVICE_SOURCES);
if (source === null) {
@@ -536,6 +596,19 @@ function loadDevices(env: Env, degraded: string[]): DevicesConfig {
source = "none";
}
// A bridge with nowhere to point is not a bridge. Demoted to `none` and never
// to `sim`, for the same reason `homeassistant` is: `first-party` is a promise
// that a real room is being read, and quietly answering it with a state
// machine is the one substitution `DeviceProvenance` exists to prevent.
if (source === "first-party" && studio.url === "") {
degraded.push(
"TERA_DEVICES_SOURCE=first-party needs TERA_STUDIO_URL. Demoted to none — a bridge " +
"with no upstream must not be answered with simulated readings under a source " +
"that promises real hardware.",
);
source = "none";
}
return {
source,
ttlSeconds: num(env, "TERA_DEVICES_TTL", 5, degraded),
@@ -543,6 +616,81 @@ function loadDevices(env: Env, degraded: string[]): DevicesConfig {
};
}
/**
* The first-party bridge's own settings, read before `loadDevices` so that the
* demotion above can see whether there is anything to point at.
*
* It takes no `degraded` list on purpose: an unset `TERA_STUDIO_URL` on a box
* that never asked for `first-party` is not a demotion, it is the default. The
* one sentence that is owed is written by `loadDevices`, where the mismatch
* actually exists.
*/
function loadStudio(env: Env): StudioConfig {
return {
url: str(env, "TERA_STUDIO_URL", "").replace(/\/+$/, ""),
key: str(env, "TERA_STUDIO_KEY", ""),
ttlSeconds: num(env, "TERA_STUDIO_TTL", 30, []),
};
}
const FIRE_SOURCES: FiresSourceId[] = ["none", "cloud1"];
/**
* Off by default, like satellites and for a stronger version of the same reason.
*
* A zero-config clone must make no outbound requests at all
* (`scripts/check-zero-config-boot.mjs`), and this one would be an outbound
* request to a *private* machine on somebody else's tailnet. There is no public
* default to fall back to and there should not be: a stranger's board shows no
* fires and says so, which is the truth about what that box knows.
*
* `none` serves a real, empty body — never an invented fire. The asymmetry with
* the flight plan is deliberate and `SatellitesBody` states it: an invented
* aeroplane is a plausible aeroplane, and an invented wildfire is a claim that a
* named place is burning.
*/
function loadFires(env: Env, degraded: string[]): FiresConfig {
const asked = str(env, "TERA_FIRES_SOURCE", "none");
let source = oneOf(asked, FIRE_SOURCES);
if (source === null) {
degraded.push(
`TERA_FIRES_SOURCE="${asked}" is not one of ${FIRE_SOURCES.join(", ")}; ` +
"serving no fires.",
);
source = "none";
}
// Note what is deliberately NOT here: an unset `TERA_FIRES_SOURCE` appends
// nothing to `degraded`. Off by default is a *choice this repo made*, not a
// default that needs configuring to work, and `degraded` is documented as one
// sentence per demotion. `scripts/check-zero-config-boot.mjs` enforces the
// distinction by refusing to pass with any demotion at all on an empty
// environment, which is the right gate: a stranger's clone is not misconfigured.
//
// What tells a viewer that a quiet board is quiet because nobody asked, rather
// than because nothing is burning, is `sources.fires` on the health body and
// `FiresBody.fetchedAt` — which is the Unix epoch on a box that has never
// fetched, and is stated on the board itself. That is a stronger signal than a
// log line, because it is in front of the person looking at the picture.
const url = str(env, "TERA_FIRES_URL", "").replace(/\/+$/, "");
if (source === "cloud1" && url === "") {
degraded.push(
"TERA_FIRES_SOURCE=cloud1 needs TERA_FIRES_URL, the base of the projection endpoint. " +
"Serving no fires: an empty board is the honest answer, and the board says how old " +
"its last answer is.",
);
source = "none";
}
return {
source,
url,
key: str(env, "TERA_FIRES_KEY", ""),
ttlSeconds: num(env, "TERA_FIRES_TTL", 600, degraded),
detectionWindowHours: num(env, "TERA_FIRES_DETECTION_HOURS", 24, degraded),
};
}
const SATELLITE_SOURCES: SatellitesSourceId[] = ["none", "celestrak"];
/**
+335
View File
@@ -0,0 +1,335 @@
/**
* A read-only bridge to an operator's own hardware, over their own network.
*
* The first source in this build that reports something nobody in this process
* invented. It is also the first that reads a machine in a room with people in
* it, so the two most important things in this file are both refusals.
*
* ### It refuses to write
*
* There is no command path through here, and that is a product decision rather
* than an unfinished one. Reading the state of a room is the demonstration; a
* POST from a public web page that unmutes a microphone in an occupied room is a
* different product and nobody has decided to build it. `commandRefusal()` says
* so out loud and `devices/index.ts` reports it, because a refusal that is only
* an absent function is a refusal somebody adds by accident.
*
* ### It refuses to invent a level, and it refuses to invent decibels
*
* Two separate refusals that both look like missing features.
*
* **There is no passive level upstream.** `GET /state` reports, per microphone,
* `{ muted, gainPct, gainRaw, reachable, error, checkedAt }` and nothing else. A
* level requires `POST /levels`, which records one and a half to three seconds
* of audio per microphone to measure it. Mirroring `level` at a device TTL would
* be a permanently open microphone in somebody's room, and it would look like a
* feature the entire time it was doing it. So `levelDb` is never mapped, the
* panel's meter row simply has no reading, and the row hides itself — which is
* what `undefined` on `DeviceState` has always meant.
*
* **There is no honest decibel figure.** Upstream speaks `gainPct`, normalised
* over four different native scales: a Blue Yeti Nano's ALSA range is 050, an
* SMY18's and an Anker C200's are 0100, a ThinkPad's internal is 063. "68%" is
* a mixer position. Rendering it as "+20.6 dB" would present a guess in the
* typography of a measurement, and it would look completely plausible. So the
* gain reading is emitted **only** when the declaration itself supplies a
* `ranges.gain` that is not in decibels — a pack that says "0100 %" gets its
* number, and a pack that says nothing gets no gain row at all. Fail-closed, in
* the direction where the missing thing is visible.
*
* That leaves mute, volume and reachability as the fields this build actually
* puts on a public wire, which is the smallest set that still makes the room
* real.
*
* ### The mapper is an allowlist, never a spread
*
* `/state` carries far more than the readings: `positionNote` (which describes
* where hardware sits relative to furniture), ALSA and PulseAudio device paths,
* sink names, host labels, free-text `error` strings, a room `layout`
* description, and a `recommendedNote`. A `...mic.state` anywhere in here is the
* bug, and it is the kind of bug that ships. Every field is named, one at a time,
* below.
*
* ### Endpoints that must never be read from here
*
* The same server exposes `GET /sleep` — which answers whether the owner is
* asleep, with the camera activity and lux readings behind it — and
* `GET /automations`, which returns log tails including a voice assistant's
* transcribed speech and occupancy edges. They are one path segment away, they
* are rich, and they would make a room feel astonishingly alive. Nothing here
* reads them, nothing here should, and a public 3D world that renders whether
* its owner is asleep is not a feature with a privacy setting.
*
* ### Its own cache, longer than the device TTL
*
* `/state` cold-probes three machines over SSH — measured at half a second, with
* karti-os alone taking 498 ms — and holds its own ten-second cache. Tera's
* device TTL is five seconds. Polling the bridge on that clock, per open tab,
* would fan SSH out to three machines the operator actually uses, forever. So
* this holds thirty seconds of its own (`TERA_STUDIO_TTL`), and a miss serves
* the last good snapshot rather than re-probing.
*
* ### It demotes to `live: false`, never to `sim`
*
* A bridge that quietly started inventing readings when the room stopped
* answering would be the `first-party-sensor`/`simulated` confusion
* `DeviceProvenance` exists to prevent, in the one direction that matters. When
* nothing answers, the devices this bridge covers report their last known state
* with `reachable: false`, and a device it has never heard about reports nothing
* at all.
*/
import { getJson } from "../http.ts";
import { createUpstream } from "../upstream.ts";
import { deviceRange, hasCapability, type DeviceDeclaration } from "../../../src/devices/types.ts";
import type { DeviceState } from "../../../src/devices/types.ts";
import type { StudioConfig } from "../config.ts";
/** One reading, keyed by the id a pack declares. */
export type FirstPartyReadings = ReadonlyMap<string, FirstPartyReading>;
/**
* The allowlist, as a type.
*
* Everything this build is willing to learn about somebody's room. Growing it is
* a privacy decision and the questions to answer first are the two the header
* asks: does obtaining it record anybody, and is the number in a unit this
* repo can name without guessing.
*/
export interface FirstPartyReading {
/** The upstream id, so a mismatch is diagnosable. Never rendered. */
id: string;
muted?: boolean;
/** 0100, in the upstream's own percent. Emitted only under a declared range. */
gainPct?: number;
/** 01. `sinkVolumePct / 100`, which is exact rather than a conversion. */
volume?: number;
reachable: boolean;
/** Epoch ms, from the upstream's `checkedAt`. */
observedAt: number;
}
export interface FirstPartySnapshot {
readings: FirstPartyReadings;
/** Epoch ms at which this box completed the fetch. */
fetchedAt: number;
}
export interface FirstPartyLog {
warn(msg: string): void;
}
export interface FirstPartySource {
/** The freshest snapshot, or `null` if nothing has ever answered. */
read(): Promise<FirstPartySnapshot | null>;
/**
* Turn one declaration plus one reading into a state.
*
* On this source, and not on `DeviceState`, because the mapping is where the
* two refusals live: which fields are carried at all, and under what
* conditions a gain figure may be named.
*/
stateFor(
declaration: DeviceDeclaration,
reading: FirstPartyReading | undefined,
observedAt: number,
): DeviceState;
}
/** The whole of the write surface. Stated, not merely absent. */
export function commandRefusal(): string {
return (
"this deployment reads its first-party hardware and does not command it — turning a " +
"microphone on in an occupied room from a public page is a decision nobody has made"
);
}
/** How long the bridge fetch may take. Three SSH probes cold. */
const TIMEOUT_MS = 6_000;
/** Bounds on the operator's dial, so neither end can become a probe storm. */
const MIN_TTL_SECONDS = 15;
const MAX_TTL_SECONDS = 600;
/** One key: the bridge answers about one studio. Same argument as satellites. */
const STUDIO_KEY = "studio";
/** A ceiling on how many devices one bridge may describe. `store.ts`'s number. */
const MAX_READINGS = 64;
export function createFirstPartySource(
config: StudioConfig,
log: FirstPartyLog,
): FirstPartySource {
const ttl = Math.min(MAX_TTL_SECONDS, Math.max(MIN_TTL_SECONDS, config.ttlSeconds));
const upstream = createUpstream<FirstPartySnapshot>({
label: "devices:first-party",
ttlSeconds: ttl,
log,
});
return {
async read(): Promise<FirstPartySnapshot | null> {
if (config.url === "") return null;
return upstream.get(STUDIO_KEY, () => fetchState(config));
},
stateFor: mapState,
};
}
/**
* One GET, mapped field by named field.
*
* `null` on every failure, which `upstream.ts` turns into "serve the last good
* snapshot" — and, once the last good snapshot has aged, into a set of readings
* that still carry `reachable: false`.
*/
async function fetchState(config: StudioConfig): Promise<FirstPartySnapshot | null> {
const headers = config.key === "" ? undefined : { "x-studio-key": config.key };
const body = await getJson<Record<string, unknown>>(`${config.url}/state`, {
timeoutMs: TIMEOUT_MS,
...(headers === undefined ? {} : { headers }),
});
if (body === null) return null;
const state = record(body.state);
if (state === null) return null;
const readings = new Map<string, FirstPartyReading>();
const now = Date.now();
// --- microphones -------------------------------------------------------
//
// `info` is read for exactly one field — the id — and `state` for four. Not
// `positionNote`, which describes where hardware sits relative to furniture;
// not `device`, which is an ALSA or PulseAudio path; not `host` or
// `hostLabel`; not `recommendedNote`; and not `error`, which is free text
// from somebody else's shell.
for (const entry of array(state.mics)) {
if (readings.size >= MAX_READINGS) break;
const mic = record(entry);
if (mic === null) continue;
const info = record(mic.info);
const reading = record(mic.state);
if (reading === null) continue;
const id = str(info?.id) ?? str(reading.id);
if (id === null) continue;
readings.set(id, {
id,
...(typeof reading.muted === "boolean" ? { muted: reading.muted } : {}),
...(finite(reading.gainPct) === null ? {} : { gainPct: finite(reading.gainPct) as number }),
reachable: reading.reachable === true,
observedAt: epoch(reading.checkedAt) ?? now,
});
}
// --- the speaker -------------------------------------------------------
//
// Upstream has one speaker object with a chosen sink, not a list. It is
// published under two ids — the literal `speaker` and the active sink's own id
// — so a pack may declare whichever reads better in its own floorplan without
// this file having to know which one it chose. `sinks[]`, `pulseName`,
// `alsaMaster`, `paplayVolume` and `ladder` are all left where they are:
// they are the operator's audio plumbing, not a reading about a room.
const speaker = record(state.speaker);
if (speaker !== null) {
const volumePct = finite(speaker.sinkVolumePct);
const reading: FirstPartyReading = {
id: str(speaker.sink) ?? "speaker",
...(typeof speaker.muted === "boolean" ? { muted: speaker.muted } : {}),
// Percent to fraction is exact arithmetic in the same unit, which is why
// it is allowed here and a percent-to-decibel conversion is not.
...(volumePct === null ? {} : { volume: Math.min(1, Math.max(0, volumePct / 100)) }),
reachable: speaker.reachable === true,
observedAt: epoch(speaker.checkedAt) ?? now,
};
readings.set("speaker", reading);
const sinkId = str(speaker.sink);
if (sinkId !== null && readings.size < MAX_READINGS) readings.set(sinkId, reading);
}
return { readings, fetchedAt: now };
}
/**
* One declaration plus one reading, as a `DeviceState`.
*
* Exported through the source rather than as a free function so that the two
* refusals stay attached to the thing that makes them: `levelDb` is never set,
* and `gainDb` is set only under a declared non-decibel range.
*/
function mapState(
declaration: DeviceDeclaration,
reading: FirstPartyReading | undefined,
observedAt: number,
): DeviceState {
const state: DeviceState = {
id: declaration.id,
kind: declaration.kind,
// A device the bridge could reach is a device that is on. There is no
// separate power reading upstream — a microphone the host can enumerate is
// powered, and one on a machine that will not answer is unreachable rather
// than off. The two are different sentences and `reachable` carries the
// second one.
powered: reading?.reachable === true,
reachable: reading?.reachable ?? false,
observedAt: reading?.observedAt ?? observedAt,
// The whole reason this source exists. Nothing here was invented.
synthetic: false,
};
if (hasCapability(declaration, "mute") && reading?.muted !== undefined) {
state.muted = reading.muted;
}
if (hasCapability(declaration, "volume") && reading?.volume !== undefined) {
state.volume = reading.volume;
}
if (hasCapability(declaration, "gain") && reading?.gainPct !== undefined) {
// The refusal, in one condition. Upstream's number is a percent of a mixer
// travel; the global default range is decibels. Emitting it under the
// default would mean printing "+68 dB" beside a microphone, which is both
// wrong and plausible. A declaration that states its own non-decibel range
// is a pack author saying what the number means, and only then is it named.
const range = deviceRange(declaration, "gain");
if (range.unit !== DEVICE_DEFAULT_GAIN_UNIT) {
state.gainDb = Math.min(range.max, Math.max(range.min, reading.gainPct));
}
}
// `levelDb` is never set. There is no passive level upstream and obtaining one
// records the room. The panel's meter row hides itself on `undefined`, which
// is what `undefined` has always meant here: this device has no such reading.
return state;
}
/** The unit the global default gain range is in. Anything else is the pack's own. */
const DEVICE_DEFAULT_GAIN_UNIT = "dB";
// ---- Reading somebody else's JSON -----------------------------------------
function record(value: unknown): Record<string, unknown> | null {
return value !== null && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
function array(value: unknown): unknown[] {
return Array.isArray(value) ? value : [];
}
function str(value: unknown): string | null {
if (typeof value !== "string") return null;
const trimmed = value.trim();
return trimmed === "" ? null : trimmed;
}
function finite(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
/** An ISO timestamp as epoch ms, or `null` where it will not parse. */
function epoch(value: unknown): number | null {
if (typeof value !== "string") return null;
const ms = Date.parse(value);
return Number.isFinite(ms) ? ms : null;
}
+52 -6
View File
@@ -21,6 +21,11 @@
* 3. **The declarations come from the pack, never from the caller.**
* `devices/store.ts` resolves them against a `Plan` this process built, which
* is what makes a command checkable at all.
* 4. **A source that reads real hardware never falls back to inventing it.**
* `first-party` demotes to `live: false` readings with `reachable: false`,
* never to `sim`. Substituting a state machine for a bridge is the one
* confusion `DeviceProvenance` exists to prevent, and it would be doing it in
* the direction that matters.
*
* ### Commands are memory-only and bounded
*
@@ -32,6 +37,11 @@
*/
import { resolveDevices } from "./store.ts";
import {
commandRefusal,
createFirstPartySource,
type FirstPartySource,
} from "./firstParty.ts";
import { createDeviceRuntime, type DeviceRuntime } from "./sim.ts";
import {
normalizeDeviceCommand,
@@ -46,8 +56,11 @@ export interface DevicesService {
/**
* Every device in one office, right now. Never throws; an office with no
* declarations, or a box with no source, is an empty list and a 200.
*
* A promise because one source is a network call. The `none` and `sim` paths
* resolve without awaiting anything, so nothing about their cadence changed.
*/
current(office: Office): DevicesBody;
current(office: Office): Promise<DevicesBody>;
/**
* Apply one command.
*
@@ -82,6 +95,15 @@ const NO_ATTRIBUTION: string[] = [];
export function createDevicesService(config: Config, log: DevicesLog): DevicesService {
const { source, ttlSeconds, seed } = config.devices;
/**
* Built only for the source that uses it, unlike the simulator runtime below.
*
* The runtime is built unconditionally because it costs an empty `Map`. This
* one holds a URL and a cache in front of somebody's house, and a box that
* never asked for it should not have constructed one.
*/
const bridge: FirstPartySource | null =
source === "first-party" ? createFirstPartySource(config.studio, log) : null;
// Built even for `none`, because it costs one empty `Map` and it means the
// two branches below differ by a single condition rather than by a structure.
const runtime: DeviceRuntime = createDeviceRuntime({ seed });
@@ -119,22 +141,41 @@ export function createDevicesService(config: Config, log: DevicesLog): DevicesSe
devices,
observedAt,
source,
// Never `false` in this build. The only implemented source is a state
// machine, and a body that claimed observation would be a lie told by a
// constructor — the same sentence `initialDeviceState` carries.
synthetic: true,
// `false` on exactly one source, and it is derived rather than asserted: a
// body is observed only if every reading in it is. An empty list on the
// bridge is still `false` — a real bridge with nothing plugged into it is a
// different picture from a box making it all up, which is the distinction
// this field carries separately from the per-device one.
synthetic: source !== "first-party",
ttlSeconds,
...(NO_ATTRIBUTION.length > 0 ? { attribution: NO_ATTRIBUTION } : {}),
});
return {
current(office: Office): DevicesBody {
async current(office: Office): Promise<DevicesBody> {
const now = Date.now();
if (source === "none") return body(office, [], now);
const resolved = resolveDevices(office);
report(office, resolved);
const { declarations } = resolved;
if (declarations.length === 0) return body(office, [], now);
if (bridge !== null) {
// A snapshot the bridge has never obtained is `null`, and every device
// then reports `reachable: false` with its readings absent. That is the
// honest picture — the instruments are declared, nobody has answered —
// and it is emphatically not an empty list, which would say this office
// declares no hardware.
const snapshot = await bridge.read();
return body(
office,
declarations.map((declaration) =>
bridge.stateFor(declaration, snapshot?.readings.get(declaration.id), now),
),
now,
);
}
const simulator = runtime.advance(office.id, declarations, now);
// Restamped with the request's clock: the simulator's own `observedAt` is
// its epoch plus its simulated elapsed time, which lags by up to a step
@@ -149,6 +190,11 @@ export function createDevicesService(config: Config, log: DevicesLog): DevicesSe
command(office: Office, command: DeviceCommand): DeviceCommandOutcome {
if (source === "none") return { ok: false, reason: "this deployment has no device source" };
// The whole write surface of the first-party bridge, and it is a refusal.
// Stated here rather than left implicit in a missing branch, because a
// refusal that is only an absence is a refusal somebody removes by
// accident. See `devices/firstParty.ts`.
if (bridge !== null) return { ok: false, reason: commandRefusal() };
const { declarations } = resolveDevices(office);
const declaration = declarations.find((d) => d.id === command.deviceId);
// The whole of the authorisation for a write, in two lines. The device
+229
View File
@@ -0,0 +1,229 @@
/**
* The fire feed's one upstream: a **projection**, served by the machine that
* owns the database.
*
* ### Read this before changing anything here
*
* The obvious way to build this feed was to copy `fires.sqlite` onto this box
* and query it locally. That was rejected, and the reason is not performance and
* not dependency count though it is also both of those.
*
* The store is centred on a private home. `observations` carries `distance_km`,
* `bearing_deg` and `threat`, and `detections` carries `distance_km`, all four
* measured from that address. `threat` is the one that was nearly missed: it is
*
* (16 / distance_to_house)^2 x log10(acres) x momentum x containment
* x wind-alignment-to-house
*
* and acreage and containment are already public they come from CAL FIRE. So
* that expression **inverts**: one fire gives a circle around the house, three
* give an intersection. A copy of that database on cloud-2, which is public
* facing, is a home address sitting on disk waiting for one careless star-query
* in a route somebody writes next year.
*
* So this module has **no database**. It makes two bounded GETs against an
* endpoint whose SELECT lists are written out by hand on the other machine, and
* it cannot be careless with columns it was never sent. That ordering is the
* security property: the leak is made impossible rather than merely avoided.
*
* It is also why there is no sqlite driver in this repo's dependency list, and
* `scripts/check-dependency-licenses.mjs` is entitled to keep it that way.
*
* ### Both halves, or neither
*
* `fetchProjection` asks for incidents and detections in parallel and returns
* `null` if **either** fails. That is deliberate and it is the opposite of what
* a partial-tolerance instinct suggests. A body carrying live incidents and a
* silently empty detection array is a board that has quietly stopped showing the
* strongest evidence it has, with nothing to say so the same class of failure
* as a cron that reports success while every snapshot inside it fails. One
* snapshot, one answer; `upstream.ts` above this keeps serving the last good
* whole body until a whole one arrives.
*/
import { getJson } from "../http.ts";
import type { FireDetection, FireIncident } from "../../../src/server/wire.ts";
/** How long either GET may take. The upstream reads sqlite behind its own cache. */
const TIMEOUT_MS = 8_000;
/**
* The most rows this build will adopt from either endpoint.
*
* The upstream caps its own SQL, so this is the second of two bounds rather than
* the only one but a caller that trusts an upstream's cap is a caller that
* inherits the day the upstream's cap changes. `flights/adsb.ts` takes the same
* belt-and-braces position for the same reason: `http.ts` bounds the *bytes*,
* and this bounds what is kept and served on.
*/
const MAX_INCIDENTS = 500;
const MAX_DETECTIONS = 2_000;
export interface FiresSnapshot {
/** Epoch ms at which this box completed the fetch. */
fetchedAt: number;
/** The upstream de-duplication watermark. Every incident row matched it. */
latestSeen: string | null;
incidents: FireIncident[];
detections: FireDetection[];
detectionWindowHours: number;
attribution: string[];
}
export interface FiresLog {
warn(msg: string): void;
}
/** What the projection endpoint answers with. Restated, never imported. */
interface IncidentsResponse {
latestSeen?: unknown;
incidents?: unknown;
attribution?: unknown;
}
interface DetectionsResponse {
windowHours?: unknown;
detections?: unknown;
attribution?: unknown;
}
export async function fetchProjection(
base: string,
key: string,
windowHours: number,
log: FiresLog,
): Promise<FiresSnapshot | null> {
if (base === "") return null;
const headers = key === "" ? undefined : { "x-tera-key": key };
const options = { timeoutMs: TIMEOUT_MS, ...(headers === undefined ? {} : { headers }) };
const [incidentsBody, detectionsBody] = await Promise.all([
getJson<IncidentsResponse>(`${base}/incidents`, options),
getJson<DetectionsResponse>(
`${base}/detections?hours=${encodeURIComponent(String(Math.round(windowHours)))}`,
options,
),
]);
// Both or neither. See the header — a live incident set beside a silently
// empty detection array is a board that has stopped saying what it knows.
if (incidentsBody === null || detectionsBody === null) {
log.warn(
"fires:cloud1: the projection did not answer with both halves " +
`(incidents ${incidentsBody === null ? "failed" : "ok"}, ` +
`detections ${detectionsBody === null ? "failed" : "ok"}); keeping the last whole body`,
);
return null;
}
const incidents = readArray(incidentsBody.incidents, MAX_INCIDENTS, readIncident);
const detections = readArray(detectionsBody.detections, MAX_DETECTIONS, readDetection);
return {
fetchedAt: Date.now(),
latestSeen: nonEmptyString(incidentsBody.latestSeen),
incidents,
detections,
detectionWindowHours: finite(detectionsBody.windowHours) ?? Math.round(windowHours),
attribution: mergeAttribution(incidentsBody.attribution, detectionsBody.attribution),
};
}
// ---- Reading somebody else's JSON -----------------------------------------
//
// Field by field, checked rather than cast. The upstream is a machine on the
// same tailnet run by the same person, which is exactly the relationship that
// produces "it will always be the right shape" — and then a collector gains a
// column and a renderer draws a fire at 0,0 in the Gulf of Guinea. Nothing here
// throws: a row that will not read is dropped, and the fetch above still returns
// a body.
function readIncident(raw: unknown): FireIncident | null {
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
const row = raw as Record<string, unknown>;
const lat = finite(row.lat);
const lon = finite(row.lon);
const id = nonEmptyString(row.id);
const lastSeen = nonEmptyString(row.lastSeen);
// No id, no coordinate or no watermark means nothing downstream can place it,
// de-duplicate it or link to it. All three are structural, not cosmetic.
if (id === null || lat === null || lon === null || lastSeen === null) return null;
return {
id,
source: nonEmptyString(row.source) ?? "",
name: nonEmptyString(row.name),
lat,
lon,
provenance: nonEmptyString(row.provenance) ?? "us-gov",
county: nonEmptyString(row.county),
type: (nonEmptyString(row.type) ?? "").trim(),
url: nonEmptyString(row.url),
firstSeen: nonEmptyString(row.firstSeen) ?? lastSeen,
lastSeen,
observedAt: nonEmptyString(row.observedAt),
acres: finite(row.acres),
pctContained: finite(row.pctContained),
};
}
function readDetection(raw: unknown): FireDetection | null {
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
const row = raw as Record<string, unknown>;
const lat = finite(row.lat);
const lon = finite(row.lon);
if (lat === null || lon === null) return null;
const persistentDays = finite(row.persistentDays) ?? 0;
return {
sat: nonEmptyString(row.sat) ?? "",
acquiredAt: nonEmptyString(row.acquiredAt) ?? "",
lat,
lon,
frp: finite(row.frp),
// Carried verbatim. MODIS puts an integer 0-100 in this column and VIIRS
// puts low/nominal/high; normalising here would mean choosing one of them to
// be wrong. `detectionConfidence()` in `src/server/fires.ts` does the branch
// once, on the client, where `sat` is beside it.
confidence: nonEmptyString(row.confidence),
// Absent means not persistent, which is the direction that draws MORE rather
// than fewer — so a projection one version behind this one shows the
// industrial flare as a weak hot pixel rather than hiding a real fire.
persistent: row.persistent === true,
persistentDays,
};
}
function readArray<T>(raw: unknown, cap: number, read: (row: unknown) => T | null): T[] {
if (!Array.isArray(raw)) return [];
const out: T[] = [];
for (const row of raw) {
if (out.length >= cap) break;
const parsed = read(row);
if (parsed !== null) out.push(parsed);
}
return out;
}
/** Both credit lines, de-duplicated, in the order they arrived. */
function mergeAttribution(a: unknown, b: unknown): string[] {
const out: string[] = [];
for (const source of [a, b]) {
if (!Array.isArray(source)) continue;
for (const line of source) {
if (typeof line !== "string" || line === "" || out.includes(line)) continue;
out.push(line);
}
}
return out;
}
function nonEmptyString(value: unknown): string | null {
if (typeof value !== "string") return null;
const trimmed = value.trim();
return trimmed === "" ? null : trimmed;
}
function finite(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
+120
View File
@@ -0,0 +1,120 @@
/**
* Which fires this box serves which is either California's or none.
*
* The same shape as `satellites/index.ts`, and it takes the same two decisions
* for the same reasons, so it is worth naming them rather than leaving them to
* be inferred from the code.
*
* ### One body, one cache key, no region parameter
*
* A weather body is per region because a rain shower over Oakland says nothing
* about Long Beach. A fire body is not, and the arithmetic is on its side: the
* whole state's live incident set is small five drawable fires on the day this
* was written, seventy-five rows before the gate and the boards this build
* draws are rectangles *inside* that set. A server that filtered by board would
* be doing a worse version of a clip the client has to do anyway (`promote()` in
* `src/server/fires.ts`), and it would trade the one property that makes this
* cheap: one body, every viewer, no variation, one key.
*
* It also keeps the key space bounded by the environment rather than by the
* caller, which is the invariant `upstream.ts` is written against.
*
* ### Ten minutes, floored at five
*
* The collector upstream runs on a ten-minute cron, and the endpoint in front of
* it holds its own sixty-second cache. Asking faster than the data changes
* spends two machines' work to receive identical bytes. The floor exists because
* `TERA_FIRES_TTL=0` reads like "as fresh as possible" and means "one fetch pair
* per inbound request" the same trap `TERA_FLIGHTS_TTL=0` and
* `TERA_SATELLITES_TTL=0` both were.
*
* ### `none` serves an empty body and never a synthetic fire
*
* The flights service falls back to a simulated plan, because an empty sky over
* a city reads as a bug. Nothing here does, and the asymmetry is the same one
* `satellites/index.ts` argues: an invented aeroplane is a plausible aeroplane,
* and an invented wildfire is a claim that a named place is burning, made to
* somebody who may live there.
*
* `fetchedAt` on the empty body is the Unix epoch rather than "now", because
* "now" would be a claim that this was fetched a moment ago. It was never
* fetched, and the board says so in words.
*/
import { fetchProjection, type FiresSnapshot } from "./cloud1.ts";
import { createUpstream } from "../upstream.ts";
import type { Config } from "../config.ts";
import type { FiresBody } from "../../../src/server/wire.ts";
export interface FiresService {
current(): Promise<FiresBody>;
}
export interface FiresLog {
warn(msg: string): void;
}
/** See the note above. One state, one key. */
const CALIFORNIA_KEY = "california";
/**
* Five minutes. Half the collector's own cadence, which is already twice as
* often as anything can change.
*/
const MIN_TTL_SECONDS = 300;
/** Bounds on what may be asked of the projection, so one bad env cannot flood it. */
const MIN_WINDOW_HOURS = 1;
const MAX_WINDOW_HOURS = 48;
function emptyBody(ttlSeconds: number): FiresBody {
return {
source: "none",
fetchedAt: new Date(0).toISOString(),
latestSeen: null,
incidents: [],
detections: [],
detectionWindowHours: 0,
ttlSeconds,
};
}
export function createFiresService(config: Config, log: FiresLog): FiresService {
const { source, url, key, ttlSeconds, detectionWindowHours } = config.fires;
const ttl = Math.max(MIN_TTL_SECONDS, ttlSeconds);
const windowHours = Math.min(
MAX_WINDOW_HOURS,
Math.max(MIN_WINDOW_HOURS, Math.round(detectionWindowHours)),
);
const upstream = createUpstream<FiresSnapshot>({
label: "fires:cloud1",
ttlSeconds: ttl,
log,
});
return {
async current(): Promise<FiresBody> {
if (source === "none") return emptyBody(ttl);
const snapshot = await upstream.get(CALIFORNIA_KEY, () =>
fetchProjection(url, key, windowHours, log),
);
// Never once answered. An empty body with an epoch-zero `fetchedAt`, which
// is what makes "the feed is dead" and "nothing is burning" tell apart on
// the board rather than in the log.
if (snapshot === null) return emptyBody(ttl);
return {
source,
fetchedAt: new Date(snapshot.fetchedAt).toISOString(),
latestSeen: snapshot.latestSeen,
incidents: snapshot.incidents,
detections: snapshot.detections,
detectionWindowHours: snapshot.detectionWindowHours,
ttlSeconds: ttl,
...(snapshot.attribution.length > 0 ? { attribution: snapshot.attribution } : {}),
};
},
};
}
+1 -1
View File
@@ -86,7 +86,7 @@ export function registerDevices(app: FastifyInstance, services: Services): void
if (office === null) return reply.code(404).send(NOT_FOUND);
// No `publicCache`, ever. See the header.
return services.devices.current(office);
return await services.devices.current(office);
});
app.post<{ Params: { id: string }; Body: unknown }>(
+39
View File
@@ -0,0 +1,39 @@
/**
* `GET /api/v1/fires` every active wildfire this box knows about, for
* everybody, everywhere.
*
* The second route here that takes no query at all, and for the same reason
* `/satellites` takes none: the answer does not vary by who asked or where they
* are looking. The whole state's live incident set is small, the boards are
* rectangles inside it, and the clip is a client-side operation
* (`promote()` in `src/server/fires.ts`) that has to happen anyway. With no
* parameter there is no key space, so none of the amplification concerns that
* shape `regions.ts` apply.
*
* ### Publicly cacheable, and this is the one that needed thinking about
*
* Every other publicly-cached route here carries data about the sky. This one
* carries data derived from a database centred on somebody's house so the
* question is not "is a fire location personal data" (it is not; CAL FIRE
* publishes every one of these on its own website) but "could a shared cache
* hold something it should not". It cannot, and the reason is structural rather
* than a review: the four home-relative columns are never in the body, because
* the machine that holds them serves a projection and this box has no database
* to be careless with. What a CDN can keep is a list of public agency records.
*
* `publicCache` still applies its own credential check a request that arrived
* with a session attached falls back to the fail-closed `private, no-store`
* default so nothing changes for a signed-in viewer either.
*/
import type { FastifyInstance } from "fastify";
import { publicCache } from "../cache.ts";
import type { Services } from "../services.ts";
export function registerFires(app: FastifyInstance, services: Services): void {
app.get("/api/v1/fires", async (req, reply) => {
const body = await services.fires.current();
publicCache(req, reply, body.ttlSeconds);
return body;
});
}
+1
View File
@@ -42,6 +42,7 @@ export function registerHealth(app: FastifyInstance, services: Services): void {
satellites: config.satellites.source,
markers: config.markers.source,
devices: config.devices.source,
fires: config.fires.source,
},
auth: {
mode: config.auth.mode,
+16 -1
View File
@@ -46,6 +46,7 @@
*/
import type { FastifyInstance } from "fastify";
import { bundledOffice } from "../media/index.ts";
import type { ErrorBody } from "../../../src/server/wire.ts";
import type { Services } from "../services.ts";
@@ -67,8 +68,22 @@ export function registerPresence(app: FastifyInstance, services: Services): void
// Then the office, by the same rule `offices.ts` applies: an office this
// viewer may not see is indistinguishable from one that is not there.
//
// Served packs win over bundled ones, and a bundled pack is the fallback —
// exactly as `routes/devices.ts` has always done it, and this route did not.
// The gap was not theoretical: the reference deployment sets neither
// `TERA_OFFICES_DIR` nor `TERA_PRESENCE_DIR`, so `offices.get()` answered
// `null` for every studio the browser was standing in, and a signed-in
// member got a 404 from the one route that exists to make signing in mean
// something. A bundled pack is not a leak: it is compiled into the bundle
// that made the request, so its floorplan is already in the caller's hands
// — and `presence.get()` below still answers with an empty roster unless an
// operator mounted one, which is the correct picture of a building nobody
// has told this box about.
const doc = await services.offices.get(req.params.id);
if (doc === null) return reply.code(404).send(NOT_FOUND);
if (doc === null && bundledOffice(req.params.id) === null) {
return reply.code(404).send(NOT_FOUND);
}
// No `publicCache`, ever. This body took a credential to obtain and names
// people; a shared cache holding it would hand one member's copy to the next
+3
View File
@@ -9,6 +9,7 @@
import { createAuth, type AuthService } from "./auth/index.ts";
import { createDevicesService, type DevicesService } from "./devices/index.ts";
import { createFiresService, type FiresService } from "./fires/index.ts";
import { createFlightsService, type FlightsService } from "./flights/index.ts";
import { createMarkerStore, type MarkerStore } from "./markers/store.ts";
import {
@@ -29,6 +30,7 @@ export interface Services {
weather: WeatherService;
flights: FlightsService;
satellites: SatellitesService;
fires: FiresService;
markers: MarkerStore;
devices: DevicesService;
media: MediaSignalService;
@@ -52,6 +54,7 @@ export function createServices(config: Config, log: ServiceLog): Services {
weather: createWeatherService(config, log),
flights: createFlightsService(config, log),
satellites: createSatellitesService(config, log),
fires: createFiresService(config, log),
markers: createMarkerStore(config, log),
devices: createDevicesService(config, log),
media: createMediaSignalService(),
+384
View File
@@ -0,0 +1,384 @@
/**
* The fire feed, and the one property it exists to guarantee.
*
* Most of this file is about **which keys reach the wire**, which is unusual for
* a feed test and is the whole point of this one. The upstream store is centred
* on a private home and carries four columns computed from the distance to it
* `observations.distance_km`, `bearing_deg`, `threat` and
* `detections.distance_km`. `threat` is
* `(16/distance)^2 x log10(acres) x momentum x containment x wind-alignment`, and
* with acreage and containment already public it inverts to a circle around the
* house; three fires give an intersection.
*
* The structural defence is that this box has no database: cloud-1 serves a
* projection with a hand-written column list, so there is nothing here to be
* careless with. The test below is the second line it asserts the adopted key
* set **equals** a hard-coded allowlist, so a field added upstream fails here
* rather than arriving in a browser. An `assert.ok(!keys.has("threat"))` would
* have passed for every column nobody thought to name, which is precisely the
* class of column that gets added later.
*
* The rest is the ordinary feed contract: off by default, no outbound request on
* a box that was handed nothing, an empty body rather than an invented fire, a
* TTL floor, and a dead upstream that degrades the body rather than the response.
*/
import assert from "node:assert/strict";
import { after, beforeEach, describe, it } from "node:test";
import { buildApp } from "../app.ts";
import { loadConfig } from "../config.ts";
import type { FiresBody, HealthBody } from "../../../src/server/wire.ts";
/**
* Every key a `FireIncident` may carry, and nothing else.
*
* Adding to this list is a **privacy decision**, not a refactor. If a field
* appears upstream and you want it here, the question to answer first is
* "can this be inverted, joined or differenced into the distance from a fire to
* somebody's front door" because `threat` could, and it took a second reading
* of the collector to notice.
*/
const INCIDENT_KEYS = [
"acres",
"county",
"firstSeen",
"id",
"lastSeen",
"lat",
"lon",
"name",
"observedAt",
"pctContained",
"provenance",
"source",
"type",
"url",
] as const;
/** The same, for a hot pixel. `distance_km` is upstream and stays there. */
const DETECTION_KEYS = [
"acquiredAt",
"confidence",
"frp",
"lat",
"lon",
"persistent",
"persistentDays",
"sat",
] as const;
/** Home-relative names, in every spelling the two sides use. None may appear. */
const FORBIDDEN = [
"distance_km",
"distanceKm",
"bearing_deg",
"bearingDeg",
"threat",
"area",
"home",
"promoted_to",
"promotedTo",
];
const realFetch = globalThis.fetch;
let calls: string[] = [];
/** What the projection answers with, per path suffix. `null` is an outage. */
let incidentsBody: unknown = null;
let detectionsBody: unknown = null;
/**
* A projection response shaped exactly as cloud-1 serves one including three
* fields this build does not read (`ghostRows`, `readAt`) and one it must never
* adopt whatever else happens.
*/
function upstreamIncidents(): unknown {
return {
latestSeen: "2026-08-22T22:20:06Z",
ghostRows: 22,
readAt: "2026-08-22T22:22:35.000Z",
attribution: ["Incidents from CAL FIRE and NIFC/WFIGS (US Government work, public domain)"],
incidents: [
{
id: "b7e4a30e-67ab-4964-882e-751da30b44e0",
source: "calfire",
name: "Timber Fire ",
lat: 36.224857,
lon: -121.72983,
provenance: "us-gov",
county: "Monterey",
type: "WF",
url: "https://www.fire.ca.gov/incidents/2026/8/8/timber-fire/",
firstSeen: "2026-08-22T21:14:43Z",
lastSeen: "2026-08-22T22:20:06Z",
observedAt: "2026-08-22T21:30:08Z",
acres: 7591,
pctContained: 29,
// The four that must not survive the hop, plus a plausible future one.
// A projection that regressed would send these; nothing may adopt them.
distance_km: 331.4,
bearing_deg: 297.5,
threat: 0.0041,
promoted_to: null,
area: "Norco and Corona",
},
],
};
}
function upstreamDetections(): unknown {
return {
windowHours: 24,
readAt: "2026-08-22T22:22:35.000Z",
attribution: ["Satellite hot pixels from NASA FIRMS (MODIS, VIIRS)"],
detections: [
{
sat: "MODIS",
acquiredAt: "2026-08-22T17:37:00Z",
lat: 36.26633,
lon: -121.71249,
frp: 112.9,
confidence: "94",
persistent: false,
persistentDays: 2,
distance_km: 331.9,
},
],
};
}
globalThis.fetch = (async (input: unknown) => {
const url = String(input);
calls.push(url);
const body = url.includes("/detections") ? detectionsBody : incidentsBody;
if (body === null) return new Response("nope", { status: 503 });
return new Response(JSON.stringify(body), {
status: 200,
headers: { "content-type": "application/json" },
});
}) as unknown as typeof globalThis.fetch;
after(() => {
globalThis.fetch = realFetch;
});
beforeEach(() => {
calls = [];
incidentsBody = upstreamIncidents();
detectionsBody = upstreamDetections();
});
function appWith(env: Record<string, string>) {
const config = loadConfig(env);
config.logLevel = "silent";
return buildApp(config);
}
const CLOUD1 = {
TERA_FIRES_SOURCE: "cloud1",
TERA_FIRES_URL: "http://127.0.0.1:9/api/fires",
};
async function fires(app: ReturnType<typeof buildApp>) {
const res = await app.inject({ method: "GET", url: "/api/v1/fires" });
assert.equal(res.statusCode, 200);
return { body: res.json() as FiresBody, headers: res.headers };
}
describe("a box with no fire source", () => {
it("serves a real empty body rather than an invented fire", async () => {
const app = appWith({});
after(() => app.close());
const { body } = await fires(app);
assert.equal(body.source, "none");
assert.deepEqual(body.incidents, []);
assert.deepEqual(body.detections, []);
// The epoch, not "now". "Now" would claim this was fetched a moment ago; it
// was never fetched, and a board with nothing on it has to be able to say
// which of those two it is looking at.
assert.equal(Date.parse(body.fetchedAt), 0);
assert.equal(body.latestSeen, null);
});
it("makes no outbound request at all, however often it is asked", async () => {
const app = appWith({});
after(() => app.close());
await fires(app);
await fires(app);
assert.deepEqual(calls, []);
});
it("says so on the health body, and says nothing in degraded", async () => {
const app = appWith({});
after(() => app.close());
const res = await app.inject({ method: "GET", url: "/api/v1/health" });
const health = res.json() as HealthBody;
assert.equal(health.sources.fires, "none");
// Off by default is a choice this repo made, not a default that needs
// configuring. `degraded` is one sentence per *demotion*, and
// `scripts/check-zero-config-boot.mjs` fails on any demotion at all in an
// empty environment — a stranger's clone is not misconfigured.
assert.deepEqual(health.degraded, []);
});
});
describe("a fire source that was asked for and cannot work", () => {
it("demotes to none with a sentence naming what is missing", async () => {
const app = appWith({ TERA_FIRES_SOURCE: "cloud1" });
after(() => app.close());
const health = (await app.inject({ method: "GET", url: "/api/v1/health" })).json() as HealthBody;
assert.equal(health.sources.fires, "none");
assert.equal(health.degraded.length, 1);
assert.match(health.degraded[0] ?? "", /TERA_FIRES_URL/);
assert.deepEqual(calls, []);
});
it("demotes an unreadable source name the same way", async () => {
const app = appWith({ TERA_FIRES_SOURCE: "sqlite", TERA_FIRES_URL: "http://x/api/fires" });
after(() => app.close());
const health = (await app.inject({ method: "GET", url: "/api/v1/health" })).json() as HealthBody;
assert.equal(health.sources.fires, "none");
assert.match(health.degraded[0] ?? "", /TERA_FIRES_SOURCE/);
});
});
describe("the projection's key set", () => {
it("adopts exactly the allowlist and nothing the upstream added", async () => {
const app = appWith(CLOUD1);
after(() => app.close());
const { body } = await fires(app);
assert.equal(body.incidents.length, 1);
// Equality, not absence. A subset assertion passes for every field nobody
// thought to forbid, and the field nobody thought of is the one that leaks.
const incidentKeys = Object.keys(body.incidents[0] as object).sort();
assert.deepEqual(incidentKeys, [...INCIDENT_KEYS].sort());
const detectionKeys = Object.keys(body.detections[0] as object).sort();
assert.deepEqual(detectionKeys, [...DETECTION_KEYS].sort());
});
it("carries no home-relative value anywhere in the serialised body", async () => {
const app = appWith(CLOUD1);
after(() => app.close());
const res = await app.inject({ method: "GET", url: "/api/v1/fires" });
for (const key of FORBIDDEN) {
assert.ok(!res.body.includes(key), `${key} reached the wire`);
}
// The values, too. `area` is the string "Norco and Corona" upstream — a
// named two-town reporting district next to the house, which is the
// coordinate with extra steps.
assert.ok(!res.body.includes("Norco"));
assert.ok(!res.body.includes("331.4"));
});
it("trims what it adopts and keeps the fields a renderer needs", async () => {
const app = appWith(CLOUD1);
after(() => app.close());
const { body } = await fires(app);
const incident = body.incidents[0];
assert.equal(incident?.name, "Timber Fire");
assert.equal(incident?.acres, 7591);
assert.equal(incident?.pctContained, 29);
assert.equal(body.latestSeen, "2026-08-22T22:20:06Z");
assert.equal(body.detectionWindowHours, 24);
assert.deepEqual(body.attribution, [
"Incidents from CAL FIRE and NIFC/WFIGS (US Government work, public domain)",
"Satellite hot pixels from NASA FIRMS (MODIS, VIIRS)",
]);
});
});
describe("both halves, or neither", () => {
it("keeps the last whole body when only the detections fail", async () => {
const app = appWith(CLOUD1);
after(() => app.close());
const first = await fires(app);
assert.equal(first.body.incidents.length, 1);
// A live incident set beside a silently empty detection array is a board
// that has stopped showing the strongest evidence it has, with nothing
// saying so. The whole snapshot is refused instead.
detectionsBody = null;
const config = loadConfig({ ...CLOUD1, TERA_FIRES_TTL: "0" });
config.logLevel = "silent";
const impatient = buildApp(config);
after(() => impatient.close());
const second = await fires(impatient);
// Nothing has ever answered for this second app, so it has no last good
// body — and an empty one that says it was never fetched is the answer.
assert.equal(Date.parse(second.body.fetchedAt), 0);
assert.equal(second.body.source, "none");
});
it("serves the last good body when the upstream goes away entirely", async () => {
const app = appWith(CLOUD1);
after(() => app.close());
const first = await fires(app);
const stamp = first.body.fetchedAt;
incidentsBody = null;
detectionsBody = null;
const again = await fires(app);
// Still the fire, still the same stamp — which is what tells a viewer the
// answer is old rather than that the fire went out.
assert.equal(again.body.incidents.length, 1);
assert.equal(again.body.fetchedAt, stamp);
});
});
describe("how often it asks", () => {
it("floors the TTL, so TERA_FIRES_TTL=0 is not a fetch per request", async () => {
const app = appWith({ ...CLOUD1, TERA_FIRES_TTL: "0" });
after(() => app.close());
const { body } = await fires(app);
// Five minutes: half the collector's own ten-minute cron, which is already
// twice as often as anything can change.
assert.equal(body.ttlSeconds, 300);
await fires(app);
await fires(app);
// Two calls — incidents and detections — for the first ask, and nothing
// more inside the floor.
assert.equal(calls.length, 2);
});
it("asks for one window of overpasses, clamped", async () => {
const app = appWith({ ...CLOUD1, TERA_FIRES_DETECTION_HOURS: "9000" });
after(() => app.close());
await fires(app);
const detections = calls.find((c) => c.includes("/detections")) ?? "";
assert.match(detections, /hours=48/);
});
it("is publicly cacheable, because a list of agency records is public", async () => {
const app = appWith(CLOUD1);
after(() => app.close());
const { headers } = await fires(app);
assert.match(String(headers["cache-control"]), /^public, max-age=/);
});
it("is not publicly cached for a caller who arrived with a session", async () => {
const app = appWith(CLOUD1);
after(() => app.close());
const res = await app.inject({
method: "GET",
url: "/api/v1/fires",
headers: { authorization: "Bearer something" },
});
assert.equal(res.headers["cache-control"], "private, no-store");
});
});
+489
View File
@@ -0,0 +1,489 @@
/**
* The first-party bridge, and the four things it must refuse.
*
* This is the only source in the build that reads a machine in a room with
* people in it, so the tests are mostly about what does *not* come out of it.
*
* The upstream body below is not invented: it is `GET /api/la-studio/state`,
* shortened but field-for-field real, including every field that must not
* survive the hop. `positionNote` describes where hardware sits relative to
* furniture ("The ThinkPad sits clamshell-CLOSED on top of the two ATX
* boxes"); `device` is an ALSA or PulseAudio path naming a serial number;
* `hostLabel` names machines; `layout` is a paragraph describing the room;
* `error` is free text from somebody else's shell. A mapper written as
* `{ ...mic.state }` would put all of it on a public wire and the panel would
* look identical.
*
* The other three refusals are units and recording:
*
* - **No level.** There is no passive level upstream; obtaining one calls
* `POST /levels`, which records 1.53 s of audio per microphone. A mirrored
* level meter would be a permanently open microphone in somebody's room, and
* it would look like a feature while doing it.
* - **No invented decibels.** Upstream's `gainPct` is normalised over four
* different native scales a Yeti Nano's ALSA range is 050, an Anker's is
* 0100, a ThinkPad's is 063. "68%" is a mixer position and "+20.6 dB" would
* be a guess wearing the typography of a measurement.
* - **No writes.** A POST from a public page that unmutes a microphone in an
* occupied room is a decision nobody has made.
*/
import assert from "node:assert/strict";
import { after, beforeEach, describe, it } from "node:test";
import { commandRefusal, createFirstPartySource } from "../devices/firstParty.ts";
import { createDevicesService } from "../devices/index.ts";
import { loadConfig } from "../config.ts";
import type { DeviceDeclaration } from "../../../src/devices/types.ts";
import type { Office } from "../../../src/interiors/types.ts";
/** Every key the mapper may produce on a reading. Equality, not absence. */
const READING_KEYS = ["gainPct", "id", "muted", "observedAt", "reachable", "volume"];
/** Every key the mapper may produce on a `DeviceState` for a mic with a gain range. */
const STATE_KEYS = [
"gainDb",
"id",
"kind",
"muted",
"observedAt",
"powered",
"reachable",
"synthetic",
];
/**
* The strings that describe the operator's house, their machines and their
* plumbing. None of them may appear anywhere in a serialised reading.
*/
const FORBIDDEN_VALUES = [
"clamshell-CLOSED",
"plughw:2,0",
"alsa_input.usb-Anker_PowerConf_C200",
"alsa_output.pci-0000_0c_00.4.analog-stereo",
"karti-os · office EQ",
"veronica-thinkpad",
"office desk",
"north-west corner",
"ssh: connect to host",
"One room: the office desk",
"40/50 is the good point",
];
const UPSTREAM = {
state: {
room: {
name: "LA Studio",
layout: "One room: the office desk (karti-os EQ + Yeti) faces the north wall…",
ledCast: true,
},
mics: [
{
info: {
id: "mic-yeti",
label: "Blue Yeti Nano",
host: "karti-os",
hostLabel: "karti-os · office EQ",
position: "office desk",
positionNote: null,
control: "alsa",
device: "plughw:2,0",
mixerControl: "Mic",
nativeMin: 0,
nativeMax: 50,
recommendedPct: 80,
recommendedNote: "Office-desk Yeti Nano. 40/50 is the good point.",
tdoaPairId: null,
},
state: {
id: "mic-yeti",
muted: false,
gainPct: 68,
gainRaw: 34,
reachable: true,
error: null,
checkedAt: "2026-08-22T22:11:29.345Z",
},
},
{
info: {
id: "mic-thinkpad",
label: "ThinkPad ALC285 internal",
host: "veronica-thinkpad",
hostLabel: "veronica-thinkpad · X1 Extreme",
position: "on top of the ATX stack",
positionNote:
"The ThinkPad sits clamshell-CLOSED on top of the two ATX boxes, so its mic faces down.",
control: "alsa",
device: "plughw:1,0",
nativeMax: 63,
},
state: {
id: "mic-thinkpad",
muted: true,
gainPct: 19,
gainRaw: 12,
reachable: false,
error: "ssh: connect to host veronica-thinkpad port 22: No route to host",
checkedAt: "2026-08-22T22:11:29.353Z",
},
},
{
info: {
id: "mic-anker",
label: "Anker PowerConf C200 mic",
position: "north-west corner",
device:
"alsa_input.usb-Anker_PowerConf_C200_Anker_PowerConf_C200_ACNV9P1D31370212-02.analog-stereo",
},
state: {
id: "mic-anker",
muted: false,
gainPct: 90,
reachable: true,
error: null,
checkedAt: "2026-08-22T22:11:29.053Z",
},
},
],
cameras: [
{
info: { id: "cam-smy18", frigateName: "bedroom_north", position: "north wall" },
state: { id: "cam-smy18", reachable: true, lastFrame: { frameId: "a4456963" } },
},
],
speaker: {
sink: "sink-desk",
sinks: [
{
id: "sink-desk",
label: "ALC1220 desk speakers",
pulseName: "alsa_output.pci-0000_0c_00.4.analog-stereo",
isDefault: true,
},
],
muted: false,
sinkVolumePct: 100,
alsaMaster: 87,
paplayVolume: 36000,
ladder: "default",
reachable: true,
error: null,
checkedAt: "2026-08-22T22:11:28.967Z",
},
hosts: [{ id: "karti-os", label: "karti-os · office EQ", reachable: true, ms: 434 }],
services: [{ key: "tts", label: "Chatterbox TTS", host: "spark-1 · GB10", ok: true }],
probedAt: "2026-08-22T22:11:29.353Z",
cacheAgeMs: 5004,
},
};
const realFetch = globalThis.fetch;
let calls: string[] = [];
let headersSent: Record<string, string>[] = [];
let upstreamUp = true;
globalThis.fetch = (async (input: unknown, init?: RequestInit) => {
calls.push(String(input));
headersSent.push((init?.headers as Record<string, string> | undefined) ?? {});
if (!upstreamUp) return new Response("nope", { status: 503 });
return new Response(JSON.stringify(UPSTREAM), {
status: 200,
headers: { "content-type": "application/json" },
});
}) as unknown as typeof globalThis.fetch;
after(() => {
globalThis.fetch = realFetch;
});
beforeEach(() => {
calls = [];
headersSent = [];
upstreamUp = true;
});
const silent = { warn: () => {} };
const STUDIO = { url: "http://127.0.0.1:9/api/la-studio", key: "shh", ttlSeconds: 30 };
/**
* A mic that declares its gain in the upstream's own unit.
*
* Not decibels, deliberately: a Yeti Nano's capture level is an ALSA position on
* a 050 scale and there is no arithmetic that turns it into a preamp figure.
*/
const YETI: DeviceDeclaration = {
id: "mic-yeti",
kind: "mic",
label: "Blue Yeti Nano",
assetId: "tera:device.mic.desk",
anchor: { levelId: "ground", propId: "desk-01" },
capabilities: ["power", "mute", "gain", "level"],
ranges: { gain: { min: 0, max: 100, initial: 68, unit: "%" } },
provenance: "first-party-sensor",
disclosure: "Live reading from the studio's own desk microphone.",
simulatedDisclosure: "Simulated in your browser — this deployment will not share the live room.",
};
/** The same instrument with no declared range, which must lose its gain row. */
const YETI_NO_RANGE: DeviceDeclaration = { ...YETI, id: "mic-anker", ranges: undefined };
const SPEAKER: DeviceDeclaration = {
id: "speaker",
kind: "speaker",
label: "Desk speakers",
assetId: "tera:device.speaker.desk",
anchor: { levelId: "ground", propId: "desk-01" },
capabilities: ["power", "volume", "playback"],
provenance: "first-party-sensor",
disclosure: "Live reading from the studio's own desk speakers.",
simulatedDisclosure: "Simulated in your browser — this deployment will not share the live room.",
};
describe("the field allowlist", () => {
it("maps exactly the allowed keys and nothing the upstream volunteered", async () => {
const source = createFirstPartySource(STUDIO, silent);
const snapshot = await source.read();
assert.ok(snapshot !== null);
const yeti = snapshot.readings.get("mic-yeti");
assert.ok(yeti !== undefined);
// Equality, not absence: a subset assertion passes for every field nobody
// thought to forbid, and the field nobody thought of is the one that leaks.
assert.deepEqual(Object.keys(yeti).sort(), READING_KEYS.filter((k) => k !== "volume").sort());
});
it("carries no sentence about the operator's house, machines or plumbing", async () => {
const source = createFirstPartySource(STUDIO, silent);
const snapshot = await source.read();
const wire = JSON.stringify([...(snapshot?.readings.values() ?? [])]);
for (const value of FORBIDDEN_VALUES) {
assert.ok(!wire.includes(value), value);
}
// Not even the free-text error, which is the one that looks harmless and is
// a shell message naming a hostname.
assert.ok(!wire.includes("No route to host"));
});
it("ignores the camera list entirely", async () => {
const source = createFirstPartySource(STUDIO, silent);
const snapshot = await source.read();
assert.equal(snapshot?.readings.get("cam-smy18"), undefined);
// `bedroom_north` is a Frigate camera name and names the room. Nothing here
// reads cameras at all, so it cannot arrive by any path.
assert.ok(!JSON.stringify([...(snapshot?.readings ?? [])]).includes("bedroom"));
});
it("reads the speaker under both the literal id and its active sink", async () => {
const source = createFirstPartySource(STUDIO, silent);
const snapshot = await source.read();
// One reading, two names, so a pack may declare whichever reads better in
// its own floorplan without this file knowing which it chose.
assert.equal(snapshot?.readings.get("speaker")?.volume, 1);
assert.equal(snapshot?.readings.get("sink-desk")?.volume, 1);
assert.equal(snapshot?.readings.get("speaker")?.muted, false);
});
it("sends the studio key and asks only for /state", async () => {
const source = createFirstPartySource(STUDIO, silent);
await source.read();
assert.deepEqual(calls, ["http://127.0.0.1:9/api/la-studio/state"]);
assert.equal(headersSent[0]?.["x-studio-key"], "shh");
// `/sleep` reports whether the owner is asleep and `/automations` returns a
// voice assistant's transcribed speech. They are one path segment away and
// nothing may ever reach for them.
assert.ok(!calls.some((c) => c.includes("/sleep") || c.includes("/automations")));
assert.ok(!calls.some((c) => c.includes("/levels") || c.includes("/measure")));
assert.ok(!calls.some((c) => c.includes("live.jpg") || c.includes("/frames/")));
});
});
describe("the two unit refusals", () => {
it("never reports a level, because obtaining one records the room", async () => {
const source = createFirstPartySource(STUDIO, silent);
const snapshot = await source.read();
const state = source.stateFor(YETI, snapshot?.readings.get("mic-yeti"), 1);
// The declaration asks for `level`. There is no passive level upstream, so
// the reading is absent and the panel's meter row hides itself — which is
// what `undefined` on `DeviceState` has always meant.
assert.equal(state.levelDb, undefined);
assert.deepEqual(Object.keys(state).sort(), STATE_KEYS.sort());
});
it("names a gain only under a declared non-decibel range", async () => {
const source = createFirstPartySource(STUDIO, silent);
const snapshot = await source.read();
// Declared as 0100 "%": the number means something and is carried.
const declared = source.stateFor(YETI, snapshot?.readings.get("mic-yeti"), 1);
assert.equal(declared.gainDb, 68);
// No declared range means the global default, which is decibels. 90% of a
// mixer travel is not 90 dB and it is not any other number of decibels
// either, so no gain is reported at all.
const undeclared = source.stateFor(YETI_NO_RANGE, snapshot?.readings.get("mic-anker"), 1);
assert.equal(undeclared.gainDb, undefined);
});
});
describe("reachability is not power", () => {
it("reports an unreachable device as unreachable, holding its last reading", async () => {
const source = createFirstPartySource(STUDIO, silent);
const snapshot = await source.read();
const state = source.stateFor(
{ ...YETI, id: "mic-thinkpad" },
snapshot?.readings.get("mic-thinkpad"),
1,
);
assert.equal(state.reachable, false);
// A microphone on a machine that will not answer is not a switched-off
// microphone, and the mute reading it last reported still stands.
assert.equal(state.muted, true);
assert.equal(state.synthetic, false);
assert.equal(state.observedAt, Date.parse("2026-08-22T22:11:29.353Z"));
});
it("reports every declared device as unreachable when nobody answers", async () => {
upstreamUp = false;
const source = createFirstPartySource(STUDIO, silent);
const snapshot = await source.read();
assert.equal(snapshot, null);
const state = source.stateFor(YETI, undefined, 4_242);
assert.equal(state.reachable, false);
assert.equal(state.powered, false);
assert.equal(state.observedAt, 4_242);
// Never invented. A bridge that started making readings up when the room
// went quiet is the one substitution `DeviceProvenance` exists to prevent.
assert.equal(state.synthetic, false);
assert.equal(state.muted, undefined);
});
});
describe("how often it probes, and what it will not do", () => {
it("holds its own cache over the upstream's, so SSH is not fanned out per poll", async () => {
const source = createFirstPartySource(STUDIO, silent);
await source.read();
await source.read();
await source.read();
// `/state` cold-probes three machines over SSH in about half a second, and
// Tera's device TTL is five seconds. One call is the whole point.
assert.equal(calls.length, 1);
});
it("floors a zero TTL rather than probing on every request", async () => {
const source = createFirstPartySource({ ...STUDIO, ttlSeconds: 0 }, silent);
await source.read();
await source.read();
assert.equal(calls.length, 1);
});
it("makes no request at all when no bridge URL is configured", async () => {
const source = createFirstPartySource({ url: "", key: "", ttlSeconds: 30 }, silent);
assert.equal(await source.read(), null);
assert.deepEqual(calls, []);
});
});
describe("the service, wired to the bridge", () => {
/**
* A studio with two real props and two first-party declarations.
*
* Written out rather than borrowed from a shipped pack, exactly as
* `devices.test.ts` argues: a bridge test that fails because somebody moved a
* desk in Los Angeles is a test nobody trusts.
*/
const office: Office = {
id: "studio",
name: "Studio",
viewpoints: [],
levels: [
{
id: "l1",
name: "Ground",
elevation: 0,
wallHeight: 3,
floorplan: {
rooms: [
{
id: "room",
name: "Room",
floor: "tera:carpet.loop",
outline: [
{ x: 0, z: 0 },
{ x: 4, z: 0 },
{ x: 4, z: 4 },
{ x: 0, z: 4 },
],
},
],
walls: [],
seats: [{ id: "desk-01", position: { x: 2, z: 2 }, facing: 0 }],
props: [
{
id: "mic-prop",
kind: "tera:device.mic.desk",
position: { x: 2, z: 2 },
rotation: 0,
},
{
id: "speaker-prop",
kind: "tera:device.speaker.desk",
position: { x: 3, z: 2 },
rotation: 0,
},
],
devices: [
{ ...YETI, anchor: { levelId: "l1", propId: "mic-prop", seatId: "desk-01" } },
{ ...SPEAKER, anchor: { levelId: "l1", propId: "speaker-prop" } },
],
},
},
],
} as unknown as Office;
function service() {
const config = loadConfig({
TERA_DEVICES_SOURCE: "first-party",
TERA_STUDIO_URL: STUDIO.url,
TERA_STUDIO_KEY: STUDIO.key,
});
return { config, devices: createDevicesService(config, silent) };
}
it("serves an observed body rather than a synthetic one", async () => {
const { config, devices } = service();
assert.equal(config.devices.source, "first-party");
assert.deepEqual(config.degraded, []);
const body = await devices.current(office);
assert.equal(body.source, "first-party");
// The whole reason the source exists: nobody in this process invented this.
assert.equal(body.synthetic, false);
assert.equal(body.devices.length, 2);
assert.equal(body.devices.find((d) => d.id === "mic-yeti")?.muted, false);
assert.equal(body.devices.find((d) => d.id === "speaker")?.volume, 1);
});
it("refuses every command, and says why", async () => {
const { devices } = service();
const outcome = devices.command(office, { deviceId: "mic-yeti", op: "mute", value: true });
assert.equal(outcome.ok, false);
assert.equal(outcome.ok === false ? outcome.reason : "", commandRefusal());
assert.match(commandRefusal(), /does not command it/);
// And it refused without asking the room anything.
assert.deepEqual(calls, []);
});
it("demotes to none, never to sim, when it has nowhere to point", () => {
const config = loadConfig({ TERA_DEVICES_SOURCE: "first-party" });
assert.equal(config.devices.source, "none");
assert.equal(config.degraded.length, 1);
assert.match(config.degraded[0] ?? "", /TERA_STUDIO_URL/);
// Not `sim`. Answering a promise of real hardware with a state machine is
// the confusion this whole vocabulary exists to prevent.
assert.ok(!(config.degraded[0] ?? "").includes("=sim"));
});
});
+59
View File
@@ -283,3 +283,62 @@ describe("the file", () => {
assert.deepEqual(res.json<PresenceBody>().people, []);
});
});
/**
* The route that could not succeed in production.
*
* `routes/devices.ts` falls back to `bundledOffice` and this route did not, so on
* a deployment that sets no `TERA_OFFICES_DIR` which is the reference
* deployment, and every clone `offices.get()` answered `null` for every studio
* the browser was actually standing in, and a signed-in member got a 404 from the
* one route that exists to make signing in mean something. Presence was
* unreachable in production for every pack, for every member.
*
* A bundled pack is not a leak here for exactly the reason `bundledOffice` gives:
* it is compiled into the browser bundle that made the request, so its floorplan
* is already in the caller's hands. The roster is a separate document and stays
* behind `TERA_PRESENCE_DIR`, which is what keeps the geometry public and the
* people not.
*/
describe("a bundled pack, which is what production actually serves", () => {
it("answers a signed-in member for a pack no directory was configured for", async () => {
const config = loadConfig(jwt);
config.logLevel = "silent";
const app = buildApp(config);
after(() => app.close());
const res = await app.inject({
method: "GET",
url: "/api/v1/offices/mateo-court/presence",
headers: bearer(),
});
assert.equal(res.statusCode, 200);
// Empty, not 404. "This office does not exist" and "nobody has told me who
// is in this office" are different facts with different fixes.
assert.deepEqual(res.json<PresenceBody>().people, []);
});
it("still refuses an anonymous caller, bundled or not", async () => {
const config = loadConfig(jwt);
config.logLevel = "silent";
const app = buildApp(config);
after(() => app.close());
const res = await app.inject({ method: "GET", url: "/api/v1/offices/mateo-court/presence" });
assert.equal(res.statusCode, 401);
});
it("still 404s an office that is neither served nor bundled", async () => {
const config = loadConfig(jwt);
config.logLevel = "silent";
const app = buildApp(config);
after(() => app.close());
const res = await app.inject({
method: "GET",
url: "/api/v1/offices/not-a-place/presence",
headers: bearer(),
});
assert.equal(res.statusCode, 404);
});
});