8c2cf93067
Implements the wiki design in packages/db + packages/shared. Event split: rename events -> ledger_events (signed business ledger) and add device_events (unsigned telemetry). ledger_events gains a signed JSON payload (amount/tariffVersionId/sessionRef/tender…) + keyId; canonicalize() includes the payload via sorted-key serialization so business data is tamper-evident. Raw Dingtian input now writes device_events, not a signed input_received. New tables: tariffs + immutable tariff_versions (composable/versioned, currency + FX-ready), permits (+ permit_credentials, permit_plates; maxConcurrent default 1), blocklist, sessions (rebuildable projection cache — not a source of truth). shared: split ParkingEvent/Type into LedgerEvent/LedgerEventType + DeviceEventKind; add LedgerPayload, Tender, TariffStructure/TariffBlock. Regenerated a single baseline migration (no production chain data existed). Verified: chain appends + verifyChain ok; tampering a payment payload breaks the signature. Full repo builds (5/5).
39 lines
1.5 KiB
TypeScript
39 lines
1.5 KiB
TypeScript
import type { FastifyInstance } from "fastify";
|
|
import { desc, ledgerEvents, 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(ledgerEvents).orderBy(desc(ledgerEvents.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(),
|
|
);
|
|
}
|