server: permit entry/exit branch + read dispatcher

A credential read now routes by what the credential IS: matches a permit
(card/QR credential or a bound plate) -> permit flow; else -> transient exit
flow. Lane resolved once (readerLaneWithAccess); ExitFlow.onRead -> handleAt so
the dispatcher owns lane resolution.

Permit direction is inferred from session state for that car (the read value is
the per-car session key): no open session -> ENTRY (enforce maxConcurrent, sign
vehicle_entry, open); open -> EXIT (sign vehicle_exit, open, close). Fleet
permit = one session per car; anti-passback falls out naturally.

maxConcurrent enforced as a fold over the signed ledger (null = unbound).
Validity window + status + plate-OR-card identity as designed. No ticket/fee;
every use is a signed event carrying permitId. Refusals (revoked / out-of-window
/ at-capacity) are signed anomalies, barrier stays closed.

Verified against stubs: card entry -> inferred exit; fleet cap 2 (F3 rejected
at 2/2, then admitted after F1 exits); plate-bound opens; revoked rejects;
unknown credential falls through to exit reject; verifyChain ok.
This commit is contained in:
2026-06-15 19:47:01 +02:00
parent b4d0dfadd6
commit c24d99b0f4
8 changed files with 317 additions and 29 deletions
+16
View File
@@ -548,3 +548,19 @@ guarantee. Recorded in [[dingtian-relay]] (new Hardening section).
valid→201 createdBy=admin; readonly publish→403; after publish the pay station quote returns 404
(session) not 409 (no tariff) — i.e. it now sees the active card. Full build 5/5.
- Updated [[tariff]] (composer as-built).
## [2026-06-15] build | Permit entry/exit branch + read dispatcher
- `apps/server/src/permit-flow.ts` + `read-dispatch.ts`. A credential read now routes by WHAT the
credential is: matches a permit (card/QR credential, or a bound plate) → permit flow; else →
transient exit flow. Lane resolved once (`readerLaneWithAccess`, shared in lane-map.ts). Refactored
ExitFlow.onRead → handleAt(lane,e) so the dispatcher owns lane resolution.
- Permit DIRECTION inferred from session state for that car (the read value is the per-car session
key): no open session → ENTRY (enforce maxConcurrent, sign vehicle_entry, open); open → EXIT (sign
vehicle_exit, open, close). Fleet permit = one session per car; anti-passback falls out.
- maxConcurrent enforced as a fold over the signed ledger (count the permit's entries whose car has
no later exit); null = unbound. Validity window + status + plate-OR-card identity as designed.
No ticket/fee; every use is a signed event carrying permitId. Refusals = signed anomaly, no open.
- VERIFIED against stubs: card entry → inferred exit; fleet maxConcurrent=2 (F1,F2 in, F3 rejected,
F1 exits → F3 enters); plate-bound permit opens; revoked → reject; unknown credential falls through
to exit-flow reject (not mis-read as permit); verifyChain ok. Full build 5/5.
- Updated [[permit]] (as-built), [[parking-session]] (read dispatch).