server: exit flow (pay-on-foot validation)
A credential read at an exit lane validates the session, then opens. Adds a 'read' channel to the device bus (DeviceReadEvent: ticket/plate/qr/card); entry stays button-driven so reads are exit/identity events. Flow: read -> fold the SIGNED ledger for that identity -> validate open + PAID + within gracePeriodExitMin -> signed vehicle_exit -> pulseOpen -> close the session cache. Unpaid / grace-expired / unknown -> signed anomaly, barrier stays closed (a deliberate business reject, not a fail-state; 'exit fails open' is about host/power loss). Validation reads the ledger (authoritative), not the cache. No payment events exist until the pay station is built, so every transient exit currently rejects -- the correct end-state, not yet passable. Verified against stubs: unpaid->anomaly+no-open; paid+grace->exit+open+closed; expired->anomaly; unknown->anomaly; verifyChain ok across entry->pay->exit. Flagged: lane_devices has no entry/exit direction model (exit door hardcoded to 1); needs a lane-direction/role model before multi-reader lanes.
This commit is contained in:
@@ -96,8 +96,25 @@ Permit sessions skip PAID: a valid [[permit]] at exit is itself the authorizatio
|
||||
|
||||
## What this unblocks (build order)
|
||||
|
||||
The device layer left the entry flow dangling — `input_received` events land in the log and stop
|
||||
([[device-input-flow]] "the entry flow itself is the next build"). The session domain is that next
|
||||
step: consume `input_received` / a reader event → mint a signed `vehicle_entry` → print + open.
|
||||
Then the pay-station and exit-validation flows. Schema + code follow this page and [[tariff]];
|
||||
the decision is recorded in [[session-model]].
|
||||
The device layer left the entry flow dangling — the session domain is that next step. Schema + code
|
||||
follow this page and [[tariff]]; the decision is recorded in [[session-model]].
|
||||
|
||||
### As-built (2026-06-15)
|
||||
|
||||
- **Entry flow** (`apps/server/src/entry-flow.ts`): access-device input edge → print ticket
|
||||
(failover) → signed `vehicle_entry` → `pulseOpen`. Holds (anomaly, no open, no entry) if printing
|
||||
fails. See [[device-input-flow]].
|
||||
- **Exit flow** (`apps/server/src/exit-flow.ts`): a credential **read** (new `read` bus channel) →
|
||||
fold the signed ledger for that identity → validate **open + PAID + within `gracePeriodExitMin`**
|
||||
→ signed `vehicle_exit` → `pulseOpen`. Unpaid / expired / unknown → signed `anomaly`, barrier
|
||||
stays closed. Validation folds the **ledger** (authoritative), then updates the `sessions` cache.
|
||||
- **Not a fail-state:** an unpaid reject keeps the barrier closed deliberately (driver returns to
|
||||
the pay station); "exit fails open" ([[fail-state-safety]]) is about the *system* being unable
|
||||
to decide (host/power loss), not an unpaid car.
|
||||
- **Currently every transient exit rejects** — no `payment` events exist until the pay station is
|
||||
built; the validation is the correct end-state, just not passable yet.
|
||||
|
||||
> **Design gap (flagged):** `lane_devices` has **no entry/exit direction** model. Entry is
|
||||
> button-driven and exit is read-driven, so they don't currently collide — but a lane with both an
|
||||
> entry reader and an exit reader can't yet be distinguished. A lane-direction/role model is needed
|
||||
> before multi-reader lanes (relates to [[open-questions]] #1 topology).
|
||||
|
||||
Reference in New Issue
Block a user