db: business-layer schema — ledger/device event split, tariffs, permits, sessions

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).
This commit is contained in:
2026-06-15 18:13:35 +02:00
parent 9a4c7ee27b
commit 8c2cf93067
11 changed files with 958 additions and 349 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
import type { FastifyInstance } from "fastify";
import { desc, events, type Db } from "@parking/db";
import { desc, ledgerEvents, type Db } from "@parking/db";
import { requireRole } from "../auth.js";
import type { EventLog } from "../event-log.js";
@@ -22,7 +22,7 @@ export async function eventRoutes(
{ 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();
const rows = db.select().from(ledgerEvents).orderBy(desc(ledgerEvents.index)).limit(limit).all();
return { events: rows };
},
);