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:
@@ -44,12 +44,9 @@ export class ExitFlow {
|
||||
this.#logger = logger;
|
||||
}
|
||||
|
||||
/** Handle a credential read at an exit lane. */
|
||||
async onRead(e: DeviceReadEvent): Promise<void> {
|
||||
// Resolve which lane this reader belongs to, and that it's an exit reader.
|
||||
const lane = await this.#exitLaneFor(e.deviceId);
|
||||
if (lane == null) return; // not an exit-lane reader — ignore (other flows may handle)
|
||||
|
||||
/** Handle a transient-ticket read at a known exit lane (lane pre-resolved by the
|
||||
* read dispatcher, which has already ruled out a permit match). */
|
||||
async handleAt(lane: number, e: DeviceReadEvent): Promise<void> {
|
||||
const key = `${e.deviceId}:${e.value}`;
|
||||
if (this.#inFlight.has(key)) return;
|
||||
this.#inFlight.add(key);
|
||||
@@ -160,21 +157,6 @@ export class ExitFlow {
|
||||
};
|
||||
}
|
||||
|
||||
/** The lane this reader belongs to, IF that lane has an access (barrier) device
|
||||
* to open. A read event is an identity/exit signal (entry is button-driven), so
|
||||
* any read at an access-equipped lane is treated as an exit attempt for now.
|
||||
* (Distinguishing entry vs. exit readers per lane is a later lane-direction model.) */
|
||||
async #exitLaneFor(deviceId: string): Promise<number | null> {
|
||||
const row = await this.#db.select().from(laneDevices).where(eq(laneDevices.id, deviceId)).get();
|
||||
if (!row || !row.enabled) return null;
|
||||
const access = await this.#db
|
||||
.select()
|
||||
.from(laneDevices)
|
||||
.where(and(eq(laneDevices.category, "access"), eq(laneDevices.lane, row.lane)))
|
||||
.get();
|
||||
return access && access.enabled ? row.lane : null;
|
||||
}
|
||||
|
||||
/** The lane's access device, to open the exit barrier. */
|
||||
async #exitAccess(lane: number): Promise<AccessControlDevice | null> {
|
||||
const row = await this.#db
|
||||
|
||||
Reference in New Issue
Block a user