Add ARCHITECTURE.md with comprehensive codebase map covering service boundaries, expression system, session engine, sandbox, database layer, and architectural invariants. Add AGENTS.md as a concise guide for AI coding agents. Expand CONTRIBUTING.md with detailed code style, naming, import, testing, and git workflow conventions.
21 KiB
Architecture
This document describes the high-level architecture of GenieACS. If you want to familiarize yourself with the codebase, you are in the right place.
Bird's Eye View
GenieACS is a TR-069 Auto Configuration Server (ACS). It manages CPE devices (routers, modems, gateways) using the CWMP protocol (TR-069). The system consists of four network-facing services that share a MongoDB database:
+------------+
CPE Devices --->| CWMP (7547)|-+
+------------+ |
+-----------+ | +---------+
CPE Devices --->| FS (7567) |--+---->| MongoDB |
+-----------+ | +---------+
+-----------+ |
OSS / Scripts ->| NBI (7557)|--+
+-----------+ |
+-----------+ |
Administrators->| UI (3000) |--+
+-----------+
- CWMP -- The core TR-069 protocol server. CPE devices connect here to report their state and receive configuration instructions via SOAP/XML over HTTP.
- NBI -- Northbound Interface. A REST API for external systems (OSS/BSS, scripts, automation) to manage devices, tasks, presets, and configuration programmatically.
- FS -- File Server. Serves firmware images and configuration files to CPE devices during download operations.
- UI -- Web interface. A Koa-based backend serving a Mithril.js single-page application for administrators to browse devices, manage configuration, and trigger operations.
All four services follow the same process model: a primary process forks
configurable worker processes via Node.js cluster (see cluster.ts). Workers
connect to MongoDB and start an HTTP(S) server.
Code Map
bin/ Service entry points (5 executables)
lib/ All backend logic
common/ Shared utilities (expression engine, Path, errors)
expression/ Expression parser, evaluator, normalizer, minimizer
cwmp/ CWMP-service-specific DB and caching
db/ Database layer (MongoDB collections, query synthesis)
ui/ UI-service-specific API, DB, and caching
types/ (empty, reserved)
ui/ Frontend SPA (Mithril.js)
components/ Reusable UI components (parameter, tags, ping, etc.)
css/ Stylesheets (vanilla CSS)
icons/ SVG icons (compiled into a sprite)
test/ Unit tests (Node.js native test runner)
build/ Build scripts (esbuild-based)
public/ Static assets (logo, favicon)
Entry Points (bin/)
Each file in bin/ bootstraps one service. They are structurally identical:
initialize logging, read config, fork workers in the primary process, connect to
MongoDB and start the HTTP server in each worker.
genieacs-cwmp.ts-- CWMP service. Unique in that it disables HTTP keep-alive (keepAliveTimeout: 0) and provides customonConnection/onClientErrorhooks for TR-069 session lifecycle management.genieacs-nbi.ts-- NBI service. Straightforward REST API server.genieacs-fs.ts-- File server. The leanest service; does not use the extensions subsystem.genieacs-ui.ts-- UI service. Wraps the Koa application as the HTTP listener.genieacs-ext.ts-- Not a service. This is a child process worker spawned by the extensions subsystem. It communicates with its parent via IPC, executing user-defined extension scripts in isolation.
The Expression System (lib/common/expression/)
The Expression type (string | number | boolean | null | any[]) is the most
important abstraction in the codebase. It is a recursive, Lisp-like S-expression
where scalars are literal values and arrays are operations (the operator at
index 0, operands following). For example:
["AND", ["=", ["PARAM", "Device.ModelName"], "BrandX"], [">", ["PARAM", "Events.Inform"], 1000]]
Expressions are used pervasively:
- Query/filter language -- Database queries for devices, faults, presets, etc. are all represented as expressions and compiled to MongoDB filters.
- Configuration values -- Config entries and preset preconditions are expressions, enabling dynamic evaluation.
- Authorization -- Permission filters and validators are expressions.
- Pagination cursors -- Keyset pagination boundaries are expressed as filter expressions.
The expression pipeline flows through four modules:
parser.ts-- Parses a SQL-like text syntax into the AST (uses Parsimmon). Also providesmap()/mapAsync()tree-walking primitives andstringify()for serialization.normalize.ts-- Algebraic normalization using exact rational polynomial arithmetic over bigints. Ensures equivalent expressions have the same canonical form (e.g.,a + 2 > banda > b - 2normalize identically).synth.ts-- Boolean logic minimization via the Espresso algorithm (espresso-iisojs). Converts expressions to minimal sum-of-products form with three-valued logic (true/false/null). Handles domain-specific constraints like comparison ordering and LIKE pattern relationships.util.ts-- Runtime evaluation. ResolvesPARAMreferences against concrete data, evaluates operators, and supports partial evaluation (returns a reduced expression if some values are unknown).
bigint.ts provides a named-function wrapper around native bigint operations
for readability. pagination.ts implements cursor-based pagination by
generating filter expressions from sort-key bookmarks.
The Path System (lib/common/path.ts, lib/common/path-set.ts)
Path represents a TR-069 parameter path (e.g., Device.WiFi.SSID.1.Name).
Paths can contain wildcards (*) and alias expressions ([key:value]) for
query-based addressing.
Key design decisions:
- Interned/flyweight -- All
Pathinstances are cached.Path.parse()is the only way to create them; the constructor is protected. Two-generation LRU cache rotated every 120 seconds. - Bitmask encoding -- The
wildcardandaliasfields are bitfields for O(1) segment-type checking. This limits paths to 32 segments. - Immutable -- Segment arrays are
Object.freeze()-d.
PathSet is a multi-indexed collection of paths supporting pattern-matching
queries. It indexes paths by length, by segment-at-position, and by string
representation. The find() method uses set intersection to efficiently match
wildcard patterns.
CWMP Protocol Layer (lib/cwmp.ts, lib/soap.ts, lib/xml-parser.ts)
xml-parser.ts is a custom single-pass XML parser (no DOM). It scans
character-by-character with bitwise state flags, building a tree of elements
with namespace support. Does not support CDATA.
soap.ts handles both parsing CPE SOAP messages and generating ACS SOAP
responses. It dispatches on the SOAP body's method name to type-specific parsers
for Inform, TransferComplete, GetParameterNamesResponse, etc. Supports CWMP
versions 1.0 through 1.4.
cwmp.ts is the HTTP-level CWMP request handler. It manages the session state
machine:
- State 0 -- Expects an Inform. Authenticates the device (Basic or Digest auth, configurable via expression). Acquires a distributed lock. Loads device data from MongoDB. Sends InformResponse.
- State 1 -- Waits for the CPE to send an empty POST (ready for ACS RPCs). Processes any TransferComplete messages.
- State 2 -- The ACS drives RPCs (GetParameterNames, GetParameterValues, SetParameterValues, AddObject, DeleteObject, Download, Reboot, FactoryReset). The CPE responds to each.
Session persistence across TCP disconnects: when a socket closes mid-session,
the entire SessionContext is serialized to Redis (the MongoDB cache
collection) and restored when the CPE reconnects (identified by a session
cookie).
Session Engine (lib/session.ts)
The session engine implements the declaration-driven data fetching pattern.
Rather than issuing RPCs imperatively, provisions create Declaration objects
stating what paths and attributes they need to read or write. The engine then:
- Processes all declarations into a
SyncState-- a structured plan of which parameters to refresh, which values to set, which instances to create/delete, etc. - Generates the minimal set of CWMP RPCs needed to fulfill the plan.
- After each RPC response, updates
DeviceDataand re-evaluates. - Iterates until all declarations are satisfied.
The preset system (applyPresets in cwmp.ts) implements a policy engine:
presets are rules with precondition expressions that, when matched, contribute
provisions to the session. After provisions execute, if device data changed,
presets are re-evaluated (up to 4 cycles to prevent infinite loops).
Provisions and Virtual Parameters
Provisions are the unit of configuration intent. Built-in provisions
(default-provisions.ts) include refresh, value, tag, reboot, reset,
download, and instances. Custom provisions are user-defined JavaScript
scripts stored in the provisions MongoDB collection.
Virtual parameters are scripts that present computed/derived values as if
they were real device parameters under the VirtualParameters.* namespace. They
run in two phases: a "get" phase reads real parameters and returns a computed
value; a "set" phase translates a desired value into real parameter changes.
Virtual parameters can reference other virtual parameters (up to depth 8).
Sandbox (lib/sandbox.ts)
The sandbox provides a secure execution environment for provision and virtual
parameter scripts using vm.Script with a 50ms timeout. It uses a
replay-based execution model:
- A script runs and calls
declare()to request data. - When
commit()is called, the script throws a sentinel symbol and exits. - The engine fetches the requested data via CWMP RPCs.
- The script is re-run from the beginning with the fetched data available.
- Earlier
declare()calls return cached results; the script progresses further. - This repeats until the script completes without throwing.
The sandbox API: declare(path, timestamps, values) returns a
ParameterWrapper proxy; clear(path, timestamp, attributes) invalidates
cached data; ext(...args) calls external extensions (results are cached per
revision to survive replays); commit() explicitly triggers a fetch cycle.
Math.random() is replaced with a seeded PRNG for determinism.
Device Data Model (lib/types.ts, lib/device.ts)
DeviceData is the in-memory working copy of a device's parameter tree during a
session:
paths: PathSet-- All known parameter paths.timestamps: VersionedMap<Path, number>-- When each path was last refreshed.attributes: VersionedMap<Path, Attributes>-- Per-path attributes (object, writable, value, notification, accessList), each paired with a timestamp.trackers/changes-- Change tracking for re-evaluation.
VersionedMap (in versioned-map.ts) provides multi-revision snapshots,
enabling the sandbox replay model where scripts may be re-run at different
revision levels.
device.ts handles setting and clearing parameter data with invariant
enforcement (e.g., if value is set, object is forced to 0; parent paths are
ensured to exist). The unpack() function resolves wildcards and alias paths
against concrete device data.
NBI (lib/nbi.ts)
A raw Node.js HTTP listener (no framework) with regex-based URL routing. Endpoints include CRUD for presets, provisions, virtual parameters, objects, and files; device task management (with optional synchronous execution via connection request); fault management; generic collection querying; and ping.
The NBI uses MongoDB-style JSON queries directly. For the devices collection,
query.ts expands user-friendly queries by auto-appending ._value to
parameter paths and generating multi-type interpretations (string, number, date,
regex) for filter values.
File Server (lib/fs.ts)
A minimal HTTP file server reading from MongoDB GridFS. Supports GET/HEAD with full HTTP caching (ETag, Last-Modified, If-None-Match, If-Modified-Since) and Range requests (HTTP 206) for partial content downloads. Files are cached in-memory via memoization.
UI Backend (lib/ui.ts, lib/ui/)
A Koa application with JWT authentication, role-based authorization, and a rich
CRUD API under /api/. The root route serves an HTML shell that bootstraps the
Mithril.js SPA with injected config, user info, and hashed asset filenames.
lib/ui/api.ts defines generic CRUD endpoints for all resource types (devices,
presets, provisions, files, config, users, permissions, faults, tasks) with
authorization checks at every level. Specialized endpoints handle file
upload/download, synchronous task execution, tag management, password changes,
and CSV export.
lib/ui/db.ts translates between the UI's flat parameter representation and
MongoDB's nested document structure. The flattenDevice() function is the key
transformation: it recursively walks the nested device document and produces a
flat key-value map (e.g.,
Device.WiFi.SSID.1.Name -> {value: [...], writable: true}).
lib/ui/local-cache.ts caches permissions, users, and config in-process with
hash-based revision tracking. The getConfig() function resolves config
expressions with dynamic context and backward compatibility for legacy config
keys.
Database Layer (lib/db/)
db/db.ts is the single entry point to MongoDB. It manages 14 collections:
| Collection | Purpose |
|---|---|
devices |
CPE device parameter trees |
presets |
Configuration rules |
provisions |
Provision scripts |
virtualParameters |
Virtual parameter scripts |
objects |
Generic objects |
tasks |
Queued device management tasks |
faults |
Error records with retry state |
operations |
In-flight async operations |
files (GridFS) |
Firmware images and config files |
permissions |
RBAC rules |
users |
User accounts |
config |
Key-value configuration |
cache |
Distributed cache (TTL index) |
locks |
Distributed locks (TTL index) |
db/synth.ts is the sophisticated expression-to-MongoDB query compiler. It
normalizes expressions, converts them to a Boolean satisfiability representation
using the Clause hierarchy, minimizes via Espresso, and emits MongoDB
$and/$or/$not filter objects. This is used by the UI backend.
cwmp/db.ts handles CWMP-specific persistence: loading the nested device
document into DeviceData (fetchDevice), diffing changes back into MongoDB
update operations (saveDevice), and managing faults, tasks, and operations.
Query Systems
There are two separate query paths:
-
NBI queries (
lib/query.ts) -- Processes MongoDB-style JSON queries from external API consumers. Expands parameter names, generates multi-type value interpretations, and passes through to MongoDB directly. -
UI/Expression queries (
lib/db/synth.ts) -- Compiles the internal expression language into optimized MongoDB filters via Boolean minimization. This is the more sophisticated path, used by the UI backend.
Frontend (ui/)
A Mithril.js SPA with hash-based routing (#!/). Key modules:
app.ts-- Route definitions. Thepagify()function wraps each page into aRouteResolverthat handles initialization, error boundaries, and data fulfillment.layout.ts-- Top-level layout (header, navigation, content, overlay).store.ts-- Centralized data store. Implements a query-based reactive cache with deduplication and incremental fetching. Thefulfill()method (called after every render) batches pending queries, computes filter diffs viaunionDiff(), and fetches only missing data. Connection monitoring polls every 3 seconds for server health, clock skew, and config changes.components.ts-- Component registry with a context propagation system. The proxiedm()function resolves string component names and them.context()API passes data (like the current device object) down the component tree without explicit prop threading.smart-query.ts-- Translates user-friendlyLabel: valuesearches into filter expressions with type-aware matching (string, number, timestamp, MAC address, tag).task-queue.ts-- Two-stage task pipeline: staging (user configures tasks) then queue (tasks are committed and executed via the backend).dynamic-loader.ts-- Lazy loading of heavy libraries (CodeMirror, YAML) via dynamicimport().
Build System (build/)
The build is a self-bootstrapping esbuild pipeline (npm run build pipes
build/build.ts through esbuild then node). It produces:
- Backend binaries -- 5 entry points bundled for Node.js 12+ with shebang
banners, executable permissions, and
.jsextension stripped. - Frontend bundle --
ui/app.tsbundled for browsers (ESM, code-split) with content-hashed filenames. - CSS --
ui/css/app.cssbundled and minified with content-hashed output. - SVG sprite -- All icons in
ui/icons/optimized via SVGO and combined into a single sprite.
build/assets.ts is a compile-time bridge: at rest it contains placeholder
filenames; during build, the assetsPlugin replaces them with actual
content-hashed names so both backend and frontend reference the correct assets.
Build metadata includes the date and a hash derived from the git state, appended to the package version.
Cross-Cutting Concerns
Multi-Level Caching
memoize.ts In-process function cache (2-4 min, two-generation rotation)
|
local-cache.ts In-process snapshot cache (5s refresh, hash-based revisions)
|
cache.ts MongoDB-backed distributed cache (configurable TTL)
|
MongoDB TTL index auto-expiration
Each service has its own local-cache that periodically checks the distributed
cache for staleness. Distributed locks (lock.ts) coordinate rebuilds so only
one worker does the expensive computation.
Configuration (lib/config.ts)
Three-tier priority: CLI args > environment variables (GENIEACS_*) > config
file (config.json) > defaults. Supports per-device overrides by appending
-OUI-ProductClass-SerialNumber to option names.
Distributed Locking (lib/lock.ts)
MongoDB-based mutual exclusion using upsert + duplicate key detection. TTL indexes prevent deadlocks from crashed processes. Clock skew tolerance of 30 seconds.
Authentication
- CWMP devices -- HTTP Basic or Digest auth, configurable per-device via
expressions (
auth.ts). - UI users -- JWT tokens in cookies. Passwords hashed with PBKDF2-SHA512
(10000 iterations). Role-based authorization via
Authorizerwith expression-based filters and validators.
Connection Requests (lib/connection-request.ts)
Three methods to ask a CPE device to initiate a CWMP session:
- HTTP -- Standard TR-069 connection request with Digest/Basic auth.
- UDP -- For NAT traversal (STUN-based) with HMAC-SHA1 signed messages.
- XMPP -- TR-069 Annex K via a full XMPP client (
xmpp-client.ts).
Extensions (lib/extensions.ts)
User-defined scripts executed in long-lived child processes (genieacs-ext.ts)
communicating via IPC. Processes are lazily spawned per script name and reused.
Each request gets a unique ID; responses are matched by ID with a configurable
timeout.
Logging (lib/logger.ts)
Dual-stream structured logging (application + access logs). Supports simple and
JSON formats, systemd journal integration, and automatic log rotation detection.
Protocol traces can be written to a debug file in YAML or JSON format
(debug.ts).
Three-Valued Logic
The system consistently implements SQL-style three-valued logic
(true/false/null). This is visible in expression evaluation, the Clause
hierarchy's separate true/false/null methods, and the 2-bit minterm encoding in
the Boolean minimizer. NULL means "unknown" and propagates through operations
following SQL semantics.
Architectural Invariants
-
The expression system does not depend on any service-specific code. The
lib/common/expression/modules are pure and shared across all services. -
The sandbox is deterministic across replays.
Math.random()is seeded from the device ID,Date.now()is controlled, and extension results are cached. A script re-run with the same inputs produces the same outputs. -
Services share no in-process state. All cross-process coordination goes through MongoDB (the
cacheandlockscollections). Each worker process is independent. -
Device data is never mutated in place during a session.
VersionedMapprovides revision-based snapshots. The sandbox writes to new revisions; earlier revisions remain readable for re-evaluation. -
CWMP session exclusivity. A distributed lock (
cwmp_session_<deviceId>) ensures only one session exists per device at a time. The lock is refreshed periodically and released at session end. -
The UI backend never queries MongoDB with raw user input. All queries go through the expression-to-MongoDB compiler (
db/synth.ts), which validates and normalizes expressions before generating filters.