Transaction Science open standards monorepo. Each subdirectory is a self-contained reference protocol with its own README and licence. - openpay/ Apache-2.0 payment-acceptance stack (Rust, 26 crates) - smart-byte/ CC-BY-4.0 content-addressed value carrier (spec) - eoc/ CC-BY-4.0 energy-optimised compute (spec) The wire format and the right to fork are public. The optional hosted services are the offer; the protocols are not.
96 lines
4.2 KiB
Rust
96 lines
4.2 KiB
Rust
//! # `op-webhook` — Outbound webhook delivery
|
|
//!
|
|
//! The crate that turns ledger events (and any other internal
|
|
//! event-shaped data) into reliably-delivered HTTP POSTs to
|
|
//! merchant-configured endpoints. This is the **fanout** edge of
|
|
//! the OpenPay stack: everything upstream produces facts; this
|
|
//! crate signs them, retries them, and surfaces dead letters.
|
|
//!
|
|
//! ## Architectural place in the stack
|
|
//!
|
|
//! ```text
|
|
//! op-orchestrator ─► op-ledger ─► op-webhook ─► merchant ERP
|
|
//! (run intent) (record) (notify) (act)
|
|
//! ```
|
|
//!
|
|
//! The crate is intentionally **decoupled** from `op-ledger` and
|
|
//! `op-orchestrator`: it takes opaque `Vec<u8>` payloads. Operators
|
|
//! choose the wire format (JSON, CBOR, MessagePack — anything). The
|
|
//! signature scheme is Stripe-compatible
|
|
//! (`t={ts},v1={hmac_sha256_hex(secret, "{ts}.{body}")}`) so any
|
|
//! merchant who has already integrated with Stripe receives webhooks
|
|
//! they can verify with the same code.
|
|
//!
|
|
//! ## Core invariants
|
|
//!
|
|
//! 1. **At-least-once delivery.** A subscribed endpoint is
|
|
//! guaranteed to be **attempted** at least once. Network failures
|
|
//! queue retries; consumers must dedupe via `event_id`.
|
|
//! 2. **Exponential backoff with full jitter.** Failed deliveries
|
|
//! are rescheduled at `min(base * 2^n, max_delay)` modulated by
|
|
//! full jitter (`rand_uniform(0, computed_delay)`), capped at a
|
|
//! configurable max attempts and max age (Stripe's 72-hour
|
|
//! window by default).
|
|
//! 3. **Stripe-compatible signing.** Header
|
|
//! `OpenPay-Signature: t={unix_secs},v1={hex_hmac}` carries the
|
|
//! signature; the signed payload is `"{ts}.{body}"`; the HMAC
|
|
//! key is the endpoint's secret. Constant-time comparison
|
|
//! (`subtle::ConstantTimeEq`) is used for verification.
|
|
//! 4. **Auto-disable on chronic failure.** After
|
|
//! `disable_after_consecutive_failures` consecutive delivery
|
|
//! failures (Stripe: signal-via-email, Razorpay: 24h then
|
|
//! disable; we default to 10 failures), the endpoint's status
|
|
//! flips to [`EndpointStatus::Disabled`] and no further attempts
|
|
//! are made until an operator manually re-enables it.
|
|
//! 5. **Replay is explicit.** Operators trigger replay via
|
|
//! [`WebhookDispatcher::replay`]; the system never silently
|
|
//! resurrects events.
|
|
//! 6. **Pluggable HTTP transport.** The crate ships with no real
|
|
//! HTTP client; operators wire `reqwest`, `hyper`, `ureq`, or
|
|
//! anything else that implements [`HttpTransport`].
|
|
//! 7. **Pluggable storage.** [`WebhookStore`] trait;
|
|
//! [`InMemoryWebhookStore`] reference impl for tests and
|
|
//! single-process kiosks.
|
|
//!
|
|
//! ## What this crate does NOT do
|
|
//!
|
|
//! - **No HTTP client.** No `reqwest`, no `hyper`. The
|
|
//! [`HttpTransport`] trait is the seam.
|
|
//! - **No async runtime.** All operations are sync. Async wrappers
|
|
//! are operator-level (`tokio::task::spawn_blocking` or a
|
|
//! long-running thread).
|
|
//! - **No durable queue.** [`InMemoryWebhookStore`] loses state on
|
|
//! process exit. Production deployments plug in their own.
|
|
//! - **No CloudEvents formatting.** The payload is opaque; encode
|
|
//! however you like.
|
|
//! - **No key rotation flow.** A single secret per endpoint. Adding
|
|
//! a "previous secret" field for rotation is documented as a
|
|
//! future extension.
|
|
|
|
#![deny(unsafe_code)]
|
|
#![warn(missing_docs)]
|
|
|
|
pub mod dispatcher;
|
|
pub mod emitter;
|
|
pub mod endpoint;
|
|
pub mod error;
|
|
pub mod event;
|
|
pub mod hexutil;
|
|
#[cfg(feature = "reqwest-transport")]
|
|
pub mod reqwest_transport;
|
|
pub mod retry;
|
|
pub mod signing;
|
|
pub mod store;
|
|
pub mod transport;
|
|
|
|
pub use dispatcher::{DispatchOutcome, WebhookDispatcher};
|
|
pub use emitter::{EventEmitter, NoOpEmitter, WebhookEmitter};
|
|
pub use endpoint::{Endpoint, EndpointId, EndpointStatus};
|
|
pub use error::{Error, Result};
|
|
pub use event::{DeliveryAttempt, DeliveryAttemptId, DeliveryStatus, WebhookEvent, WebhookEventId};
|
|
#[cfg(feature = "reqwest-transport")]
|
|
pub use reqwest_transport::ReqwestTransport;
|
|
pub use retry::{ExponentialBackoffPolicy, RetryPolicy, jitter_full};
|
|
pub use signing::{SIGNATURE_HEADER, SignedPayload, compute_signature, verify_signature};
|
|
pub use store::{InMemoryWebhookStore, WebhookStore};
|
|
pub use transport::{HttpRequest, HttpResponse, HttpTransport, MockTransport};
|