The Invariant
Don’t trust us — verify the record. The event oracle is public, rate-limited, and leaks nothing: no tenant_id, no credentials, no payload — just the integrity verdict.
The Contract
GET /api/v1/public/verify/event/:id — unauthenticated, rate-limited 30/min
Three independent checks, all must pass for proof: "attributable_integrity_verified":
signature_valid— Ed25519 over the preservedraw_bodyvs the node’s registered pubkey;event_in_signed_batch— theevent_idbelongs to the signed envelope;stored_payload_matches— the storedcp_eventsrow was not altered post-ingest.
Framing: this is attributable integrity evidence — what an auditor, insurer or counterparty needs for ISO-9001-style verification. It is not qualified-eIDAS legal timestamping; that is an explicitly declared out-of-scope extension.
Key scopes (key_scope)
One API key per tenant, rotated via POST /admin/tenants/:id/api-key — rotation kills the previous key instantly (also the live-revocation demo). Denied → 403 SCOPE_INSUFFICIENT from authTenant before the heartbeat is touched.
| Scope | Capability | Use |
|---|---|---|
full |
Everything (back-compat default) | operator consoles, legacy |
sync |
/api/v1/sync/* only |
edge nodes (issued at claim) |
read-only |
GET/HEAD/OPTIONS only, every surface |
BI, auditors, dashboards |
ingest-only |
POST /sync/ingest + /sync/edge-audit only |
pure telemetry producers |
The Flow
auditor: GET /public/verify/event/<id>
→ cp_events.batch_id → cp_ingest_batches (raw_body + signature)
→ cp_edge_nodes.node_pubkey → verify Ed25519 over raw bytes
→ event present in signed events[]? → stored payload == signed payload?
→ proof: attributable_integrity_verified
Payload Example
// 200 — verified
{ "event_id": "…", "signed": true, "node_id": "…",
"signature_valid": true, "event_in_signed_batch": true,
"stored_payload_matches": true,
"proof": "attributable_integrity_verified" }
// 200 — unknown or unsigned event
{ "event_id": "…", "found": false } // or signed: false — never a false positive
Known Failures
| Symptom | Cause → fix |
|---|---|
403 SCOPE_INSUFFICIENT |
Key scope doesn’t cover method+path → rotate with the right scope |
found:false on a known event |
Event arrived via unsigned legacy path (no batch) or wrong id — check signed flag |
Oracle 429 |
30/min public rate limit — wait Retry-After |
EDGE_SIGNATURE_REQUIRED |
Node has a registered pubkey → send x-edge-signature on exact body bytes |