import type { FastifyInstance } from "fastify"; import { desc, gte, ledgerEvents, type Db } from "@parking/db"; import type { LedgerEvent } from "@parking/shared"; import { requirePermission } from "../auth.js"; import { enrichEvent } 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 the page to events at/after that instant — the // booth passes the current shift's start so the live feed shows ONLY this shift's // activity (logs are per-shift, not all history). See wiki/concepts/shift.md. app.get<{ Querystring: { limit?: string; since?: 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 rows = db .select() .from(ledgerEvents) .where(since ? gte(ledgerEvents.occurredAt, since) : undefined) .orderBy(desc(ledgerEvents.index)) .limit(limit) .all(); // Attach read-time display fields (e.g. subscriber name) without touching the // signed record. The cast bridges the Drizzle row to the shared LedgerEvent. const events = rows.map((r) => enrichEvent(db, r 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(), ); }