feat(entry): admin bypass of the presence gate for faulty radar/camera
The entry button (physical press AND the operator-issued mint) requires
radar/loop presence + camera detection to confirm a real vehicle. When one
of those devices is faulty, the gate blocks legitimate transient entry. Let
the ADMIN drop a specific signal as a requirement until support fixes the
hardware — the admin is not the adversary, but weakening an anti-fraud gate
stays attributed and auditable:
- Granular: bypass radar and camera independently (Setup → controller
section). A dead camera drops only the camera check; a dead radar only
radar. Both off = normal gate; both on = press-to-print.
- Signed: a DEDICATED endpoint (PUT /api/site-config/presence-bypass,
site:update) appends a signed config_change {setting, value, prev,
operator} per actually-changed signal — new ledger type. No-op toggles
sign nothing; disabling signs too. Kept out of the generic site PUT.
- Flagged: every vehicle_entry issued (and every refusal anomaly) while
bypassed carries presenceBypassed:[...] in its signed payload.
- Persists until turned off; amber warning in Setup while active. The
booth entry light treats a bypassed signal as satisfied (server
re-checks authoritatively). Physical-button path falls through to the
cooldown backstop when radar is bypassed.
- Migration 0020: two boolean site_config columns (default off).
Fixes a latent bug surfaced by the tests: firstRelayByDirection returned no
presenceInput, so issueForOperator's radar gate always read "presence loop
unavailable" — operator-issue never actually gated on radar. The resolver
now attaches the presence input serving the relay (mirrors relayForButton).
10 new tests: 5 gate combinations (each bypass drops only its signal +
records it), 5 route tests (RBAC, signed transitions, no-op, validation).
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
-- Entry presence-gate bypass (2026-07-02). The operator-issued entry button (and the
|
||||
-- physical entry button) require a REAL vehicle at the barrier: radar/loop presence AND a
|
||||
-- camera vehicle-detection. When a device is FAULTY (dead radar, dead camera), that gate
|
||||
-- blocks legitimate transient entry. These flags let the ADMIN drop a specific signal as a
|
||||
-- requirement until support fixes the hardware. Granular on purpose: a faulty camera drops
|
||||
-- only the camera check (radar still gates); a faulty radar drops only radar. Both null/0 =
|
||||
-- the normal both-required gate. Enabling/disabling is ALSO signed into the ledger
|
||||
-- (config_change) — the admin is not the adversary, but weakening an anti-fraud gate stays
|
||||
-- attributed + auditable. See wiki/concepts/entry-presence-bypass.md.
|
||||
ALTER TABLE `site_config` ADD `bypass_presence_radar` integer DEFAULT 0 NOT NULL;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `site_config` ADD `bypass_presence_camera` integer DEFAULT 0 NOT NULL;
|
||||
@@ -141,6 +141,13 @@
|
||||
"when": 1781886200000,
|
||||
"tag": "0019_operator_session_create",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 20,
|
||||
"version": "6",
|
||||
"when": 1781886300000,
|
||||
"tag": "0020_entry_presence_bypass",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -251,6 +251,20 @@ export const siteConfig = sqliteTable("site_config", {
|
||||
anprEntryEnabled: integer("anpr_entry_enabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(true),
|
||||
/** Entry presence-gate BYPASS (2026-07-02). The entry button — physical press and the
|
||||
* operator-issued mint — requires a real vehicle at the barrier: radar/loop presence AND
|
||||
* camera detection. When a device is FAULTY, the admin can drop one of those signals as a
|
||||
* requirement until support fixes it (the admin is not the adversary). Granular: a dead
|
||||
* camera → set bypassPresenceCamera (radar still gates); a dead radar → bypassPresenceRadar.
|
||||
* Both false (default) = the normal both-required gate; both true = press-to-print with no
|
||||
* presence check. Enabling/disabling is signed as a `config_change` and every ticket issued
|
||||
* while bypassed is flagged. Stored 0/1. See wiki/concepts/entry-presence-bypass.md. */
|
||||
bypassPresenceRadar: integer("bypass_presence_radar", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
bypassPresenceCamera: integer("bypass_presence_camera", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
/** IANA timezone the site operates in (e.g. "Europe/Tirane"). Used to evaluate a
|
||||
* tariff's wall-clock pricing windows (happy hour / night / seasonal). COPIED into
|
||||
* each published tariff version's structure.tz so the windows are frozen/immutable
|
||||
|
||||
@@ -265,6 +265,13 @@ export type LedgerEventType =
|
||||
// touch the drawer balance. Append-only, signed, so the decision is itself auditable.
|
||||
// See wiki/concepts/shift.md.
|
||||
| "cash_review"
|
||||
// Signed record of an admin changing a FRAUD-RELEVANT setting, so the change is
|
||||
// itself in the tamper-evident chain (who/when/what). Payload: { setting, value,
|
||||
// operator, prev? }. First use: the entry presence-gate bypass (a faulty radar/
|
||||
// camera lets the admin drop that signal as a requirement until support fixes it —
|
||||
// the admin is NOT the adversary, but weakening an anti-fraud gate must still be
|
||||
// attributed + auditable). See wiki/concepts/entry-presence-bypass.md.
|
||||
| "config_change"
|
||||
| "anomaly";
|
||||
|
||||
/** How money was tendered (for payment events + the shift Z-report). */
|
||||
|
||||
Reference in New Issue
Block a user