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:
2026-06-15 18:57:14 +02:00
parent 2696d281ce
commit 2a36830880
5 changed files with 263 additions and 5 deletions
+17
View File
@@ -500,3 +500,20 @@ guarantee. Recorded in [[dingtian-relay]] (new Hardening section).
new custody/session shape. Captured as [[valet-overcapacity]] + made [[capacity-occupancy]] FULL a
soft policy; NOT built into the entry flow (clean seam left). Deferred.
- New page [[valet-overcapacity]]; updated [[capacity-occupancy]], [[index]].
## [2026-06-15] build | Exit flow (pay-on-foot validation)
- Built `apps/server/src/exit-flow.ts`. Added a `read` channel to the device bus (DeviceReadEvent:
ticket/plate/qr/card) — readers/LPR emit reads; entry stays button-driven, so reads are
unambiguously exit/identity events for now.
- 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.
- Pay station doesn't exist yet → no `payment` events → every transient exit currently REJECTS.
Correct end-state, not passable until pay-station lands (decided).
- VERIFIED against stubs: unpaid→anomaly+no-open; paid+grace→vehicle_exit+open+closed; grace-expired
→anomaly; unknown ticket→anomaly; verifyChain ok across entry→pay→exit.
- GAP flagged: lane_devices has no entry/exit DIRECTION model (door mapping hardcoded to 1 for exit);
fine while entry=button/exit=read, but multi-reader lanes need a lane-direction/role model (ties to
[[open-questions]] #1). Updated [[parking-session]] as-built + gap, [[index]].