Append-only signed event log; persist Dingtian input pushes
Implement the core anti-fraud primitive: an append-only, hash-chained, signed event log (the schema + types predated this; the writer/signer are new). - EventLog (apps/server): serialized append, monotonic index, prevHash chain, signature; verifyChain() detects tamper/reorder/delete. No update/delete paths. - Signer abstraction (packages/shared) over the ATECC608 secure element, with a SoftwareSigner (HMAC, EVENT_SIGNING_KEY) shipped now since the chip is still open-question #6. Documented: software signer is tamper-evident but NOT unforgeable-by-owner. - Add ParkingEventType "input_received" for raw device inputs (not yet a vehicle_entry, which the entry flow will append later). - Read API: GET /api/events; integrity self-check: GET /api/events/verify (admin). Verified on hardware: shorting the Dingtian inputs produced signed, chained input_received events; verifyChain ok; direct DB tamper/delete detected. NOTE: the log captures host-originated actions only. Out-of-band relay actuation (sniffed relay_pw, string protocol, ip_watchdog) produces no event by design -- the control is reconciliation vs. an independent witness, which is not yet built. See wiki/concepts/append-only-event-chain.md.
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { desc, events, type Db } from "@parking/db";
|
||||
import { requireRole } from "../auth.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<void> {
|
||||
// Any authenticated role may read the log (it's the audit trail).
|
||||
const guard = requireRole("admin", "operator", "cashier", "readonly");
|
||||
|
||||
// Recent events, newest first. `limit` caps the page (default 100, max 1000).
|
||||
app.get<{ Querystring: { limit?: string } }>(
|
||||
"/api/events",
|
||||
{ preHandler: guard },
|
||||
async (req) => {
|
||||
const limit = Math.min(Math.max(Number(req.query.limit) || 100, 1), 1000);
|
||||
const rows = db.select().from(events).orderBy(desc(events.index)).limit(limit).all();
|
||||
return { events: rows };
|
||||
},
|
||||
);
|
||||
|
||||
// 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: requireRole("admin") },
|
||||
async () => eventLog.verifyChain(),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user