import type { FastifyInstance } from "fastify"; import { requirePermission } from "../auth.js"; import type { EntryFlow } from "../entry-flow.js"; import type { LaneStatus } from "../lane-status.js"; import type { ShiftService } from "../shift-service.js"; import { NoShiftOpenError } from "../shift-service.js"; // Operator-issued entry (2026-07-01). When the physical entry button is broken, an operator // may issue an entry ticket — a FLAGGED mint (vehicle_entry source=manual + operatorInitiated // + a companion anomaly), gated EXACTLY like the physical button: a real vehicle must be // present (radar/loop AND camera). The presence gate is enforced HERE (server-side), so a // direct POST can't bypass a disabled UI button. Money-adjacent → requires an open shift. // See wiki/concepts/operator-issued-entry.md. export async function entryRoutes( app: FastifyInstance, entryFlow: EntryFlow, laneStatus: LaneStatus, shift: ShiftService, ): Promise { const guard = requirePermission("session:create"); app.post("/api/entry/issue", { preHandler: guard }, async (req, reply) => { // Gate on an open shift (a minted entry belongs to an accountable operator). if (!shift.currentOpenShift()) { return reply.code(409).send({ error: new NoShiftOpenError().message }); } // The camera side of the presence gate = the live entry lane-busy state; the radar/loop // side is checked inside the flow (its per-relay presence guard). const cameraBusy = laneStatus.snapshot().entry; const res = await entryFlow.issueForOperator(req.user.username, cameraBusy); if (!res.ok) return reply.code(409).send({ error: res.reason }); return res; }); }