import type { FastifyInstance } from "fastify"; import { and, desc, gte, lte, ledgerEvents, type Db } from "@parking/db"; import type { LedgerEvent } from "@parking/shared"; import { requirePermission } from "../auth.js"; import { enrichEvents } from "../event-enrich.js"; import type { EventLog } from "../event-log.js"; // Read access to the append-only signed event log. NO write/update/delete routes // exist by design — events are only ever appended internally (entry flow, device // pushes). Corrections are new appended events, never edits. See // wiki/concepts/append-only-event-chain.md. export async function eventRoutes( app: FastifyInstance, db: Db, eventLog: EventLog, ): Promise { // Reading the log (the audit trail). const guard = requirePermission("event:read"); // Recent events, newest first. `limit` caps the page (default 100, max 1000). // Optional `since` (ISO) scopes to events at/after that instant — the booth passes // the current shift's start so the live feed shows ONLY this shift's activity. An // optional `until` (ISO) closes the upper bound — the shift-history screen passes a // selected shift's [start, end] to show just that shift's signed activity log. // (logs are per-shift, not all history). See wiki/concepts/shift.md. app.get<{ Querystring: { limit?: string; since?: string; until?: string } }>( "/api/events", { preHandler: guard }, async (req) => { const limit = Math.min(Math.max(Number(req.query.limit) || 100, 1), 1000); const since = (req.query.since ?? "").trim(); const until = (req.query.until ?? "").trim(); const bounds = [ since ? gte(ledgerEvents.occurredAt, since) : undefined, until ? lte(ledgerEvents.occurredAt, until) : undefined, ].filter(Boolean); const rows = db .select() .from(ledgerEvents) .where(bounds.length ? and(...bounds) : undefined) .orderBy(desc(ledgerEvents.index)) .limit(limit) .all(); // Attach read-time display fields (subscriber name, advisory plate) without // touching the signed record. One plate scan for the whole page (enrichEvents). // The cast bridges the Drizzle row to the shared LedgerEvent. const events = enrichEvents(db, rows as unknown as LedgerEvent[]); return { events }; }, ); // Integrity self-check: walk the chain and verify hashes + signatures. Admin- // only (it's an audit action). Returns the first break, or ok. This is what a // reconciliation job / "is the log intact?" check calls. app.get( "/api/events/verify", { preHandler: requirePermission("event:read") }, async () => eventLog.verifyChain(), ); }