Skip to content

Durable callback receipts

preloop.models.crud.crud_callback_receipt.complete_once supports signed provider adapters without storing request content. Integrations reuse account-owned SecretReference rows. Adapters authenticate raw bytes and bind the account before receipt lookup, including retries. Store keyed body and delivery digests, bounded content-free verdicts and evidence only.

PostgreSQL unique-index arbitration and a row lock serialize identical deliveries across workers. Reservation, verdict and audit writes commit together. There is no committed pending lease: process death or evaluation failure rolls back the reservation, so a retry can evaluate again. Completed identical retries return the stored verdict; changed replay raises CallbackReplayConflictError. Evaluators must implement a hard execution deadline, avoid remote I/O, and append audits through CRUD with commit=False. The receipt CRUD applies bounded lock and SQL statement timeouts. Database outage or lock exhaustion is a protocol failure, never an inferred policy verdict.

Receipt retention defaults to 24 hours, bounded 600..86400 seconds. Call prune with at most 1000 rows per batch; expired metadata remains until maintenance runs, so adapters must register periodic pruning. Retention covers the signing window and documented callback retry lifetime, not arbitrary future retries.

This core change provides an optional adapter seam, with no production routes registered by itself. An installed adapter must register its specific private URL namespace through register_private_callback_prefix before exclusions take effect. The prefix requires at least three path segments; root and broad API prefixes are rejected. The shared Sentry filters then drop both error events and transaction traces for those paths. The separate experimental EE adapter registers its callback and administrator secret-write namespace only when enabled. Proxy body capture and independent tracing must also be disabled in deployment configuration.

Signing bindings reserve only a domain-separated keyed fingerprint of each usable key. Adapters derive it with callback_fingerprint(domain, material) and pass callback_digest_epoch(); both are HMAC-SHA256 under the server encryption key, so an unkeyed hash of a webhook secret is not a binding. The epoch is stable until that key changes. A previous key may stay usable for at most 24 hours (MAX_KEY_OVERLAP) and cannot have that window extended. A transaction advisory lock serializes registration across tenants and epochs. Reusing any fingerprint, including an expired previous key, is rejected; expiry does not silently reassign authority. Rotation registers the new fingerprint and retires the old one in the same transaction as encrypted config. Secret material stays in SecretReference. Changing the server digest key fails closed until explicit offline maintenance disables integrations and rebuilds all fingerprints atomically; the normal adapter provides no automatic remint or epoch-reset operation. Never log fingerprints or keys.