refactor: enforce import directions and expose source factories

This commit is contained in:
Sameh Khamis committed 2026-09-14 21:12:01 -07:00
1 parent 704e0fcc51
commit cc960b0dbe
41 files changed
+1080 -417

No files matched your search

+1 -1
View File
@@ -18,7 +18,7 @@ import { createApplicationFirms } from './layers/firms.js';
import { createApplicationEarthquakes } from './layers/earthquakes.js';
import { createApplicationCables } from './layers/submarineCables.js';
import { createInfrastructureLayers } from '../data/infrastructure.js';
import { localGeoJsonServices } from '../data/localGeojson.js';
import { localGeoJsonServices } from './localGeojsonServices.js';
const SOURCE_METHODS = Object.freeze({
flights: ['getSnapshot'],
+25
View File
@@ -0,0 +1,25 @@
import { governorRequestRender } from '../renderGovernor.js';
import {
clearSelectedEntityContextForLayer,
registerEntityContext,
removeEntityContextsForLayer,
selectEntityContext,
} from '../data/contextStore.js';
import {
clearOverlaySource,
setOverlayEntries,
setOverlaySourceVisible,
} from '../overlays/worldOverlay.js';
/** Existing standalone application operations, shared by its local layers. */
export const localGeoJsonServices = Object.freeze({
overlayHost: Object.freeze({
clearSource: clearOverlaySource,
setEntries: setOverlayEntries,
setVisible: setOverlaySourceVisible,
}),
clearSelectedEntityContextForLayer,
registerEntityContext,
removeEntityContextsForLayer,
selectEntityContext,
governorRequestRender,
});
+2 -52
View File
@@ -1,52 +1,2 @@
/** Bind a source contract once for the application without replacing its layer instance. */
export function createSourceSlot(
initial,
methods,
label = 'Source',
optional = {},
) {
let active = initial;
let binding = null;
const source = Object.fromEntries(
[...methods, ...Object.keys(optional)].map((method) => [
method,
(...args) => {
if (typeof active?.[method] !== 'function' && optional[method])
return optional[method](...args);
if (typeof active?.[method] !== 'function')
throw new Error(`${label} is not configured`);
const owner = binding;
const provider = active;
const value = provider[method](...args);
if (value && typeof value.then === 'function')
return value.then((result) => {
if (binding !== owner || active !== provider)
throw new DOMException(`${label} was replaced`, 'AbortError');
return result;
});
return value;
},
]),
);
for (const property of ['label', 'attribution']) {
Object.defineProperty(source, property, {
enumerable: true,
get: () => active?.[property],
});
}
return {
source,
configure(next) {
if (methods.some((method) => typeof next?.[method] !== 'function'))
throw new TypeError(`Invalid ${label.toLowerCase()}`);
const owner = Symbol(label);
active = next;
binding = owner;
return () => {
if (binding !== owner) return;
active = null;
binding = null;
};
},
};
}
// Compatibility entry; source contracts are owned by sources/.
export { createSourceSlot } from '../sources/sourceSlot.js';
+1 -1
View File
@@ -1,6 +1,6 @@
import { defaultSurface } from './surfaceServices.js';
import { createApplicationAlpr } from '../app/layers/alprCameras.js';
import { createSourceSlot } from '../app/sourceSlot.js';
import { createSourceSlot } from '../sources/sourceSlot.js';
import { createOverpassAlprSource } from '../layers/alpr/index.js';
export * from '../layers/alpr/index.js';
const slot = createSourceSlot(
+1 -1
View File
@@ -1,5 +1,5 @@
import { createApplicationBikeshare } from '../app/layers/bikeshare.js';
import { createSourceSlot } from '../app/sourceSlot.js';
import { createSourceSlot } from '../sources/sourceSlot.js';
import { createBikeshareSource } from '../layers/bikeshare/source.js';
const sourceSlot = createSourceSlot(
+1 -1
View File
@@ -1,6 +1,6 @@
import { defaultSurface } from './surfaceServices.js';
import { createApplicationCctv } from '../app/layers/cctv.js';
import { createSourceSlot } from '../app/sourceSlot.js';
import { createSourceSlot } from '../sources/sourceSlot.js';
import { createCctvSource } from '../layers/cctv/source.js';
const sourceSlot = createSourceSlot(
+54
View File
@@ -0,0 +1,54 @@
import { GUIDANCE_STATUSES } from '../loadingFeedback.js';
/**
* Normalize heterogeneous layer stats into one honest control-chip state.
* @param {object|null} stats Layer getStats() result.
* @returns {'nominal'|'loading'|'degraded'|'stale'|'fallback'|'unavailable'} Feed state.
*/
export function layerFeedState(stats = {}) {
const state = stats || {};
const status =
typeof state.status === 'string' ? state.status.toLowerCase() : '';
const source = `${state.source || ''} ${state.coverage || ''}`;
const hasExplicitFallback = typeof state.fallback === 'boolean';
const hasPriorData = Number(state.count) > 0 || Boolean(state.lastUpdate);
const presentedError =
state.error || state.lastError || state.managerRefreshError;
if (['unavailable', 'offline', 'down', 'error'].includes(status))
return 'unavailable';
if (
(presentedError ||
state.unavailable === true ||
state.available === false) &&
!hasPriorData &&
!GUIDANCE_STATUSES.includes(status)
) {
return 'unavailable';
}
if (state.loading) return 'loading';
// Guidance states ask the user to act (zoom in, run a search) — normal
// operation, not feed faults. One honesty carve-out: layers keep their
// rendered records through the guidance state, so a genuinely stale cache
// still reads STALE; a guidance prompt alone never reads DEGRADED.
if (GUIDANCE_STATUSES.includes(status)) {
return state.stale ? 'stale' : 'nominal';
}
if (
state.fallback === true ||
status === 'fallback' ||
state.mode === 'sim' ||
/\bfallback\b/i.test(source) ||
(!hasExplicitFallback && /\badsb\.lol\b/i.test(source))
) {
return 'fallback';
}
if (state.stale || status === 'stale') return 'stale';
if (
state.degraded ||
presentedError ||
state.unavailable === true ||
state.available === false
)
return 'degraded';
return 'nominal';
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { defaultSurface } from './surfaceServices.js';
import { createApplicationFirms, firmsServices } from '../app/layers/firms.js';
import { createSourceSlot } from '../app/sourceSlot.js';
import { createSourceSlot } from '../sources/sourceSlot.js';
import {
createFirmsHelpers,
createFirmsSource,
+2 -26
View File
@@ -1,15 +1,5 @@
import { governorRequestRender } from '../renderGovernor.js';
import {
clearSelectedEntityContextForLayer,
registerEntityContext,
removeEntityContextsForLayer,
selectEntityContext,
} from './contextStore.js';
import {
clearOverlaySource,
setOverlayEntries,
setOverlaySourceVisible,
} from '../overlays/worldOverlay.js';
import { localGeoJsonServices } from '../app/localGeojsonServices.js';
export { localGeoJsonServices } from '../app/localGeojsonServices.js';
import {
createLocalGeoJsonLayer as createLayer,
createLocalInfrastructureOverlayPublisher as createPublisher,
@@ -17,20 +7,6 @@ import {
export * from './localGeojsonCore.js';
/** Existing standalone application operations, shared by its local layers. */
export const localGeoJsonServices = Object.freeze({
overlayHost: Object.freeze({
clearSource: clearOverlaySource,
setEntries: setOverlayEntries,
setVisible: setOverlaySourceVisible,
}),
clearSelectedEntityContextForLayer,
registerEntityContext,
removeEntityContextsForLayer,
selectEntityContext,
governorRequestRender,
});
/** Create a layer using the standalone app's operations; retain overlay overrides. */
export function createLocalGeoJsonLayer(options) {
return createLayer(options, {
+1 -1
View File
@@ -1,6 +1,6 @@
import { defaultSurface } from './surfaceServices.js';
import { createApplicationInstallations } from '../app/layers/militaryInstallations.js';
import { createSourceSlot } from '../app/sourceSlot.js';
import { createSourceSlot } from '../sources/sourceSlot.js';
import { createInstallationSource } from '../layers/installations/index.js';
const sourceSlot = createSourceSlot(
+1 -1
View File
@@ -1,6 +1,6 @@
import { defaultSurface } from './surfaceServices.js';
import { createApplicationRadio } from '../app/layers/radio.js';
import { createSourceSlot } from '../app/sourceSlot.js';
import { createSourceSlot } from '../sources/sourceSlot.js';
import { createRadioSource } from '../layers/radio/source.js';
const sourceSlot = createSourceSlot(
+1 -1
View File
@@ -1,5 +1,5 @@
import { createApplicationLaunches } from '../app/layers/rocketLaunches.js';
import { createSourceSlot } from '../app/sourceSlot.js';
import { createSourceSlot } from '../sources/sourceSlot.js';
import { createLaunchSource } from '../layers/launches/index.js';
import * as satellites from './satellites.js';
+1 -1
View File
@@ -1,5 +1,5 @@
import { createApplicationSatellites } from '../app/layers/satellites.js';
import { createSourceSlot } from '../app/sourceSlot.js';
import { createSourceSlot } from '../sources/sourceSlot.js';
import { createSatelliteSource } from '../layers/satellites/index.js';
const sourceSlot = createSourceSlot(
+1 -1
View File
@@ -1,5 +1,5 @@
import { createApplicationTraffic } from '../app/layers/traffic.js';
import { createSourceSlot } from '../app/sourceSlot.js';
import { createSourceSlot } from '../sources/sourceSlot.js';
import { createTrafficSource } from '../layers/traffic/source.js';
const sourceSlot = createSourceSlot(
-194
View File
@@ -1,194 +0,0 @@
import fs from 'node:fs';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import {
commandCompletedSuccessfully,
parseWindowsUserSid,
} from './keySetupCore.mjs';
/** PowerShell verification for the exact owner-only Windows credential DACL. */
const WINDOWS_ACL_VERIFY_SCRIPT = [
"$ErrorActionPreference = 'Stop'",
// Load Microsoft.PowerShell.Security (Get-Acl) from the module tree of the
// interpreter that is actually running. $PSHOME is that interpreter's own
// physical directory, so this is correct even when the executable was named
// through the Sysnative bridge, and it cannot be steered by anything the
// parent environment set.
"$env:PSModulePath = Join-Path $PSHOME 'Modules'",
'$acl = Get-Acl -LiteralPath $env:GEV_ACL_FILE',
'if (-not $acl.AreAccessRulesProtected) { exit 2 }',
"$allowed = @($env:GEV_ACL_USER_SID, 'S-1-5-18', 'S-1-5-32-544')",
'$seen = @{}',
'$rules = @($acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier]))',
'if ($rules.Count -ne 3) { exit 7 }',
'foreach ($rule in $rules) {',
' $ruleSid = $rule.IdentityReference.Value',
' if ($rule.IsInherited) { exit 3 }',
' if ($rule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow) { exit 4 }',
' if ($allowed -notcontains $ruleSid) { exit 5 }',
' if ($seen.ContainsKey($ruleSid)) { exit 8 }',
' $full = [System.Security.AccessControl.FileSystemRights]::FullControl',
' if ($rule.FileSystemRights -ne $full) { exit 6 }',
' $seen[$ruleSid] = $true',
'}',
'if ($seen.Count -ne 3) { exit 9 }',
].join('; ');
/**
* Resolve the native Windows ACL tools without consulting PATH.
*
* Provider Settings supports the standard Windows installation layout only:
* a local drive root named `Windows` (for example C:\\Windows or D:\\Windows).
* Requiring consistent aliases, canonical paths, and regular files prevents an
* inherited environment override, UNC share, device path, junction, or PATH
* shim from being treated as an operating-system security tool.
*/
function resolveWindowsNativeTools(environment, fileSystem, architecture) {
const aliases = ['SystemRoot', 'SYSTEMROOT', 'WINDIR', 'windir'];
const configured = aliases
.map((name) => environment[name])
.filter((value) => typeof value === 'string' && value.length > 0);
if (configured.length === 0) return null;
const roots = configured.map((value) => {
if (value !== value.trim() || !/^[A-Za-z]:\\Windows\\?$/i.test(value)) return null;
return value.endsWith('\\') ? value.slice(0, -1) : value;
});
if (roots.some((root) => !root)) return null;
if (roots.some((root) => root.toLowerCase() !== roots[0].toLowerCase())) return null;
const systemRoot = roots[0];
const systemDirectory = architecture === 'ia32' ? 'Sysnative' : 'System32';
const expected = {
whoami: path.win32.join(systemRoot, systemDirectory, 'whoami.exe'),
icacls: path.win32.join(systemRoot, systemDirectory, 'icacls.exe'),
powershell: path.win32.join(
systemRoot,
systemDirectory,
'WindowsPowerShell',
'v1.0',
'powershell.exe',
),
};
try {
const realpath = fileSystem.realpathSync.native || fileSystem.realpathSync;
const rootEntry = fileSystem.lstatSync(systemRoot);
if (!rootEntry.isDirectory() || rootEntry.isSymbolicLink()) return null;
const canonicalRoot = realpath.call(fileSystem.realpathSync, systemRoot);
if (canonicalRoot.toLowerCase() !== systemRoot.toLowerCase()) return null;
for (const executable of Object.values(expected)) {
const entry = fileSystem.lstatSync(executable);
if (!entry.isFile() || entry.isSymbolicLink()) return null;
const canonicalExecutable = realpath.call(fileSystem.realpathSync, executable);
const canonicalCandidates = [executable];
if (architecture === 'ia32') {
canonicalCandidates.push(executable.replace('\\Sysnative\\', '\\System32\\'));
}
if (!canonicalCandidates.some(
(candidate) => candidate.toLowerCase() === canonicalExecutable.toLowerCase(),
)) return null;
}
} catch {
return null;
}
return expected;
}
/**
* Restrict a credential file before any secret is written to it.
* Dependencies are injectable so every fail-closed branch is unit-testable.
*/
export function hardenCredentialFile(filepath, {
platform = process.platform,
architecture = process.arch,
spawn = spawnSync,
fileSystem = fs,
environment = process.env,
} = {}) {
if (platform !== 'win32') {
try {
if (platform === 'darwin') {
const aclRemoval = spawn('chmod', ['-N', filepath], { stdio: 'ignore' });
if (!commandCompletedSuccessfully(aclRemoval)) return false;
}
fileSystem.chmodSync(filepath, 0o600);
return (fileSystem.statSync(filepath).mode & 0o777) === 0o600;
} catch {
return false;
}
}
const tools = resolveWindowsNativeTools(environment, fileSystem, architecture);
if (!tools) return false;
try {
// Grant by the CURRENT PROCESS TOKEN'S SID, never a bare username. Parsing
// the second CSV field structurally prevents an SID-looking account name or
// a broad group SID from becoming the credential owner.
const whoami = spawn(tools.whoami, ['/user', '/fo', 'csv', '/nh'], {
encoding: 'utf8',
windowsHide: true,
});
const sid = commandCompletedSuccessfully(whoami)
? parseWindowsUserSid(whoami.stdout)
: null;
if (!sid) return false;
const applied = spawn(tools.icacls, [
filepath,
'/inheritance:r',
'/grant:r',
`*${sid}:F`,
'*S-1-5-18:F',
'*S-1-5-32-544:F',
], { stdio: 'ignore', windowsHide: true });
if (!commandCompletedSuccessfully(applied)) return false;
// Command success is not proof of the resulting DACL. Query it back and
// accept only three explicit FullControl allow principals, with inheritance
// disabled. Any unexpected rule, right, command error, or missing principal
// fails closed before the secret reaches disk.
//
// The verify process must load Microsoft.PowerShell.Security (Get-Acl)
// from the Windows PowerShell system module tree ONLY. A side-by-side
// PowerShell 7 install prepends its own module trees to PSModulePath at
// startup; inherited into a 5.1 process, the incompatible 7.x manifest
// cannot be autoloaded and the verify step fails. The script itself sets
// the path from $PSHOME; this is the same value computed ahead of time, so
// nothing inherited is in force even for the moment before it runs. The
// Sysnative spelling is a 32-bit caller's bridge and not a directory the
// launched native process can read, so the physical name is used.
const powershellModuleDirectory = path.win32.join(
path.win32
.dirname(tools.powershell)
.replace(/\\Sysnative\\/i, '\\System32\\'),
'Modules',
);
// Windows environment names are case-insensitive, and a child can end up
// carrying a differently cased alias alongside the value set here. Drop
// every spelling before setting the trusted one.
const verifyEnvironment = Object.fromEntries(
Object.entries(environment).filter(
([name]) => name.toLowerCase() !== 'psmodulepath',
),
);
const verified = spawn(tools.powershell, [
'-NoProfile',
'-NonInteractive',
'-Command', WINDOWS_ACL_VERIFY_SCRIPT,
], {
env: {
...verifyEnvironment,
GEV_ACL_FILE: filepath,
GEV_ACL_USER_SID: sid,
PSModulePath: powershellModuleDirectory,
},
stdio: 'ignore',
windowsHide: true,
});
return commandCompletedSuccessfully(verified);
} catch {
return false;
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { hardenCredentialFile } from './keySetupHardening.mjs';
import { hardenCredentialFile } from '../server/standalone/key-setup-hardening.mjs';
const FILE = path.join(os.tmpdir(), 'provider-settings-test');
const USER_SID = 'S-1-5-21-1111111111-2222222222-3333333333-1001';
+2 -159
View File
@@ -1,52 +1,6 @@
import * as Cesium from 'cesium';
import {
QUERY_LIMIT,
EARTH_MEAN_RADIUS_M,
QUERY_SNAP_DEGREES,
} from './policy.js';
export function textTag(value) {
const t = String(value ?? '').trim();
return t || null;
}
export function numTag(value) {
// `Number('')` and `Number(' ')` are 0, not NaN — a blank or whitespace-only
// tag value must read as missing, never as a due-north bearing.
if (value == null) return null;
const trimmed = String(value).trim();
if (!trimmed) return null;
const n = Number(trimmed);
return Number.isFinite(n) ? n : null;
}
/**
* Snap a viewport box OUTWARD to the query grid, clamped to valid ranges.
* Pure so the reuse contract is pinnable.
* @param {{south:number,west:number,north:number,east:number}} box
* @param {number} [step=QUERY_SNAP_DEGREES]
*/
export function snapAlprBox(box, step = QUERY_SNAP_DEGREES) {
const down = (v) => Math.floor(v / step) * step;
const up = (v) => Math.ceil(v / step) * step;
return {
south: Math.max(-90, down(box.south)),
west: Math.max(-180, down(box.west)),
north: Math.min(90, up(box.north)),
east: Math.min(180, up(box.east)),
};
}
/** Whether `inner` lies entirely within `outer` (no dateline handling; the viewport gate already rejects east <= west). */
export function boxContains(outer, inner) {
if (!outer || !inner) return false;
return (
inner.south >= outer.south &&
inner.north <= outer.north &&
inner.west >= outer.west &&
inner.east <= outer.east
);
}
import { EARTH_MEAN_RADIUS_M } from './policy.js';
export * from './records.js';
/**
* Great-circle destination point — used only to draw the short
@@ -77,117 +31,6 @@ export function destinationPointDeg(latDeg, lonDeg, bearingDeg, distanceM) {
};
}
/**
* Backoff progression for the unavailable-state retry: 30 s, doubling to a
* 240 s ceiling. Pure so the progression is pinnable without booting the layer.
*/
export function alprRetryDelayMs(prevDelayMs) {
const RETRY_MIN_MS = 30000;
const RETRY_CEIL_MS = 240000;
if (!Number.isFinite(prevDelayMs) || prevDelayMs <= 0) return RETRY_MIN_MS;
return Math.min(prevDelayMs * 2, RETRY_CEIL_MS);
}
/**
* Whether a `surveillance:type` value denotes an ALPR camera. OSM allows
* semicolon multi-values (`camera;ALPR`) and mixed case, so an exact match
* would silently miss tagged cameras.
* @param {*} value - raw `surveillance:type` tag value
* @returns {boolean}
*/
export function isAlprSurveillanceType(value) {
return String(value ?? '')
.split(';')
.some((part) => part.trim().toUpperCase() === 'ALPR');
}
/** Map one raw Overpass node element to a plain camera record. Null for anything unusable. */
export function normalizeAlprNode(el) {
if (
!el ||
el.type !== 'node' ||
!Number.isSafeInteger(el.id) ||
el.id <= 0 ||
!Number.isFinite(el.lat) ||
Math.abs(el.lat) > 90 ||
!Number.isFinite(el.lon) ||
Math.abs(el.lon) > 180
)
return null;
const tags = el.tags || {};
if (!isAlprSurveillanceType(tags['surveillance:type'])) return null;
return {
id: `alpr:${el.id}`,
osmId: el.id,
latitude: el.lat,
longitude: el.lon,
operator: textTag(tags.operator),
manufacturer: textTag(tags.manufacturer),
cameraType: textTag(tags['camera:type']),
zone: textTag(tags['surveillance:zone']),
// Only numeric bearings are parsed; compass-word directions ("N"/"NE")
// are rare on this tag and simply render with no facing line.
directionDeg: normalizeDirection(
tags['camera:direction'] ?? tags.direction,
),
ref: textTag(tags.ref),
lastVerified: textTag(tags.check_date) || textTag(tags['survey:date']),
source: textTag(tags.source),
};
}
export function normalizeDirection(value) {
const degrees = numTag(value);
return degrees != null && degrees >= 0 && degrees <= 360
? degrees % 360
: null;
}
/**
* Overpass QL for the viewport. The tag match is a case-insensitive regex, not
* an exact `="ALPR"`, because OSM semicolon multi-values such as
* `surveillance:type=camera;ALPR` are common and an exact match drops them.
* Exported so the query shape is pinnable.
*/
export function buildOverpassQuery(south, west, north, east) {
return (
`[out:json][timeout:20];node["man_made"="surveillance"]["surveillance:type"~"(^|;)\\s*ALPR\\s*(;|$)",i]` +
`(${south},${west},${north},${east});out body ${QUERY_LIMIT};`
);
}
/** Validate a source-independent snapshot before replacing the accepted display. */
export function validateAlprSnapshot(snapshot) {
if (
!Array.isArray(snapshot?.records) ||
typeof snapshot.stale !== 'boolean' ||
typeof snapshot.saturated !== 'boolean'
) {
throw new TypeError('Camera source returned an invalid snapshot');
}
const ids = new Set();
for (const record of snapshot.records) {
if (
!record ||
typeof record.id !== 'string' ||
!record.id.trim() ||
ids.has(record.id) ||
!Number.isFinite(record.latitude) ||
Math.abs(record.latitude) > 90 ||
!Number.isFinite(record.longitude) ||
Math.abs(record.longitude) > 180 ||
(record.directionDeg != null &&
(!Number.isFinite(record.directionDeg) ||
record.directionDeg < 0 ||
record.directionDeg >= 360))
) {
throw new TypeError('Camera source returned an invalid record');
}
ids.add(record.id);
}
return snapshot;
}
/** Build a linked attribution from plain text and an HTTPS URL only. */
export function alprCreditMarkup(attribution) {
if (!attribution?.text || !attribution?.href) return null;
+155
View File
@@ -0,0 +1,155 @@
import { QUERY_LIMIT, QUERY_SNAP_DEGREES } from './policy.js';
export function textTag(value) {
const t = String(value ?? '').trim();
return t || null;
}
export function numTag(value) {
// `Number('')` and `Number(' ')` are 0, not NaN — a blank or whitespace-only
// tag value must read as missing, never as a due-north bearing.
if (value == null) return null;
const trimmed = String(value).trim();
if (!trimmed) return null;
const n = Number(trimmed);
return Number.isFinite(n) ? n : null;
}
/**
* Snap a viewport box OUTWARD to the query grid, clamped to valid ranges.
* Pure so the reuse contract is pinnable.
* @param {{south:number,west:number,north:number,east:number}} box
* @param {number} [step=QUERY_SNAP_DEGREES]
*/
export function snapAlprBox(box, step = QUERY_SNAP_DEGREES) {
const down = (v) => Math.floor(v / step) * step;
const up = (v) => Math.ceil(v / step) * step;
return {
south: Math.max(-90, down(box.south)),
west: Math.max(-180, down(box.west)),
north: Math.min(90, up(box.north)),
east: Math.min(180, up(box.east)),
};
}
/** Whether `inner` lies entirely within `outer` (no dateline handling; the viewport gate already rejects east <= west). */
export function boxContains(outer, inner) {
if (!outer || !inner) return false;
return (
inner.south >= outer.south &&
inner.north <= outer.north &&
inner.west >= outer.west &&
inner.east <= outer.east
);
}
/**
* Backoff progression for the unavailable-state retry: 30 s, doubling to a
* 240 s ceiling. Pure so the progression is pinnable without booting the layer.
*/
export function alprRetryDelayMs(prevDelayMs) {
const RETRY_MIN_MS = 30000;
const RETRY_CEIL_MS = 240000;
if (!Number.isFinite(prevDelayMs) || prevDelayMs <= 0) return RETRY_MIN_MS;
return Math.min(prevDelayMs * 2, RETRY_CEIL_MS);
}
/**
* Whether a `surveillance:type` value denotes an ALPR camera. OSM allows
* semicolon multi-values (`camera;ALPR`) and mixed case, so an exact match
* would silently miss tagged cameras.
* @param {*} value - raw `surveillance:type` tag value
* @returns {boolean}
*/
export function isAlprSurveillanceType(value) {
return String(value ?? '')
.split(';')
.some((part) => part.trim().toUpperCase() === 'ALPR');
}
/** Map one raw Overpass node element to a plain camera record. Null for anything unusable. */
export function normalizeAlprNode(el) {
if (
!el ||
el.type !== 'node' ||
!Number.isSafeInteger(el.id) ||
el.id <= 0 ||
!Number.isFinite(el.lat) ||
Math.abs(el.lat) > 90 ||
!Number.isFinite(el.lon) ||
Math.abs(el.lon) > 180
)
return null;
const tags = el.tags || {};
if (!isAlprSurveillanceType(tags['surveillance:type'])) return null;
return {
id: `alpr:${el.id}`,
osmId: el.id,
latitude: el.lat,
longitude: el.lon,
operator: textTag(tags.operator),
manufacturer: textTag(tags.manufacturer),
cameraType: textTag(tags['camera:type']),
zone: textTag(tags['surveillance:zone']),
// Only numeric bearings are parsed; compass-word directions ("N"/"NE")
// are rare on this tag and simply render with no facing line.
directionDeg: normalizeDirection(
tags['camera:direction'] ?? tags.direction,
),
ref: textTag(tags.ref),
lastVerified: textTag(tags.check_date) || textTag(tags['survey:date']),
source: textTag(tags.source),
};
}
export function normalizeDirection(value) {
const degrees = numTag(value);
return degrees != null && degrees >= 0 && degrees <= 360
? degrees % 360
: null;
}
/**
* Overpass QL for the viewport. The tag match is a case-insensitive regex, not
* an exact `="ALPR"`, because OSM semicolon multi-values such as
* `surveillance:type=camera;ALPR` are common and an exact match drops them.
* Exported so the query shape is pinnable.
*/
export function buildOverpassQuery(south, west, north, east) {
return (
`[out:json][timeout:20];node["man_made"="surveillance"]["surveillance:type"~"(^|;)\\s*ALPR\\s*(;|$)",i]` +
`(${south},${west},${north},${east});out body ${QUERY_LIMIT};`
);
}
/** Validate a source-independent snapshot before replacing the accepted display. */
export function validateAlprSnapshot(snapshot) {
if (
!Array.isArray(snapshot?.records) ||
typeof snapshot.stale !== 'boolean' ||
typeof snapshot.saturated !== 'boolean'
) {
throw new TypeError('Camera source returned an invalid snapshot');
}
const ids = new Set();
for (const record of snapshot.records) {
if (
!record ||
typeof record.id !== 'string' ||
!record.id.trim() ||
ids.has(record.id) ||
!Number.isFinite(record.latitude) ||
Math.abs(record.latitude) > 90 ||
!Number.isFinite(record.longitude) ||
Math.abs(record.longitude) > 180 ||
(record.directionDeg != null &&
(!Number.isFinite(record.directionDeg) ||
record.directionDeg < 0 ||
record.directionDeg >= 360))
) {
throw new TypeError('Camera source returned an invalid record');
}
ids.add(record.id);
}
return snapshot;
}
+3 -1
View File
@@ -4,7 +4,7 @@ import {
QUERY_SNAP_DEGREES,
QUERY_LIMIT,
} from './policy.js';
import { buildOverpassQuery, normalizeAlprNode } from './model.js';
import { buildOverpassQuery, normalizeAlprNode } from './records.js';
/** Construct the bounded OSM request adapter without starting a request. */
export function createOverpassAlprSource({
fetchImpl = (...args) => globalThis.fetch(...args),
@@ -80,3 +80,5 @@ export function createOverpassAlprSource({
},
};
}
export { buildOverpassQuery } from './records.js';
+7 -10
View File
@@ -1,3 +1,10 @@
export {
FRAME_ENDPOINT,
SOURCE_ENDPOINT,
HEALTH_ENDPOINT,
MEDIA_ENDPOINT,
ACTIVE_FRAME_REFRESH_MS,
} from './sourcePolicy.js';
import * as Cesium from 'cesium';
import { CCTV_CARD_FETCH_BURST_SPACING_MS } from '../../data/cctvCards.js';
import { CCTV_AMBIENT_CARD_MAX } from '../../data/cctvLod.js';
@@ -6,14 +13,6 @@ import { CCTV_AMBIENT_CARD_MAX } from '../../data/cctvLod.js';
// API endpoints
// ---------------------------------------------------------------------------
export const FRAME_ENDPOINT = '/api/cctv/frame';
export const SOURCE_ENDPOINT = '/api/cctv/sources';
export const HEALTH_ENDPOINT = '/api/cctv/health';
export const MEDIA_ENDPOINT = '/api/cctv/media';
// ---------------------------------------------------------------------------
// Timing and geometry constants
// ---------------------------------------------------------------------------
@@ -26,8 +25,6 @@ export const MAX_AUTO_HOP_SEC = 90;
export const HEALTH_SYNC_INTERVAL_MS = 7000;
export const ACTIVE_FRAME_REFRESH_MS = 10000;
export const IDLE_FRAME_REFRESH_MS = 60000;
export const PROJECTION_ACTIVE_REFRESH_MS = 10000;
+1 -1
View File
@@ -2,7 +2,7 @@ import {
ACTIVE_FRAME_REFRESH_MS,
FRAME_ENDPOINT,
MEDIA_ENDPOINT,
} from './policy.js';
} from './sourcePolicy.js';
function safeNumber(value, fallback = NaN) {
const n = Number(value);
return Number.isFinite(n) ? n : fallback;
+9
View File
@@ -0,0 +1,9 @@
export const FRAME_ENDPOINT = '/api/cctv/frame';
export const SOURCE_ENDPOINT = '/api/cctv/sources';
export const HEALTH_ENDPOINT = '/api/cctv/health';
export const MEDIA_ENDPOINT = '/api/cctv/media';
export const ACTIVE_FRAME_REFRESH_MS = 10000;
+1 -49
View File
@@ -99,52 +99,4 @@ export function mapAnalystRecord(raw, index = 0) {
};
}
/** Validate a complete feed before replacing the last good earthquake snapshot. */
export function normalizeEarthquakeSnapshot(geojson) {
if (!Array.isArray(geojson?.features)) return null;
const rows = [];
const ids = new Set();
for (const [index, feature] of geojson.features.entries()) {
const coordinates = feature?.geometry?.coordinates;
const properties = feature?.properties;
if (
!Array.isArray(coordinates) ||
coordinates.length < 2 ||
!properties ||
typeof properties !== 'object' ||
Array.isArray(properties) ||
(feature.geometry.type != null && feature.geometry.type !== 'Point')
)
return null;
const [lon, lat, depthKm] = coordinates;
const mag = properties.mag;
if (
!Number.isFinite(lon) ||
Math.abs(lon) > 180 ||
!Number.isFinite(lat) ||
Math.abs(lat) > 90 ||
(depthKm != null && !Number.isFinite(depthKm)) ||
(mag != null && (!Number.isFinite(mag) || mag > 10))
)
return null;
// A missing magnitude cannot establish that this event meets M2.5+.
if (mag == null || mag < 2.5) continue;
const stableId =
feature.id == null || feature.id === ''
? `event-${index + 1}`
: String(feature.id);
if (ids.has(stableId)) return null;
ids.add(stableId);
rows.push({
stableId,
usgsId: feature.id ?? null,
lon,
lat,
depthKm: depthKm ?? null,
mag,
place: typeof properties.place === 'string' ? properties.place : null,
time: Number.isFinite(properties.time) ? properties.time : null,
});
}
return rows;
}
export { normalizeEarthquakeSnapshot } from './records.js';
+49
View File
@@ -0,0 +1,49 @@
/** Validate a complete feed before replacing the last good earthquake snapshot. */
export function normalizeEarthquakeSnapshot(geojson) {
if (!Array.isArray(geojson?.features)) return null;
const rows = [];
const ids = new Set();
for (const [index, feature] of geojson.features.entries()) {
const coordinates = feature?.geometry?.coordinates;
const properties = feature?.properties;
if (
!Array.isArray(coordinates) ||
coordinates.length < 2 ||
!properties ||
typeof properties !== 'object' ||
Array.isArray(properties) ||
(feature.geometry.type != null && feature.geometry.type !== 'Point')
)
return null;
const [lon, lat, depthKm] = coordinates;
const mag = properties.mag;
if (
!Number.isFinite(lon) ||
Math.abs(lon) > 180 ||
!Number.isFinite(lat) ||
Math.abs(lat) > 90 ||
(depthKm != null && !Number.isFinite(depthKm)) ||
(mag != null && (!Number.isFinite(mag) || mag > 10))
)
return null;
// A missing magnitude cannot establish that this event meets M2.5+.
if (mag == null || mag < 2.5) continue;
const stableId =
feature.id == null || feature.id === ''
? `event-${index + 1}`
: String(feature.id);
if (ids.has(stableId)) return null;
ids.add(stableId);
rows.push({
stableId,
usgsId: feature.id ?? null,
lon,
lat,
depthKm: depthKm ?? null,
mag,
place: typeof properties.place === 'string' ? properties.place : null,
time: Number.isFinite(properties.time) ? properties.time : null,
});
}
return rows;
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { normalizeEarthquakeSnapshot } from './model.js';
import { normalizeEarthquakeSnapshot } from './records.js';
const API_URL =
'https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson';
/** Request and validate a complete USGS snapshot before it can replace displayed events. */
@@ -9,7 +9,7 @@ import {
import {
LOCAL_OVERLAY_COHORT_LIMIT,
createLocalInfrastructureOverlayEntry,
} from '../data/localGeojson.js';
} from '../data/localGeojsonCore.js';
import {
FIRMS_AMBIENT_COHORT_LIMIT,
FIRMS_OVERLAY_SOURCE_ID,
+1 -1
View File
@@ -1,4 +1,4 @@
import { createSourceSlot } from '../app/sourceSlot.js';
import { createSourceSlot } from '../sources/sourceSlot.js';
import { createApplicationRequestServices } from './requests.js';
// Compatibility owners are page-scoped, like the viewer and its layer registry.
+52
View File
@@ -0,0 +1,52 @@
/** Bind a source contract once for the application without replacing its layer instance. */
export function createSourceSlot(
initial,
methods,
label = 'Source',
optional = {},
) {
let active = initial;
let binding = null;
const source = Object.fromEntries(
[...methods, ...Object.keys(optional)].map((method) => [
method,
(...args) => {
if (typeof active?.[method] !== 'function' && optional[method])
return optional[method](...args);
if (typeof active?.[method] !== 'function')
throw new Error(`${label} is not configured`);
const owner = binding;
const provider = active;
const value = provider[method](...args);
if (value && typeof value.then === 'function')
return value.then((result) => {
if (binding !== owner || active !== provider)
throw new DOMException(`${label} was replaced`, 'AbortError');
return result;
});
return value;
},
]),
);
for (const property of ['label', 'attribution']) {
Object.defineProperty(source, property, {
enumerable: true,
get: () => active?.[property],
});
}
return {
source,
configure(next) {
if (methods.some((method) => typeof next?.[method] !== 'function'))
throw new TypeError(`Invalid ${label.toLowerCase()}`);
const owner = Symbol(label);
active = next;
binding = owner;
return () => {
if (binding !== owner) return;
active = null;
binding = null;
};
},
};
}
+179
View File
@@ -0,0 +1,179 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { execFileSync } from 'node:child_process';
import {
mkdtempSync,
mkdirSync,
writeFileSync,
rmSync,
symlinkSync,
} from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { analyzeModule } from '../../scripts/module-analysis.mjs';
import { checkImportDirections } from '../../scripts/check-import-directions.mjs';
function fixture(t, exports = {}) {
const root = mkdtempSync(path.join(os.tmpdir(), 'gev-directions-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
execFileSync('git', ['init', '--quiet'], { cwd: root });
const write = (name, source = '') => {
mkdirSync(path.dirname(path.join(root, name)), { recursive: true });
writeFileSync(path.join(root, name), source);
};
write(
'package.json',
JSON.stringify({ name: '@gods-eye-view/core', exports }),
);
write('src/data/feedState.js');
return { root, write };
}
test('module analysis parses dependencies and platform references without reading comments or property labels', () => {
assert.deepEqual(
analyzeModule(
"// import('node:fs'); document\nimport x from 'one'; export { x } from 'two'; export * from 'three'; import('four'); const data = { document: 1 }; data.window;",
),
{ imports: ['one', 'two', 'three', 'four'], browser: [] },
);
assert.deepEqual(
analyzeModule(
"const data = { element: document }; globalThis['navigator'];",
).browser,
['document', 'navigator'],
);
assert.throws(() => analyzeModule('import(name)'), /Computed/);
assert.throws(() => analyzeModule("require('node:fs')"), /ES imports/);
});
test('portable sources can share records, and standalone can assemble rendering', (t) => {
const { root, write } = fixture(t, {
'./layers/demo/source': './src/layers/demo/source.js',
});
write('src/layers/demo/source.js', "export { read } from './records.js';");
write('src/layers/demo/records.js', 'export const read = (value) => value;');
write('src/standalone/catalog.js', "import '../layers/demo/index.js';");
write('src/layers/demo/index.js', "import * as Cesium from 'cesium';");
assert.equal(checkImportDirections(root).modules, 5);
});
for (const [name, files, pattern] of [
[
'unreachable source imports rendering',
{
'src/layers/demo/source.js': "import './rendering.js';",
'src/layers/demo/rendering.js': '',
},
/Source imports rendering/,
],
[
'source reaches a platform through a record helper',
{
'src/layers/demo/source.js': "import './records.js';",
'src/layers/demo/records.js': "import 'cesium';",
},
/reaches platform\/rendering/,
],
[
'source reaches a DOM helper',
{
'src/sources/demo.js': "import '../data/helper.js';",
'src/data/helper.js': 'export const view = () => globalThis.document;',
},
/reaches browser globals/,
],
[
'browser imports Node without prefix',
{ 'src/unused.mjs': "import 'fs/promises';" },
/Node builtin/,
],
[
'browser hides Node outside src',
{
'src/unused.js': "import '../shared/helper.js';",
'shared/helper.js': "export * from 'node:fs';",
},
/Browser graph reaches Node/,
],
[
'browser imports server',
{
'src/data/demo.js': "import '../../server/helper.js';",
'server/helper.js': '',
},
/Browser imports server/,
],
[
'reusable module selects standalone',
{
'src/app/demo.js': "import '../standalone/catalog.js';",
'src/standalone/catalog.js': '',
},
/Reusable module imports standalone/,
],
[
'provider selects application setup',
{
'server/providers/demo.js': "import '../standalone/key-setup.js';",
'server/standalone/key-setup.js': '',
},
/Provider imports application/,
],
[
'voice controls reach protocol indirectly',
{
'src/voice/sessionCommands.js': "import './helper.js';",
'src/voice/helper.js': "import './realtimeSession.js';",
'src/voice/realtimeSession.js': '',
},
/Common voice controls import protocol/,
],
[
'dynamic import cannot evade direction',
{ 'src/app/demo.js': "const target = './module.js'; import(target);" },
/Computed/,
],
[
'browser imports test helper',
{
'src/app/demo.js': "import '../testSupport/helper.js';",
'src/testSupport/helper.js': '',
},
/non-runtime owner/,
],
]) {
test(`rejects ${name}`, (t) => {
const { root, write } = fixture(t);
for (const [file, body] of Object.entries(files)) write(file, body);
assert.throws(() => checkImportDirections(root), pattern);
});
}
test('self package imports cannot evade portable ownership', (t) => {
const { root, write } = fixture(t, { './view': './src/ui/view.js' });
write('src/sources/demo.js', "import '@gods-eye-view/core/view';");
write('src/ui/view.js');
assert.throws(() => checkImportDirections(root), /Source imports rendering/);
});
test('compatibility exceptions are limited to the two existing composition entries', (t) => {
const { root, write } = fixture(t);
write('src/ui.js', "import './standalone/catalog.js';");
write('src/standalone/catalog.js');
write('server/providers/local.js', "import '../standalone/key-setup.js';");
write('server/standalone/key-setup.js');
assert.doesNotThrow(() => checkImportDirections(root));
write('src/ui/new.js', "import '../standalone/catalog.js';");
assert.throws(
() => checkImportDirections(root),
/Reusable module imports standalone/,
);
});
test('symlink aliases do not hide renderer ownership', (t) => {
const { root, write } = fixture(t);
write('src/sources/demo.js', "import './helper.js';");
write('src/ui/view.js');
symlinkSync('../ui/view.js', path.join(root, 'src/sources/helper.js'));
assert.throws(() => checkImportDirections(root), /rendering|symlinks/);
});
+2 -53
View File
@@ -1,3 +1,5 @@
import { layerFeedState } from '../data/feedState.js';
export { layerFeedState } from '../data/feedState.js';
import { GUIDANCE_STATUSES } from '../loadingFeedback.js';
import { keySetupRequirement } from '../keySetupCore.mjs';
const FEED_STATE_LABELS = Object.freeze({
@@ -29,59 +31,6 @@ export function layerKeyRequirementTooltip(layer = {}) {
return requiresKeyId ? keySetupRequirement(requiresKeyId) : '';
}
/**
* Normalize heterogeneous layer stats into one honest control-chip state.
* @param {object|null} stats Layer getStats() result.
* @returns {'nominal'|'loading'|'degraded'|'stale'|'fallback'|'unavailable'} Feed state.
*/
export function layerFeedState(stats = {}) {
const state = stats || {};
const status =
typeof state.status === 'string' ? state.status.toLowerCase() : '';
const source = `${state.source || ''} ${state.coverage || ''}`;
const hasExplicitFallback = typeof state.fallback === 'boolean';
const hasPriorData = Number(state.count) > 0 || Boolean(state.lastUpdate);
const presentedError =
state.error || state.lastError || state.managerRefreshError;
if (['unavailable', 'offline', 'down', 'error'].includes(status))
return 'unavailable';
if (
(presentedError ||
state.unavailable === true ||
state.available === false) &&
!hasPriorData &&
!GUIDANCE_STATUSES.includes(status)
) {
return 'unavailable';
}
if (state.loading) return 'loading';
// Guidance states ask the user to act (zoom in, run a search) — normal
// operation, not feed faults. One honesty carve-out: layers keep their
// rendered records through the guidance state, so a genuinely stale cache
// still reads STALE; a guidance prompt alone never reads DEGRADED.
if (GUIDANCE_STATUSES.includes(status)) {
return state.stale ? 'stale' : 'nominal';
}
if (
state.fallback === true ||
status === 'fallback' ||
state.mode === 'sim' ||
/\bfallback\b/i.test(source) ||
(!hasExplicitFallback && /\badsb\.lol\b/i.test(source))
) {
return 'fallback';
}
if (state.stale || status === 'stale') return 'stale';
if (
state.degraded ||
presentedError ||
state.unavailable === true ||
state.available === false
)
return 'degraded';
return 'nominal';
}
/** Layer row presentation over supplied state and actions; no layer imports. */
export class LayerPanel {
constructor({
+1 -1
View File
@@ -11,7 +11,7 @@ import {
import { CCTV_FOCUS_RESULT } from '../layers/cctv/index.js';
import { contextModeWord } from '../contextModePolicy.js';
import { createAnalystEngine } from '../data/analystEngine.js';
import { layerFeedState } from '../data/manager.js';
import { layerFeedState } from '../data/feedState.js';
import { initCameraVerbs, moveCamera, flyRoute, interruptCameraMotion, adjustOrbitRange } from '../cameraVerbs.js';
import * as defaultFloorServices from '../data/groundFloor.js';
import { isPickedWorldPosition } from '../data/scenePick.js';