--- type: concept tags: [parking, architecture, devices, entry-flow] sources: [] updated: 2026-06-15 --- # Device Input Flow (button → backend → relay) How a physical button press drives the entry lane. The **backend is the source of truth**: the device only *reports* the press; the host decides and commands the relay. This is the host-in-the- loop flow the [[dingtian-relay]] makes possible (and the [[uhppote-controller]] could not). ## The path (no polling) ``` car arrives → driver presses button (input I_N, dry contact to GND) → device HTTP-pushes GET …/api/devices/dingtian//input//on → backend: emit internal device event (device-events bus) → backend entry flow: create + sign an entry event, print the ticket → backend: pulseOpen(N) over UDP → barrier opens → (on release) device pushes …/input//off ``` - **Push, not poll.** The device's `input_link_url` feature is configured (by the driver's `configureInputPush()`) to call the backend on each input edge — see [[dingtian-relay]]. The driver's poll path remains only as a dev/fallback aid. - **Per-input path** carries the input number in the URL (`…/input/3/on`), so routing needs no body parsing. Both edges (`on`/`off`) are sent. - **Internal event bus** (`device-events.ts`, a Node `EventEmitter`) decouples the HTTP/transport layer from business logic — drivers/pushes emit; the entry flow subscribes. Keeps the app [[device-adapter-pattern|device-agnostic]]. ## Trust model (important — flat network, no VLAN) The relay-control direction (host → device) is **unauthenticated UDP**, and the site is a **flat network with no VLAN** ([[network-isolation]] is not yet enforceable here). So we do **not** trust the device or the network. Instead: - **Every barrier open is a host decision, recorded as a signed event BEFORE the relay fires** ([[append-only-event-chain]]). If anyone opens the relay out-of-band (which the flat network allows), there is **no matching signed event → a detectable anomaly**. The anti-fraud guarantee is the **signed log**, not device/network auth. - The inbound push endpoint is intentionally **not behind the SPA's cookie/CSRF auth** (it's a machine call from the device). It is guarded by **HTTP Digest auth** + a **source-IP allowlist** (defence-in-depth), but these are *not* the security boundary. - This sharpens under the [[autonomous-direction|unmanned]] roadmap: with no operator, tamper detection via the signed log matters more than perimeter auth. ## Push authentication — Digest (decided by hardware testing) The secret must not be in the URL (sniffable, logged) and the password must not cross the wire in the clear. We **empirically tested the device** to pick the strongest achievable option: | Option | Device result | | --- | --- | | HTTPS (self-signed) | ❌ device won't push to a self-signed cert | | **Digest auth** (`auth=2`) | ✅ **works** — full 401-nonce challenge/response | | Basic auth | ✅ works (but password base64 on the wire) | | URL token | rejected by design (visible in URL/logs) | → **HTTP Digest** (MD5, qop=auth). The password is never sent (only a nonce-keyed hash); nonces are **single-use** (replay resistance). Per-device credentials (`pushUser`/`pushPassword`) are generated by the backend on **device assign**, written to the device's `input_link_url` config, and stored in `lane_devices` — the admin never types a URL or secret. HTTPS would be stronger but the device can't do it here; Digest + the signed log is the practical answer on a flat network. See `apps/server/src/digest-auth.ts`. ## Dingtian config-write gotchas (cost a lot of debugging) Writing the device's config API (`/api/v2/config_set.cgi`) has two non-obvious traps — both now handled in the driver: 1. **Content-Length is mandatory.** The device's embedded HTTP server does **not** accept chunked request bodies. Node uses chunked encoding when `Content-Length` is absent, so the device silently ignores the body and returns `{"status":0}` anyway — the write looks successful but nothing changes. Always set `Content-Length`. 2. **The `pass` field caps at 31 chars** (longer is silently truncated → Digest mismatch). The generated push password is 24 hex chars (96 bits). 3. (Also: the device reboots on apply, so the driver writes then **polls until the change is verified**, retrying — back-to-back writes onto a rebooting device are lost.) ## Status Input push **verified on hardware** with Digest auth (all 4 inputs, real presses authenticated, no failures). The entry flow itself (signed event + ticket print + `pulseOpen`) is the next build — see [[dingtian-relay]].